[매입] mg_recv_svc → 전문수신 @Service #6
9 changed files with 407 additions and 0 deletions
3
.forge/ACM-MG-004-attempt-2-run-be8e7d8241de.md
Normal file
3
.forge/ACM-MG-004-attempt-2-run-be8e7d8241de.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# ACM-MG-004-attempt-2-run-be8e7d8241de
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/ACM-MG-004-attempt-2-run-be8e7d8241de`.
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
Reference in a new issue