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/ledger/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>ledger</artifactId>
<name>ledger</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,91 @@
package com.klaro.acquiring.ledger;
import com.klaro.acquiring.common.error.ErrorCode;
import com.klaro.acquiring.common.error.AcquiringException;
import com.klaro.acquiring.common.util.AmountUtil;
import com.klaro.acquiring.domain.entity.LedgerEntry;
import com.klaro.acquiring.domain.entity.Purchase;
import com.klaro.acquiring.domain.enums.DrCr;
import com.klaro.acquiring.domain.enums.EntryType;
import com.klaro.acquiring.persistence.repository.LedgerEntryRepository;
import com.klaro.acquiring.persistence.repository.PurchaseRepository;
import com.klaro.acquiring.domain.enums.TxStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* 원장(lg) 반영·잔액검증 서비스.
*
* <p>매입 1건 대변(CREDIT, 매입총액) + 차변(DEBIT, 수수료) 전표 2건.
* 가맹점·영업일 순잔액 = Σ대변 Σ차변 = Σ순지급액(net) 이어야 한다.
*/
@Service
public class LedgerService {
private final LedgerEntryRepository ledgerRepository;
private final PurchaseRepository purchaseRepository;
public LedgerService(LedgerEntryRepository ledgerRepository,
PurchaseRepository purchaseRepository) {
this.ledgerRepository = ledgerRepository;
this.purchaseRepository = purchaseRepository;
}
/** 매입 1건을 원장에 반영(전표 2건 생성). */
@Transactional
public void postPurchase(Purchase p) {
ledgerRepository.save(new LedgerEntry(p.getMerchId(), p.getBizDate(), EntryType.PURCHASE,
DrCr.CREDIT, p.getAmount(), "PURCHASE", String.valueOf(p.getPurchaseId())));
if (p.totalFee().signum() > 0) {
ledgerRepository.save(new LedgerEntry(p.getMerchId(), p.getBizDate(), EntryType.FEE,
DrCr.DEBIT, p.totalFee(), "PURCHASE", String.valueOf(p.getPurchaseId())));
}
}
/** 영업일의 모든 매입완료 건을 원장에 반영하고 반영 건수를 반환. */
@Transactional
public int reflectDaily(LocalDate bizDate) {
List<Purchase> purchases = purchaseRepository.findByBizDateAndStatus(bizDate, TxStatus.DONE);
for (Purchase p : purchases) {
postPurchase(p);
}
return purchases.size();
}
/** 가맹점·영업일 순잔액 = Σ대변 Σ차변. */
@Transactional(readOnly = true)
public BigDecimal balanceOf(String merchId, LocalDate bizDate) {
BigDecimal credit = BigDecimal.ZERO;
BigDecimal debit = BigDecimal.ZERO;
for (LedgerEntry e : ledgerRepository.findByMerchIdAndBizDate(merchId, bizDate)) {
if (e.getDrCr() == DrCr.CREDIT) {
credit = credit.add(e.getAmount());
} else {
debit = debit.add(e.getAmount());
}
}
return credit.subtract(debit);
}
/**
* 잔액검증: 원장 순잔액이 매입 순지급액 합계와 일치하는지 확인.
* 불일치 예외.
*/
@Transactional(readOnly = true)
public BigDecimal verifyBalance(String merchId, LocalDate bizDate) {
BigDecimal ledgerBalance = balanceOf(merchId, bizDate);
BigDecimal expectedNet = purchaseRepository.findByMerchIdAndBizDate(merchId, bizDate).stream()
.filter(p -> p.getStatus() == TxStatus.DONE || p.getStatus() == TxStatus.SETTLED)
.map(Purchase::getNetAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (!AmountUtil.withinTolerance(ledgerBalance, expectedNet, BigDecimal.ZERO)) {
throw new AcquiringException(ErrorCode.BALANCE_MISMATCH,
"원장 잔액 불일치 merch=" + merchId + " 원장=" + ledgerBalance + " 기대=" + expectedNet);
}
return ledgerBalance;
}
}