[매입] mg_recv_svc → 전문수신 @Service #3

Open
forge-bot wants to merge 9 commits from forge/ACM-MG-004-attempt-1-run-4e397b504ae6 into main
9 changed files with 634 additions and 0 deletions

View file

@ -0,0 +1,3 @@
# ACM-MG-004-attempt-1-run-4e397b504ae6
Forge 이슈 작업 브랜치 `forge/ACM-MG-004-attempt-1-run-4e397b504ae6`.

View file

@ -0,0 +1,105 @@
package com.klaro.acquirecore.acquiring.codec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 전문 언팩/ 커덱 (codec)
* 바이너리 전문 파싱 조립 담당
*/
@Component
public class MessageCodec {
private static final Logger log = LoggerFactory.getLogger(MessageCodec.class);
// 전문 헤더 길이
private static final int HEADER_LENGTH = 20;
// 전문 트레일러 길이
private static final int TRAILER_LENGTH = 4;
/**
* 원시 전문 언팩
* @param rawMessage 원시 바이트 배열
* @return 파싱된 전문 객체
*/
public ParsedMessage unpack(byte[] rawMessage) {
log.debug("전문 언팩 시작 - 길이: {}", rawMessage.length);
if (rawMessage == null || rawMessage.length < HEADER_LENGTH + TRAILER_LENGTH) {
throw new IllegalArgumentException("전문 길이 오류");
}
ParsedMessage parsed = new ParsedMessage();
// 헤더 파싱
parsed.setMessageType(decodeString(rawMessage, 0, 4));
parsed.setTxId(decodeString(rawMessage, 4, 12));
parsed.setTxCode(decodeString(rawMessage, 16, 4));
// 바디 파싱 (가변 길이)
int bodyLength = rawMessage.length - HEADER_LENGTH - TRAILER_LENGTH;
if (bodyLength > 0) {
parsed.setMerchantId(decodeString(rawMessage, HEADER_LENGTH, 15));
parsed.setCardNumber(decodeString(rawMessage, HEADER_LENGTH + 15, 20));
parsed.setAmount(decodeString(rawMessage, HEADER_LENGTH + 35, 12));
parsed.setApprovalCode(decodeString(rawMessage, HEADER_LENGTH + 47, 10));
}
// 트레일러 검증
byte[] trailer = new byte[TRAILER_LENGTH];
System.arraycopy(rawMessage, rawMessage.length - TRAILER_LENGTH, trailer, 0, TRAILER_LENGTH);
parsed.setTrailer(new String(trailer));
log.debug("전문 언팩 완료 - TX_ID: {}, TX_CODE: {}", parsed.getTxId(), parsed.getTxCode());
return parsed;
}
/**
* 전문 (응답 전문 생성)
* @param parsed 파싱된 전문
* @return 원시 바이트 배열
*/
public byte[] pack(ParsedMessage parsed) {
log.debug("전문 팩 시작 - TX_ID: {}", parsed.getTxId());
StringBuilder sb = new StringBuilder();
sb.append(padRight(parsed.getMessageType(), 4));
sb.append(padRight(parsed.getTxId(), 12));
sb.append(padRight(parsed.getTxCode(), 4));
sb.append(padRight(parsed.getMerchantId(), 15));
sb.append(padRight(parsed.getCardNumber(), 20));
sb.append(padRight(parsed.getAmount(), 12));
sb.append(padRight(parsed.getApprovalCode(), 10));
sb.append("ETX");
return sb.toString().getBytes();
}
/**
* 바이트 배열에서 문자열 추출
*/
private String decodeString(byte[] data, int offset, int length) {
if (offset + length > data.length) {
return "";
}
byte[] sub = new byte[length];
System.arraycopy(data, offset, sub, 0, length);
return new String(sub).trim();
}
/**
* 문자열을 지정 길이만큼 패딩
*/
private String padRight(String value, int length) {
if (value == null) value = "";
if (value.length() >= length) {
return value.substring(0, length);
}
StringBuilder sb = new StringBuilder(value);
while (sb.length() < length) {
sb.append(' ');
}
return sb.toString();
}
}

View file

@ -0,0 +1,83 @@
package com.klaro.acquirecore.acquiring.codec;
/**
* 파싱된 전문 데이터 객체
*/
public class ParsedMessage {
private String messageType;
private String txId;
private String txCode;
private String merchantId;
private String cardNumber;
private String amount;
private String approvalCode;
private String trailer;
public ParsedMessage() {
}
public String getMessageType() {
return messageType;
}
public void setMessageType(String messageType) {
this.messageType = messageType;
}
public String getTxId() {
return txId;
}
public void setTxId(String txId) {
this.txId = txId;
}
public String getTxCode() {
return txCode;
}
public void setTxCode(String txCode) {
this.txCode = txCode;
}
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public String getCardNumber() {
return cardNumber;
}
public void setCardNumber(String cardNumber) {
this.cardNumber = cardNumber;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public String getApprovalCode() {
return approvalCode;
}
public void setApprovalCode(String approvalCode) {
this.approvalCode = approvalCode;
}
public String getTrailer() {
return trailer;
}
public void setTrailer(String trailer) {
this.trailer = trailer;
}
}

View file

@ -0,0 +1,85 @@
package com.klaro.acquirecore.acquiring.dto;
import java.time.LocalDateTime;
/**
* 매입 전문 DTO
*/
public class AcquireMessageDto {
private String txId;
private String txCode;
private String merchantId;
private String cardNumber;
private String amount;
private String approvalCode;
private String messageType;
private LocalDateTime receiveTime;
public AcquireMessageDto() {
}
public String getTxId() {
return txId;
}
public void setTxId(String txId) {
this.txId = txId;
}
public String getTxCode() {
return txCode;
}
public void setTxCode(String txCode) {
this.txCode = txCode;
}
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public String getCardNumber() {
return cardNumber;
}
public void setCardNumber(String cardNumber) {
this.cardNumber = cardNumber;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public String getApprovalCode() {
return approvalCode;
}
public void setApprovalCode(String approvalCode) {
this.approvalCode = approvalCode;
}
public String getMessageType() {
return messageType;
}
public void setMessageType(String messageType) {
this.messageType = messageType;
}
public LocalDateTime getReceiveTime() {
return receiveTime;
}
public void setReceiveTime(LocalDateTime receiveTime) {
this.receiveTime = receiveTime;
}
}

View file

@ -0,0 +1,56 @@
package com.klaro.acquirecore.acquiring.dto;
/**
* 매입 응답 DTO
*/
public class AcquireResponseDto {
private String resultCode;
private String resultMessage;
private String txId;
public AcquireResponseDto() {
}
public AcquireResponseDto(String resultCode, String resultMessage, String txId) {
this.resultCode = resultCode;
this.resultMessage = resultMessage;
this.txId = txId;
}
public static AcquireResponseDto success(String txId) {
return new AcquireResponseDto("0000", "성공", txId);
}
public static AcquireResponseDto success(String resultCode, String txId) {
return new AcquireResponseDto(resultCode, "성공", txId);
}
public static AcquireResponseDto error(String resultCode, String message) {
return new AcquireResponseDto(resultCode, message, null);
}
public String getResultCode() {
return resultCode;
}
public void setResultCode(String resultCode) {
this.resultCode = resultCode;
}
public String getResultMessage() {
return resultMessage;
}
public void setResultMessage(String resultMessage) {
this.resultMessage = resultMessage;
}
public String getTxId() {
return txId;
}
public void setTxId(String txId) {
this.txId = txId;
}
}

View file

@ -0,0 +1,32 @@
package com.klaro.acquirecore.acquiring.repository;
import com.klaro.acquirecore.acquiring.codec.ParsedMessage;
import java.util.Optional;
/**
* 매입 거래 Repository 인터페이스 (DB 접근 스텁)
*/
public interface AcquireTransactionRepository {
/**
* 거래ID로 거래 조회
* @param txId 거래ID
* @return 거래 정보
*/
Optional<ParsedMessage> findByTxId(String txId);
/**
* 거래 저장
* @param message 거래 정보
* @return 저장 성공 여부
*/
boolean save(ParsedMessage message);
/**
* 거래ID로 삭제
* @param txId 거래ID
* @return 삭제 성공 여부
*/
boolean deleteByTxId(String txId);
}

View file

@ -0,0 +1,121 @@
package com.klaro.acquirecore.acquiring.service;
import com.klaro.acquirecore.acquiring.codec.MessageCodec;
import com.klaro.acquirecore.acquiring.codec.ParsedMessage;
import com.klaro.acquirecore.acquiring.dto.AcquireMessageDto;
import com.klaro.acquirecore.acquiring.dto.AcquireResponseDto;
import com.klaro.acquirecore.acquiring.repository.AcquireTransactionRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* 매입 전문수신 서비스 (TX_SERVICE @Service)
* legacy/app/online/mg_recv_svc.pgc 전환
*/
@Service
public class MessageReceiveService {
private static final Logger log = LoggerFactory.getLogger(MessageReceiveService.class);
private final MessageCodec messageCodec;
private final AcquireTransactionRepository transactionRepository;
public MessageReceiveService(MessageCodec messageCodec,
AcquireTransactionRepository transactionRepository) {
this.messageCodec = messageCodec;
this.transactionRepository = transactionRepository;
}
/**
* 전문 수신 처리
* @param rawMessage 원시 전문 데이터
* @return 처리 결과 DTO
*/
public AcquireResponseDto receiveAndProcess(byte[] rawMessage) {
log.info("전문 수신 시작 - 길이: {}", rawMessage.length);
try {
// 1. 전문 언팩 (codec)
ParsedMessage parsed = messageCodec.unpack(rawMessage);
log.debug("전문 언팩 완료 - TX_CODE: {}", parsed.getTxCode());
// 2. DTO 변환
AcquireMessageDto messageDto = toDto(parsed);
// 3. 거래 처리
AcquireResponseDto response = processTransaction(messageDto);
log.info("전문 처리 완료 - 응답코드: {}", response.getResultCode());
return response;
} catch (Exception e) {
log.error("전문 처리 오류", e);
return AcquireResponseDto.error("9999", "전문 처리 실패: " + e.getMessage());
}
}
/**
* 거래 처리 로직
*/
private AcquireResponseDto processTransaction(AcquireMessageDto dto) {
String txCode = dto.getTxCode();
switch (txCode) {
case "TX001":
return processTx001(dto);
case "TX002":
return processTx002(dto);
case "TX003":
return processTx003(dto);
default:
log.warn("알 수 없는 거래코드: {}", txCode);
return AcquireResponseDto.error("9998", "알 수 없는 거래코드: " + txCode);
}
}
private AcquireResponseDto processTx001(AcquireMessageDto dto) {
log.debug("TX001 처리: {}", dto.getMerchantId());
// 매입 요청 처리
return AcquireResponseDto.success("0000", dto.getTxId());
}
private AcquireResponseDto processTx002(AcquireMessageDto dto) {
log.debug("TX002 처리: {}", dto.getMerchantId());
// 매입 취소 처리
return AcquireResponseDto.success("0000", dto.getTxId());
}
private AcquireResponseDto processTx003(AcquireMessageDto dto) {
log.debug("TX003 처리: {}", dto.getMerchantId());
// 매입 조회 처리
return AcquireResponseDto.success("0000", dto.getTxId());
}
/**
* ParsedMessage AcquireMessageDto 변환
*/
private AcquireMessageDto toDto(ParsedMessage parsed) {
AcquireMessageDto dto = new AcquireMessageDto();
dto.setTxId(parsed.getTxId());
dto.setTxCode(parsed.getTxCode());
dto.setMerchantId(parsed.getMerchantId());
dto.setAmount(parsed.getAmount());
dto.setCardNumber(parsed.getCardNumber());
dto.setApprovalCode(parsed.getApprovalCode());
dto.setMessageType(parsed.getMessageType());
dto.setReceiveTime(LocalDateTime.now());
return dto;
}
/**
* 거래ID로 거래 정보 조회
*/
public Optional<AcquireMessageDto> findByTxId(String txId) {
return transactionRepository.findByTxId(txId)
.map(this::toDto);
}
}

View file

@ -0,0 +1,70 @@
package com.klaro.acquirecore.acquiring.codec;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MessageCodecTest {
private MessageCodec codec;
@BeforeEach
void setUp() {
codec = new MessageCodec();
}
@Test
void unpack_validMessage_parsesCorrectly() {
// given
String raw = "MSGT123456789012TX01MER00001 1234567890123456789000000012340A12345678ETX";
byte[] rawMessage = raw.getBytes();
// when
ParsedMessage parsed = codec.unpack(rawMessage);
// then
assertEquals("MSGT", parsed.getMessageType());
assertEquals("123456789012", parsed.getTxId());
assertEquals("TX01", parsed.getTxCode());
assertEquals("MER00001", parsed.getMerchantId());
assertEquals("12345678901234567890", parsed.getCardNumber());
assertEquals("0000001234", parsed.getAmount());
assertEquals("A12345678", parsed.getApprovalCode());
assertEquals("ETX", parsed.getTrailer());
}
@Test
void unpack_nullMessage_throwsException() {
assertThrows(IllegalArgumentException.class, () -> codec.unpack(null));
}
@Test
void unpack_shortMessage_throwsException() {
byte[] shortMessage = new byte[10];
assertThrows(IllegalArgumentException.class, () -> codec.unpack(shortMessage));
}
@Test
void pack_validMessage_packsCorrectly() {
// given
ParsedMessage parsed = new ParsedMessage();
parsed.setMessageType("MSGT");
parsed.setTxId("123456789012");
parsed.setTxCode("TX01");
parsed.setMerchantId("MER00001");
parsed.setCardNumber("12345678901234567890");
parsed.setAmount("0000001234");
parsed.setApprovalCode("A12345678");
// when
byte[] packed = codec.pack(parsed);
// then
assertNotNull(packed);
assertTrue(packed.length > 0);
String packedStr = new String(packed);
assertTrue(packedStr.startsWith("MSGT"));
assertTrue(packedStr.endsWith("ETX"));
}
}

File diff suppressed because one or more lines are too long