[매입] mg_recv_svc → 전문수신 @Service #8
9 changed files with 861 additions and 0 deletions
3
.forge/ACM-MG-004-attempt-3-run-56504178208a.md
Normal file
3
.forge/ACM-MG-004-attempt-3-run-56504178208a.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# ACM-MG-004-attempt-3-run-56504178208a
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/ACM-MG-004-attempt-3-run-56504178208a`.
|
||||
|
|
@ -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<String, String> 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<String, String> getExtra() { return extra; }
|
||||
public void setExtra(Map<String, String> 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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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<String, TxMessage> transactions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Boolean> activeMerchants = new ConcurrentHashMap<>();
|
||||
private final Map<String, Boolean> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Reference in a new issue