diff --git a/card-settlement-migration/src/main/java/com/bais/card/settlement/SettlementCalculator.java b/card-settlement-migration/src/main/java/com/bais/card/settlement/SettlementCalculator.java new file mode 100644 index 0000000..585cfd7 --- /dev/null +++ b/card-settlement-migration/src/main/java/com/bais/card/settlement/SettlementCalculator.java @@ -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; } + } +}