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

46
modules/closing/pom.xml Normal file
View file

@ -0,0 +1,46 @@
<?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>closing</artifactId>
<name>closing</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>com.klaro.acquiring</groupId>
<artifactId>settlement</artifactId>
</dependency>
<dependency>
<groupId>com.klaro.acquiring</groupId>
<artifactId>ledger</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</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,77 @@
package com.klaro.acquiring.closing;
import com.klaro.acquiring.domain.entity.Settlement;
import com.klaro.acquiring.ledger.LedgerService;
import com.klaro.acquiring.settlement.SettlementService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;
/**
* 마감(cl) 서비스. 마감: 당일 정산 집계 + 가맹점별 원장 잔액검증.
* 마감: 월중 일별 정산 순지급액 합산.
*/
@Service
public class ClosingService {
private final SettlementService settlementService;
private final LedgerService ledgerService;
public ClosingService(SettlementService settlementService, LedgerService ledgerService) {
this.settlementService = settlementService;
this.ledgerService = ledgerService;
}
/** 일 마감. 정산 집계 + 원장 잔액검증(불일치 시 예외). */
@Transactional(readOnly = true)
public DailyClosing closeDaily(LocalDate bizDate) {
List<Settlement> settlements = settlementService.listByBizDate(bizDate);
BigDecimal gross = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal net = BigDecimal.ZERO;
int txnCount = 0;
for (Settlement s : settlements) {
gross = gross.add(s.getGrossAmount());
fee = fee.add(s.getTotalFee());
net = net.add(s.getNetAmount());
txnCount += s.getTxnCount();
// 원장 반영이 있으면 잔액검증(반영 전이면 0 == 0 통과)
ledgerService.verifyBalance(s.getMerchId(), bizDate);
}
return new DailyClosing(bizDate, settlements.size(), txnCount, gross, fee, net);
}
/** 월 마감. 해당 월 각 일자 일마감의 순지급액 합산. */
@Transactional(readOnly = true)
public MonthlyClosing closeMonthly(YearMonth month) {
BigDecimal net = BigDecimal.ZERO;
BigDecimal gross = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
int settlementCount = 0;
LocalDate d = month.atDay(1);
LocalDate end = month.atEndOfMonth();
while (!d.isAfter(end)) {
List<Settlement> settlements = settlementService.listByBizDate(d);
for (Settlement s : settlements) {
gross = gross.add(s.getGrossAmount());
fee = fee.add(s.getTotalFee());
net = net.add(s.getNetAmount());
settlementCount++;
}
d = d.plusDays(1);
}
return new MonthlyClosing(month, settlementCount, gross, fee, net);
}
public record DailyClosing(LocalDate bizDate, int settlementCount, int txnCount,
BigDecimal grossAmount, BigDecimal totalFee, BigDecimal netAmount) {
}
public record MonthlyClosing(YearMonth month, int settlementCount,
BigDecimal grossAmount, BigDecimal totalFee, BigDecimal netAmount) {
}
}

View file

@ -0,0 +1,67 @@
package com.klaro.acquiring.closing.batch;
import com.klaro.acquiring.closing.ClosingService;
import com.klaro.acquiring.common.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import java.time.LocalDate;
/**
* 마감 배치 Job (cl). Tasklet 으로 마감 집계·잔액검증을 수행한다.
*/
@Configuration
public class ClosingJobConfig {
public static final String JOB_NAME = "closingJob";
private static final Logger log = LoggerFactory.getLogger(ClosingJobConfig.class);
private final ClosingService closingService;
public ClosingJobConfig(ClosingService closingService) {
this.closingService = closingService;
}
@Bean
@StepScope
public Tasklet closingTasklet(@Value("#{jobParameters['bizDate']}") String bizDate) {
return (contribution, chunkContext) -> {
LocalDate date = DateUtil.parse(bizDate);
ClosingService.DailyClosing c = closingService.closeDaily(date);
log.info("[cl] 일마감 date={} 정산건수={} 거래건수={} gross={} fee={} net={}",
date, c.settlementCount(), c.txnCount(), c.grossAmount(), c.totalFee(), c.netAmount());
chunkContext.getStepContext().getStepExecution().getExecutionContext()
.put("netAmount", c.netAmount().toString());
return RepeatStatus.FINISHED;
};
}
@Bean
public Step closingStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
Tasklet closingTasklet) {
return new StepBuilder("closingStep", jobRepository)
.tasklet(closingTasklet, transactionManager)
.build();
}
@Bean
public Job closingJob(JobRepository jobRepository, Step closingStep) {
return new JobBuilder(JOB_NAME, jobRepository)
.start(closingStep)
.build();
}
}