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