카드 정산 업무 규칙과 전환 불변식 추출 #4

Open
forge-bot wants to merge 9 commits from forge/codex-bais-final3-20260713-192718-BAIS-SPRING-ANA-CARD-001-attempt-1-run-4a55ce2ead99 into main
Showing only changes of commit 8513e6b71c - Show all commits

View file

@ -0,0 +1,248 @@
package com.bais.card.settlement;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* Settlement Calculator
*
* 카드 정산 계산 로직 (Brand Settlement + IRD + Issuer Fee)
*
* INVARIANTS:
* - BRAND_SETTLEMENT_NON_NEGATIVE: 정산금은 0 이상
* - BRAND_SETTLEMENT_FORMULA: 정산금 = 승인금액 - 취소금액 - 수수료
* - BRAND_COMMISSION_FORMULA: 수수료 = 승인금액 * 정산요율
* - BRAND_COMMISSION_RATE_DEFAULT: 미지정 0.02
* - IRD_NON_NEGATIVE: IRD는 0 이상
* - IRD_FORMULA: IRD = 승인금액 * IRD율
* - IRD_RATE_DEFAULT: 미지정 0.015
* - ISSUER_FEE_NON_NEGATIVE: 발급사 수수료는 0 이상
* - ISSUER_FEE_FORMULA: 발급사 수수료 = IRD * 발급사 수수료율
* - ISSUER_FEE_RATE_DEFAULT: 미지정 0.005
* - ISSUER_SETTLEMENT_NON_NEGATIVE: 발급사 정산금은 0 이상
* - ISSUER_SETTLEMENT_FORMULA: 발급사 정산금 = IRD - 발급사 수수료
* - AMOUNT_NOT_NULL: 금액은 null 불가
* - AMOUNT_NON_NEGATIVE: 금액은 0 이상
* - AMOUNT_MAX_LIMIT: 최대 한도 이하여야
* - AMOUNT_SCALE: 소수점 2자리 표현
* - ROUNDING_MODE: HALF_UP 반올림
*/
public class SettlementCalculator {
private static final BigDecimal DEFAULT_COMMISSION_RATE = new BigDecimal("0.02");
private static final BigDecimal DEFAULT_IRD_RATE = new BigDecimal("0.015");
private static final BigDecimal DEFAULT_ISSUER_FEE_RATE = new BigDecimal("0.005");
private static final BigDecimal MIN_AMOUNT = BigDecimal.ZERO;
private static final BigDecimal MAX_AMOUNT = new BigDecimal("999999999999.99");
/**
* 브랜드 정산금 계산
* INVARIANT: 정산금 = 승인금액 - 취소금액 - 수수료
*/
public BrandSettlementResult calculateBrandSettlement(BigDecimal approvedAmount,
BigDecimal cancelledAmount,
BigDecimal commissionRate) {
validateAmount(approvedAmount);
validateAmount(cancelledAmount);
BigDecimal effectiveCommissionRate =
commissionRate != null ? commissionRate : DEFAULT_COMMISSION_RATE;
BigDecimal commission = approvedAmount
.multiply(effectiveCommissionRate)
.setScale(2, java.math.RoundingMode.HALF_UP);
BigDecimal settlementAmount = approvedAmount
.subtract(cancelledAmount)
.subtract(commission);
if (settlementAmount.compareTo(MIN_AMOUNT) < 0) {
settlementAmount = MIN_AMOUNT;
}
return new BrandSettlementResult(
settlementAmount, commission, approvedAmount, cancelledAmount,
effectiveCommissionRate, LocalDateTime.now()
);
}
/**
* IRD 계산
* INVARIANT: IRD = 승인금액 * IRD율
*/
public BigDecimal calculateIRD(BigDecimal approvedAmount, BigDecimal irdRate) {
validateAmount(approvedAmount);
BigDecimal effectiveIrdRate = irdRate != null ? irdRate : DEFAULT_IRD_RATE;
BigDecimal ird = approvedAmount
.multiply(effectiveIrdRate)
.setScale(2, java.math.RoundingMode.HALF_UP);
if (ird.compareTo(MIN_AMOUNT) < 0) {
return MIN_AMOUNT;
}
return ird;
}
/**
* 발급사 수수료 계산
* INVARIANT: 발급사 수수료 = IRD * 발급사 수수료율
*/
public BigDecimal calculateIssuerFee(BigDecimal ird, BigDecimal issuerFeeRate) {
validateAmount(ird);
BigDecimal effectiveRate =
issuerFeeRate != null ? issuerFeeRate : DEFAULT_ISSUER_FEE_RATE;
BigDecimal issuerFee = ird
.multiply(effectiveRate)
.setScale(2, java.math.RoundingMode.HALF_UP);
if (issuerFee.compareTo(MIN_AMOUNT) < 0) {
return MIN_AMOUNT;
}
return issuerFee;
}
/**
* 발급사 정산금 계산
* INVARIANT: 발급사 정산금 = IRD - 발급사 수수료
*/
public IssuerSettlementResult calculateIssuerSettlement(BigDecimal ird,
BigDecimal issuerFeeRate) {
validateAmount(ird);
BigDecimal effectiveRate =
issuerFeeRate != null ? issuerFeeRate : DEFAULT_ISSUER_FEE_RATE;
BigDecimal issuerFee = ird
.multiply(effectiveRate)
.setScale(2, java.math.RoundingMode.HALF_UP);
BigDecimal issuerSettlement = ird.subtract(issuerFee);
if (issuerSettlement.compareTo(MIN_AMOUNT) < 0) {
issuerSettlement = MIN_AMOUNT;
}
return new IssuerSettlementResult(
issuerSettlement, issuerFee, ird, effectiveRate, LocalDateTime.now()
);
}
/**
* 전체 정산 계산 (Brand + Issuer)
*/
public FullSettlementResult calculateFullSettlement(BigDecimal approvedAmount,
BigDecimal cancelledAmount,
BigDecimal commissionRate,
BigDecimal irdRate,
BigDecimal issuerFeeRate) {
BrandSettlementResult brandResult = calculateBrandSettlement(
approvedAmount, cancelledAmount, commissionRate);
BigDecimal ird = calculateIRD(approvedAmount, irdRate);
IssuerSettlementResult issuerResult = calculateIssuerSettlement(ird, issuerFeeRate);
return new FullSettlementResult(
brandResult.getSettlementAmount(),
brandResult.getCommission(),
ird,
issuerResult.getIssuerFee(),
issuerResult.getIssuerSettlement(),
LocalDateTime.now()
);
}
private void validateAmount(BigDecimal amount) {
Objects.requireNonNull(amount, "Amount must not be null");
if (amount.compareTo(MIN_AMOUNT) < 0) {
throw new IllegalArgumentException("Amount must be non-negative");
}
if (amount.compareTo(MAX_AMOUNT) > 0) {
throw new IllegalArgumentException("Amount exceeds maximum limit");
}
}
public static class BrandSettlementResult {
private final BigDecimal settlementAmount;
private final BigDecimal commission;
private final BigDecimal approvedAmount;
private final BigDecimal cancelledAmount;
private final BigDecimal commissionRate;
private final LocalDateTime calculatedAt;
public BrandSettlementResult(BigDecimal settlementAmount, BigDecimal commission,
BigDecimal approvedAmount, BigDecimal cancelledAmount,
BigDecimal commissionRate, LocalDateTime calculatedAt) {
this.settlementAmount = settlementAmount;
this.commission = commission;
this.approvedAmount = approvedAmount;
this.cancelledAmount = cancelledAmount;
this.commissionRate = commissionRate;
this.calculatedAt = calculatedAt;
}
public BigDecimal getSettlementAmount() { return settlementAmount; }
public BigDecimal getCommission() { return commission; }
public BigDecimal getApprovedAmount() { return approvedAmount; }
public BigDecimal getCancelledAmount() { return cancelledAmount; }
public BigDecimal getCommissionRate() { return commissionRate; }
public LocalDateTime getCalculatedAt() { return calculatedAt; }
}
public static class IssuerSettlementResult {
private final BigDecimal issuerSettlement;
private final BigDecimal issuerFee;
private final BigDecimal ird;
private final BigDecimal issuerFeeRate;
private final LocalDateTime calculatedAt;
public IssuerSettlementResult(BigDecimal issuerSettlement, BigDecimal issuerFee,
BigDecimal ird, BigDecimal issuerFeeRate,
LocalDateTime calculatedAt) {
this.issuerSettlement = issuerSettlement;
this.issuerFee = issuerFee;
this.ird = ird;
this.issuerFeeRate = issuerFeeRate;
this.calculatedAt = calculatedAt;
}
public BigDecimal getIssuerSettlement() { return issuerSettlement; }
public BigDecimal getIssuerFee() { return issuerFee; }
public BigDecimal getIrd() { return ird; }
public BigDecimal getIssuerFeeRate() { return issuerFeeRate; }
public LocalDateTime getCalculatedAt() { return calculatedAt; }
}
public static class FullSettlementResult {
private final BigDecimal brandSettlement;
private final BigDecimal brandCommission;
private final BigDecimal ird;
private final BigDecimal issuerFee;
private final BigDecimal issuerSettlement;
private final LocalDateTime calculatedAt;
public FullSettlementResult(BigDecimal brandSettlement, BigDecimal brandCommission,
BigDecimal ird, BigDecimal issuerFee,
BigDecimal issuerSettlement, LocalDateTime calculatedAt) {
this.brandSettlement = brandSettlement;
this.brandCommission = brandCommission;
this.ird = ird;
this.issuerFee = issuerFee;
this.issuerSettlement = issuerSettlement;
this.calculatedAt = calculatedAt;
}
public BigDecimal getBrandSettlement() { return brandSettlement; }
public BigDecimal getBrandCommission() { return brandCommission; }
public BigDecimal getIrd() { return ird; }
public BigDecimal getIssuerFee() { return issuerFee; }
public BigDecimal getIssuerSettlement() { return issuerSettlement; }
public LocalDateTime getCalculatedAt() { return calculatedAt; }
}
}