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
38
persistence/pom.xml
Normal file
38
persistence/pom.xml
Normal 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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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> {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
135
persistence/src/main/resources/db/migration/V1__schema.sql
Normal file
135
persistence/src/main/resources/db/migration/V1__schema.sql
Normal 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);
|
||||
12
persistence/src/main/resources/db/migration/V2__seed.sql
Normal file
12
persistence/src/main/resources/db/migration/V2__seed.sql
Normal 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');
|
||||
|
|
@ -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 {
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
15
persistence/src/test/resources/application.yml
Normal file
15
persistence/src/test/resources/application.yml
Normal 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
|
||||
Reference in a new issue