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

38
persistence/pom.xml Normal file
View file

@ -0,0 +1,38 @@
<?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>persistence</artifactId>
<name>persistence</name>
<description>Spring Data JPA 리포지토리 + Flyway 스키마 + H2</description>
<dependencies>
<dependency>
<groupId>com.klaro.acquiring</groupId>
<artifactId>domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</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,15 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Approval;
import com.klaro.acquiring.domain.enums.TxStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface ApprovalRepository extends JpaRepository<Approval, String> {
Optional<Approval> findByApprNo(String apprNo);
List<Approval> findByBizDateAndStatus(LocalDate bizDate, TxStatus status);
List<Approval> findByBizDate(LocalDate bizDate);
}

View file

@ -0,0 +1,7 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Card;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CardRepository extends JpaRepository<Card, String> {
}

View file

@ -0,0 +1,13 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Deposit;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface DepositRepository extends JpaRepository<Deposit, Long> {
Optional<Deposit> findByApprNo(String apprNo);
List<Deposit> findByBizDate(LocalDate bizDate);
}

View file

@ -0,0 +1,10 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Fee;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface FeeRepository extends JpaRepository<Fee, Long> {
List<Fee> findByPurchaseId(Long purchaseId);
}

View file

@ -0,0 +1,12 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.LedgerEntry;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
public interface LedgerEntryRepository extends JpaRepository<LedgerEntry, Long> {
List<LedgerEntry> findByMerchIdAndBizDate(String merchId, LocalDate bizDate);
List<LedgerEntry> findByBizDate(LocalDate bizDate);
}

View file

@ -0,0 +1,11 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Merchant;
import com.klaro.acquiring.domain.enums.MerchantStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface MerchantRepository extends JpaRepository<Merchant, String> {
List<Merchant> findByStatus(MerchantStatus status);
}

View file

@ -0,0 +1,14 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Payment;
import com.klaro.acquiring.domain.enums.PaymentStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
public interface PaymentRepository extends JpaRepository<Payment, Long> {
List<Payment> findByPayDate(LocalDate payDate);
List<Payment> findByPayDateAndStatus(LocalDate payDate, PaymentStatus status);
List<Payment> findByMerchId(String merchId);
}

View file

@ -0,0 +1,18 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Purchase;
import com.klaro.acquiring.domain.enums.TxStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface PurchaseRepository extends JpaRepository<Purchase, Long> {
boolean existsByApprNo(String apprNo);
Optional<Purchase> findByApprNo(String apprNo);
List<Purchase> findByBizDateAndStatus(LocalDate bizDate, TxStatus status);
List<Purchase> findByMerchIdAndBizDateAndStatus(String merchId, LocalDate bizDate, TxStatus status);
List<Purchase> findByMerchIdAndBizDate(String merchId, LocalDate bizDate);
List<Purchase> findByBizDate(LocalDate bizDate);
}

View file

@ -0,0 +1,14 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.ReconcileResult;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface ReconcileResultRepository extends JpaRepository<ReconcileResult, Long> {
List<ReconcileResult> findByBizDate(LocalDate bizDate);
List<ReconcileResult> findByBizDateAndMatched(LocalDate bizDate, boolean matched);
Optional<ReconcileResult> findByBizDateAndApprNo(LocalDate bizDate, String apprNo);
}

View file

@ -0,0 +1,14 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Settlement;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface SettlementRepository extends JpaRepository<Settlement, Long> {
Optional<Settlement> findByMerchIdAndBizDate(String merchId, LocalDate bizDate);
List<Settlement> findByBizDate(LocalDate bizDate);
List<Settlement> findByMerchId(String merchId);
}

View file

@ -0,0 +1,135 @@
-- V1__schema.sql
-- 카드 매입·정산 스키마. 레거시 acquire-core-full/db/schema.sql + 모듈별 *_ledger 를
-- 정규화하여 재구성. 상태코드는 enum name(VARCHAR)으로 저장(RECEIVED/DONE/FAILED/SETTLED).
-- H2/PostgreSQL 공통 호환 DDL.
CREATE TABLE merchant (
merch_id VARCHAR(15) NOT NULL,
merch_name VARCHAR(60) NOT NULL,
biz_no VARCHAR(12),
status VARCHAR(12) NOT NULL DEFAULT 'ACTIVE',
mdr_rate DECIMAL(6,4) NOT NULL DEFAULT 0,
van_fee DECIMAL(18,2) NOT NULL DEFAULT 0,
settlement_cycle INT NOT NULL DEFAULT 2,
bank_code VARCHAR(3),
account_no VARCHAR(20),
CONSTRAINT pk_merchant PRIMARY KEY (merch_id)
);
CREATE TABLE card (
card_no VARCHAR(19) NOT NULL,
bin VARCHAR(6) NOT NULL,
brand VARCHAR(10) NOT NULL,
issuer_code VARCHAR(4),
status VARCHAR(1) NOT NULL DEFAULT 'A',
CONSTRAINT pk_card PRIMARY KEY (card_no)
);
CREATE TABLE approval (
appr_no VARCHAR(12) NOT NULL,
merch_id VARCHAR(15) NOT NULL,
card_no VARCHAR(19) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
appr_date VARCHAR(8) NOT NULL,
biz_date DATE NOT NULL,
status VARCHAR(12) NOT NULL DEFAULT 'RECEIVED',
CONSTRAINT pk_approval PRIMARY KEY (appr_no)
);
CREATE INDEX ix_approval_biz ON approval (biz_date, status);
CREATE TABLE purchase (
purchase_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
appr_no VARCHAR(12) NOT NULL,
merch_id VARCHAR(15) NOT NULL,
card_no VARCHAR(19) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
mdr_fee DECIMAL(18,2) NOT NULL DEFAULT 0,
van_fee DECIMAL(18,2) NOT NULL DEFAULT 0,
net_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
biz_date DATE NOT NULL,
status VARCHAR(12) NOT NULL DEFAULT 'RECEIVED',
CONSTRAINT pk_purchase PRIMARY KEY (purchase_id),
CONSTRAINT uq_purchase_appr UNIQUE (appr_no)
);
CREATE INDEX ix_purchase_biz ON purchase (biz_date, status);
CREATE INDEX ix_purchase_merch ON purchase (merch_id, biz_date);
CREATE TABLE deposit (
deposit_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
appr_no VARCHAR(12) NOT NULL,
merch_id VARCHAR(15) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
biz_date DATE NOT NULL,
CONSTRAINT pk_deposit PRIMARY KEY (deposit_id)
);
CREATE INDEX ix_deposit_appr ON deposit (appr_no);
CREATE TABLE fee (
fee_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
purchase_id BIGINT NOT NULL,
merch_id VARCHAR(15) NOT NULL,
fee_type VARCHAR(8) NOT NULL,
base_amount DECIMAL(18,2) NOT NULL,
rate DECIMAL(6,4) NOT NULL DEFAULT 0,
fee_amount DECIMAL(18,2) NOT NULL,
biz_date DATE NOT NULL,
CONSTRAINT pk_fee PRIMARY KEY (fee_id)
);
CREATE INDEX ix_fee_purchase ON fee (purchase_id);
CREATE TABLE reconcile_result (
recon_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
biz_date DATE NOT NULL,
appr_no VARCHAR(12) NOT NULL,
merch_id VARCHAR(15) NOT NULL,
approval_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
purchase_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
deposit_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
diff_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
matched BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(12) NOT NULL DEFAULT 'RECEIVED',
reason VARCHAR(100),
CONSTRAINT pk_reconcile PRIMARY KEY (recon_id)
);
CREATE INDEX ix_reconcile_biz ON reconcile_result (biz_date);
CREATE TABLE settlement (
settlement_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
merch_id VARCHAR(15) NOT NULL,
biz_date DATE NOT NULL,
gross_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
total_fee DECIMAL(18,2) NOT NULL DEFAULT 0,
net_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
txn_count INT NOT NULL DEFAULT 0,
pay_date DATE NOT NULL,
status VARCHAR(12) NOT NULL DEFAULT 'DONE',
CONSTRAINT pk_settlement PRIMARY KEY (settlement_id),
CONSTRAINT uq_settlement_merch_date UNIQUE (merch_id, biz_date)
);
CREATE INDEX ix_settlement_biz ON settlement (biz_date);
CREATE TABLE ledger_entry (
entry_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
merch_id VARCHAR(15) NOT NULL,
biz_date DATE NOT NULL,
entry_type VARCHAR(12) NOT NULL,
dr_cr VARCHAR(8) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
ref_type VARCHAR(20),
ref_id VARCHAR(30),
CONSTRAINT pk_ledger_entry PRIMARY KEY (entry_id)
);
CREATE INDEX ix_ledger_merch ON ledger_entry (merch_id, biz_date);
CREATE TABLE payment (
payment_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
settlement_id BIGINT NOT NULL,
merch_id VARCHAR(15) NOT NULL,
pay_date DATE NOT NULL,
amount DECIMAL(18,2) NOT NULL,
bank_code VARCHAR(3),
account_no VARCHAR(20),
status VARCHAR(15) NOT NULL DEFAULT 'READY',
CONSTRAINT pk_payment PRIMARY KEY (payment_id)
);
CREATE INDEX ix_payment_paydate ON payment (pay_date, status);

View file

@ -0,0 +1,12 @@
-- V2__seed.sql - 최소 참조/시드 데이터
-- 가맹점 3개(수수료율/정산주기/지급계좌 상이), 카드 3개.
INSERT INTO merchant (merch_id, merch_name, biz_no, status, mdr_rate, van_fee, settlement_cycle, bank_code, account_no) VALUES
('M0000000000001', '한빛마트', '111-11-11111', 'ACTIVE', 0.0230, 30.00, 2, '004', '11122233344455'),
('M0000000000002', '가온카페', '222-22-22222', 'ACTIVE', 0.0180, 25.00, 3, '088', '22233344455566'),
('M0000000000003', '다올주유소', '333-33-33333', 'ACTIVE', 0.0150, 20.00, 1, '020', '33344455566677');
INSERT INTO card (card_no, bin, brand, issuer_code, status) VALUES
('123456******7890', '123456', 'LOCAL', '0301', 'A'),
('654321******0987', '654321', 'VISA', '0302', 'A'),
('987654******3210', '987654', 'MASTER','0303', 'A');

View file

@ -0,0 +1,14 @@
package com.klaro.acquiring.persistence;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/** persistence 모듈 슬라이스 테스트용 부트 설정. */
@SpringBootConfiguration
@EnableAutoConfiguration
@EntityScan(basePackages = "com.klaro.acquiring.domain.entity")
@EnableJpaRepositories(basePackages = "com.klaro.acquiring.persistence.repository")
public class PersistenceTestApp {
}

View file

@ -0,0 +1,46 @@
package com.klaro.acquiring.persistence.repository;
import com.klaro.acquiring.domain.entity.Approval;
import com.klaro.acquiring.domain.entity.Merchant;
import com.klaro.acquiring.domain.enums.MerchantStatus;
import com.klaro.acquiring.domain.enums.TxStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MerchantRepositoryTest {
@Autowired
private MerchantRepository merchantRepository;
@Autowired
private ApprovalRepository approvalRepository;
@Test
void flywaySeedLoadsThreeMerchants() {
List<Merchant> active = merchantRepository.findByStatus(MerchantStatus.ACTIVE);
assertThat(active).hasSize(3);
Merchant m = merchantRepository.findById("M0000000000001").orElseThrow();
assertThat(m.getMdrRate()).isEqualByComparingTo("0.0230");
assertThat(m.getSettlementCycle()).isEqualTo(2);
}
@Test
void persistsAndQueriesApprovalByBizDate() {
LocalDate bizDate = LocalDate.of(2026, 7, 17);
approvalRepository.save(new Approval("A0001", "M0000000000001", "123456******7890",
new BigDecimal("10000.00"), "20260717", bizDate));
List<Approval> found = approvalRepository.findByBizDateAndStatus(bizDate, TxStatus.RECEIVED);
assertThat(found).extracting(Approval::getApprNo).containsExactly("A0001");
}
}

View file

@ -0,0 +1,15 @@
spring:
datasource:
url: jdbc:h2:mem:persist;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.format_sql: false
flyway:
enabled: true
locations: classpath:db/migration