Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/main/java/org/creditto/core_banking/common/vo/Money.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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() {
Expand All @@ -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);
}

}
private Money currentBalance() {
return Money.of(this.balance, ACCOUNT_CURRENCY);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down
54 changes: 54 additions & 0 deletions src/test/java/org/creditto/core_banking/common/vo/MoneyTest.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading