feat: complete Spring Boot 카드 매입·정산 system (acquire-core migration target, mvn verify green, 24 tests)

This commit is contained in:
forge-bot 2026-07-18 15:27:41 +00:00
commit 0f6e3acfe6
92 changed files with 4320 additions and 0 deletions

35
modules/payment/pom.xml Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.klaro.acquiring</groupId>
<artifactId>card-acquiring-boot</artifactId>
<version>1.0.0</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>payment</artifactId>
<name>payment</name>
<dependencies>
<dependency>
<groupId>com.klaro.acquiring</groupId>
<artifactId>common-framework</artifactId>
</dependency>
<dependency>
<groupId>com.klaro.acquiring</groupId>
<artifactId>domain</artifactId>
</dependency>
<dependency>
<groupId>com.klaro.acquiring</groupId>
<artifactId>persistence</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,106 @@
package com.klaro.acquiring.payment;
import com.klaro.acquiring.common.codec.FieldSpec;
import com.klaro.acquiring.common.codec.MessageCodec;
import com.klaro.acquiring.common.codec.MessageSpec;
import com.klaro.acquiring.common.error.NotFoundException;
import com.klaro.acquiring.common.util.DateUtil;
import com.klaro.acquiring.domain.entity.Merchant;
import com.klaro.acquiring.domain.entity.Payment;
import com.klaro.acquiring.domain.entity.Settlement;
import com.klaro.acquiring.domain.enums.PaymentStatus;
import com.klaro.acquiring.persistence.repository.MerchantRepository;
import com.klaro.acquiring.persistence.repository.PaymentRepository;
import com.klaro.acquiring.persistence.repository.SettlementRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 지급(py) 서비스. 정산 결과로 지급 파일(고정길이 전문) 생성하고 지급결과를 반영한다.
*/
@Service
public class PaymentService {
/** 지급 레코드 전문 레이아웃 (은행 송금 원장 파일 1행). */
public static final MessageSpec PAYMENT_SPEC = MessageSpec.of("PAY_REC",
FieldSpec.an("BANKCODE", 3),
FieldSpec.an("ACCOUNTNO", 20),
FieldSpec.num("AMOUNT", 15),
FieldSpec.an("MERCHID", 15),
FieldSpec.an("PAYDATE", 8));
private static final MessageCodec CODEC = new MessageCodec(PAYMENT_SPEC);
private final SettlementRepository settlementRepository;
private final MerchantRepository merchantRepository;
private final PaymentRepository paymentRepository;
public PaymentService(SettlementRepository settlementRepository,
MerchantRepository merchantRepository,
PaymentRepository paymentRepository) {
this.settlementRepository = settlementRepository;
this.merchantRepository = merchantRepository;
this.paymentRepository = paymentRepository;
}
/**
* 해당 지급일의 정산건에 대해 지급 레코드를 생성하고, 고정길이 지급파일 문자열을 반환.
*/
@Transactional
public PaymentFile generateFile(LocalDate payDate) {
List<Settlement> settlements = settlementRepository.findAll().stream()
.filter(s -> s.getPayDate().equals(payDate))
.toList();
StringBuilder file = new StringBuilder();
int count = 0;
BigDecimal total = BigDecimal.ZERO;
for (Settlement s : settlements) {
if (s.getNetAmount().signum() <= 0) {
continue;
}
Merchant m = merchantRepository.findById(s.getMerchId())
.orElseThrow(() -> new NotFoundException("가맹점 없음: " + s.getMerchId()));
Payment payment = new Payment(s.getSettlementId(), m.getMerchId(), payDate,
s.getNetAmount(), m.getBankCode(), m.getAccountNo());
payment.setStatus(PaymentStatus.FILE_CREATED);
paymentRepository.save(payment);
Map<String, String> rec = new LinkedHashMap<>();
rec.put("BANKCODE", m.getBankCode() == null ? "" : m.getBankCode());
rec.put("ACCOUNTNO", m.getAccountNo() == null ? "" : m.getAccountNo());
rec.put("AMOUNT", s.getNetAmount().toBigInteger().toString());
rec.put("MERCHID", m.getMerchId());
rec.put("PAYDATE", DateUtil.format(payDate));
file.append(CODEC.encode(rec)).append('\n');
count++;
total = total.add(s.getNetAmount());
}
return new PaymentFile(payDate, count, total, file.toString());
}
/** 지급 결과 반영(성공/실패). */
@Transactional
public Payment applyResult(Long paymentId, boolean success) {
Payment p = paymentRepository.findById(paymentId)
.orElseThrow(() -> new NotFoundException("지급 없음: " + paymentId));
p.setStatus(success ? PaymentStatus.PAID : PaymentStatus.FAILED);
return paymentRepository.save(p);
}
@Transactional(readOnly = true)
public List<Payment> listByPayDate(LocalDate payDate) {
return paymentRepository.findByPayDate(payDate);
}
/** 지급 파일 생성 결과. */
public record PaymentFile(LocalDate payDate, int count, BigDecimal totalAmount, String content) {
}
}