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

Open
forge-bot wants to merge 9 commits from forge/ACM-MG-004-attempt-2-run-be8e7d8241de into main
9 changed files with 407 additions and 0 deletions

View file

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

View file

@ -0,0 +1,91 @@
package com.klaro.acquirecore.acquiring.codec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 전문 인코딩/디코딩 컴포넌트
* Fixed-length 필드 기반 전문 파싱
*/
@Component
public class MessageCodec {
private static final Logger log = LoggerFactory.getLogger(MessageCodec.class);
private static final int LEN_TX_TYPE = 4;
private static final int LEN_TRACE_NO = 12;
private static final int LEN_MERCHANT_ID = 15;
private static final int LEN_AMOUNT = 12;
private static final int LEN_CARD_NO = 20;
private static final int LEN_EXPIRY = 4;
private static final int LEN_INSTALLMENTS = 2;
private static final int LEN_FILLER = 99;
public UnpackedMessage unpack(byte[] rawMessage) {
if (rawMessage == null || rawMessage.length == 0) {
throw new IllegalArgumentException("Empty message");
}
String data = new String(rawMessage);
int offset = 0;
UnpackedMessage unpacked = new UnpackedMessage();
unpacked.setRawData(data);
unpacked.setTxType(extract(data, offset, LEN_TX_TYPE));
offset += LEN_TX_TYPE;
unpacked.setTraceNo(extract(data, offset, LEN_TRACE_NO));
offset += LEN_TRACE_NO;
unpacked.setMerchantId(extract(data, offset, LEN_MERCHANT_ID));
offset += LEN_MERCHANT_ID;
unpacked.setAmount(extract(data, offset, LEN_AMOUNT));
offset += LEN_AMOUNT;
unpacked.setCardNo(extract(data, offset, LEN_CARD_NO));
offset += LEN_CARD_NO;
unpacked.setExpiryDate(extract(data, offset, LEN_EXPIRY));
offset += LEN_EXPIRY;
unpacked.setInstallments(extract(data, offset, LEN_INSTALLMENTS));
offset += LEN_INSTALLMENTS;
if (data.length() > offset) {
unpacked.setFiller(extract(data, offset, Math.min(LEN_FILLER, data.length() - offset)));
}
log.debug("Message unpacked: txType={}, traceNo={}", unpacked.getTxType(), unpacked.getTraceNo());
return unpacked;
}
public byte[] pack(UnpackedMessage unpacked) {
StringBuilder sb = new StringBuilder();
sb.append(padRight(unpacked.getTxType(), LEN_TX_TYPE));
sb.append(padRight(unpacked.getTraceNo(), LEN_TRACE_NO));
sb.append(padRight(unpacked.getMerchantId(), LEN_MERCHANT_ID));
sb.append(padRight(unpacked.getAmount(), LEN_AMOUNT));
sb.append(padRight(unpacked.getCardNo(), LEN_CARD_NO));
sb.append(padRight(unpacked.getExpiryDate(), LEN_EXPIRY));
sb.append(padRight(unpacked.getInstallments(), LEN_INSTALLMENTS));
sb.append(padRight(unpacked.getFiller(), LEN_FILLER));
return sb.toString().getBytes();
}
private String extract(String data, int offset, int length) {
if (offset >= data.length()) return "";
int end = Math.min(offset + length, data.length());
return data.substring(offset, end).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,36 @@
package com.klaro.acquirecore.acquiring.codec;
/**
* 언팩된 전문 메시지 DTO
*/
public class UnpackedMessage {
private String txType;
private String traceNo;
private String merchantId;
private String amount;
private String cardNo;
private String expiryDate;
private String installments;
private String filler;
private String rawData;
public String getTxType() { return txType; }
public void setTxType(String txType) { this.txType = txType; }
public String getTraceNo() { return traceNo; }
public void setTraceNo(String traceNo) { this.traceNo = traceNo; }
public String getMerchantId() { return merchantId; }
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
public String getAmount() { return amount; }
public void setAmount(String amount) { this.amount = amount; }
public String getCardNo() { return cardNo; }
public void setCardNo(String cardNo) { this.cardNo = cardNo; }
public String getExpiryDate() { return expiryDate; }
public void setExpiryDate(String expiryDate) { this.expiryDate = expiryDate; }
public String getInstallments() { return installments; }
public void setInstallments(String installments) { this.installments = installments; }
public String getFiller() { return filler; }
public void setFiller(String filler) { this.filler = filler; }
public String getRawData() { return rawData; }
public void setRawData(String rawData) { this.rawData = rawData; }
}

View file

@ -0,0 +1,33 @@
package com.klaro.acquirecore.acquiring.dto;
/**
* 거래 요청 DTO
*/
public class TxRequest {
private String traceNo;
private String txType;
private String merchantId;
private String amount;
private String cardNo;
private String expiryDate;
private String installments;
private String messageRaw;
public String getTraceNo() { return traceNo; }
public void setTraceNo(String traceNo) { this.traceNo = traceNo; }
public String getTxType() { return txType; }
public void setTxType(String txType) { this.txType = txType; }
public String getMerchantId() { return merchantId; }
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
public String getAmount() { return amount; }
public void setAmount(String amount) { this.amount = amount; }
public String getCardNo() { return cardNo; }
public void setCardNo(String cardNo) { this.cardNo = cardNo; }
public String getExpiryDate() { return expiryDate; }
public void setExpiryDate(String expiryDate) { this.expiryDate = expiryDate; }
public String getInstallments() { return installments; }
public void setInstallments(String installments) { this.installments = installments; }
public String getMessageRaw() { return messageRaw; }
public void setMessageRaw(String messageRaw) { this.messageRaw = messageRaw; }
}

View file

@ -0,0 +1,21 @@
package com.klaro.acquirecore.acquiring.dto;
/**
* 거래 응답 DTO
*/
public class TxResponse {
private String traceNo;
private String txType;
private String respCode;
private String respMsg;
public String getTraceNo() { return traceNo; }
public void setTraceNo(String traceNo) { this.traceNo = traceNo; }
public String getTxType() { return txType; }
public void setTxType(String txType) { this.txType = txType; }
public String getRespCode() { return respCode; }
public void setRespCode(String respCode) { this.respCode = respCode; }
public String getRespMsg() { return respMsg; }
public void setRespMsg(String respMsg) { this.respMsg = respMsg; }
}

View file

@ -0,0 +1,13 @@
package com.klaro.acquirecore.acquiring.repository;
import com.klaro.acquirecore.acquiring.dto.TxRequest;
/**
* 거래 DB 접근 인터페이스 (스텁)
* Legacy: mg_recv_svc.pgc의 DB 처리 부분
*/
public interface TxRepository {
void save(TxRequest request);
TxRequest findByTraceNo(String traceNo);
}

View file

@ -0,0 +1,28 @@
package com.klaro.acquirecore.acquiring.repository;
import com.klaro.acquirecore.acquiring.dto.TxRequest;
import org.springframework.stereotype.Repository;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 거래 DB 접근 스텁 구현 (인메모리)
*/
@Repository
public class TxRepositoryStub implements TxRepository {
private final Map<String, TxRequest> store = new ConcurrentHashMap<>();
@Override
public void save(TxRequest request) {
if (request != null && request.getTraceNo() != null) {
store.put(request.getTraceNo(), request);
}
}
@Override
public TxRequest findByTraceNo(String traceNo) {
return store.get(traceNo);
}
}

View file

@ -0,0 +1,88 @@
package com.klaro.acquirecore.acquiring.service;
import com.klaro.acquirecore.acquiring.codec.MessageCodec;
import com.klaro.acquirecore.acquiring.codec.UnpackedMessage;
import com.klaro.acquirecore.acquiring.dto.TxRequest;
import com.klaro.acquirecore.acquiring.dto.TxResponse;
import com.klaro.acquirecore.acquiring.repository.TxRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* TX_SERVICE: 전문수신 서비스
* Legacy: mg_recv_svc.pgc
* Handles incoming acquiring transaction messages, unpacks, processes, and stores.
*/
@Service
public class TxService {
private static final Logger log = LoggerFactory.getLogger(TxService.class);
private final MessageCodec messageCodec;
private final TxRepository txRepository;
public TxService(MessageCodec messageCodec, TxRepository txRepository) {
this.messageCodec = messageCodec;
this.txRepository = txRepository;
}
/**
* 수신된 전문을 처리합니다.
*
* @param rawMessage 원본 바이트 전문
* @return 처리 결과 응답
*/
public TxResponse processMessage(byte[] rawMessage) {
log.info("Processing incoming message, length={}", rawMessage.length);
try {
// 1. 전문 언팩 (디코딩)
UnpackedMessage unpacked = messageCodec.unpack(rawMessage);
log.debug("Message unpacked: txType={}, traceNo={}",
unpacked.getTxType(), unpacked.getTraceNo());
// 2. TX 요청 변환
TxRequest request = toTxRequest(unpacked);
// 3. 거래 처리 (DB 저장)
txRepository.save(request);
// 4. 응답 생성
return buildResponse(unpacked, "00", "Approved");
} catch (Exception e) {
log.error("Message processing failed", e);
return buildErrorResponse(e.getMessage());
}
}
private TxRequest toTxRequest(UnpackedMessage unpacked) {
TxRequest request = new TxRequest();
request.setTraceNo(unpacked.getTraceNo());
request.setTxType(unpacked.getTxType());
request.setMerchantId(unpacked.getMerchantId());
request.setAmount(unpacked.getAmount());
request.setCardNo(unpacked.getCardNo());
request.setExpiryDate(unpacked.getExpiryDate());
request.setInstallments(unpacked.getInstallments());
request.setMessageRaw(unpacked.getRawData());
return request;
}
private TxResponse buildResponse(UnpackedMessage unpacked, String respCode, String respMsg) {
TxResponse response = new TxResponse();
response.setTraceNo(unpacked.getTraceNo());
response.setRespCode(respCode);
response.setRespMsg(respMsg);
response.setTxType(unpacked.getTxType());
return response;
}
private TxResponse buildErrorResponse(String errorMessage) {
TxResponse response = new TxResponse();
response.setRespCode("99");
response.setRespMsg(errorMessage != null ? errorMessage : "Unknown error");
return response;
}
}

View file

@ -0,0 +1,94 @@
package com.klaro.acquirecore.acquiring.service;
import com.klaro.acquirecore.acquiring.codec.MessageCodec;
import com.klaro.acquirecore.acquiring.codec.UnpackedMessage;
import com.klaro.acquirecore.acquiring.dto.TxRequest;
import com.klaro.acquirecore.acquiring.dto.TxResponse;
import com.klaro.acquirecore.acquiring.repository.TxRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TxServiceTest {
@Mock
private TxRepository txRepository;
private MessageCodec messageCodec;
private TxService txService;
@BeforeEach
void setUp() {
messageCodec = new MessageCodec();
txService = new TxService(messageCodec, txRepository);
}
@Test
void processMessage_validMessage_returnsApproved() {
String rawMessage = "0100TRX123456789MER00000012345 41111111111111112025/1230N ";
TxResponse response = txService.processMessage(rawMessage.getBytes());
assertNotNull(response);
assertEquals("00", response.getRespCode());
assertEquals("Approved", response.getRespMsg());
verify(txRepository, times(1)).save(any(TxRequest.class));
}
@Test
void processMessage_emptyMessage_returnsError() {
TxResponse response = txService.processMessage(new byte[0]);
assertNotNull(response);
assertEquals("99", response.getRespCode());
}
@Test
void processMessage_nullMessage_returnsError() {
TxResponse response = txService.processMessage(null);
assertNotNull(response);
assertEquals("99", response.getRespCode());
}
@Test
void codec_unpackValidMessage_parsesFields() {
String raw = "0100TRX123456789MER00000012345 41111111111111112025/1230N ";
UnpackedMessage unpacked = messageCodec.unpack(raw.getBytes());
assertEquals("0100", unpacked.getTxType());
assertEquals("TRX123456789", unpacked.getTraceNo());
assertEquals("MER00000012345", unpacked.getMerchantId());
assertEquals("4111111111111111", unpacked.getCardNo());
}
@Test
void codec_unpackEmpty_throwsException() {
assertThrows(IllegalArgumentException.class, () -> messageCodec.unpack(new byte[0]));
}
@Test
void codec_packUnpacked_roundTrips() {
UnpackedMessage original = new UnpackedMessage();
original.setTxType("0100");
original.setTraceNo("TRX123456789");
original.setMerchantId("MER00000012345");
original.setAmount("000000100000");
original.setCardNo("4111111111111111");
original.setExpiryDate("2025/1230");
original.setInstallments("00");
original.setFiller("");
byte[] packed = messageCodec.pack(original);
UnpackedMessage result = messageCodec.unpack(packed);
assertEquals(original.getTxType(), result.getTxType());
assertEquals(original.getTraceNo(), result.getTraceNo());
}
}