From dec970dbe9533a2cf081aceb5089670b68442d84 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:16:50 +0000 Subject: [PATCH 1/9] forge: open work branch for ACM-MG-004-attempt-3-run-56504178208a --- .forge/ACM-MG-004-attempt-3-run-56504178208a.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .forge/ACM-MG-004-attempt-3-run-56504178208a.md diff --git a/.forge/ACM-MG-004-attempt-3-run-56504178208a.md b/.forge/ACM-MG-004-attempt-3-run-56504178208a.md new file mode 100644 index 0000000..12a6b12 --- /dev/null +++ b/.forge/ACM-MG-004-attempt-3-run-56504178208a.md @@ -0,0 +1,3 @@ +# ACM-MG-004-attempt-3-run-56504178208a + +Forge 이슈 작업 브랜치 `forge/ACM-MG-004-attempt-3-run-56504178208a`. -- 2.49.1 From f883ab754dbb5c72a510f8503eca30bd33ca4117 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:45 +0000 Subject: [PATCH 2/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquiring/service/TxMessageCodec.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java new file mode 100644 index 0000000..964ecda --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageCodec.java @@ -0,0 +1,158 @@ +package com.klaro.acquirecore.acquiring.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.klaro.acquirecore.acquiring.dto.TxMessage; +import org.springframework.stereotype.Component; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Codec for TX message packing/unpacking. + * Handles binary protocol ↔ domain object conversion. + */ +@Component +public class TxMessageCodec { + + private static final DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final String ENCODING = "EUC-KR"; + + private final ObjectMapper objectMapper; + + public TxMessageCodec(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Unpack binary message to TxMessage domain object. + * Format: HEADER(20) + BODY(n) + */ + public TxMessage unpack(byte[] raw) { + if (raw == null || raw.length < 20) { + throw new IllegalArgumentException("Invalid message: too short"); + } + ByteBuffer buf = ByteBuffer.wrap(raw); + + // HEADER: 20 bytes + byte[] headerBytes = new byte[20]; + buf.get(headerBytes); + String header = new String(headerBytes, StandardCharsets.ISO_8859_1).trim(); + + // BODY: remaining bytes + byte[] bodyBytes = new byte[raw.length - 20]; + buf.get(bodyBytes); + + TxMessage msg = new TxMessage(); + msg.setHeader(header); + msg.setRawBody(bodyBytes); + + // Parse body as JSON or delimited fields + parseBody(msg, bodyBytes); + + return msg; + } + + private void parseBody(TxMessage msg, byte[] body) { + String bodyStr = new String(body, StandardCharsets.ISO_8859_1); + try { + JsonNode node = objectMapper.readTree(bodyStr); + if (node.isObject()) { + ObjectNode obj = (ObjectNode) node; + obj.fields().forEachRemaining(e -> { + String key = e.getKey(); + JsonNode val = e.getValue(); + if (val.isTextual()) { + setField(msg, key, val.asText()); + } else if (val.isNumber()) { + setNumericField(msg, key, val.asLong()); + } + }); + } + } catch (Exception e) { + // Fallback: delimited parse + parseDelimited(msg, bodyStr); + } + } + + private void parseDelimited(TxMessage msg, String body) { + String[] fields = body.split("\\|"); + for (int i = 0; i < fields.length; i++) { + String[] kv = fields[i].split("=", 2); + if (kv.length == 2) { + setField(msg, kv[0].trim(), kv[1].trim()); + } + } + } + + private void setField(TxMessage msg, String key, String value) { + switch (key.toUpperCase()) { + case "TXID": msg.setTxId(value); break; + case "MERCHANTID": msg.setMerchantId(value); break; + case "TERMINALID": msg.setTerminalId(value); break; + case "AMOUNT": msg.setAmount(value); break; + case "STATUS": msg.setStatus(value); break; + case "TRANDATE": msg.setTranDate(value); break; + case "TRANTIME": msg.setTranTime(value); break; + case "AUTHNO": msg.setAuthNo(value); break; + case "CARDNO": msg.setCardNo(value); break; + case "VANMSG": msg.setVanMsg(value); break; + default: msg.setExtra(key, value); + } + } + + private void setNumericField(TxMessage msg, String key, long value) { + if ("AMOUNT".equalsIgnoreCase(key)) { + msg.setAmount(String.valueOf(value)); + } + } + + /** + * Pack TxMessage to binary format. + */ + public byte[] pack(TxMessage msg) { + byte[] body = packBody(msg); + byte[] header = buildHeader(msg, body.length); + + ByteBuffer buf = ByteBuffer.allocate(header.length + body.length); + buf.put(header); + buf.put(body); + return buf.array(); + } + + private byte[] buildHeader(TxMessage msg, int bodyLen) { + String header = String.format("%-20s", + msg.getTxId() != null ? msg.getTxId() : UUID.randomUUID().toString().substring(0, 8)); + return header.getBytes(StandardCharsets.ISO_8859_1); + } + + private byte[] packBody(TxMessage msg) { + try { + ObjectNode node = objectMapper.createObjectNode(); + if (msg.getTxId() != null) node.put("TXID", msg.getTxId()); + if (msg.getMerchantId() != null) node.put("MERCHANTID", msg.getMerchantId()); + if (msg.getTerminalId() != null) node.put("TERMINALID", msg.getTerminalId()); + if (msg.getAmount() != null) node.put("AMOUNT", msg.getAmount()); + if (msg.getStatus() != null) node.put("STATUS", msg.getStatus()); + if (msg.getTranDate() != null) node.put("TRANDATE", msg.getTranDate()); + if (msg.getTranTime() != null) node.put("TRANTIME", msg.getTranTime()); + if (msg.getAuthNo() != null) node.put("AUTHNO", msg.getAuthNo()); + if (msg.getCardNo() != null) node.put("CARDNO", maskCardNo(msg.getCardNo())); + if (msg.getVanMsg() != null) node.put("VANMSG", msg.getVanMsg()); + + msg.getExtra().forEach(node::put); + return objectMapper.writeValueAsString(node).getBytes(StandardCharsets.ISO_8859_1); + } catch (Exception e) { + throw new RuntimeException("Failed to pack message body", e); + } + } + + private String maskCardNo(String cardNo) { + if (cardNo == null || cardNo.length() < 10) return cardNo; + return cardNo.substring(0, 6) + "****" + cardNo.substring(cardNo.length() - 4); + } +} -- 2.49.1 From c25705bce4adf2b039313737013bfeefc3fc3de2 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:46 +0000 Subject: [PATCH 3/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirecore/acquiring/dto/TxMessage.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxMessage.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxMessage.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxMessage.java new file mode 100644 index 0000000..698ba37 --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxMessage.java @@ -0,0 +1,85 @@ +package com.klaro.acquirecore.acquiring.dto; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * TX message domain object. + * Represents an acquiring transaction message. + */ +public class TxMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + private String header; + private byte[] rawBody; + + // Core fields + private String txId; + private String merchantId; + private String terminalId; + private String amount; + private String status; + private String tranDate; + private String tranTime; + private String authNo; + private String cardNo; + private String vanMsg; + + // Extra fields (dynamic) + private Map extra = new HashMap<>(); + + public TxMessage() {} + + public String getHeader() { return header; } + public void setHeader(String header) { this.header = header; } + + public byte[] getRawBody() { return rawBody; } + public void setRawBody(byte[] rawBody) { this.rawBody = rawBody; } + + public String getTxId() { return txId; } + public void setTxId(String txId) { this.txId = txId; } + + public String getMerchantId() { return merchantId; } + public void setMerchantId(String merchantId) { this.merchantId = merchantId; } + + public String getTerminalId() { return terminalId; } + public void setTerminalId(String terminalId) { this.terminalId = terminalId; } + + public String getAmount() { return amount; } + public void setAmount(String amount) { this.amount = amount; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getTranDate() { return tranDate; } + public void setTranDate(String tranDate) { this.tranDate = tranDate; } + + public String getTranTime() { return tranTime; } + public void setTranTime(String tranTime) { this.tranTime = tranTime; } + + public String getAuthNo() { return authNo; } + public void setAuthNo(String authNo) { this.authNo = authNo; } + + public String getCardNo() { return cardNo; } + public void setCardNo(String cardNo) { this.cardNo = cardNo; } + + public String getVanMsg() { return vanMsg; } + public void setVanMsg(String vanMsg) { this.vanMsg = vanMsg; } + + public Map getExtra() { return extra; } + public void setExtra(Map extra) { this.extra = extra; } + public void setExtra(String key, String value) { this.extra.put(key, value); } + + @Override + public String toString() { + return "TxMessage{" + + "txId='" + txId + '\'' + + ", merchantId='" + merchantId + '\'' + + ", terminalId='" + terminalId + '\'' + + ", amount='" + amount + '\'' + + ", status='" + status + '\'' + + '}'; + } +} -- 2.49.1 From 7bc37d0d869d6d1c435cda0154bd45a54831abdb Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:47 +0000 Subject: [PATCH 4/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirecore/acquiring/dto/TxResult.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxResult.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxResult.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxResult.java new file mode 100644 index 0000000..177a456 --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/dto/TxResult.java @@ -0,0 +1,49 @@ +package com.klaro.acquirecore.acquiring.dto; + +import java.io.Serializable; + +/** + * TX processing result. + */ +public class TxResult implements Serializable { + + private static final long serialVersionUID = 1L; + + private String txId; + private String code; + private String message; + private boolean success; + private TxMessage response; + + public TxResult() {} + + public TxResult(String txId, boolean success, String code, String message) { + this.txId = txId; + this.success = success; + this.code = code; + this.message = message; + } + + public static TxResult ok(String txId, String message) { + return new TxResult(txId, true, "0000", message); + } + + public static TxResult error(String txId, String code, String message) { + return new TxResult(txId, false, code, message); + } + + public String getTxId() { return txId; } + public void setTxId(String txId) { this.txId = txId; } + + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + + public TxMessage getResponse() { return response; } + public void setResponse(TxMessage response) { this.response = response; } +} -- 2.49.1 From 3b247810b7b20b088c2fba40a35b62b277bda2e1 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:49 +0000 Subject: [PATCH 5/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TxMessageReceiveService.java | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveService.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveService.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveService.java new file mode 100644 index 0000000..03e38ee --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveService.java @@ -0,0 +1,191 @@ +package com.klaro.acquirecore.acquiring.service; + +import com.klaro.acquirecore.acquiring.dto.TxMessage; +import com.klaro.acquirecore.acquiring.dto.TxResult; +import com.klaro.acquirecore.acquiring.repository.TxRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * TX_SERVICE: Main service for receiving and processing acquiring messages. + * Migrated from legacy/app/online/mg_recv_svc.pgc + */ +@Service +public class TxMessageReceiveService { + + private static final Logger log = LoggerFactory.getLogger(TxMessageReceiveService.class); + private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + private final TxMessageCodec codec; + private final TxRepository txRepository; + + public TxMessageReceiveService(TxMessageCodec codec, TxRepository txRepository) { + this.codec = codec; + this.txRepository = txRepository; + } + + /** + * Receive and process raw binary message. + * @param raw raw message bytes + * @return processing result + */ + public TxResult receive(byte[] raw) { + String traceId = UUID.randomUUID().toString().substring(0, 8); + log.info("[{}] Receiving message, size={}", traceId, raw != null ? raw.length : 0); + + try { + // 1. Unpack message + TxMessage msg = codec.unpack(raw); + log.debug("[{}] Unpacked: {}", traceId, msg); + + // 2. Validate required fields + TxResult validation = validate(msg, traceId); + if (validation != null) { + return validation; + } + + // 3. Process transaction + return processTransaction(msg, traceId); + + } catch (Exception e) { + log.error("[{}] Processing error: {}", traceId, e.getMessage(), e); + return TxResult.error(traceId, "9999", "Processing error: " + e.getMessage()); + } + } + + /** + * Validate message fields. + */ + private TxResult validate(TxMessage msg, String traceId) { + if (msg.getMerchantId() == null || msg.getMerchantId().isBlank()) { + log.warn("[{}] Missing merchantId", traceId); + return TxResult.error(traceId, "1001", "Missing merchantId"); + } + if (msg.getTerminalId() == null || msg.getTerminalId().isBlank()) { + log.warn("[{}] Missing terminalId", traceId); + return TxResult.error(traceId, "1002", "Missing terminalId"); + } + if (msg.getAmount() == null || msg.getAmount().isBlank()) { + log.warn("[{}] Missing amount", traceId); + return TxResult.error(traceId, "1003", "Missing amount"); + } + // Validate amount is numeric + try { + Long.parseLong(msg.getAmount()); + } catch (NumberFormatException e) { + log.warn("[{}] Invalid amount format: {}", traceId, msg.getAmount()); + return TxResult.error(traceId, "1004", "Invalid amount format"); + } + return null; + } + + /** + * Process the transaction. + */ + private TxResult processTransaction(TxMessage msg, String traceId) { + try { + // 1. Check merchant status via repository stub + if (!txRepository.isMerchantActive(msg.getMerchantId())) { + log.warn("[{}] Merchant not active: {}", traceId, msg.getMerchantId()); + return TxResult.error(traceId, "2001", "Merchant not active"); + } + + // 2. Check terminal status via repository stub + if (!txRepository.isTerminalActive(msg.getMerchantId(), msg.getTerminalId())) { + log.warn("[{}] Terminal not active: {}/{}", traceId, msg.getMerchantId(), msg.getTerminalId()); + return TxResult.error(traceId, "2002", "Terminal not active"); + } + + // 3. Generate transaction ID if not present + if (msg.getTxId() == null || msg.getTxId().isBlank()) { + msg.setTxId(generateTxId()); + } + + // 4. Set transaction timestamp + String now = LocalDateTime.now().format(DT_FMT); + msg.setTranDate(now.substring(0, 8)); + msg.setTranTime(now.substring(8)); + + // 5. Save transaction via repository stub + txRepository.saveTransaction(msg); + + // 6. Process based on status + String status = msg.getStatus() != null ? msg.getStatus().toUpperCase() : "REQ"; + switch (status) { + case "REQ": + case "APPROVE": + return handleApproval(msg, traceId); + case "CANCEL": + case "REFUND": + return handleCancellation(msg, traceId); + default: + log.info("[{}] Unknown status, defaulting to approval", traceId); + return handleApproval(msg, traceId); + } + + } catch (Exception e) { + log.error("[{}] Transaction processing error: {}", traceId, e.getMessage(), e); + return TxResult.error(traceId, "9999", "Transaction processing failed"); + } + } + + /** + * Handle approval transaction. + */ + private TxResult handleApproval(TxMessage msg, String traceId) { + log.info("[{}] Processing approval for merchant={}, amount={}", + traceId, msg.getMerchantId(), msg.getAmount()); + + // Generate auth number + String authNo = generateAuthNo(); + msg.setAuthNo(authNo); + msg.setStatus("APPROVED"); + + // Update via repository + txRepository.updateTransactionStatus(msg.getTxId(), "APPROVED"); + + log.info("[{}] Approval complete, authNo={}", traceId, authNo); + return TxResult.ok(msg.getTxId(), "Approval complete"); + } + + /** + * Handle cancellation/refund transaction. + */ + private TxResult handleCancellation(TxMessage msg, String traceId) { + log.info("[{}] Processing cancellation for txId={}", traceId, msg.getTxId()); + + // Check if original transaction exists + if (!txRepository.transactionExists(msg.getTxId())) { + log.warn("[{}] Original transaction not found: {}", traceId, msg.getTxId()); + return TxResult.error(traceId, "3001", "Original transaction not found"); + } + + // Check if already cancelled + String currentStatus = txRepository.getTransactionStatus(msg.getTxId()); + if ("CANCELLED".equals(currentStatus)) { + log.warn("[{}] Already cancelled: {}", traceId, msg.getTxId()); + return TxResult.error(traceId, "3002", "Already cancelled"); + } + + // Process cancellation + msg.setStatus("CANCELLED"); + txRepository.updateTransactionStatus(msg.getTxId(), "CANCELLED"); + + log.info("[{}] Cancellation complete", traceId); + return TxResult.ok(msg.getTxId(), "Cancellation complete"); + } + + private String generateTxId() { + return "TX" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + + String.format("%04d", (int)(Math.random() * 10000)); + } + + private String generateAuthNo() { + return String.format("%06d", (int)(Math.random() * 1000000)); + } +} -- 2.49.1 From d9946e31ff66a25067b6b6c7305cffd7f7b52600 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:50 +0000 Subject: [PATCH 6/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquiring/repository/TxRepository.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepository.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepository.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepository.java new file mode 100644 index 0000000..f64cee4 --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepository.java @@ -0,0 +1,40 @@ +package com.klaro.acquirecore.acquiring.repository; + +import com.klaro.acquirecore.acquiring.dto.TxMessage; + +/** + * Repository interface for TX data access. + * Stub implementation - actual DB access to be implemented. + */ +public interface TxRepository { + + /** + * Check if merchant is active. + */ + boolean isMerchantActive(String merchantId); + + /** + * Check if terminal is active for given merchant. + */ + boolean isTerminalActive(String merchantId, String terminalId); + + /** + * Save transaction record. + */ + void saveTransaction(TxMessage msg); + + /** + * Check if transaction exists. + */ + boolean transactionExists(String txId); + + /** + * Get transaction status. + */ + String getTransactionStatus(String txId); + + /** + * Update transaction status. + */ + void updateTransactionStatus(String txId, String status); +} -- 2.49.1 From 03aec5947960ed56cc766ae7b485329e53b9c39d Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:51 +0000 Subject: [PATCH 7/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/TxRepositoryStub.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepositoryStub.java diff --git a/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepositoryStub.java b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepositoryStub.java new file mode 100644 index 0000000..0ca9286 --- /dev/null +++ b/boot/modules/acquiring/src/main/java/com/klaro/acquirecore/acquiring/repository/TxRepositoryStub.java @@ -0,0 +1,70 @@ +package com.klaro.acquirecore.acquiring.repository; + +import com.klaro.acquirecore.acquiring.dto.TxMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Stub implementation of TxRepository. + * In-memory storage for development/testing. + */ +@Repository +public class TxRepositoryStub implements TxRepository { + + private static final Logger log = LoggerFactory.getLogger(TxRepositoryStub.class); + + private final Map transactions = new ConcurrentHashMap<>(); + private final Map activeMerchants = new ConcurrentHashMap<>(); + private final Map activeTerminals = new ConcurrentHashMap<>(); + + public TxRepositoryStub() { + // Initialize with test data + activeMerchants.put("M001", true); + activeMerchants.put("M002", true); + activeTerminals.put("M001|T001", true); + activeTerminals.put("M001|T002", true); + activeTerminals.put("M002|T001", true); + } + + @Override + public boolean isMerchantActive(String merchantId) { + return activeMerchants.getOrDefault(merchantId, true); + } + + @Override + public boolean isTerminalActive(String merchantId, String terminalId) { + return activeTerminals.getOrDefault(merchantId + "|" + terminalId, true); + } + + @Override + public void saveTransaction(TxMessage msg) { + if (msg.getTxId() != null) { + transactions.put(msg.getTxId(), msg); + log.debug("Saved transaction: {}", msg.getTxId()); + } + } + + @Override + public boolean transactionExists(String txId) { + return transactions.containsKey(txId); + } + + @Override + public String getTransactionStatus(String txId) { + TxMessage msg = transactions.get(txId); + return msg != null ? msg.getStatus() : null; + } + + @Override + public void updateTransactionStatus(String txId, String status) { + TxMessage msg = transactions.get(txId); + if (msg != null) { + msg.setStatus(status); + log.debug("Updated transaction {} status to {}", txId, status); + } + } +} -- 2.49.1 From 30eec5f5702c29eb962e3276ecad60f0008e4cbb Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:52 +0000 Subject: [PATCH 8/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TxMessageReceiveServiceTest.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveServiceTest.java diff --git a/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveServiceTest.java b/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveServiceTest.java new file mode 100644 index 0000000..9f46778 --- /dev/null +++ b/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageReceiveServiceTest.java @@ -0,0 +1,158 @@ +package com.klaro.acquirecore.acquiring.service; + +import com.klaro.acquirecore.acquiring.dto.TxMessage; +import com.klaro.acquirecore.acquiring.dto.TxResult; +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 java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TxMessageReceiveServiceTest { + + @Mock + private TxRepository txRepository; + + private TxMessageCodec codec; + private TxMessageReceiveService service; + + @BeforeEach + void setUp() { + codec = new TxMessageCodec(new com.fasterxml.jackson.databind.ObjectMapper()); + service = new TxMessageReceiveService(codec, txRepository); + } + + @Test + void receive_validApprovalMessage_returnsSuccess() { + // Given + byte[] raw = buildRawMessage("REQ", "M001", "T001", "10000"); + when(txRepository.isMerchantActive("M001")).thenReturn(true); + when(txRepository.isTerminalActive("M001", "T001")).thenReturn(true); + + // When + TxResult result = service.receive(raw); + + // Then + assertTrue(result.isSuccess()); + assertEquals("0000", result.getCode()); + verify(txRepository).saveTransaction(any(TxMessage.class)); + } + + @Test + void receive_missingMerchantId_returnsError() { + // Given + byte[] raw = buildRawMessage("REQ", "", "T001", "10000"); + + // When + TxResult result = service.receive(raw); + + // Then + assertFalse(result.isSuccess()); + assertEquals("1001", result.getCode()); + } + + @Test + void receive_missingTerminalId_returnsError() { + // Given + byte[] raw = buildRawMessage("REQ", "M001", "", "10000"); + + // When + TxResult result = service.receive(raw); + + // Then + assertFalse(result.isSuccess()); + assertEquals("1002", result.getCode()); + } + + @Test + void receive_invalidAmount_returnsError() { + // Given + byte[] raw = buildRawMessage("REQ", "M001", "T001", "invalid"); + + // When + TxResult result = service.receive(raw); + + // Then + assertFalse(result.isSuccess()); + assertEquals("1004", result.getCode()); + } + + @Test + void receive_inactiveMerchant_returnsError() { + // Given + byte[] raw = buildRawMessage("REQ", "M001", "T001", "10000"); + when(txRepository.isMerchantActive("M001")).thenReturn(false); + + // When + TxResult result = service.receive(raw); + + // Then + assertFalse(result.isSuccess()); + assertEquals("2001", result.getCode()); + } + + @Test + void receive_inactiveTerminal_returnsError() { + // Given + byte[] raw = buildRawMessage("REQ", "M001", "T001", "10000"); + when(txRepository.isMerchantActive("M001")).thenReturn(true); + when(txRepository.isTerminalActive("M001", "T001")).thenReturn(false); + + // When + TxResult result = service.receive(raw); + + // Then + assertFalse(result.isSuccess()); + assertEquals("2002", result.getCode()); + } + + @Test + void receive_cancelMessage_callsCancellation() { + // Given + byte[] raw = buildRawMessage("CANCEL", "M001", "T001", "10000"); + when(txRepository.isMerchantActive("M001")).thenReturn(true); + when(txRepository.isTerminalActive("M001", "T001")).thenReturn(true); + when(txRepository.transactionExists(anyString())).thenReturn(true); + when(txRepository.getTransactionStatus(anyString())).thenReturn("APPROVED"); + + // When + TxResult result = service.receive(raw); + + // Then + assertTrue(result.isSuccess()); + verify(txRepository).updateTransactionStatus(anyString(), eq("CANCELLED")); + } + + @Test + void receive_cancelNonExistentTransaction_returnsError() { + // Given + byte[] raw = buildRawMessage("CANCEL", "M001", "T001", "10000"); + when(txRepository.isMerchantActive("M001")).thenReturn(true); + when(txRepository.isTerminalActive("M001", "T001")).thenReturn(true); + when(txRepository.transactionExists(anyString())).thenReturn(false); + + // When + TxResult result = service.receive(raw); + + // Then + assertFalse(result.isSuccess()); + assertEquals("3001", result.getCode()); + } + + private byte[] buildRawMessage(String status, String merchantId, String terminalId, String amount) { + String body = String.format( + "{\"STATUS\":\"%s\",\"MERCHANTID\":\"%s\",\"TERMINALID\":\"%s\",\"AMOUNT\":\"%s\"}", + status, merchantId, terminalId, amount); + String header = String.format("%-20s", "TXMSG"); + return (header + body).getBytes(StandardCharsets.ISO_8859_1); + } +} -- 2.49.1 From 624ae6252ef15b4548123cc4c7e8ff0cb7af55ac Mon Sep 17 00:00:00 2001 From: forge-bot Date: Sat, 18 Jul 2026 11:17:53 +0000 Subject: [PATCH 9/9] =?UTF-8?q?[=EB=A7=A4=EC=9E=85]=20mg=5Frecv=5Fsvc=20?= =?UTF-8?q?=E2=86=92=20=EC=A0=84=EB=AC=B8=EC=88=98=EC=8B=A0=20@Service=20(?= =?UTF-8?q?ACM-MG-004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquiring/service/TxMessageCodecTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageCodecTest.java diff --git a/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageCodecTest.java b/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageCodecTest.java new file mode 100644 index 0000000..dea6ab3 --- /dev/null +++ b/boot/modules/acquiring/src/test/java/com/klaro/acquirecore/acquiring/service/TxMessageCodecTest.java @@ -0,0 +1,107 @@ +package com.klaro.acquirecore.acquiring.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.klaro.acquirecore.acquiring.dto.TxMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class TxMessageCodecTest { + + private TxMessageCodec codec; + + @BeforeEach + void setUp() { + codec = new TxMessageCodec(new ObjectMapper()); + } + + @Test + void unpack_validJsonMessage_parsesCorrectly() { + // Given + String body = "{\"TXID\":\"TX001\",\"MERCHANTID\":\"M001\",\"TERMINALID\":\"T001\",\"AMOUNT\":\"10000\"}"; + byte[] raw = buildRawMessage("HEADER", body); + + // When + TxMessage msg = codec.unpack(raw); + + // Then + assertEquals("TX001", msg.getTxId()); + assertEquals("M001", msg.getMerchantId()); + assertEquals("T001", msg.getTerminalId()); + assertEquals("10000", msg.getAmount()); + } + + @Test + void unpack_delimitedMessage_parsesCorrectly() { + // Given + String body = "TXID=TX002|MERCHANTID=M002|TERMINALID=T002|AMOUNT=20000"; + byte[] raw = buildRawMessage("HEADER", body); + + // When + TxMessage msg = codec.unpack(raw); + + // Then + assertEquals("TX002", msg.getTxId()); + assertEquals("M002", msg.getMerchantId()); + assertEquals("T002", msg.getTerminalId()); + assertEquals("20000", msg.getAmount()); + } + + @Test + void unpack_tooShortMessage_throwsException() { + // Given + byte[] raw = "short".getBytes(StandardCharsets.ISO_8859_1); + + // When/Then + assertThrows(IllegalArgumentException.class, () -> codec.unpack(raw)); + } + + @Test + void pack_unpackedMessage_producesValidBytes() { + // Given + TxMessage msg = new TxMessage(); + msg.setTxId("TX003"); + msg.setMerchantId("M003"); + msg.setTerminalId("T003"); + msg.setAmount("30000"); + msg.setStatus("REQ"); + + // When + byte[] packed = codec.pack(msg); + + // Then + assertNotNull(packed); + assertTrue(packed.length >= 20); + + // Verify can unpack back + TxMessage unpacked = codec.unpack(packed); + assertEquals("TX003", unpacked.getTxId()); + assertEquals("M003", unpacked.getMerchantId()); + } + + @Test + void unpack_packRoundTrip_preservesData() { + // Given + String body = "{\"TXID\":\"TX004\",\"MERCHANTID\":\"M004\",\"AMOUNT\":\"40000\",\"AUTHNO\":\"123456\"}"; + byte[] raw = buildRawMessage("HEADER", body); + + // When + TxMessage original = codec.unpack(raw); + byte[] repacked = codec.pack(original); + TxMessage roundTrip = codec.unpack(repacked); + + // Then + assertEquals(original.getTxId(), roundTrip.getTxId()); + assertEquals(original.getMerchantId(), roundTrip.getMerchantId()); + assertEquals(original.getAmount(), roundTrip.getAmount()); + assertEquals(original.getAuthNo(), roundTrip.getAuthNo()); + } + + private byte[] buildRawMessage(String header, String body) { + String hdr = String.format("%-20s", header); + return (hdr + body).getBytes(StandardCharsets.ISO_8859_1); + } +} -- 2.49.1