feat: complete Spring Boot 카드 매입·정산 system (acquire-core migration target, mvn verify green, 24 tests)
This commit is contained in:
commit
0f6e3acfe6
92 changed files with 4320 additions and 0 deletions
91
app/pom.xml
Normal file
91
app/pom.xml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?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>
|
||||
</parent>
|
||||
<artifactId>app</artifactId>
|
||||
<name>app</name>
|
||||
<description>Spring Boot 부트 모듈: 전 모듈 조립 + REST + 배치 런처</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>gateway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>acquiring</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>reconcile</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>settlement</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>payment</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>ledger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klaro.acquiring</groupId>
|
||||
<artifactId>closing</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-batch</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.batch</groupId>
|
||||
<artifactId>spring-batch-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
<mainClass>com.klaro.acquiring.app.CardAcquiringApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.klaro.acquiring.app;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* 카드 매입·정산 시스템 부트 애플리케이션. 전 모듈(mg/ac/rc/vl/st/py/lg/mm/cm/cl)을 조립한다.
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.klaro.acquiring")
|
||||
@EntityScan(basePackages = "com.klaro.acquiring.domain.entity")
|
||||
@EnableJpaRepositories(basePackages = "com.klaro.acquiring.persistence.repository")
|
||||
public class CardAcquiringApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CardAcquiringApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.klaro.acquiring.app.web;
|
||||
|
||||
import com.klaro.acquiring.acquiring.AcquiringService;
|
||||
import com.klaro.acquiring.acquiring.dto.PurchaseCommand;
|
||||
import com.klaro.acquiring.common.dto.ApiResponse;
|
||||
import com.klaro.acquiring.domain.entity.Purchase;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 매입(ac) 온라인 REST. */
|
||||
@RestController
|
||||
@RequestMapping("/api/acquiring/purchases")
|
||||
public class AcquiringController {
|
||||
|
||||
private final AcquiringService acquiringService;
|
||||
|
||||
public AcquiringController(AcquiringService acquiringService) {
|
||||
this.acquiringService = acquiringService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<PurchaseView> accept(@Valid @RequestBody PurchaseAcceptRequest req) {
|
||||
Purchase p = acquiringService.acceptPurchase(new PurchaseCommand(req.apprNo(), req.amount()));
|
||||
return ApiResponse.ok(PurchaseView.from(p));
|
||||
}
|
||||
|
||||
@GetMapping("/{apprNo}")
|
||||
public ApiResponse<PurchaseView> get(@PathVariable String apprNo) {
|
||||
return ApiResponse.ok(PurchaseView.from(acquiringService.getByApprNo(apprNo)));
|
||||
}
|
||||
|
||||
public record PurchaseView(Long purchaseId, String apprNo, String merchId, BigDecimal amount,
|
||||
BigDecimal mdrFee, BigDecimal vanFee, BigDecimal netAmount,
|
||||
String status) {
|
||||
static PurchaseView from(Purchase p) {
|
||||
return new PurchaseView(p.getPurchaseId(), p.getApprNo(), p.getMerchId(), p.getAmount(),
|
||||
p.getMdrFee(), p.getVanFee(), p.getNetAmount(), p.getStatus().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.klaro.acquiring.app.web;
|
||||
|
||||
import com.klaro.acquiring.common.dto.ApiResponse;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobParameters;
|
||||
import org.springframework.batch.core.JobParametersBuilder;
|
||||
import org.springframework.batch.core.launch.JobLauncher;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 배치 잡 런처 REST (대사/정산/마감). */
|
||||
@RestController
|
||||
@RequestMapping("/api/batch")
|
||||
public class BatchController {
|
||||
|
||||
private final JobLauncher jobLauncher;
|
||||
private final Job reconcileJob;
|
||||
private final Job settlementJob;
|
||||
private final Job closingJob;
|
||||
|
||||
public BatchController(JobLauncher jobLauncher,
|
||||
@Qualifier("reconcileJob") Job reconcileJob,
|
||||
@Qualifier("settlementJob") Job settlementJob,
|
||||
@Qualifier("closingJob") Job closingJob) {
|
||||
this.jobLauncher = jobLauncher;
|
||||
this.reconcileJob = reconcileJob;
|
||||
this.settlementJob = settlementJob;
|
||||
this.closingJob = closingJob;
|
||||
}
|
||||
|
||||
@PostMapping("/reconcile")
|
||||
public ApiResponse<String> reconcile(@RequestParam String bizDate) throws Exception {
|
||||
return run(reconcileJob, bizDate);
|
||||
}
|
||||
|
||||
@PostMapping("/settlement")
|
||||
public ApiResponse<String> settlement(@RequestParam String bizDate) throws Exception {
|
||||
return run(settlementJob, bizDate);
|
||||
}
|
||||
|
||||
@PostMapping("/closing")
|
||||
public ApiResponse<String> closing(@RequestParam String bizDate) throws Exception {
|
||||
return run(closingJob, bizDate);
|
||||
}
|
||||
|
||||
private ApiResponse<String> run(Job job, String bizDate) throws Exception {
|
||||
JobParameters params = new JobParametersBuilder()
|
||||
.addString("bizDate", bizDate)
|
||||
.addLong("ts", System.currentTimeMillis())
|
||||
.toJobParameters();
|
||||
JobExecution exec = jobLauncher.run(job, params);
|
||||
return ApiResponse.ok(exec.getStatus().name());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.klaro.acquiring.app.web;
|
||||
|
||||
import com.klaro.acquiring.gateway.GatewayService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 전문 게이트웨이(mg) REST. 고정길이 전문 in/out(text/plain). */
|
||||
@RestController
|
||||
@RequestMapping("/api/gateway")
|
||||
public class GatewayController {
|
||||
|
||||
private final GatewayService gatewayService;
|
||||
|
||||
public GatewayController(GatewayService gatewayService) {
|
||||
this.gatewayService = gatewayService;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/message", consumes = MediaType.TEXT_PLAIN_VALUE,
|
||||
produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String message(@RequestBody String message) {
|
||||
return gatewayService.handle(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.klaro.acquiring.app.web;
|
||||
|
||||
import com.klaro.acquiring.common.dto.ApiResponse;
|
||||
import com.klaro.acquiring.common.error.ErrorCode;
|
||||
import com.klaro.acquiring.common.error.AcquiringException;
|
||||
import com.klaro.acquiring.common.error.NotFoundException;
|
||||
import com.klaro.acquiring.common.error.ValidationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/** 도메인 예외 → 표준 응답 매핑. */
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(NotFoundException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleNotFound(NotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(ApiResponse.error(e.getErrorCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleValidation(ValidationException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error(e.getErrorCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AcquiringException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleDomain(AcquiringException e) {
|
||||
return ResponseEntity.unprocessableEntity()
|
||||
.body(ApiResponse.error(e.getErrorCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleBeanValidation(MethodArgumentNotValidException e) {
|
||||
String msg = e.getBindingResult().getFieldErrors().stream()
|
||||
.findFirst()
|
||||
.map(f -> f.getField() + ": " + f.getDefaultMessage())
|
||||
.orElse("요청값 오류");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.error(ErrorCode.INVALID_REQUEST, msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.klaro.acquiring.app.web;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 매입 접수 REST 요청. */
|
||||
public record PurchaseAcceptRequest(
|
||||
@NotBlank String apprNo,
|
||||
@NotNull @DecimalMin(value = "0.01") BigDecimal amount) {
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.klaro.acquiring.app.web;
|
||||
|
||||
import com.klaro.acquiring.common.dto.ApiResponse;
|
||||
import com.klaro.acquiring.common.util.DateUtil;
|
||||
import com.klaro.acquiring.domain.entity.Settlement;
|
||||
import com.klaro.acquiring.settlement.SettlementService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/** 정산(st) 조회 REST. */
|
||||
@RestController
|
||||
@RequestMapping("/api/settlement")
|
||||
public class SettlementController {
|
||||
|
||||
private final SettlementService settlementService;
|
||||
|
||||
public SettlementController(SettlementService settlementService) {
|
||||
this.settlementService = settlementService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<List<SettlementView>> byBizDate(@RequestParam String bizDate) {
|
||||
LocalDate date = DateUtil.parse(bizDate);
|
||||
return ApiResponse.ok(settlementService.listByBizDate(date).stream()
|
||||
.map(SettlementView::from).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/merchant")
|
||||
public ApiResponse<SettlementView> byMerchant(@RequestParam String merchId,
|
||||
@RequestParam String bizDate) {
|
||||
LocalDate date = DateUtil.parse(bizDate);
|
||||
return ApiResponse.ok(SettlementView.from(settlementService.get(merchId, date)));
|
||||
}
|
||||
|
||||
public record SettlementView(Long settlementId, String merchId, String bizDate,
|
||||
BigDecimal grossAmount, BigDecimal totalFee, BigDecimal netAmount,
|
||||
int txnCount, String payDate, String status) {
|
||||
static SettlementView from(Settlement s) {
|
||||
return new SettlementView(s.getSettlementId(), s.getMerchId(),
|
||||
DateUtil.format(s.getBizDate()), s.getGrossAmount(), s.getTotalFee(),
|
||||
s.getNetAmount(), s.getTxnCount(), DateUtil.format(s.getPayDate()),
|
||||
s.getStatus().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/src/main/resources/application.yml
Normal file
32
app/src/main/resources/application.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
spring:
|
||||
application:
|
||||
name: card-acquiring-boot
|
||||
datasource:
|
||||
url: jdbc:h2:mem:acquiring;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password: ""
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate.jdbc.time_zone: Asia/Seoul
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
batch:
|
||||
job:
|
||||
enabled: false
|
||||
jdbc:
|
||||
initialize-schema: always
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.klaro.acquiring: INFO
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.klaro.acquiring.app;
|
||||
|
||||
import com.klaro.acquiring.acquiring.AcquiringService;
|
||||
import com.klaro.acquiring.acquiring.dto.PurchaseCommand;
|
||||
import com.klaro.acquiring.common.error.AcquiringException;
|
||||
import com.klaro.acquiring.domain.entity.Approval;
|
||||
import com.klaro.acquiring.domain.entity.Purchase;
|
||||
import com.klaro.acquiring.domain.enums.TxStatus;
|
||||
import com.klaro.acquiring.persistence.repository.ApprovalRepository;
|
||||
import com.klaro.acquiring.persistence.repository.FeeRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/** 매입 서비스 + 리포지토리 슬라이스(@SpringBootTest, H2). */
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
class AcquiringServiceTest {
|
||||
|
||||
@Autowired
|
||||
private AcquiringService acquiringService;
|
||||
@Autowired
|
||||
private ApprovalRepository approvalRepository;
|
||||
@Autowired
|
||||
private FeeRepository feeRepository;
|
||||
|
||||
@Test
|
||||
void acceptsPurchaseAndComputesFee() {
|
||||
LocalDate bizDate = LocalDate.of(2026, 7, 17);
|
||||
approvalRepository.save(new Approval("IT-A-0001", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("10000.00"), "20260717", bizDate));
|
||||
|
||||
Purchase p = acquiringService.acceptPurchase(new PurchaseCommand("IT-A-0001",
|
||||
new BigDecimal("10000.00")));
|
||||
|
||||
// MDR 2.3% = 230, VAN 30 => fee 260, net 9740
|
||||
assertThat(p.getMdrFee()).isEqualByComparingTo("230");
|
||||
assertThat(p.getVanFee()).isEqualByComparingTo("30");
|
||||
assertThat(p.getNetAmount()).isEqualByComparingTo("9740");
|
||||
assertThat(p.getStatus()).isEqualTo(TxStatus.DONE);
|
||||
assertThat(feeRepository.findByPurchaseId(p.getPurchaseId())).hasSize(2);
|
||||
assertThat(approvalRepository.findByApprNo("IT-A-0001").orElseThrow().getStatus())
|
||||
.isEqualTo(TxStatus.DONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicatePurchase() {
|
||||
LocalDate bizDate = LocalDate.of(2026, 7, 17);
|
||||
approvalRepository.save(new Approval("IT-A-0002", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("5000.00"), "20260717", bizDate));
|
||||
acquiringService.acceptPurchase(new PurchaseCommand("IT-A-0002", new BigDecimal("5000.00")));
|
||||
|
||||
assertThatThrownBy(() -> acquiringService.acceptPurchase(
|
||||
new PurchaseCommand("IT-A-0002", new BigDecimal("5000.00"))))
|
||||
.isInstanceOf(AcquiringException.class)
|
||||
.hasMessageContaining("이미 매입");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCaptureOverApproval() {
|
||||
LocalDate bizDate = LocalDate.of(2026, 7, 17);
|
||||
approvalRepository.save(new Approval("IT-A-0003", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("5000.00"), "20260717", bizDate));
|
||||
|
||||
assertThatThrownBy(() -> acquiringService.acceptPurchase(
|
||||
new PurchaseCommand("IT-A-0003", new BigDecimal("9000.00"))))
|
||||
.isInstanceOf(AcquiringException.class)
|
||||
.hasMessageContaining("초과");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.klaro.acquiring.app;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/** 전 모듈 조립(ApplicationContext) 로드 검증. */
|
||||
@SpringBootTest
|
||||
class CardAcquiringApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.klaro.acquiring.app;
|
||||
|
||||
import com.klaro.acquiring.acquiring.AcquiringService;
|
||||
import com.klaro.acquiring.acquiring.dto.PurchaseCommand;
|
||||
import com.klaro.acquiring.domain.entity.Approval;
|
||||
import com.klaro.acquiring.domain.entity.Deposit;
|
||||
import com.klaro.acquiring.domain.entity.ReconcileResult;
|
||||
import com.klaro.acquiring.persistence.repository.ApprovalRepository;
|
||||
import com.klaro.acquiring.persistence.repository.DepositRepository;
|
||||
import com.klaro.acquiring.persistence.repository.ReconcileResultRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.batch.core.BatchStatus;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobParameters;
|
||||
import org.springframework.batch.core.JobParametersBuilder;
|
||||
import org.springframework.batch.test.JobLauncherTestUtils;
|
||||
import org.springframework.batch.test.context.SpringBatchTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** 대사 배치 Job 테스트: 매칭 1건 + 불일치(입금누락) 1건. */
|
||||
@SpringBootTest
|
||||
@SpringBatchTest
|
||||
class ReconcileJobTest {
|
||||
|
||||
@Autowired
|
||||
private JobLauncherTestUtils jobLauncherTestUtils;
|
||||
@Autowired
|
||||
@Qualifier("reconcileJob")
|
||||
private Job reconcileJob;
|
||||
@Autowired
|
||||
private AcquiringService acquiringService;
|
||||
@Autowired
|
||||
private ApprovalRepository approvalRepository;
|
||||
@Autowired
|
||||
private DepositRepository depositRepository;
|
||||
@Autowired
|
||||
private ReconcileResultRepository reconcileRepository;
|
||||
|
||||
@Test
|
||||
void reconcilesThreeWayMatchAndDetectsMismatch() throws Exception {
|
||||
LocalDate bizDate = LocalDate.of(2026, 7, 21);
|
||||
// 정상 매칭 건: 승인=매입=입금=10000
|
||||
approvalRepository.save(new Approval("IT-R-0001", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("10000.00"), "20260721", bizDate));
|
||||
acquiringService.acceptPurchase(new PurchaseCommand("IT-R-0001", new BigDecimal("10000.00")));
|
||||
depositRepository.save(new Deposit("IT-R-0001", "M0000000000001",
|
||||
new BigDecimal("10000.00"), bizDate));
|
||||
// 불일치 건: 매입은 있으나 입금 누락
|
||||
approvalRepository.save(new Approval("IT-R-0002", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("5000.00"), "20260721", bizDate));
|
||||
acquiringService.acceptPurchase(new PurchaseCommand("IT-R-0002", new BigDecimal("5000.00")));
|
||||
|
||||
jobLauncherTestUtils.setJob(reconcileJob);
|
||||
JobParameters params = new JobParametersBuilder()
|
||||
.addString("bizDate", "20260721")
|
||||
.addLong("ts", System.currentTimeMillis())
|
||||
.toJobParameters();
|
||||
JobExecution exec = jobLauncherTestUtils.launchJob(params);
|
||||
|
||||
assertThat(exec.getStatus()).isEqualTo(BatchStatus.COMPLETED);
|
||||
List<ReconcileResult> results = reconcileRepository.findByBizDate(bizDate);
|
||||
assertThat(results).hasSize(2);
|
||||
ReconcileResult matched = reconcileRepository
|
||||
.findByBizDateAndApprNo(bizDate, "IT-R-0001").orElseThrow();
|
||||
assertThat(matched.isMatched()).isTrue();
|
||||
ReconcileResult mismatch = reconcileRepository
|
||||
.findByBizDateAndApprNo(bizDate, "IT-R-0002").orElseThrow();
|
||||
assertThat(mismatch.isMatched()).isFalse();
|
||||
assertThat(mismatch.getReason()).contains("입금 누락");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.klaro.acquiring.app;
|
||||
|
||||
import com.klaro.acquiring.acquiring.AcquiringService;
|
||||
import com.klaro.acquiring.acquiring.dto.PurchaseCommand;
|
||||
import com.klaro.acquiring.domain.entity.Approval;
|
||||
import com.klaro.acquiring.domain.entity.Settlement;
|
||||
import com.klaro.acquiring.persistence.repository.ApprovalRepository;
|
||||
import com.klaro.acquiring.persistence.repository.SettlementRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.batch.core.BatchStatus;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobParameters;
|
||||
import org.springframework.batch.core.JobParametersBuilder;
|
||||
import org.springframework.batch.test.JobLauncherTestUtils;
|
||||
import org.springframework.batch.test.context.SpringBatchTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** 정산 배치 Job 테스트(JobLauncherTestUtils). */
|
||||
@SpringBootTest
|
||||
@SpringBatchTest
|
||||
class SettlementJobTest {
|
||||
|
||||
@Autowired
|
||||
private JobLauncherTestUtils jobLauncherTestUtils;
|
||||
@Autowired
|
||||
@Qualifier("settlementJob")
|
||||
private Job settlementJob;
|
||||
@Autowired
|
||||
private AcquiringService acquiringService;
|
||||
@Autowired
|
||||
private ApprovalRepository approvalRepository;
|
||||
@Autowired
|
||||
private SettlementRepository settlementRepository;
|
||||
|
||||
@Test
|
||||
void runsSettlementJobAndAggregatesNet() throws Exception {
|
||||
LocalDate bizDate = LocalDate.of(2026, 7, 20);
|
||||
// 두 건 매입 -> 정산 대상
|
||||
approvalRepository.save(new Approval("IT-S-0001", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("10000.00"), "20260720", bizDate));
|
||||
approvalRepository.save(new Approval("IT-S-0002", "M0000000000001", "123456******7890",
|
||||
new BigDecimal("20000.00"), "20260720", bizDate));
|
||||
acquiringService.acceptPurchase(new PurchaseCommand("IT-S-0001", new BigDecimal("10000.00")));
|
||||
acquiringService.acceptPurchase(new PurchaseCommand("IT-S-0002", new BigDecimal("20000.00")));
|
||||
|
||||
jobLauncherTestUtils.setJob(settlementJob);
|
||||
JobParameters params = new JobParametersBuilder()
|
||||
.addString("bizDate", "20260720")
|
||||
.addLong("ts", System.currentTimeMillis())
|
||||
.toJobParameters();
|
||||
JobExecution exec = jobLauncherTestUtils.launchJob(params);
|
||||
|
||||
assertThat(exec.getStatus()).isEqualTo(BatchStatus.COMPLETED);
|
||||
Settlement s = settlementRepository
|
||||
.findByMerchIdAndBizDate("M0000000000001", bizDate).orElseThrow();
|
||||
// gross 30000, MDR 2.3% => 690, VAN 30*2=60, fee 750, net 29250
|
||||
assertThat(s.getGrossAmount()).isEqualByComparingTo("30000");
|
||||
assertThat(s.getTotalFee()).isEqualByComparingTo("750");
|
||||
assertThat(s.getNetAmount()).isEqualByComparingTo("29250");
|
||||
assertThat(s.getTxnCount()).isEqualTo(2);
|
||||
// 정산주기 T+2 영업일: 2026-07-20(월) +2 => 2026-07-22(수)
|
||||
assertThat(s.getPayDate()).isEqualTo(LocalDate.of(2026, 7, 22));
|
||||
}
|
||||
}
|
||||
Reference in a new issue