diff --git a/src/main/java/org/creditto/core_banking/common/vo/Money.java b/src/main/java/org/creditto/core_banking/common/vo/Money.java new file mode 100644 index 0000000..73f75de --- /dev/null +++ b/src/main/java/org/creditto/core_banking/common/vo/Money.java @@ -0,0 +1,66 @@ +package org.creditto.core_banking.common.vo; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.creditto.core_banking.global.common.CurrencyCode; +import org.creditto.core_banking.global.response.error.ErrorBaseCode; +import org.creditto.core_banking.global.response.exception.CustomBaseException; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +@Getter +@EqualsAndHashCode +public final class Money { + private final BigDecimal amount; + + private final CurrencyCode currency; + + private Money(final BigDecimal amount, final CurrencyCode currency) { + if (currency == null) { + throw new CustomBaseException(ErrorBaseCode.CURRENCY_NOT_SUPPORTED); + } + if (amount == null || amount.signum() < 0) { + throw new CustomBaseException(ErrorBaseCode.BAD_REQUEST); + } + this.amount = normalize(amount, currency); + this.currency = currency; + } + + public static Money of(BigDecimal amount, CurrencyCode currency) { + return new Money(amount, currency); + } + + public Money plus(Money other) { + assertSameCurrency(other); + return new Money(this.amount.add(other.amount), this.currency); + } + + public Money minus(Money other) { + assertSameCurrency(other); + BigDecimal result = this.amount.subtract(other.amount); + if (result.signum() < 0) { + throw new CustomBaseException(ErrorBaseCode.INSUFFICIENT_FUNDS); + } + return new Money(result, this.currency); + } + + public boolean gte(Money other) { + assertSameCurrency(other); + return this.amount.compareTo(other.amount) >= 0; + } + + private void assertSameCurrency(Money other) { + if (other == null) { + throw new CustomBaseException(ErrorBaseCode.BAD_REQUEST); + } + if (this.currency != other.currency) { + throw new CustomBaseException(ErrorBaseCode.CURRENCY_NOT_SUPPORTED); + } + } + + private static BigDecimal normalize(BigDecimal value, CurrencyCode currency) { + int scale = (currency == CurrencyCode.KRW) ? 0 : 2; + return value.setScale(scale, RoundingMode.HALF_UP); + } +} diff --git a/src/main/java/org/creditto/core_banking/domain/account/entity/Account.java b/src/main/java/org/creditto/core_banking/domain/account/entity/Account.java index f80dc86..461a136 100644 --- a/src/main/java/org/creditto/core_banking/domain/account/entity/Account.java +++ b/src/main/java/org/creditto/core_banking/domain/account/entity/Account.java @@ -2,7 +2,9 @@ import jakarta.persistence.*; import lombok.*; +import org.creditto.core_banking.common.vo.Money; import org.creditto.core_banking.global.common.BaseEntity; +import org.creditto.core_banking.global.common.CurrencyCode; import org.creditto.core_banking.global.response.error.ErrorBaseCode; import org.creditto.core_banking.global.response.exception.CustomBaseException; @@ -63,6 +65,7 @@ public static Account of(String accountNo, String password, String accountName, private static final int ACCOUNT_NO_LENGTH = 13; private static final SecureRandom RANDOM = new SecureRandom(); + private static final CurrencyCode ACCOUNT_CURRENCY = CurrencyCode.KRW; @PrePersist protected void prePersist() { @@ -89,22 +92,21 @@ private void generateAccountNo() { // 입금 - public void deposit(BigDecimal amount) { - this.balance = this.balance.add(amount); + public void deposit(Money amount) { + this.balance = currentBalance().plus(amount).getAmount(); } // 출금 - public void withdraw(BigDecimal amount) { - if (!this.checkSufficientBalance(amount)) { - throw new CustomBaseException(ErrorBaseCode.INSUFFICIENT_FUNDS); - } - - this.balance = balance.subtract(amount); + public void withdraw(Money amount) { + this.balance = currentBalance().minus(amount).getAmount(); } // 출금 가능한지 확인 - public boolean checkSufficientBalance(BigDecimal amount) { - return this.balance.compareTo(amount) >= 0; + public boolean checkSufficientBalance(Money amount) { + return currentBalance().gte(amount); } -} \ No newline at end of file + private Money currentBalance() { + return Money.of(this.balance, ACCOUNT_CURRENCY); + } +} diff --git a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/DepositStrategy.java b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/DepositStrategy.java index 3dc5386..6c287d2 100644 --- a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/DepositStrategy.java +++ b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/DepositStrategy.java @@ -1,8 +1,10 @@ package org.creditto.core_banking.domain.account.service.strategy; +import org.creditto.core_banking.common.vo.Money; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.transaction.entity.TxnType; import org.creditto.core_banking.domain.transaction.service.TransactionService; +import org.creditto.core_banking.global.common.CurrencyCode; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -16,7 +18,7 @@ public DepositStrategy(TransactionService transactionService) { @Override protected void process(Account account, BigDecimal amount, Long typeId) { - account.deposit(amount); + account.deposit(Money.of(amount, CurrencyCode.KRW)); } @Override diff --git a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/ExchangeStrategy.java b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/ExchangeStrategy.java index d7b729e..d1692e5 100644 --- a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/ExchangeStrategy.java +++ b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/ExchangeStrategy.java @@ -1,8 +1,10 @@ package org.creditto.core_banking.domain.account.service.strategy; +import org.creditto.core_banking.common.vo.Money; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.transaction.entity.TxnType; import org.creditto.core_banking.domain.transaction.service.TransactionService; +import org.creditto.core_banking.global.common.CurrencyCode; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -16,7 +18,7 @@ public ExchangeStrategy(TransactionService transactionService) { @Override protected void process(Account account, BigDecimal amount, Long typeId) { - account.withdraw(amount); + account.withdraw(Money.of(amount, CurrencyCode.KRW)); } @Override diff --git a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/FeeStrategy.java b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/FeeStrategy.java index 23062a8..467c858 100644 --- a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/FeeStrategy.java +++ b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/FeeStrategy.java @@ -1,8 +1,10 @@ package org.creditto.core_banking.domain.account.service.strategy; +import org.creditto.core_banking.common.vo.Money; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.transaction.entity.TxnType; import org.creditto.core_banking.domain.transaction.service.TransactionService; +import org.creditto.core_banking.global.common.CurrencyCode; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -16,7 +18,7 @@ public FeeStrategy(TransactionService transactionService) { @Override protected void process(Account account, BigDecimal amount, Long typeId) { - account.withdraw(amount); + account.withdraw(Money.of(amount, CurrencyCode.KRW)); } @Override diff --git a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/WithdrawalStrategy.java b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/WithdrawalStrategy.java index b96ea46..971c53f 100644 --- a/src/main/java/org/creditto/core_banking/domain/account/service/strategy/WithdrawalStrategy.java +++ b/src/main/java/org/creditto/core_banking/domain/account/service/strategy/WithdrawalStrategy.java @@ -1,8 +1,10 @@ package org.creditto.core_banking.domain.account.service.strategy; +import org.creditto.core_banking.common.vo.Money; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.transaction.entity.TxnType; import org.creditto.core_banking.domain.transaction.service.TransactionService; +import org.creditto.core_banking.global.common.CurrencyCode; import org.springframework.stereotype.Component; import java.math.BigDecimal; @@ -16,7 +18,7 @@ public WithdrawalStrategy(TransactionService transactionService) { @Override protected void process(Account account, BigDecimal amount, Long typeId) { - account.withdraw(amount); + account.withdraw(Money.of(amount, CurrencyCode.KRW)); } @Override diff --git a/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java b/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java index ee7b0e3..e611c60 100644 --- a/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java +++ b/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java @@ -1,6 +1,7 @@ package org.creditto.core_banking.domain.overseasremittance.service; import lombok.RequiredArgsConstructor; +import org.creditto.core_banking.common.vo.Money; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.account.repository.AccountRepository; import org.creditto.core_banking.domain.account.service.AccountLockService; @@ -89,15 +90,17 @@ public OverseasRemittanceResponseDto execute(final ExecuteRemittanceCommand comm // 실제 송금해야 할 금액 BigDecimal actualSendAmount = exchangeRes.exchangeAmount(); + Money sendAmountMoney = Money.of(actualSendAmount, CurrencyCode.KRW); // 수수료 계산 FeeRecord feeRecord = calculateFee(exchangeRes, command.receiveCurrency()); // 총 수수료 BigDecimal totalFee = feeRecord.getTotalFee(); + Money totalFeeMoney = Money.of(totalFee, CurrencyCode.KRW); // 총 차감될 금액 계산 (실제 보낼 금액 + 총 수수료) - BigDecimal totalDeduction = actualSendAmount.add(totalFee); + Money totalDeductionMoney = sendAmountMoney.plus(totalFeeMoney); // 3. DTO에 담겨올 ID로 Exchange 엔티티 다시 조회 Long exchangeId = exchangeRes.exchangeId(); @@ -109,7 +112,7 @@ public OverseasRemittanceResponseDto execute(final ExecuteRemittanceCommand comm Account lockedAccount = accountRepository.findByIdForUpdate(command.accountId()) .orElseThrow(() -> new CustomBaseException(NOT_FOUND_ACCOUNT)); - if (!lockedAccount.checkSufficientBalance(totalDeduction)) { + if (!lockedAccount.checkSufficientBalance(totalDeductionMoney)) { transactionService.saveTransaction(lockedAccount, actualSendAmount, TxnType.WITHDRAWAL, null, TxnResult.FAILURE); throw new CustomBaseException(ErrorBaseCode.INSUFFICIENT_FUNDS); } @@ -126,11 +129,11 @@ public OverseasRemittanceResponseDto execute(final ExecuteRemittanceCommand comm remittanceRepository.save(overseasRemittance); if (totalFee.compareTo(BigDecimal.ZERO) > 0) { - lockedAccount.withdraw(totalFee); + lockedAccount.withdraw(totalFeeMoney); transactionService.saveTransaction(lockedAccount, totalFee, TxnType.FEE, overseasRemittance.getRemittanceId(), TxnResult.SUCCESS); } - lockedAccount.withdraw(actualSendAmount); + lockedAccount.withdraw(sendAmountMoney); transactionService.saveTransaction(lockedAccount, actualSendAmount, TxnType.WITHDRAWAL, overseasRemittance.getRemittanceId(), TxnResult.SUCCESS); accountRepository.save(lockedAccount); diff --git a/src/test/java/org/creditto/core_banking/common/vo/MoneyTest.java b/src/test/java/org/creditto/core_banking/common/vo/MoneyTest.java new file mode 100644 index 0000000..eeefb67 --- /dev/null +++ b/src/test/java/org/creditto/core_banking/common/vo/MoneyTest.java @@ -0,0 +1,54 @@ +package org.creditto.core_banking.common.vo; + +import org.creditto.core_banking.global.common.CurrencyCode; +import org.creditto.core_banking.global.response.exception.CustomBaseException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MoneyTest { + + @Test + @DisplayName("같은 통화의 금액은 더할 수 있다") + void plus_sameCurrency_success() { + Money a = Money.of(new BigDecimal("1000"), CurrencyCode.KRW); + Money b = Money.of(new BigDecimal("2000"), CurrencyCode.KRW); + + Money result = a.plus(b); + + assertThat(result.getAmount()).isEqualByComparingTo("3000"); + assertThat(result.getCurrency()).isEqualTo(CurrencyCode.KRW); + } + + @Test + @DisplayName("잔액보다 큰 금액을 빼면 예외가 발생한다") + void minus_insufficientFunds_fail() { + Money a = Money.of(new BigDecimal("1000"), CurrencyCode.KRW); + Money b = Money.of(new BigDecimal("1001"), CurrencyCode.KRW); + + assertThatThrownBy(() -> a.minus(b)) + .isInstanceOf(CustomBaseException.class); + } + + @Test + @DisplayName("통화가 다르면 연산할 수 없다") + void plus_differentCurrency_fail() { + Money krw = Money.of(new BigDecimal("1000"), CurrencyCode.KRW); + Money usd = Money.of(new BigDecimal("1"), CurrencyCode.USD); + + assertThatThrownBy(() -> krw.plus(usd)) + .isInstanceOf(CustomBaseException.class); + } + + @Test + @DisplayName("KRW는 소수점 없이 정규화된다") + void of_krw_normalizeScale_zero() { + Money krw = Money.of(new BigDecimal("1000.6"), CurrencyCode.KRW); + + assertThat(krw.getAmount()).isEqualByComparingTo("1001"); + } +}