From 54ae8387f6dca767129c37e3656dbe3fb2c1cb89 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sat, 10 Aug 2024 19:06:04 +0900 Subject: [PATCH 01/62] =?UTF-8?q?[feat]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20balance=20=EB=A9=A4=EB=B2=84=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 잔고는 int형 이상을 가지고 있는 사람이 있을 수 있으므로 long타입. - default값은 0 - 잔고의 값은 null이 될 수 없음. --- .../woowak/lab/payaccount/domain/PayAccount.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 3406a60f..c14e01cc 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -1,5 +1,8 @@ package camp.woowak.lab.payaccount.domain; +import org.hibernate.annotations.ColumnDefault; + +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -7,7 +10,11 @@ @Entity public class PayAccount { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "balance", nullable = false) + @ColumnDefault("0") + private long balance; } From 4c9c117671751a2584873c0234c82b0c3b1d7098 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 01:14:14 +0900 Subject: [PATCH 02/62] =?UTF-8?q?[feat]=20InsufficientBalanceException=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 출금을 진행할 때, 잔액 부족에 대한 exception 생성 --- .../payaccount/exception/InsufficientBalanceException.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java b/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java new file mode 100644 index 00000000..c6ba2695 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.payaccount.exception; + +public class InsufficientBalanceException extends RuntimeException { + public InsufficientBalanceException(String message) { + super(message); + } +} From d8444fe6647e58364c0a290caace9932f8898d80 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 01:14:37 +0900 Subject: [PATCH 03/62] =?UTF-8?q?[feat]=20InvalidTransactionAmountExceptio?= =?UTF-8?q?n=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입/출금을 진행하며 0원 이하로 들어오는 행위에 대한 exception 생성 --- .../exception/InvalidTransactionAmountException.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java b/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java new file mode 100644 index 00000000..519dae12 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.payaccount.exception; + +public class InvalidTransactionAmountException extends RuntimeException { + public InvalidTransactionAmountException(String message) { + super(message); + } +} From bc82d4d0ce60424600ae6a5de70da2ceea17589c Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 01:22:17 +0900 Subject: [PATCH 04/62] =?UTF-8?q?[test]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=85/=EC=B6=9C=EA=B8=88=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기본 생성자로 생성 시 잔고는 0원으로 셋팅 - 0원 이하의 금액으로 입/출금 진행시 InvalidTransactionAmountException throw - 출금은 자신의 잔고보다 더 많은 금액을 출금시 InsufficientBalanceException throw - 아무런 문제가 없다면 입/출금 진행 뒤 잔고 업데이트 --- .../lab/payaccount/domain/PayAccountTest.java | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java diff --git a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java new file mode 100644 index 00000000..df73efc1 --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java @@ -0,0 +1,133 @@ +package camp.woowak.lab.payaccount.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; +import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; + +@DisplayName("PayAccount 클래스") +class PayAccountTest { + private PayAccount payAccount; + + @BeforeEach + void setUp() { + payAccount = new PayAccount(); + } + + @Nested + @DisplayName("기본 생성자는") + class DefaultConstructorTest { + @Test + @DisplayName("기본 생성자로 호출하면 balance는 0이된다.") + void initializeBalanceToZero() { + // then + assertThat(payAccount.getBalance()).isZero(); + } + } + + @Nested + @DisplayName("Deposit 메서드는") + class DepositTest { + @Test + @DisplayName("입금한 만큼 잔고가 증가한다.") + void increaseBalanceByDepositedAmount() { + // given + long amount = 1000; + + // when + payAccount.deposit(amount); + + // then + assertThat(payAccount.getBalance()).isEqualTo(amount); + } + + @Test + @DisplayName("음수를 입금하려하면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForNegativeAmount() { + // given + long amount = -1000; + + // when & then + assertThatThrownBy(() -> payAccount.deposit(amount)) + .isExactlyInstanceOf(InvalidTransactionAmountException.class); + assertThat(payAccount.getBalance()).isZero(); + } + + @Test + @DisplayName("0원을 입금하려면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForZeroAmount() { + // given + long amount = 0; + + // when & then + assertThatThrownBy(() -> payAccount.deposit(amount)) + .isExactlyInstanceOf(InvalidTransactionAmountException.class); + assertThat(payAccount.getBalance()).isZero(); + } + } + + @Nested + @DisplayName("Withdraw 메서드는") + class WithdrawTest { + private long originBalance = 1000; + + @BeforeEach + void setUpBalance() { + payAccount.deposit(originBalance); + } + + @Test + @DisplayName("출금한 만큼 잔고에서 차감된다.") + void decreaseBalanceByWithdrawnAmount() { + // given + long withdrawAmount = 100; + + // when + payAccount.withdraw(withdrawAmount); + + // then + assertThat(payAccount.getBalance()).isEqualTo(originBalance - withdrawAmount); + } + + @Test + @DisplayName("음수를 출금하려면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForNegativeAmount() { + // given + long amount = -100; + + // when & then + assertThatThrownBy(() -> payAccount.withdraw(amount)) + .isExactlyInstanceOf(InvalidTransactionAmountException.class); + assertThat(payAccount.getBalance()).isEqualTo(originBalance); + } + + @Test + @DisplayName("0원을 출금하려면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForZeroAmount() { + // given + long amount = 0; + + // when & then + assertThatThrownBy(() -> payAccount.withdraw(amount)) + .isExactlyInstanceOf(InvalidTransactionAmountException.class); + assertThat(payAccount.getBalance()).isEqualTo(originBalance); + } + + @Test + @DisplayName("남은 잔고보다 더 많은 돈을 출금하려면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForInsufficientBalance() { + // given + long amount = originBalance + 1; + + // when & then + assertThatThrownBy(() -> payAccount.withdraw(amount)) + .isExactlyInstanceOf(InsufficientBalanceException.class); + assertThat(payAccount.getBalance()).isEqualTo(originBalance); + } + } +} \ No newline at end of file From 3a9cf9a7b3c1d3d5627a04d1e83496db339ecc95 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 01:56:20 +0900 Subject: [PATCH 05/62] =?UTF-8?q?[feat]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=85/=EC=B6=9C=EA=B8=88=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기본 생성자로 생성 시 잔고는 0원으로 셋팅 - 0원 이하의 금액으로 입/출금 진행시 InvalidTransactionAmountException throw - 출금은 자신의 잔고보다 더 많은 금액을 출금시 InsufficientBalanceException throw - 아무런 문제가 없다면 입/출금 진행 뒤 잔고 업데이트 --- .../lab/payaccount/domain/PayAccount.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index c14e01cc..1e61c91a 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -2,6 +2,8 @@ import org.hibernate.annotations.ColumnDefault; +import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; +import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -17,4 +19,33 @@ public class PayAccount { @Column(name = "balance", nullable = false) @ColumnDefault("0") private long balance; + + public PayAccount() { + this.balance = 0; + } + + public long getBalance() { + return this.balance; + } + + public void withdraw(long amount) { + validateTransactionAmount(amount); + validateInsufficientBalance(amount); + this.balance -= amount; + } + + public void deposit(long amount) { + validateTransactionAmount(amount); + this.balance += amount; + } + + private void validateTransactionAmount(long amount) { + if (amount <= 0) + throw new InvalidTransactionAmountException("Transaction amount must be greater than zero."); + } + + private void validateInsufficientBalance(long amount) { + if (this.balance - amount < 0) + throw new InsufficientBalanceException("Insufficient balance for this transaction."); + } } From 56b9970d7b14beb7c7b7d9325837ca618c8fcb07 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 10:35:12 +0900 Subject: [PATCH 06/62] =?UTF-8?q?[feat]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20getId=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PayAccount 고유의 Id를 조회할 일이 많기 때문에 메서드 추가 --- .../java/camp/woowak/lab/payaccount/domain/PayAccount.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 1e61c91a..2aa2ea87 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -24,6 +24,10 @@ public PayAccount() { this.balance = 0; } + public Long getId() { + return id; + } + public long getBalance() { return this.balance; } From f994af5c4ea4dafa06b2071603fef54493e0cc10 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:15:26 +0900 Subject: [PATCH 07/62] =?UTF-8?q?[feat]=20PayAccount=20Repository=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JpaRepository를 extends 하여 생성 --- .../lab/payaccount/repository/PayAccountRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java new file mode 100644 index 00000000..1ca593af --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.payaccount.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.payaccount.domain.PayAccount; + +public interface PayAccountRepository extends JpaRepository { +} From 33f252298d976616a147d6d3391c64f14ce4b149 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:17:18 +0900 Subject: [PATCH 08/62] =?UTF-8?q?[feat]=20PayAccount=EC=97=90=20x-lock?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계좌 잔고와 같은 민감한 데이터는 x-lock을 통해 lock을 걸고 업데이트해야하기 때문에 추가 --- .../lab/payaccount/repository/PayAccountRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java index 1ca593af..0b4559ac 100644 --- a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java +++ b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java @@ -1,8 +1,17 @@ package camp.woowak.lab.payaccount.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import camp.woowak.lab.payaccount.domain.PayAccount; +import jakarta.persistence.LockModeType; public interface PayAccountRepository extends JpaRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT pa FROM PayAccount pa where pa.id = :id") + Optional findByIdForUpdate(@Param("id") Long id); } From 1c99da6e00099c10eea85739411a46d63d0b9de5 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:27:07 +0900 Subject: [PATCH 09/62] =?UTF-8?q?[feat]=20PayAccount=EB=A5=BC=20=EC=B0=BE?= =?UTF-8?q?=EC=A7=80=20=EB=AA=BB=ED=95=9C=20=EA=B2=BD=EC=9A=B0=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20exception=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ID로 찾지 못한 경우에 대한 예외입니다. --- .../lab/payaccount/exception/NotFoundAccountException.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java b/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java new file mode 100644 index 00000000..f7f72806 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.payaccount.exception; + +public class NotFoundAccountException extends RuntimeException { + public NotFoundAccountException(String message) { + super(message); + } +} From 5ebac92756bd85ee707fd44a7ca6cb7ae5c2df41 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:28:16 +0900 Subject: [PATCH 10/62] =?UTF-8?q?[feat]=20AccountTransactionCommand=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계좌에 입/출금을 진행하기 위해 공통적으로 사용되는 매개변수들입니다. - 입/출금을 진행하기 위해서 해당 payAccountId와 얼마를 입/출금할지에 대한 amount 변수가 있습니다. --- .../service/command/AccountTransactionCommand.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java diff --git a/src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java b/src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java new file mode 100644 index 00000000..054b5f4b --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java @@ -0,0 +1,6 @@ +package camp.woowak.lab.payaccount.service.command; + +public record AccountTransactionCommand( + Long payAccountId, + long amount) { +} \ No newline at end of file From 4d87bada2562c1b5ef828512e0010683c1c39b04 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:30:01 +0900 Subject: [PATCH 11/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=20=EC=B6=9C?= =?UTF-8?q?=EA=B8=88=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계좌를 찾은 뒤 요청받은 액수만큼 출금을 진행한 뒤 남은 잔고를 반환합니다. - 계좌가 없으면 NotFoundAccountException을 throw 합니다. --- .../service/PayAccountWithdrawService.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java new file mode 100644 index 00000000..37029c68 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java @@ -0,0 +1,31 @@ +package camp.woowak.lab.payaccount.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class PayAccountWithdrawService { + private final PayAccountRepository payAccountRepository; + + public PayAccountWithdrawService(PayAccountRepository payAccountRepository) { + this.payAccountRepository = payAccountRepository; + } + + @Transactional + public long withdrawAccount(AccountTransactionCommand command) { + PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) + .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); + + payAccount.withdraw(command.amount()); + log.info("A withdrawal of {} has been completed from Account ID {}", command.amount(), command.payAccountId()); + + return payAccount.getBalance(); + } +} From 0371bde61c392012e083321ff004ad6a9e89e7b5 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:30:57 +0900 Subject: [PATCH 12/62] =?UTF-8?q?[test]=20=EC=B6=9C=EA=B8=88=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Account가 존재하는 경우에 대한 성공 처리 테스트코드 작성 - Account가 존재하지 않는 경우에 대한 롤백 처리 테스트코드 작성 --- .../PayAccountWithdrawServiceTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java new file mode 100644 index 00000000..be63ccdf --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java @@ -0,0 +1,78 @@ +package camp.woowak.lab.payaccount.service; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; + +@SpringBootTest +@DisplayName("PayAccountWithdrawService 클래스") +class PayAccountWithdrawServiceTest { + @Autowired + private PayAccountRepository payAccountRepository; + + @Autowired + private PayAccountWithdrawService payAccountWithdrawService; + + private PayAccount payAccount; + private long originBalance = 1000L; + + @BeforeEach + void setUp() throws Exception { + payAccount = new PayAccount(); + payAccount.deposit(originBalance); + payAccountRepository.save(payAccount); + payAccountRepository.flush(); + } + + @Nested + @DisplayName("withdrawAccount 메서드는") + class WithdrawAccount { + @Test + @DisplayName("AccountId와 Amount를 제공하면 잔고에서 출금된다.") + void withdrawAccount() { + //given + long amount = 100L; + AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); + + //when + long afterBalance = payAccountWithdrawService.withdrawAccount(command); + + //then + assertThat(afterBalance).isEqualTo(originBalance - amount); + validateAfterBalanceInPersistenceLayer(payAccount.getId(), afterBalance); + } + + @Test + @DisplayName("없는 AccountId를 호출하면 NotFoundAccountException을 던진다. 잔고는 유지된다.") + void withdrawAccountNotFound() { + //given + Long unknownAccountId = Long.MAX_VALUE; + long amount = 100L; + AccountTransactionCommand command = new AccountTransactionCommand(unknownAccountId, amount); + + //when & then + assertThatThrownBy(() -> payAccountWithdrawService.withdrawAccount(command)) + .isExactlyInstanceOf(NotFoundAccountException.class); + validateAfterBalanceInPersistenceLayer(payAccount.getId(), originBalance); + } + + private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { + Optional byId = payAccountRepository.findById(accountId); + assertThat(byId).isNotEmpty(); + PayAccount targetAccount = byId.get(); + assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); + } + } +} \ No newline at end of file From 2e34cea46b8ed46b2f1acaca8066b72b7aa76f97 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:31:50 +0900 Subject: [PATCH 13/62] =?UTF-8?q?[test]=20=EC=B6=9C=EA=B8=88=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동시에 여러 요청이 들어오더라도 일관적인 결과를 반환하는지에 대한 테스트코드 작성 --- ...AccountWithdrawServiceConcurrencyTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java new file mode 100644 index 00000000..057e48ed --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java @@ -0,0 +1,79 @@ +package camp.woowak.lab.payaccount.service; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; + +@SpringBootTest +@DisplayName("PayAccountWithdrawService 클래스") +class PayAccountWithdrawServiceConcurrencyTest { + @Autowired + private PayAccountRepository payAccountRepository; + + @Autowired + private PayAccountWithdrawService payAccountWithdrawService; + + private PayAccount payAccount; + private final long originBalance = 1000L; + + @BeforeEach + void setUp() throws Exception { + payAccount = new PayAccount(); + payAccount.deposit(originBalance); + payAccountRepository.save(payAccount); + payAccountRepository.flush(); + } + + @Nested + @DisplayName("withdrawAccount 메서드는") + class WithdrawAccount { + @Test + @DisplayName("동시에 여러 요청이 들어오면 요청에 맞게 출금이 모두 완료되어야한다.") + void withdrawAccountWithdrawMultipleRequest() throws InterruptedException { + //given + int multipleRequestCount = 100; + long eachAmount = 1L; + AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), eachAmount); + + ExecutorService executorService = Executors.newFixedThreadPool(multipleRequestCount); + CountDownLatch latch = new CountDownLatch(multipleRequestCount); + + //when + IntStream.range(0, multipleRequestCount) + .forEach(i -> { + executorService.submit(() -> { + payAccountWithdrawService.withdrawAccount(command); + latch.countDown(); + }); + }); + + latch.await(); + + //then + validateAfterBalanceInPersistenceLayer(payAccount.getId(), + originBalance - (multipleRequestCount * eachAmount)); + } + + private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { + Optional byId = payAccountRepository.findById(accountId); + assertThat(byId).isNotEmpty(); + PayAccount targetAccount = byId.get(); + assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); + } + } +} \ No newline at end of file From 48e0658b876bf559b3aa7a8695174bcd254ebf0d Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:32:35 +0900 Subject: [PATCH 14/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=20=EC=9E=85?= =?UTF-8?q?=EA=B8=88=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계좌를 찾은 뒤 요청받은 액수만큼 입금을 진행한 뒤 남은 잔고를 반환합니다. - 계좌가 없으면 NotFoundAccountException을 throw 합니다. --- .../service/PayAccountDepositService.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java new file mode 100644 index 00000000..35ce7b4e --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java @@ -0,0 +1,31 @@ +package camp.woowak.lab.payaccount.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class PayAccountDepositService { + private final PayAccountRepository payAccountRepository; + + public PayAccountDepositService(PayAccountRepository payAccountRepository) { + this.payAccountRepository = payAccountRepository; + } + + @Transactional + public long depositAccount(AccountTransactionCommand command) { + PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) + .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); + + payAccount.deposit(command.amount()); + log.info("A deposit of {} has been completed into Account ID {}.", command.amount(), command.payAccountId()); + + return payAccount.getBalance(); + } +} From 3ed17b3095d5e20a9acd9d49775a5e26510c419b Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:32:59 +0900 Subject: [PATCH 15/62] =?UTF-8?q?[test]=20=EC=9E=85=EA=B8=88=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Account가 존재하는 경우에 대한 성공 처리 테스트코드 작성 - Account가 존재하지 않는 경우에 대한 롤백 처리 테스트코드 작성 --- .../service/PayAccountDepositServiceTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java new file mode 100644 index 00000000..deead23b --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java @@ -0,0 +1,78 @@ +package camp.woowak.lab.payaccount.service; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; + +@SpringBootTest +@DisplayName("PayAccountWithdrawService 클래스") +class PayAccountDepositServiceTest { + @Autowired + private PayAccountRepository payAccountRepository; + + @Autowired + private PayAccountDepositService payAccountDepositService; + + private PayAccount payAccount; + private final long originBalance = 1000L; + + @BeforeEach + void setUp() throws Exception { + payAccount = new PayAccount(); + payAccount.deposit(originBalance); + payAccountRepository.save(payAccount); + payAccountRepository.flush(); + } + + @Nested + @DisplayName("withdrawAccount 메서드는") + class WithdrawAccount { + @Test + @DisplayName("AccountId와 Amount를 제공하면 잔고에서 출금된다.") + void withdrawAccount() { + //given + long amount = 100L; + AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); + + //when + long afterBalance = payAccountDepositService.depositAccount(command); + + //then + assertThat(afterBalance).isEqualTo(originBalance + amount); + validateAfterBalanceInPersistenceLayer(payAccount.getId(), afterBalance); + } + + @Test + @DisplayName("없는 AccountId를 호출하면 NotFoundAccountException을 던진다. 잔고는 유지된다.") + void withdrawAccountNotFound() { + //given + Long unknownAccountId = Long.MAX_VALUE; + long amount = 100L; + AccountTransactionCommand command = new AccountTransactionCommand(unknownAccountId, amount); + + //when & then + assertThatThrownBy(() -> payAccountDepositService.depositAccount(command)) + .isExactlyInstanceOf(NotFoundAccountException.class); + validateAfterBalanceInPersistenceLayer(payAccount.getId(), originBalance); + } + + private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { + Optional byId = payAccountRepository.findById(accountId); + assertThat(byId).isNotEmpty(); + PayAccount targetAccount = byId.get(); + assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); + } + } +} \ No newline at end of file From c957024906591e7d48f371735da6053304f1afd6 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 12:33:20 +0900 Subject: [PATCH 16/62] =?UTF-8?q?[test]=20=EC=9E=85=EA=B8=88=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동시에 여러 요청이 들어오더라도 일관적인 결과를 반환하는지에 대한 테스트코드 작성 --- ...yAccountDepositServiceConcurrencyTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java new file mode 100644 index 00000000..026857fa --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java @@ -0,0 +1,79 @@ +package camp.woowak.lab.payaccount.service; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; + +@SpringBootTest +@DisplayName("PayAccountWithdrawService 클래스") +class PayAccountDepositServiceConcurrencyTest { + @Autowired + private PayAccountRepository payAccountRepository; + + @Autowired + private PayAccountDepositService payAccountDepositService; + + private PayAccount payAccount; + private final long originBalance = 1000L; + + @BeforeEach + void setUp() throws Exception { + payAccount = new PayAccount(); + payAccount.deposit(originBalance); + payAccountRepository.save(payAccount); + payAccountRepository.flush(); + } + + @Nested + @DisplayName("withdrawAccount 메서드는") + class WithdrawAccount { + @Test + @DisplayName("동시에 여러 오청이 들어오면 요청에 맞게 출금이 모두 완료되어야한다.") + void withdrawAccountWithdrawMultipleRequest() throws InterruptedException { + //given + int multipleRequestCount = 100; + long eachAmount = 1L; + AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), eachAmount); + + ExecutorService executorService = Executors.newFixedThreadPool(multipleRequestCount); + CountDownLatch latch = new CountDownLatch(multipleRequestCount); + + //when + IntStream.range(0, multipleRequestCount) + .forEach(i -> { + executorService.submit(() -> { + payAccountDepositService.depositAccount(command); + latch.countDown(); + }); + }); + + latch.await(); + + //then + validateAfterBalanceInPersistenceLayer(payAccount.getId(), + originBalance + (multipleRequestCount * eachAmount)); + } + + private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { + Optional byId = payAccountRepository.findById(accountId); + assertThat(byId).isNotEmpty(); + PayAccount targetAccount = byId.get(); + assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); + } + } +} \ No newline at end of file From 0048f61b3b3a075f5cdfe640912d606558d8374a Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:05:01 +0900 Subject: [PATCH 17/62] =?UTF-8?q?[fix]=20deposit=20service=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9D=98=20@DisplayName=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - withdraw 로 표기된것을 deposit으로 수정 --- .../payaccount/service/PayAccountDepositServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java index deead23b..1b7f3a96 100644 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java @@ -17,7 +17,7 @@ import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; @SpringBootTest -@DisplayName("PayAccountWithdrawService 클래스") +@DisplayName("PayAccountDepositService 클래스") class PayAccountDepositServiceTest { @Autowired private PayAccountRepository payAccountRepository; @@ -37,10 +37,10 @@ void setUp() throws Exception { } @Nested - @DisplayName("withdrawAccount 메서드는") + @DisplayName("Deposit 메서드는") class WithdrawAccount { @Test - @DisplayName("AccountId와 Amount를 제공하면 잔고에서 출금된다.") + @DisplayName("AccountId와 Amount를 제공하면 잔고에 입금된다.") void withdrawAccount() { //given long amount = 100L; From 30f78dd969f2a8953791a9e03458c97ad700c4e5 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:05:57 +0900 Subject: [PATCH 18/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=20=EC=9E=85?= =?UTF-8?q?=EC=B6=9C=EA=B8=88=EC=9D=98=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EC=A0=80=EC=9E=A5=ED=95=A0=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=92=EC=9D=84=20enum=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DEPOSIT : 입금 - WITHDRAW : 출금 --- .../woowak/lab/payaccount/domain/AccountTransactionType.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java b/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java new file mode 100644 index 00000000..7ad1e6d5 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java @@ -0,0 +1,5 @@ +package camp.woowak.lab.payaccount.domain; + +public enum AccountTransactionType { + DEPOSIT, WITHDRAW; +} From a61c64d3cfa1795d54ec041bb9b6496338467f63 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:07:57 +0900 Subject: [PATCH 19/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=20=EC=9E=85?= =?UTF-8?q?=EC=B6=9C=EA=B8=88=EC=9D=98=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PayAccount를 외래키로 가지고 있음 - 관계는 1:N - 생성 시간을 JPA에서 관리할 수 있도록 AuditingEntityListener 추가 --- .../java/camp/woowak/lab/LabApplication.java | 2 + .../payaccount/domain/PayAccountHistory.java | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java diff --git a/src/main/java/camp/woowak/lab/LabApplication.java b/src/main/java/camp/woowak/lab/LabApplication.java index d59c7bf8..6042cf05 100644 --- a/src/main/java/camp/woowak/lab/LabApplication.java +++ b/src/main/java/camp/woowak/lab/LabApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class LabApplication { public static void main(String[] args) { diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java new file mode 100644 index 00000000..94e79121 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java @@ -0,0 +1,60 @@ +package camp.woowak.lab.payaccount.domain; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Entity +@EntityListeners(value = AuditingEntityListener.class) +public class PayAccountHistory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private PayAccount payAccount; + + @Column + private long amount; + + @Enumerated(value = EnumType.STRING) + private AccountTransactionType type; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + public PayAccountHistory() { + } + + public PayAccountHistory(PayAccount payAccount, long amount, AccountTransactionType type) { + this.payAccount = payAccount; + this.amount = amount; + this.type = type; + } + + public Long getId() { + return id; + } + + public long getAmount() { + return amount; + } + + public AccountTransactionType getType() { + return type; + } +} From 079a8420d6b220483d7516623f36a99ca1e16865 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:08:55 +0900 Subject: [PATCH 20/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=85/=EC=B6=9C=EA=B8=88=EC=9D=84=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=ED=95=A0=20=EB=95=8C=20History=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=B4=EC=84=9C=20return=20=ED=95=B4=EC=A3=BC?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입금과 출금은 항상 기록을 남겨야하기 때문에 필요하다고 생각 --- .../camp/woowak/lab/payaccount/domain/PayAccount.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 2aa2ea87..8118dfe6 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -32,15 +32,19 @@ public long getBalance() { return this.balance; } - public void withdraw(long amount) { + public PayAccountHistory withdraw(long amount) { validateTransactionAmount(amount); validateInsufficientBalance(amount); this.balance -= amount; + + return new PayAccountHistory(this, amount, AccountTransactionType.WITHDRAW); } - public void deposit(long amount) { + public PayAccountHistory deposit(long amount) { validateTransactionAmount(amount); this.balance += amount; + + return new PayAccountHistory(this, amount, AccountTransactionType.DEPOSIT); } private void validateTransactionAmount(long amount) { From e5f20232ea5ae9b643ddbefb710c14c2c773bf18 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:09:34 +0900 Subject: [PATCH 21/62] =?UTF-8?q?[test]=20=EA=B3=84=EC=A2=8C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=85/=EC=B6=9C=EA=B8=88=EC=9D=84=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=ED=95=A0=20=EB=95=8C=20History=EB=A5=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=B4=EC=84=9C=20return=20=ED=95=B4=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=EA=B2=83=EC=9D=84=20=EA=B2=80=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - history에 상태와 값이 잘 저장되는지 확인 --- .../camp/woowak/lab/payaccount/domain/PayAccountTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java index df73efc1..b88c3c6b 100644 --- a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java +++ b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java @@ -40,10 +40,12 @@ void increaseBalanceByDepositedAmount() { long amount = 1000; // when - payAccount.deposit(amount); + PayAccountHistory depositHistory = payAccount.deposit(amount); // then assertThat(payAccount.getBalance()).isEqualTo(amount); + assertThat(depositHistory.getAmount()).isEqualTo(amount); + assertThat(depositHistory.getType()).isEqualTo(AccountTransactionType.DEPOSIT); } @Test @@ -88,10 +90,12 @@ void decreaseBalanceByWithdrawnAmount() { long withdrawAmount = 100; // when - payAccount.withdraw(withdrawAmount); + PayAccountHistory withdrawHistory = payAccount.withdraw(withdrawAmount); // then assertThat(payAccount.getBalance()).isEqualTo(originBalance - withdrawAmount); + assertThat(withdrawHistory.getAmount()).isEqualTo(withdrawAmount); + assertThat(withdrawHistory.getType()).isEqualTo(AccountTransactionType.WITHDRAW); } @Test From 212c23bb63a7aed484274917cd48120823913473 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:10:31 +0900 Subject: [PATCH 22/62] =?UTF-8?q?[feat]=20account=20history=EB=A5=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?repository=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JpaRepository를 extends 하여 생성 --- .../repository/PayAccountHistoryRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/repository/PayAccountHistoryRepository.java diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountHistoryRepository.java b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountHistoryRepository.java new file mode 100644 index 00000000..e4d8613a --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountHistoryRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.payaccount.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.payaccount.domain.PayAccountHistory; + +public interface PayAccountHistoryRepository extends JpaRepository { +} From 8b683826787c2e764fbe2babfb78d0a6280a43d4 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 21:11:27 +0900 Subject: [PATCH 23/62] =?UTF-8?q?[feat]=20deposit=EA=B3=BC=20withdraw=20se?= =?UTF-8?q?rvice=EC=97=90=20history=EB=A5=BC=20=EB=B0=9B=EC=95=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입/출금 서비스는 항상 기록을 남겨야하기 때문에 추가 --- .../payaccount/service/PayAccountDepositService.java | 10 ++++++++-- .../payaccount/service/PayAccountWithdrawService.java | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java index 35ce7b4e..fdbdb80a 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java @@ -4,7 +4,9 @@ import org.springframework.transaction.annotation.Transactional; import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.PayAccountHistory; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountHistoryRepository; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; import lombok.extern.slf4j.Slf4j; @@ -13,9 +15,12 @@ @Slf4j public class PayAccountDepositService { private final PayAccountRepository payAccountRepository; + private final PayAccountHistoryRepository payAccountHistoryRepository; - public PayAccountDepositService(PayAccountRepository payAccountRepository) { + public PayAccountDepositService(PayAccountRepository payAccountRepository, + PayAccountHistoryRepository payAccountHistoryRepository) { this.payAccountRepository = payAccountRepository; + this.payAccountHistoryRepository = payAccountHistoryRepository; } @Transactional @@ -23,8 +28,9 @@ public long depositAccount(AccountTransactionCommand command) { PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); - payAccount.deposit(command.amount()); + PayAccountHistory depositHistory = payAccount.deposit(command.amount()); log.info("A deposit of {} has been completed into Account ID {}.", command.amount(), command.payAccountId()); + payAccountHistoryRepository.save(depositHistory); return payAccount.getBalance(); } diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java index 37029c68..490d1e29 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java @@ -4,7 +4,9 @@ import org.springframework.transaction.annotation.Transactional; import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.PayAccountHistory; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountHistoryRepository; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; import lombok.extern.slf4j.Slf4j; @@ -13,9 +15,12 @@ @Slf4j public class PayAccountWithdrawService { private final PayAccountRepository payAccountRepository; + private final PayAccountHistoryRepository payAccountHistoryRepository; - public PayAccountWithdrawService(PayAccountRepository payAccountRepository) { + public PayAccountWithdrawService(PayAccountRepository payAccountRepository, + PayAccountHistoryRepository payAccountHistoryRepository) { this.payAccountRepository = payAccountRepository; + this.payAccountHistoryRepository = payAccountHistoryRepository; } @Transactional @@ -23,8 +28,9 @@ public long withdrawAccount(AccountTransactionCommand command) { PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); - payAccount.withdraw(command.amount()); + PayAccountHistory withdrawHistory = payAccount.withdraw(command.amount()); log.info("A withdrawal of {} has been completed from Account ID {}", command.amount(), command.payAccountId()); + payAccountHistoryRepository.save(withdrawHistory); return payAccount.getBalance(); } From 7014f8682c3264df1e64cfb67179096042736af2 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 22:04:42 +0900 Subject: [PATCH 24/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=EC=97=90=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?API=20Controller=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API End-Point를 알맞게 조절해야할 필요가 있음 --- .../payaccount/PayAccountApiController.java | 44 +++++++++++++++++++ .../payaccount/PayAccountChargeRequest.java | 9 ++++ .../payaccount/PayAccountChargeResponse.java | 6 +++ 3 files changed, 59 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/response/payaccount/PayAccountChargeResponse.java diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java new file mode 100644 index 00000000..cb2d0eca --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java @@ -0,0 +1,44 @@ +package camp.woowak.lab.web.api.payaccount; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import camp.woowak.lab.payaccount.service.PayAccountDepositService; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; +import camp.woowak.lab.web.dto.response.payaccount.PayAccountChargeResponse; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/account") +@Slf4j +public class PayAccountApiController { + private final PayAccountDepositService payAccountDepositService; + + public PayAccountApiController(PayAccountDepositService payAccountDepositService) { + this.payAccountDepositService = payAccountDepositService; + } + + /** + * TODO 1. api end-point 설계 논의 + * TODO 2. 인증 방법에 대한 유저 구분값 가져오는 방법 논의 + * TODO 3. API Response Format에 대한 논의 후 적용 필요 + */ + @PostMapping("/{payAccountId}/charge") + public ResponseEntity payAccountCharge(@PathVariable("payAccountId") Long payAccountId, + @Validated @RequestBody PayAccountChargeRequest request) { + AccountTransactionCommand command = new AccountTransactionCommand(payAccountId, request.amount()); + log.info("Pay account charge request received. Account ID: {}, Charge Amount: {}", payAccountId, + request.amount()); + + long remainBalance = payAccountDepositService.depositAccount(command); + log.info("Charge successful. Account ID: {}, New Balance: {}", payAccountId, remainBalance); + + return ResponseEntity.ok(new PayAccountChargeResponse(remainBalance)); + } +} diff --git a/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java new file mode 100644 index 00000000..05542d5b --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.web.dto.request.payaccount; + +import jakarta.validation.constraints.NotNull; + +public record PayAccountChargeRequest( + @NotNull(message="amount can't be null") + Long amount +) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/payaccount/PayAccountChargeResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/payaccount/PayAccountChargeResponse.java new file mode 100644 index 00000000..ea0b6f32 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/payaccount/PayAccountChargeResponse.java @@ -0,0 +1,6 @@ +package camp.woowak.lab.web.dto.response.payaccount; + +public record PayAccountChargeResponse( + long balance +) { +} From f1cdf4f07131bbe51b69fcf7788235ad57319154 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 22:05:33 +0900 Subject: [PATCH 25/62] =?UTF-8?q?[feat]=20=EA=B3=84=EC=A2=8C=EC=97=90=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?API=20Controller=EC=97=90=EC=84=9C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20exception=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=B4=20=EC=B2=98=EB=A6=AC=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20Advice=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재는 api format과 예외처리에 대한 논의가 진행되지 않은 상태라 논의 후 리팩토링 --- .../PayAccountApiControllerAdvice.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java new file mode 100644 index 00000000..86dbf4ee --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java @@ -0,0 +1,50 @@ +package camp.woowak.lab.web.api.payaccount; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; +import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import lombok.extern.slf4j.Slf4j; + +//TODO : exception구체화 및 error code 정의 +@RestControllerAdvice(basePackageClasses = PayAccountApiController.class) +@Slf4j +public class PayAccountApiControllerAdvice { + @ExceptionHandler(value = MethodArgumentNotValidException.class) + public ResponseEntity bindingException(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError)error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + log.warn("Bad Request with parameter binding : {}", errors); + return ResponseEntity.badRequest().build(); + } + + @ExceptionHandler(value = {InsufficientBalanceException.class, InvalidTransactionAmountException.class}) + public ResponseEntity badRequestException(Exception e) { + log.warn("Bad Request", e); + return ResponseEntity.badRequest().build(); + } + + @ExceptionHandler(value = {NotFoundAccountException.class}) + public ResponseEntity notFoundAccountException(Exception e) { + log.warn("Not Found Request", e); + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(value = {Exception.class}) + public ResponseEntity internalServerException(Exception e) { + log.error("Internal Server Error", e); + return ResponseEntity.internalServerError().build(); + } +} From f6ad7208a5bf1362fb4c68f13b0214fc9b78fe66 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 22:08:20 +0900 Subject: [PATCH 26/62] =?UTF-8?q?[test]=20=EA=B3=84=EC=A2=8C=EC=97=90=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?API=20Controller=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정상 충전되는 경우에 대한 테스트 - account Id가 null이 들어올 경우 400 - 존재하지 않는 Id가 들어온 경우 404 - amount가 음수 혹은 0이 들어온다면 400 --- .../web/api/PayAccountApiControllerTest.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java new file mode 100644 index 00000000..11aab8de --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -0,0 +1,124 @@ +package camp.woowak.lab.web.api; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; +import jakarta.persistence.EntityManager; + +@AutoConfigureMockMvc +@SpringBootTest +@DisplayName("PayAccountApiController 클래스") +class PayAccountApiControllerTest { + private final String BASE_URL = "/account/"; + @Autowired + private MockMvc mvc; + @Autowired + private PayAccountRepository payAccountRepository; + + @Autowired + private EntityManager em; + + @Autowired + private ObjectMapper objectMapper; + + private PayAccount payAccount; + private long originBalance; + + @BeforeEach + void setUp() throws Exception { + originBalance = 1000L; + payAccount = new PayAccount(); + payAccount.deposit(originBalance); + payAccountRepository.save(payAccount); + em.detach(payAccount); + } + + private void verificationPersistedBalance(Long payAccountId, long amount) { + Optional byId = payAccountRepository.findById(payAccountId); + assertThat(byId).isPresent(); + PayAccount persistedAccount = byId.get(); + assertThat(persistedAccount.getBalance()).isEqualTo(amount); + } + + @Nested + @DisplayName("충전 요청은") + class PayAccountChargeAPITest { + @Test + @DisplayName("존재하는 계정 ID에 정상범위의 금액을 입력하면 충전된다.") + void successTest() throws Exception { + //given + long amount = 1000L; + PayAccountChargeRequest command = new PayAccountChargeRequest(amount); + + //when & then + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsBytes(command))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.balance").value(amount + originBalance)); + + verificationPersistedBalance(payAccount.getId(), amount + originBalance); + } + + //TODO : 아직 API Response Format이 정해지지 않았으므로, 논의 후 추가 + @Test + @DisplayName("존재하지 않는 계정 ID를 입력하면 404를 return한다.") + void notExistsAccountIdTest() throws Exception { + //given + long amount = 1000L; + Long notExistsId = Long.MAX_VALUE; + PayAccountChargeRequest command = new PayAccountChargeRequest(amount); + + //when & then + mvc.perform(post(BASE_URL + notExistsId + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsBytes(command))) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("요청 Amount가 Null인 경우 400을 return한다. 기존 잔고는 유지된다.") + void nullAmountTest() throws Exception { + //given + PayAccountChargeRequest command = new PayAccountChargeRequest(null); + + //when & then + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsBytes(command))) + .andExpect(status().isBadRequest()); + + verificationPersistedBalance(payAccount.getId(), originBalance); + } + + @Test + @DisplayName("요청 Amount가 음수인 경우 400을 return한다. 기존 잔고는 유지된다.") + void negativeAmountTest() throws Exception { + //given + long amount = -1L; + PayAccountChargeRequest command = new PayAccountChargeRequest(amount); + + //when & then + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsBytes(command))) + .andExpect(status().isBadRequest()); + + verificationPersistedBalance(payAccount.getId(), originBalance); + } + } +} From 54b0820240660d89ea067d7d1f99f1e57d9b5d66 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 22:27:30 +0900 Subject: [PATCH 27/62] =?UTF-8?q?[chore]=20.gitkeep=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/camp/woowak/lab/payaccount/repository/.gitkeep | 0 src/main/java/camp/woowak/lab/payaccount/service/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/camp/woowak/lab/payaccount/repository/.gitkeep delete mode 100644 src/main/java/camp/woowak/lab/payaccount/service/.gitkeep diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/.gitkeep b/src/main/java/camp/woowak/lab/payaccount/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/payaccount/service/.gitkeep b/src/main/java/camp/woowak/lab/payaccount/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 From f34aed0264396530838b60ed4970a14fb28f2df3 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 22:28:11 +0900 Subject: [PATCH 28/62] =?UTF-8?q?[chore]=20PayAccountChargeRequest?= =?UTF-8?q?=EC=9D=98=20format=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공통 포멧으로 재 적용 --- .../lab/web/dto/request/payaccount/PayAccountChargeRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java index 05542d5b..46cdcd20 100644 --- a/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java +++ b/src/main/java/camp/woowak/lab/web/dto/request/payaccount/PayAccountChargeRequest.java @@ -3,7 +3,7 @@ import jakarta.validation.constraints.NotNull; public record PayAccountChargeRequest( - @NotNull(message="amount can't be null") + @NotNull(message = "amount can't be null") Long amount ) { } From 30c80ab7428080791b14b81236b5e1b1f990efe6 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Sun, 11 Aug 2024 22:50:18 +0900 Subject: [PATCH 29/62] =?UTF-8?q?[chore]=20chaining=20method=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=8E=B8=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lab/web/api/PayAccountApiControllerTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index 11aab8de..7afd231b 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -69,7 +69,8 @@ void successTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command))) .andExpect(status().isOk()) .andExpect(jsonPath("$.balance").value(amount + originBalance)); @@ -87,7 +88,8 @@ void notExistsAccountIdTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + notExistsId + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + mvc.perform(post(BASE_URL + notExistsId + "/charge") + .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command))) .andExpect(status().isNotFound()); } @@ -99,7 +101,8 @@ void nullAmountTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(null); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command))) .andExpect(status().isBadRequest()); @@ -114,7 +117,8 @@ void negativeAmountTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge").contentType(MediaType.APPLICATION_JSON_VALUE) + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command))) .andExpect(status().isBadRequest()); From 20f5755c0534d56e1e7771877ec285e1d843c355 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Mon, 12 Aug 2024 23:51:54 +0900 Subject: [PATCH 30/62] =?UTF-8?q?[feat]=20AccountTransactionType=EC=97=90?= =?UTF-8?q?=20CHARGE=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 충전에 대한 HISTORY를 입/출금과 나눌 필요성을 느끼게 되어서 추가 --- .../woowak/lab/payaccount/domain/AccountTransactionType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java b/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java index 7ad1e6d5..d56d91ec 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/AccountTransactionType.java @@ -1,5 +1,5 @@ package camp.woowak.lab.payaccount.domain; public enum AccountTransactionType { - DEPOSIT, WITHDRAW; + DEPOSIT, WITHDRAW, CHARGE; } From 77d9c14c89b00571488d0360b98f13cd135ea4fb Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Mon, 12 Aug 2024 23:54:12 +0900 Subject: [PATCH 31/62] =?UTF-8?q?[feat]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20charge=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 입/출금과 충전에 대한 히스토리를 격리시켜야 할 필요성을 느끼게되어 메서드 추가 --- .../java/camp/woowak/lab/payaccount/domain/PayAccount.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 8118dfe6..e2cb0113 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -47,6 +47,13 @@ public PayAccountHistory deposit(long amount) { return new PayAccountHistory(this, amount, AccountTransactionType.DEPOSIT); } + public PayAccountHistory charge(long amount) { + validateTransactionAmount(amount); + this.balance += amount; + + return new PayAccountHistory(this, amount, AccountTransactionType.CHARGE); + } + private void validateTransactionAmount(long amount) { if (amount <= 0) throw new InvalidTransactionAmountException("Transaction amount must be greater than zero."); From 15a6278276e67061b128638bee78833bac54d34d Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:25:00 +0900 Subject: [PATCH 32/62] =?UTF-8?q?[feat]=20=EC=9D=BC=EC=9D=BC=20=ED=95=9C?= =?UTF-8?q?=EB=8F=84=20=EC=B4=88=EA=B3=BC=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?Exception=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일일 충전 한도 100만원을 초과했을 시 던져지는 exception입니다. --- .../payaccount/exception/DailyLimitExceededException.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java b/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java new file mode 100644 index 00000000..1366d961 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java @@ -0,0 +1,7 @@ +package camp.woowak.lab.payaccount.exception; + +public class DailyLimitExceededException extends RuntimeException { + public DailyLimitExceededException(String message) { + super(message); + } +} From b1cda7f4f8591fe84ca1a75b1c24b73be55b19ac Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:38:24 +0900 Subject: [PATCH 33/62] =?UTF-8?q?[feat]=20PayAccountHistory=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=97=90=20cre?= =?UTF-8?q?atedAt=EC=9D=98=20defaultValue=EB=A5=BC=20now()=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20Getter=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일일 한도에 대한 비즈니스 로직을 위해 추가 --- .../woowak/lab/payaccount/domain/PayAccountHistory.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java index 94e79121..ec2a90c3 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccountHistory.java @@ -35,7 +35,7 @@ public class PayAccountHistory { @CreatedDate @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; + private LocalDateTime createdAt = LocalDateTime.now(); public PayAccountHistory() { } @@ -57,4 +57,8 @@ public long getAmount() { public AccountTransactionType getType() { return type; } + + public LocalDateTime getCreatedAt() { + return createdAt; + } } From 5273aa89dd37056d27e9a8373936c90a58895f44 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:40:59 +0900 Subject: [PATCH 34/62] =?UTF-8?q?[feat]=20PayAccount=20->=20PayAccountHist?= =?UTF-8?q?ory=EB=A1=9C=20=ED=96=A5=ED=95=98=EB=8A=94=20=EB=8B=A8=EB=B0=A9?= =?UTF-8?q?=ED=96=A5=20=EB=A7=A4=ED=95=91=20=EC=B6=94=EA=B0=80=20=ED=9B=84?= =?UTF-8?q?=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반대방향 단방향 매핑을 추가함으로써 도메인 객체 내부에서 입/출금, 충전에 대한 History를 공통 로직으로 저장할 수 있게됨 --- .../lab/payaccount/domain/PayAccount.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index e2cb0113..9657d7cb 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -1,14 +1,20 @@ package camp.woowak.lab.payaccount.domain; +import java.util.ArrayList; +import java.util.List; + import org.hibernate.annotations.ColumnDefault; import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; @Entity public class PayAccount { @@ -20,6 +26,9 @@ public class PayAccount { @ColumnDefault("0") private long balance; + @OneToMany(mappedBy = "payAccount", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private List history = new ArrayList<>(); + public PayAccount() { this.balance = 0; } @@ -37,21 +46,28 @@ public PayAccountHistory withdraw(long amount) { validateInsufficientBalance(amount); this.balance -= amount; - return new PayAccountHistory(this, amount, AccountTransactionType.WITHDRAW); + return issueAndSavePayAccountHistory(amount, AccountTransactionType.WITHDRAW); } public PayAccountHistory deposit(long amount) { validateTransactionAmount(amount); this.balance += amount; - return new PayAccountHistory(this, amount, AccountTransactionType.DEPOSIT); + return issueAndSavePayAccountHistory(amount, AccountTransactionType.DEPOSIT); } public PayAccountHistory charge(long amount) { validateTransactionAmount(amount); this.balance += amount; - return new PayAccountHistory(this, amount, AccountTransactionType.CHARGE); + return issueAndSavePayAccountHistory(amount, AccountTransactionType.CHARGE); + } + + private PayAccountHistory issueAndSavePayAccountHistory(long amount, AccountTransactionType type) { + PayAccountHistory payAccountHistory = new PayAccountHistory(this, amount, type); + this.history.add(payAccountHistory); + + return payAccountHistory; } private void validateTransactionAmount(long amount) { From 9d520419ff9133d4fbb46bf5cfae69eea2052425 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:42:34 +0900 Subject: [PATCH 35/62] =?UTF-8?q?[feat]=20charge=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EC=97=90=20=EC=9D=BC=EC=9D=BC=20=ED=95=9C=EB=8F=84?= =?UTF-8?q?=EB=A5=BC=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일일 한도는 100만원이기 때문에, 초과하면 DailyLimitExceededException을 던진다. --- .../lab/payaccount/domain/PayAccount.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 9657d7cb..415b9cf9 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -1,10 +1,12 @@ package camp.woowak.lab.payaccount.domain; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import org.hibernate.annotations.ColumnDefault; +import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; import jakarta.persistence.CascadeType; @@ -58,6 +60,7 @@ public PayAccountHistory deposit(long amount) { public PayAccountHistory charge(long amount) { validateTransactionAmount(amount); + validateDailyChargeLimit(amount); this.balance += amount; return issueAndSavePayAccountHistory(amount, AccountTransactionType.CHARGE); @@ -70,6 +73,21 @@ private PayAccountHistory issueAndSavePayAccountHistory(long amount, AccountTran return payAccountHistory; } + private void validateDailyChargeLimit(long amount) { + LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1); + + long todayTotalCharge = history.stream() + .filter(h -> h.getType() == AccountTransactionType.CHARGE) + .filter(h -> h.getCreatedAt().isAfter(startOfDay) && h.getCreatedAt().isBefore(endOfDay)) + .mapToLong(PayAccountHistory::getAmount) + .sum(); + + if (todayTotalCharge + amount > 1_000_000) { + throw new DailyLimitExceededException("Daily charge limit of " + 1_000_000 + " exceeded."); + } + } + private void validateTransactionAmount(long amount) { if (amount <= 0) throw new InvalidTransactionAmountException("Transaction amount must be greater than zero."); From 581f4c5a3138d8440022aefbc6cbf580ba7ebaa3 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:44:25 +0900 Subject: [PATCH 36/62] =?UTF-8?q?[test]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=9D=98=20charge=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 입/출금과 공통되는 테스트 추가 - 일일 한도 100만원을 초과하는 예외에 대한 테스트 추가 --- .../lab/payaccount/domain/PayAccountTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java index b88c3c6b..1c7cecac 100644 --- a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java +++ b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; @@ -134,4 +135,61 @@ void throwExceptionForInsufficientBalance() { assertThat(payAccount.getBalance()).isEqualTo(originBalance); } } + + @Nested + @DisplayName("Charge 메서드는") + class ChargeTest { + private long dailyMaximumChargeAmount = 1_000_000L; + + @Test + @DisplayName("충전한 만큼 잔고가 증가한다.") + void increaseBalanceByDepositedAmount() { + // given + long amount = 1000; + + // when + PayAccountHistory depositHistory = payAccount.charge(amount); + + // then + assertThat(payAccount.getBalance()).isEqualTo(amount); + assertThat(depositHistory.getAmount()).isEqualTo(amount); + assertThat(depositHistory.getType()).isEqualTo(AccountTransactionType.DEPOSIT); + } + + @Test + @DisplayName("일일 한도를 초과한 경우, DailyLimitExceededException을 던진다.") + void dailyLimitExceededException() { + //given + payAccount.charge(dailyMaximumChargeAmount); + long amount = 1000L; + + //when & then + assertThatThrownBy(() -> payAccount.charge(amount)) + .isExactlyInstanceOf(DailyLimitExceededException.class); + } + + @Test + @DisplayName("음수를 입금하려하면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForNegativeAmount() { + // given + long amount = -1000; + + // when & then + assertThatThrownBy(() -> payAccount.charge(amount)) + .isExactlyInstanceOf(InvalidTransactionAmountException.class); + assertThat(payAccount.getBalance()).isZero(); + } + + @Test + @DisplayName("0원을 입금하려면 exception을 던진다. 잔고는 유지된다.") + void throwExceptionForZeroAmount() { + // given + long amount = 0; + + // when & then + assertThatThrownBy(() -> payAccount.charge(amount)) + .isExactlyInstanceOf(InvalidTransactionAmountException.class); + assertThat(payAccount.getBalance()).isZero(); + } + } } \ No newline at end of file From ec96339b74c088ead7d757a7fa075d1eca771297 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:46:25 +0900 Subject: [PATCH 37/62] =?UTF-8?q?[refactor]=20domain=EC=97=90=EC=84=9C=20h?= =?UTF-8?q?istory=EB=A5=BC=20=EC=A0=80=EC=9E=A5=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EB=95=8C=EB=AC=B8=EC=97=90=20service=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EC=97=90=EC=84=9C=20history=EB=A5=BC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - history를 저장하는 책임을 도메인에게 넘겨버렸기 때문에 필요가 없어졌습니다. --- .../lab/payaccount/service/PayAccountDepositService.java | 7 +------ .../lab/payaccount/service/PayAccountWithdrawService.java | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java index fdbdb80a..65b1c2a7 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java @@ -6,7 +6,6 @@ import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.domain.PayAccountHistory; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; -import camp.woowak.lab.payaccount.repository.PayAccountHistoryRepository; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; import lombok.extern.slf4j.Slf4j; @@ -15,12 +14,9 @@ @Slf4j public class PayAccountDepositService { private final PayAccountRepository payAccountRepository; - private final PayAccountHistoryRepository payAccountHistoryRepository; - public PayAccountDepositService(PayAccountRepository payAccountRepository, - PayAccountHistoryRepository payAccountHistoryRepository) { + public PayAccountDepositService(PayAccountRepository payAccountRepository) { this.payAccountRepository = payAccountRepository; - this.payAccountHistoryRepository = payAccountHistoryRepository; } @Transactional @@ -30,7 +26,6 @@ public long depositAccount(AccountTransactionCommand command) { PayAccountHistory depositHistory = payAccount.deposit(command.amount()); log.info("A deposit of {} has been completed into Account ID {}.", command.amount(), command.payAccountId()); - payAccountHistoryRepository.save(depositHistory); return payAccount.getBalance(); } diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java index 490d1e29..23409d7d 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java @@ -6,7 +6,6 @@ import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.domain.PayAccountHistory; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; -import camp.woowak.lab.payaccount.repository.PayAccountHistoryRepository; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; import lombok.extern.slf4j.Slf4j; @@ -15,12 +14,9 @@ @Slf4j public class PayAccountWithdrawService { private final PayAccountRepository payAccountRepository; - private final PayAccountHistoryRepository payAccountHistoryRepository; - public PayAccountWithdrawService(PayAccountRepository payAccountRepository, - PayAccountHistoryRepository payAccountHistoryRepository) { + public PayAccountWithdrawService(PayAccountRepository payAccountRepository) { this.payAccountRepository = payAccountRepository; - this.payAccountHistoryRepository = payAccountHistoryRepository; } @Transactional @@ -30,7 +26,6 @@ public long withdrawAccount(AccountTransactionCommand command) { PayAccountHistory withdrawHistory = payAccount.withdraw(command.amount()); log.info("A withdrawal of {} has been completed from Account ID {}", command.amount(), command.payAccountId()); - payAccountHistoryRepository.save(withdrawHistory); return payAccount.getBalance(); } From e4cb60d7e9648a1e81d43c59debcb785e91cb456 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:52:48 +0900 Subject: [PATCH 38/62] =?UTF-8?q?[feat]=20PayAccountChargeService=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 입/출금 service와 charge를 구분하기 위해서 새로운 charge service 구현 --- .../service/PayAccountChargeService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java new file mode 100644 index 00000000..6a35a015 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java @@ -0,0 +1,32 @@ +package camp.woowak.lab.payaccount.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.domain.PayAccountHistory; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class PayAccountChargeService { + private final PayAccountRepository payAccountRepository; + + public PayAccountChargeService(PayAccountRepository payAccountRepository) { + this.payAccountRepository = payAccountRepository; + } + + @Transactional + public long chargeAccount(AccountTransactionCommand command) { + PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) + .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); + + PayAccountHistory chargeHistory = payAccount.charge(command.amount()); + log.info("A Charge of {} has been completed from Account ID {}", command.amount(), command.payAccountId()); + + return payAccount.getBalance(); + } +} From 30be760caa5ccbb6bfeb8083406e846711421869 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 00:53:23 +0900 Subject: [PATCH 39/62] =?UTF-8?q?[test]=20PayAccountChargeService=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 입/출금 service와 동일한 테스트 케이스 - 추가적으로 일일 한도 초과에 대한 테스트 코드 작성 --- .../service/PayAccountChargeServiceTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java new file mode 100644 index 00000000..03e51bac --- /dev/null +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java @@ -0,0 +1,93 @@ +package camp.woowak.lab.payaccount.service; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; + +@SpringBootTest +@DisplayName("PayAccountChargeService 클래스") +class PayAccountChargeServiceTest { + @Autowired + private PayAccountRepository payAccountRepository; + + @Autowired + private PayAccountChargeService payAccountChargeService; + + private PayAccount payAccount; + private final long originBalance = 1000L; + + @BeforeEach + void setUp() throws Exception { + payAccount = new PayAccount(); + payAccount.deposit(originBalance); + payAccountRepository.save(payAccount); + payAccountRepository.flush(); + } + + @Nested + @DisplayName("ChargeAccount 메서드는") + class WithdrawAccount { + @Test + @DisplayName("AccountId와 Amount를 제공하면 잔고에 입금된다.") + void withdrawAccount() { + //given + long amount = 100L; + AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); + + //when + long afterBalance = payAccountChargeService.chargeAccount(command); + + //then + assertThat(afterBalance).isEqualTo(originBalance + amount); + validateAfterBalanceInPersistenceLayer(payAccount.getId(), afterBalance); + } + + @Test + @DisplayName("없는 AccountId를 호출하면 NotFoundAccountException을 던진다. 잔고는 유지된다.") + void withdrawAccountNotFound() { + //given + Long unknownAccountId = Long.MAX_VALUE; + long amount = 100L; + AccountTransactionCommand command = new AccountTransactionCommand(unknownAccountId, amount); + + //when & then + assertThatThrownBy(() -> payAccountChargeService.chargeAccount(command)) + .isExactlyInstanceOf(NotFoundAccountException.class); + validateAfterBalanceInPersistenceLayer(payAccount.getId(), originBalance); + } + + @Test + @DisplayName("일일 한도 100만원을 초과해서 충전하면 DailyLimitExceededException을 던진다.") + void dailyLimitExceeded() { + //given + long dailyLimit = 1_000_000L; + long amount = 1000L; + payAccountChargeService.chargeAccount(new AccountTransactionCommand(payAccount.getId(), dailyLimit)); + AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); + + //when & then + assertThatThrownBy(() -> payAccountChargeService.chargeAccount(command)) + .isExactlyInstanceOf(DailyLimitExceededException.class); + } + + private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { + Optional byId = payAccountRepository.findById(accountId); + assertThat(byId).isNotEmpty(); + PayAccount targetAccount = byId.get(); + assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); + } + } +} \ No newline at end of file From 60dcd8a2f04d83091d77514d17c67a3be8308480 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 01:00:07 +0900 Subject: [PATCH 40/62] =?UTF-8?q?[feat]=20PayAccountAPIController=EC=9D=98?= =?UTF-8?q?=20PayAccountDepositService=EB=A5=BC=20PayAccountChargeService?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 금액 충전에 대한 서비스를 의존하도록 변경 --- .../web/api/payaccount/PayAccountApiController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java index cb2d0eca..77d30c83 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import camp.woowak.lab.payaccount.service.PayAccountDepositService; +import camp.woowak.lab.payaccount.service.PayAccountChargeService; import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; import camp.woowak.lab.web.dto.response.payaccount.PayAccountChargeResponse; @@ -18,10 +18,10 @@ @RequestMapping("/account") @Slf4j public class PayAccountApiController { - private final PayAccountDepositService payAccountDepositService; + private final PayAccountChargeService payAccountChargeService; - public PayAccountApiController(PayAccountDepositService payAccountDepositService) { - this.payAccountDepositService = payAccountDepositService; + public PayAccountApiController(PayAccountChargeService payAccountChargeService) { + this.payAccountChargeService = payAccountChargeService; } /** @@ -36,7 +36,7 @@ public ResponseEntity payAccountCharge(@PathVariable(" log.info("Pay account charge request received. Account ID: {}, Charge Amount: {}", payAccountId, request.amount()); - long remainBalance = payAccountDepositService.depositAccount(command); + long remainBalance = payAccountChargeService.chargeAccount(command); log.info("Charge successful. Account ID: {}, New Balance: {}", payAccountId, remainBalance); return ResponseEntity.ok(new PayAccountChargeResponse(remainBalance)); From 3949d6545c9811c3c9e3ff367d01a652abefbbde Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 01:01:03 +0900 Subject: [PATCH 41/62] =?UTF-8?q?[feat]=20@DomainExceptionHandler=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20DailyLimitExceededException?= =?UTF-8?q?=EC=9D=84=20bad=20request=EC=97=90=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일일 한도 초과 exception은 bad-request로 매핑되어야한다고 생각합니다. --- .../web/api/payaccount/PayAccountApiControllerAdvice.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java index 86dbf4ee..71eee9eb 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java @@ -7,15 +7,16 @@ import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; +import camp.woowak.lab.common.advice.DomainExceptionHandler; +import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import lombok.extern.slf4j.Slf4j; //TODO : exception구체화 및 error code 정의 -@RestControllerAdvice(basePackageClasses = PayAccountApiController.class) +@DomainExceptionHandler(basePackageClasses = PayAccountApiController.class) @Slf4j public class PayAccountApiControllerAdvice { @ExceptionHandler(value = MethodArgumentNotValidException.class) @@ -30,7 +31,8 @@ public ResponseEntity bindingException(MethodArgumentNotValidException ex) { return ResponseEntity.badRequest().build(); } - @ExceptionHandler(value = {InsufficientBalanceException.class, InvalidTransactionAmountException.class}) + @ExceptionHandler(value = {DailyLimitExceededException.class, InsufficientBalanceException.class, + InvalidTransactionAmountException.class}) public ResponseEntity badRequestException(Exception e) { log.warn("Bad Request", e); return ResponseEntity.badRequest().build(); From 3b4bf544c54606ba8893c6d5b4f5f15d21606e02 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 01:03:27 +0900 Subject: [PATCH 42/62] =?UTF-8?q?[fix]=20PayAccount=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=9D=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 복사 붙여넣기의 부작용으로 인해 CHARGE에서 DEPOSIT이라고 검증한 오류를 CHARGE로 수정 --- .../java/camp/woowak/lab/payaccount/domain/PayAccountTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java index 1c7cecac..2e271e37 100644 --- a/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java +++ b/src/test/java/camp/woowak/lab/payaccount/domain/PayAccountTest.java @@ -153,7 +153,7 @@ void increaseBalanceByDepositedAmount() { // then assertThat(payAccount.getBalance()).isEqualTo(amount); assertThat(depositHistory.getAmount()).isEqualTo(amount); - assertThat(depositHistory.getType()).isEqualTo(AccountTransactionType.DEPOSIT); + assertThat(depositHistory.getType()).isEqualTo(AccountTransactionType.CHARGE); } @Test From d73de8194c2362a29b66f4da3126677433b40099 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 01:05:30 +0900 Subject: [PATCH 43/62] =?UTF-8?q?[test]=20PayAccountApiController=EC=9D=98?= =?UTF-8?q?=20=EC=B6=A9=EC=A0=84=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일일 한도 100만원을 초과할 시 bad request를 응답하는것을 검증하는 테스트코드 추가 --- .../web/api/PayAccountApiControllerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index 7afd231b..46b132be 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -20,6 +20,8 @@ import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.repository.PayAccountRepository; +import camp.woowak.lab.payaccount.service.PayAccountChargeService; +import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; import jakarta.persistence.EntityManager; @@ -32,6 +34,8 @@ class PayAccountApiControllerTest { private MockMvc mvc; @Autowired private PayAccountRepository payAccountRepository; + @Autowired + private PayAccountChargeService payAccountChargeService; @Autowired private EntityManager em; @@ -61,6 +65,8 @@ private void verificationPersistedBalance(Long payAccountId, long amount) { @Nested @DisplayName("충전 요청은") class PayAccountChargeAPITest { + private final long DAILY_LIMIT = 1_000_000L; + @Test @DisplayName("존재하는 계정 ID에 정상범위의 금액을 입력하면 충전된다.") void successTest() throws Exception { @@ -78,6 +84,21 @@ void successTest() throws Exception { verificationPersistedBalance(payAccount.getId(), amount + originBalance); } + @Test + @DisplayName("일일 한도(100만원) 이상인 경우, 400을 return한다.") + void dailyLimitExceededTest() throws Exception { + //given + long amount = 1000L; + payAccountChargeService.chargeAccount(new AccountTransactionCommand(payAccount.getId(), DAILY_LIMIT)); + PayAccountChargeRequest command = new PayAccountChargeRequest(amount); + + //when & then + mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsBytes(command))) + .andExpect(status().isBadRequest()); + } + //TODO : 아직 API Response Format이 정해지지 않았으므로, 논의 후 추가 @Test @DisplayName("존재하지 않는 계정 ID를 입력하면 404를 return한다.") From 9fd2f2be7fcd447cb21a24be12b2e5d229b72174 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 22:04:42 +0900 Subject: [PATCH 44/62] =?UTF-8?q?[fix]=20=ED=98=84=EC=9E=AC=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=EC=9D=98=20=EB=B2=94=EC=9C=84=EB=A5=BC=20=EB=84=98?= =?UTF-8?q?=EC=96=B4=EC=84=A0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - withdraw와 deposit은 충전에 해당하지 않는다는 판단하에 제거 --- .../service/PayAccountDepositService.java | 32 -------- .../service/PayAccountWithdrawService.java | 32 -------- ...yAccountDepositServiceConcurrencyTest.java | 79 ------------------- .../service/PayAccountDepositServiceTest.java | 78 ------------------ ...AccountWithdrawServiceConcurrencyTest.java | 79 ------------------- .../PayAccountWithdrawServiceTest.java | 78 ------------------ 6 files changed, 378 deletions(-) delete mode 100644 src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java delete mode 100644 src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java delete mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java delete mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java delete mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java delete mode 100644 src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java deleted file mode 100644 index 65b1c2a7..00000000 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountDepositService.java +++ /dev/null @@ -1,32 +0,0 @@ -package camp.woowak.lab.payaccount.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import camp.woowak.lab.payaccount.domain.PayAccount; -import camp.woowak.lab.payaccount.domain.PayAccountHistory; -import camp.woowak.lab.payaccount.exception.NotFoundAccountException; -import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; -import lombok.extern.slf4j.Slf4j; - -@Service -@Slf4j -public class PayAccountDepositService { - private final PayAccountRepository payAccountRepository; - - public PayAccountDepositService(PayAccountRepository payAccountRepository) { - this.payAccountRepository = payAccountRepository; - } - - @Transactional - public long depositAccount(AccountTransactionCommand command) { - PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) - .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); - - PayAccountHistory depositHistory = payAccount.deposit(command.amount()); - log.info("A deposit of {} has been completed into Account ID {}.", command.amount(), command.payAccountId()); - - return payAccount.getBalance(); - } -} diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java deleted file mode 100644 index 23409d7d..00000000 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawService.java +++ /dev/null @@ -1,32 +0,0 @@ -package camp.woowak.lab.payaccount.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import camp.woowak.lab.payaccount.domain.PayAccount; -import camp.woowak.lab.payaccount.domain.PayAccountHistory; -import camp.woowak.lab.payaccount.exception.NotFoundAccountException; -import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; -import lombok.extern.slf4j.Slf4j; - -@Service -@Slf4j -public class PayAccountWithdrawService { - private final PayAccountRepository payAccountRepository; - - public PayAccountWithdrawService(PayAccountRepository payAccountRepository) { - this.payAccountRepository = payAccountRepository; - } - - @Transactional - public long withdrawAccount(AccountTransactionCommand command) { - PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) - .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); - - PayAccountHistory withdrawHistory = payAccount.withdraw(command.amount()); - log.info("A withdrawal of {} has been completed from Account ID {}", command.amount(), command.payAccountId()); - - return payAccount.getBalance(); - } -} diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java deleted file mode 100644 index 026857fa..00000000 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceConcurrencyTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package camp.woowak.lab.payaccount.service; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.IntStream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import camp.woowak.lab.payaccount.domain.PayAccount; -import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; - -@SpringBootTest -@DisplayName("PayAccountWithdrawService 클래스") -class PayAccountDepositServiceConcurrencyTest { - @Autowired - private PayAccountRepository payAccountRepository; - - @Autowired - private PayAccountDepositService payAccountDepositService; - - private PayAccount payAccount; - private final long originBalance = 1000L; - - @BeforeEach - void setUp() throws Exception { - payAccount = new PayAccount(); - payAccount.deposit(originBalance); - payAccountRepository.save(payAccount); - payAccountRepository.flush(); - } - - @Nested - @DisplayName("withdrawAccount 메서드는") - class WithdrawAccount { - @Test - @DisplayName("동시에 여러 오청이 들어오면 요청에 맞게 출금이 모두 완료되어야한다.") - void withdrawAccountWithdrawMultipleRequest() throws InterruptedException { - //given - int multipleRequestCount = 100; - long eachAmount = 1L; - AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), eachAmount); - - ExecutorService executorService = Executors.newFixedThreadPool(multipleRequestCount); - CountDownLatch latch = new CountDownLatch(multipleRequestCount); - - //when - IntStream.range(0, multipleRequestCount) - .forEach(i -> { - executorService.submit(() -> { - payAccountDepositService.depositAccount(command); - latch.countDown(); - }); - }); - - latch.await(); - - //then - validateAfterBalanceInPersistenceLayer(payAccount.getId(), - originBalance + (multipleRequestCount * eachAmount)); - } - - private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { - Optional byId = payAccountRepository.findById(accountId); - assertThat(byId).isNotEmpty(); - PayAccount targetAccount = byId.get(); - assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); - } - } -} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java deleted file mode 100644 index 1b7f3a96..00000000 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountDepositServiceTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package camp.woowak.lab.payaccount.service; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Optional; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import camp.woowak.lab.payaccount.domain.PayAccount; -import camp.woowak.lab.payaccount.exception.NotFoundAccountException; -import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; - -@SpringBootTest -@DisplayName("PayAccountDepositService 클래스") -class PayAccountDepositServiceTest { - @Autowired - private PayAccountRepository payAccountRepository; - - @Autowired - private PayAccountDepositService payAccountDepositService; - - private PayAccount payAccount; - private final long originBalance = 1000L; - - @BeforeEach - void setUp() throws Exception { - payAccount = new PayAccount(); - payAccount.deposit(originBalance); - payAccountRepository.save(payAccount); - payAccountRepository.flush(); - } - - @Nested - @DisplayName("Deposit 메서드는") - class WithdrawAccount { - @Test - @DisplayName("AccountId와 Amount를 제공하면 잔고에 입금된다.") - void withdrawAccount() { - //given - long amount = 100L; - AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); - - //when - long afterBalance = payAccountDepositService.depositAccount(command); - - //then - assertThat(afterBalance).isEqualTo(originBalance + amount); - validateAfterBalanceInPersistenceLayer(payAccount.getId(), afterBalance); - } - - @Test - @DisplayName("없는 AccountId를 호출하면 NotFoundAccountException을 던진다. 잔고는 유지된다.") - void withdrawAccountNotFound() { - //given - Long unknownAccountId = Long.MAX_VALUE; - long amount = 100L; - AccountTransactionCommand command = new AccountTransactionCommand(unknownAccountId, amount); - - //when & then - assertThatThrownBy(() -> payAccountDepositService.depositAccount(command)) - .isExactlyInstanceOf(NotFoundAccountException.class); - validateAfterBalanceInPersistenceLayer(payAccount.getId(), originBalance); - } - - private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { - Optional byId = payAccountRepository.findById(accountId); - assertThat(byId).isNotEmpty(); - PayAccount targetAccount = byId.get(); - assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); - } - } -} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java deleted file mode 100644 index 057e48ed..00000000 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceConcurrencyTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package camp.woowak.lab.payaccount.service; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.IntStream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import camp.woowak.lab.payaccount.domain.PayAccount; -import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; - -@SpringBootTest -@DisplayName("PayAccountWithdrawService 클래스") -class PayAccountWithdrawServiceConcurrencyTest { - @Autowired - private PayAccountRepository payAccountRepository; - - @Autowired - private PayAccountWithdrawService payAccountWithdrawService; - - private PayAccount payAccount; - private final long originBalance = 1000L; - - @BeforeEach - void setUp() throws Exception { - payAccount = new PayAccount(); - payAccount.deposit(originBalance); - payAccountRepository.save(payAccount); - payAccountRepository.flush(); - } - - @Nested - @DisplayName("withdrawAccount 메서드는") - class WithdrawAccount { - @Test - @DisplayName("동시에 여러 요청이 들어오면 요청에 맞게 출금이 모두 완료되어야한다.") - void withdrawAccountWithdrawMultipleRequest() throws InterruptedException { - //given - int multipleRequestCount = 100; - long eachAmount = 1L; - AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), eachAmount); - - ExecutorService executorService = Executors.newFixedThreadPool(multipleRequestCount); - CountDownLatch latch = new CountDownLatch(multipleRequestCount); - - //when - IntStream.range(0, multipleRequestCount) - .forEach(i -> { - executorService.submit(() -> { - payAccountWithdrawService.withdrawAccount(command); - latch.countDown(); - }); - }); - - latch.await(); - - //then - validateAfterBalanceInPersistenceLayer(payAccount.getId(), - originBalance - (multipleRequestCount * eachAmount)); - } - - private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { - Optional byId = payAccountRepository.findById(accountId); - assertThat(byId).isNotEmpty(); - PayAccount targetAccount = byId.get(); - assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); - } - } -} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java deleted file mode 100644 index be63ccdf..00000000 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountWithdrawServiceTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package camp.woowak.lab.payaccount.service; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Optional; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import camp.woowak.lab.payaccount.domain.PayAccount; -import camp.woowak.lab.payaccount.exception.NotFoundAccountException; -import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; - -@SpringBootTest -@DisplayName("PayAccountWithdrawService 클래스") -class PayAccountWithdrawServiceTest { - @Autowired - private PayAccountRepository payAccountRepository; - - @Autowired - private PayAccountWithdrawService payAccountWithdrawService; - - private PayAccount payAccount; - private long originBalance = 1000L; - - @BeforeEach - void setUp() throws Exception { - payAccount = new PayAccount(); - payAccount.deposit(originBalance); - payAccountRepository.save(payAccount); - payAccountRepository.flush(); - } - - @Nested - @DisplayName("withdrawAccount 메서드는") - class WithdrawAccount { - @Test - @DisplayName("AccountId와 Amount를 제공하면 잔고에서 출금된다.") - void withdrawAccount() { - //given - long amount = 100L; - AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); - - //when - long afterBalance = payAccountWithdrawService.withdrawAccount(command); - - //then - assertThat(afterBalance).isEqualTo(originBalance - amount); - validateAfterBalanceInPersistenceLayer(payAccount.getId(), afterBalance); - } - - @Test - @DisplayName("없는 AccountId를 호출하면 NotFoundAccountException을 던진다. 잔고는 유지된다.") - void withdrawAccountNotFound() { - //given - Long unknownAccountId = Long.MAX_VALUE; - long amount = 100L; - AccountTransactionCommand command = new AccountTransactionCommand(unknownAccountId, amount); - - //when & then - assertThatThrownBy(() -> payAccountWithdrawService.withdrawAccount(command)) - .isExactlyInstanceOf(NotFoundAccountException.class); - validateAfterBalanceInPersistenceLayer(payAccount.getId(), originBalance); - } - - private void validateAfterBalanceInPersistenceLayer(Long accountId, long afterBalance) { - Optional byId = payAccountRepository.findById(accountId); - assertThat(byId).isNotEmpty(); - PayAccount targetAccount = byId.get(); - assertThat(targetAccount.getBalance()).isEqualTo(afterBalance); - } - } -} \ No newline at end of file From fc2fa5e91e58bcf8863e6dbe0297b2dcdd72f578 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:14:46 +0900 Subject: [PATCH 45/62] =?UTF-8?q?[fix]=20API=20Response=20format=EC=97=90?= =?UTF-8?q?=20=EB=A7=9E=EA=B2=8C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API Response 형식을 회의 후 결정하여 포멧에 맞게 던져야함 --- .../web/api/payaccount/PayAccountApiController.java | 10 +++++++--- .../lab/web/api/PayAccountApiControllerTest.java | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java index 77d30c83..a1585d65 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java @@ -1,5 +1,6 @@ package camp.woowak.lab.web.api.payaccount; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PathVariable; @@ -10,6 +11,8 @@ import camp.woowak.lab.payaccount.service.PayAccountChargeService; import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import camp.woowak.lab.web.api.utils.APIResponse; +import camp.woowak.lab.web.api.utils.APIUtils; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; import camp.woowak.lab.web.dto.response.payaccount.PayAccountChargeResponse; import lombok.extern.slf4j.Slf4j; @@ -30,8 +33,9 @@ public PayAccountApiController(PayAccountChargeService payAccountChargeService) * TODO 3. API Response Format에 대한 논의 후 적용 필요 */ @PostMapping("/{payAccountId}/charge") - public ResponseEntity payAccountCharge(@PathVariable("payAccountId") Long payAccountId, - @Validated @RequestBody PayAccountChargeRequest request) { + public ResponseEntity> payAccountCharge( + @PathVariable("payAccountId") Long payAccountId, + @Validated @RequestBody PayAccountChargeRequest request) { AccountTransactionCommand command = new AccountTransactionCommand(payAccountId, request.amount()); log.info("Pay account charge request received. Account ID: {}, Charge Amount: {}", payAccountId, request.amount()); @@ -39,6 +43,6 @@ public ResponseEntity payAccountCharge(@PathVariable(" long remainBalance = payAccountChargeService.chargeAccount(command); log.info("Charge successful. Account ID: {}, New Balance: {}", payAccountId, remainBalance); - return ResponseEntity.ok(new PayAccountChargeResponse(remainBalance)); + return APIUtils.of(HttpStatus.OK, new PayAccountChargeResponse(remainBalance)); } } diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index 46b132be..5c40b5ce 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -79,7 +80,8 @@ void successTest() throws Exception { .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.balance").value(amount + originBalance)); + .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data.balance").value(amount + originBalance)); verificationPersistedBalance(payAccount.getId(), amount + originBalance); } From c7a576ed1ddc8b29d8105ea8e6c80686a42bce30 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:17:38 +0900 Subject: [PATCH 46/62] =?UTF-8?q?[fix]=20AccountTransactionCommand=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20PayAccountChargeCommand=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deposit과 withdraw가 사라지고, 충전에 대한 command라는것을 확실히 명시하기 위해 이름 변경 --- .../payaccount/service/PayAccountChargeService.java | 4 ++-- ...actionCommand.java => PayAccountChargeCommand.java} | 2 +- .../web/api/payaccount/PayAccountApiController.java | 4 ++-- .../service/PayAccountChargeServiceTest.java | 10 +++++----- .../lab/web/api/PayAccountApiControllerTest.java | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) rename src/main/java/camp/woowak/lab/payaccount/service/command/{AccountTransactionCommand.java => PayAccountChargeCommand.java} (68%) diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java index 6a35a015..7e1fdc7f 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java @@ -7,7 +7,7 @@ import camp.woowak.lab.payaccount.domain.PayAccountHistory; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; import lombok.extern.slf4j.Slf4j; @Service @@ -20,7 +20,7 @@ public PayAccountChargeService(PayAccountRepository payAccountRepository) { } @Transactional - public long chargeAccount(AccountTransactionCommand command) { + public long chargeAccount(PayAccountChargeCommand command) { PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); diff --git a/src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java b/src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java similarity index 68% rename from src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java rename to src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java index 054b5f4b..ec265048 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/command/AccountTransactionCommand.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java @@ -1,6 +1,6 @@ package camp.woowak.lab.payaccount.service.command; -public record AccountTransactionCommand( +public record PayAccountChargeCommand( Long payAccountId, long amount) { } \ No newline at end of file diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java index a1585d65..73779400 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import camp.woowak.lab.payaccount.service.PayAccountChargeService; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; import camp.woowak.lab.web.api.utils.APIResponse; import camp.woowak.lab.web.api.utils.APIUtils; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; @@ -36,7 +36,7 @@ public PayAccountApiController(PayAccountChargeService payAccountChargeService) public ResponseEntity> payAccountCharge( @PathVariable("payAccountId") Long payAccountId, @Validated @RequestBody PayAccountChargeRequest request) { - AccountTransactionCommand command = new AccountTransactionCommand(payAccountId, request.amount()); + PayAccountChargeCommand command = new PayAccountChargeCommand(payAccountId, request.amount()); log.info("Pay account charge request received. Account ID: {}, Charge Amount: {}", payAccountId, request.amount()); diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java index 03e51bac..ce5cb284 100644 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java @@ -15,7 +15,7 @@ import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import camp.woowak.lab.payaccount.repository.PayAccountRepository; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; @SpringBootTest @DisplayName("PayAccountChargeService 클래스") @@ -45,7 +45,7 @@ class WithdrawAccount { void withdrawAccount() { //given long amount = 100L; - AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); + PayAccountChargeCommand command = new PayAccountChargeCommand(payAccount.getId(), amount); //when long afterBalance = payAccountChargeService.chargeAccount(command); @@ -61,7 +61,7 @@ void withdrawAccountNotFound() { //given Long unknownAccountId = Long.MAX_VALUE; long amount = 100L; - AccountTransactionCommand command = new AccountTransactionCommand(unknownAccountId, amount); + PayAccountChargeCommand command = new PayAccountChargeCommand(unknownAccountId, amount); //when & then assertThatThrownBy(() -> payAccountChargeService.chargeAccount(command)) @@ -75,8 +75,8 @@ void dailyLimitExceeded() { //given long dailyLimit = 1_000_000L; long amount = 1000L; - payAccountChargeService.chargeAccount(new AccountTransactionCommand(payAccount.getId(), dailyLimit)); - AccountTransactionCommand command = new AccountTransactionCommand(payAccount.getId(), amount); + payAccountChargeService.chargeAccount(new PayAccountChargeCommand(payAccount.getId(), dailyLimit)); + PayAccountChargeCommand command = new PayAccountChargeCommand(payAccount.getId(), amount); //when & then assertThatThrownBy(() -> payAccountChargeService.chargeAccount(command)) diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index 5c40b5ce..3f4d2412 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -22,7 +22,7 @@ import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.PayAccountChargeService; -import camp.woowak.lab.payaccount.service.command.AccountTransactionCommand; +import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; import jakarta.persistence.EntityManager; @@ -91,7 +91,7 @@ void successTest() throws Exception { void dailyLimitExceededTest() throws Exception { //given long amount = 1000L; - payAccountChargeService.chargeAccount(new AccountTransactionCommand(payAccount.getId(), DAILY_LIMIT)); + payAccountChargeService.chargeAccount(new PayAccountChargeCommand(payAccount.getId(), DAILY_LIMIT)); PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then From 9364157c6efc4af6a1b5f89b93e66445e989bb48 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:18:37 +0900 Subject: [PATCH 47/62] =?UTF-8?q?[fix]=20ChargeCommand=EC=97=90=EC=84=9C?= =?UTF-8?q?=20payAccountId=20=EB=8C=80=EC=8B=A0=20customerId=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 충전은 customer만 가능하기 때문에, customerId를 받도록 수정 --- .../lab/payaccount/service/command/PayAccountChargeCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java b/src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java index ec265048..a1c46ff8 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/command/PayAccountChargeCommand.java @@ -1,6 +1,6 @@ package camp.woowak.lab.payaccount.service.command; public record PayAccountChargeCommand( - Long payAccountId, + Long customerId, long amount) { } \ No newline at end of file From c0faca6d81f3ad3a11e970f8172be6c86187d7c5 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:21:17 +0900 Subject: [PATCH 48/62] =?UTF-8?q?[fix]=20CustomerId=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EC=84=9C=20PayAccount=EB=A5=BC=20=EC=B0=BE?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 충전을 위해 구매자의 계좌를 찾는 쿼리로 변경 --- .../lab/payaccount/repository/PayAccountRepository.java | 4 ++-- .../lab/payaccount/service/PayAccountChargeService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java index 0b4559ac..054c2393 100644 --- a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java +++ b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java @@ -12,6 +12,6 @@ public interface PayAccountRepository extends JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT pa FROM PayAccount pa where pa.id = :id") - Optional findByIdForUpdate(@Param("id") Long id); + @Query("SELECT pa FROM PayAccount pa LEFT JOIN Customer c on pa where c.id = :customerId") + Optional findByCustomerIdForUpdate(@Param("id") Long customerId); } diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java index 7e1fdc7f..c08d8f82 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java @@ -21,7 +21,7 @@ public PayAccountChargeService(PayAccountRepository payAccountRepository) { @Transactional public long chargeAccount(PayAccountChargeCommand command) { - PayAccount payAccount = payAccountRepository.findByIdForUpdate(command.payAccountId()) + PayAccount payAccount = payAccountRepository.findByCustomerIdForUpdate(command.payAccountId()) .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); PayAccountHistory chargeHistory = payAccount.charge(command.amount()); From d2794b80096397f37f39eb8bccfde81069df1c14 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:22:09 +0900 Subject: [PATCH 49/62] =?UTF-8?q?[fix]=20CustomerId=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EC=84=9C=20PayAccount=EB=A5=BC=20=EC=B0=BE?= =?UTF-8?q?=EC=9D=80=20=EB=92=A4=20=EC=B6=A9=EC=A0=84=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lab/payaccount/service/PayAccountChargeService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java index c08d8f82..6bdc6b78 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java @@ -21,11 +21,11 @@ public PayAccountChargeService(PayAccountRepository payAccountRepository) { @Transactional public long chargeAccount(PayAccountChargeCommand command) { - PayAccount payAccount = payAccountRepository.findByCustomerIdForUpdate(command.payAccountId()) - .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.payAccountId())); + PayAccount payAccount = payAccountRepository.findByCustomerIdForUpdate(command.customerId()) + .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.customerId())); PayAccountHistory chargeHistory = payAccount.charge(command.amount()); - log.info("A Charge of {} has been completed from Account ID {}", command.amount(), command.payAccountId()); + log.info("A Charge of {} has been completed from Account ID {}", command.amount(), payAccount.getId()); return payAccount.getBalance(); } From c592ccb7e1e6b583ffd23fab2f3e0ff264742174 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:37:30 +0900 Subject: [PATCH 50/62] =?UTF-8?q?[fix]=20PayAccountRepository=EC=97=90?= =?UTF-8?q?=EC=84=9C=20CustomerId=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EA=B3=84=EC=A2=8C=EB=A5=BC=20=EC=B0=BE=EB=8A=94=20?= =?UTF-8?q?Query=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @Param 에서 id => customerId로 수정 - Join 조건절 수정 --- .../lab/payaccount/repository/PayAccountRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java index 054c2393..22120a4c 100644 --- a/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java +++ b/src/main/java/camp/woowak/lab/payaccount/repository/PayAccountRepository.java @@ -12,6 +12,6 @@ public interface PayAccountRepository extends JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT pa FROM PayAccount pa LEFT JOIN Customer c on pa where c.id = :customerId") - Optional findByCustomerIdForUpdate(@Param("id") Long customerId); + @Query("SELECT pa FROM PayAccount pa LEFT JOIN Customer c on c.payAccount = pa where c.id = :customerId") + Optional findByCustomerIdForUpdate(@Param("customerId") Long customerId); } From 13301d7386325f407ac66110d7a0468773130d25 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:38:26 +0900 Subject: [PATCH 51/62] =?UTF-8?q?[feat]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=B4=20Customer=EC=9D=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20=EB=B0=8F=20Repository=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트에서 setup시에 customer의 정보를 저장할 필요가 있어서 추가 --- .../java/camp/woowak/lab/customer/domain/Customer.java | 7 +++++++ .../lab/customer/repository/CustomerRepository.java | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java diff --git a/src/main/java/camp/woowak/lab/customer/domain/Customer.java b/src/main/java/camp/woowak/lab/customer/domain/Customer.java index d3fc6511..30c36e60 100644 --- a/src/main/java/camp/woowak/lab/customer/domain/Customer.java +++ b/src/main/java/camp/woowak/lab/customer/domain/Customer.java @@ -10,4 +10,11 @@ public class Customer { private Long id; @OneToOne(fetch = FetchType.LAZY) private PayAccount payAccount; + + public Customer(PayAccount payAccount) { + this.payAccount = payAccount; + } + public Long getId(){ + return id; + } } diff --git a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java new file mode 100644 index 00000000..32b96ef2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.customer.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.customer.domain.Customer; + +public interface CustomerRepository extends JpaRepository { +} From a74c1e18681bbfd75f425a08c4b0ea1d36c1f9bc Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:39:06 +0900 Subject: [PATCH 52/62] =?UTF-8?q?[fix]=20Command=EC=9D=98=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20PayAccountId=20->=20CustomerId=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=9C=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PayAccountChargeServiceTest.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java index ce5cb284..a0f52128 100644 --- a/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java +++ b/src/test/java/camp/woowak/lab/payaccount/service/PayAccountChargeServiceTest.java @@ -11,21 +11,28 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.repository.CustomerRepository; import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; +import jakarta.transaction.Transactional; @SpringBootTest @DisplayName("PayAccountChargeService 클래스") +@Transactional class PayAccountChargeServiceTest { @Autowired private PayAccountRepository payAccountRepository; + @Autowired + private CustomerRepository customerRepository; @Autowired private PayAccountChargeService payAccountChargeService; + private Customer customer; private PayAccount payAccount; private final long originBalance = 1000L; @@ -33,8 +40,10 @@ class PayAccountChargeServiceTest { void setUp() throws Exception { payAccount = new PayAccount(); payAccount.deposit(originBalance); - payAccountRepository.save(payAccount); - payAccountRepository.flush(); + payAccountRepository.saveAndFlush(payAccount); + + customer = new Customer(payAccount); + customerRepository.saveAndFlush(customer); } @Nested @@ -45,7 +54,7 @@ class WithdrawAccount { void withdrawAccount() { //given long amount = 100L; - PayAccountChargeCommand command = new PayAccountChargeCommand(payAccount.getId(), amount); + PayAccountChargeCommand command = new PayAccountChargeCommand(customer.getId(), amount); //when long afterBalance = payAccountChargeService.chargeAccount(command); @@ -75,8 +84,8 @@ void dailyLimitExceeded() { //given long dailyLimit = 1_000_000L; long amount = 1000L; - payAccountChargeService.chargeAccount(new PayAccountChargeCommand(payAccount.getId(), dailyLimit)); - PayAccountChargeCommand command = new PayAccountChargeCommand(payAccount.getId(), amount); + payAccountChargeService.chargeAccount(new PayAccountChargeCommand(customer.getId(), dailyLimit)); + PayAccountChargeCommand command = new PayAccountChargeCommand(customer.getId(), amount); //when & then assertThatThrownBy(() -> payAccountChargeService.chargeAccount(command)) From 6c6e338ebfc3b67a1c21e9e59ad715307f685c31 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Tue, 13 Aug 2024 23:40:45 +0900 Subject: [PATCH 53/62] =?UTF-8?q?[fix]=20=EC=84=B8=EC=85=98=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=9C=20=EC=9D=B8=EC=A6=9D=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EC=9D=98=20CustomerId=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/api/payaccount/PayAccountApiController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java index 73779400..72f6c43f 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java @@ -3,7 +3,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,6 +12,8 @@ import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; import camp.woowak.lab.web.api.utils.APIResponse; import camp.woowak.lab.web.api.utils.APIUtils; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; import camp.woowak.lab.web.dto.response.payaccount.PayAccountChargeResponse; import lombok.extern.slf4j.Slf4j; @@ -32,16 +33,16 @@ public PayAccountApiController(PayAccountChargeService payAccountChargeService) * TODO 2. 인증 방법에 대한 유저 구분값 가져오는 방법 논의 * TODO 3. API Response Format에 대한 논의 후 적용 필요 */ - @PostMapping("/{payAccountId}/charge") + @PostMapping("/charge") public ResponseEntity> payAccountCharge( - @PathVariable("payAccountId") Long payAccountId, + @AuthenticationPrincipal LoginCustomer loginCustomer, @Validated @RequestBody PayAccountChargeRequest request) { - PayAccountChargeCommand command = new PayAccountChargeCommand(payAccountId, request.amount()); - log.info("Pay account charge request received. Account ID: {}, Charge Amount: {}", payAccountId, + PayAccountChargeCommand command = new PayAccountChargeCommand(loginCustomer.getId(), request.amount()); + log.info("Pay account charge request received. Account Owner ID: {}, Charge Amount: {}", loginCustomer.getId(), request.amount()); long remainBalance = payAccountChargeService.chargeAccount(command); - log.info("Charge successful. Account ID: {}, New Balance: {}", payAccountId, remainBalance); + log.info("Charge successful. Account Owner ID: {}, New Balance: {}", loginCustomer.getId(), remainBalance); return APIUtils.of(HttpStatus.OK, new PayAccountChargeResponse(remainBalance)); } From 281f6b2a33f53089f1854ec49b1132f8ed477647 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 00:39:26 +0900 Subject: [PATCH 54/62] =?UTF-8?q?[feat]=20=ED=98=84=EC=9E=AC=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=EC=97=90=EC=84=9C=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EB=A1=9C=20ArgumentResolver=EB=A5=BC=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../camp/woowak/lab/web/config/WebConfig.java | 23 +++++++++++++++++++ .../SessionCustomerArgumentResolver.java | 3 +++ .../SessionVendorArgumentResolver.java | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/web/config/WebConfig.java diff --git a/src/main/java/camp/woowak/lab/web/config/WebConfig.java b/src/main/java/camp/woowak/lab/web/config/WebConfig.java new file mode 100644 index 00000000..1c4374ba --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/config/WebConfig.java @@ -0,0 +1,23 @@ +package camp.woowak.lab.web.config; + +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import camp.woowak.lab.web.resolver.session.SessionCustomerArgumentResolver; +import camp.woowak.lab.web.resolver.session.SessionVendorArgumentResolver; +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + private final SessionCustomerArgumentResolver sessionCustomerArgumentResolver; + private final SessionVendorArgumentResolver sessionVendorArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.addAll(List.of(sessionCustomerArgumentResolver, sessionVendorArgumentResolver)); + } +} diff --git a/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java b/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java index a172b231..bf934972 100644 --- a/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java +++ b/src/main/java/camp/woowak/lab/web/resolver/session/SessionCustomerArgumentResolver.java @@ -1,6 +1,7 @@ package camp.woowak.lab.web.resolver.session; import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; @@ -11,7 +12,9 @@ import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +@Component public class SessionCustomerArgumentResolver extends LoginMemberArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java b/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java index c6cafbfd..d12c50ac 100644 --- a/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java +++ b/src/main/java/camp/woowak/lab/web/resolver/session/SessionVendorArgumentResolver.java @@ -1,6 +1,7 @@ package camp.woowak.lab.web.resolver.session; import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; @@ -12,6 +13,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +@Component public class SessionVendorArgumentResolver extends LoginMemberArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { From f519ed676e3c9ee6f9a43355fb49d5345686ebe6 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 00:40:53 +0900 Subject: [PATCH 55/62] =?UTF-8?q?[fix]=20=EC=84=B8=EC=85=98=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=9C=20=EC=9D=B8=EC=A6=9D=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EB=A1=9C=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C=20=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/api/PayAccountApiControllerTest.java | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index 3f4d2412..70d16a76 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -6,6 +6,7 @@ import java.util.Optional; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -15,35 +16,39 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; import com.fasterxml.jackson.databind.ObjectMapper; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.repository.CustomerRepository; import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.PayAccountChargeService; import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; +import camp.woowak.lab.web.authentication.LoginCustomer; import camp.woowak.lab.web.dto.request.payaccount.PayAccountChargeRequest; -import jakarta.persistence.EntityManager; +import camp.woowak.lab.web.resolver.session.SessionConst; @AutoConfigureMockMvc @SpringBootTest @DisplayName("PayAccountApiController 클래스") class PayAccountApiControllerTest { - private final String BASE_URL = "/account/"; @Autowired private MockMvc mvc; @Autowired private PayAccountRepository payAccountRepository; @Autowired - private PayAccountChargeService payAccountChargeService; - + private CustomerRepository customerRepository; @Autowired - private EntityManager em; + private PayAccountChargeService payAccountChargeService; @Autowired private ObjectMapper objectMapper; + private MockHttpSession session; + private Customer customer; private PayAccount payAccount; private long originBalance; @@ -52,8 +57,18 @@ void setUp() throws Exception { originBalance = 1000L; payAccount = new PayAccount(); payAccount.deposit(originBalance); - payAccountRepository.save(payAccount); - em.detach(payAccount); + payAccountRepository.saveAndFlush(payAccount); + + customer = new Customer(payAccount); + customerRepository.saveAndFlush(customer); + + session = new MockHttpSession(); + session.setAttribute(SessionConst.SESSION_CUSTOMER_KEY, new LoginCustomer(customer.getId())); + } + + @AfterEach + void clearSession() throws Exception { + session.clearAttributes(); } private void verificationPersistedBalance(Long payAccountId, long amount) { @@ -67,6 +82,7 @@ private void verificationPersistedBalance(Long payAccountId, long amount) { @DisplayName("충전 요청은") class PayAccountChargeAPITest { private final long DAILY_LIMIT = 1_000_000L; + private final String BASE_URL = "/account/charge"; @Test @DisplayName("존재하는 계정 ID에 정상범위의 금액을 입력하면 충전된다.") @@ -76,9 +92,10 @@ void successTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(objectMapper.writeValueAsBytes(command))) + .content(objectMapper.writeValueAsBytes(command)) + .session(session)) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) .andExpect(jsonPath("$.data.balance").value(amount + originBalance)); @@ -91,13 +108,14 @@ void successTest() throws Exception { void dailyLimitExceededTest() throws Exception { //given long amount = 1000L; - payAccountChargeService.chargeAccount(new PayAccountChargeCommand(payAccount.getId(), DAILY_LIMIT)); + payAccountChargeService.chargeAccount(new PayAccountChargeCommand(customer.getId(), DAILY_LIMIT)); PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(objectMapper.writeValueAsBytes(command))) + .content(objectMapper.writeValueAsBytes(command)) + .session(session)) .andExpect(status().isBadRequest()); } @@ -108,12 +126,15 @@ void notExistsAccountIdTest() throws Exception { //given long amount = 1000L; Long notExistsId = Long.MAX_VALUE; + MockHttpSession notExistsSession = new MockHttpSession(); + notExistsSession.setAttribute(SessionConst.SESSION_CUSTOMER_KEY, new LoginCustomer(notExistsId)); PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + notExistsId + "/charge") + mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(objectMapper.writeValueAsBytes(command))) + .content(objectMapper.writeValueAsBytes(command)) + .session(notExistsSession)) .andExpect(status().isNotFound()); } @@ -124,9 +145,10 @@ void nullAmountTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(null); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(objectMapper.writeValueAsBytes(command))) + .content(objectMapper.writeValueAsBytes(command)) + .session(session)) .andExpect(status().isBadRequest()); verificationPersistedBalance(payAccount.getId(), originBalance); @@ -140,9 +162,10 @@ void negativeAmountTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL + payAccount.getId() + "/charge") + mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) - .content(objectMapper.writeValueAsBytes(command))) + .content(objectMapper.writeValueAsBytes(command)) + .session(session)) .andExpect(status().isBadRequest()); verificationPersistedBalance(payAccount.getId(), originBalance); From bdf74520089724cd6f596128e6d68d85bd571916 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 00:56:38 +0900 Subject: [PATCH 56/62] =?UTF-8?q?[feat]=20PayAccount=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20ErrorCode=20enum=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PayAccount에서 발생하는 예외에 대한 메세지와 상태값들로 저장 --- .../exception/PayAccountErrorCode.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/payaccount/exception/PayAccountErrorCode.java diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/PayAccountErrorCode.java b/src/main/java/camp/woowak/lab/payaccount/exception/PayAccountErrorCode.java new file mode 100644 index 00000000..827ffeb4 --- /dev/null +++ b/src/main/java/camp/woowak/lab/payaccount/exception/PayAccountErrorCode.java @@ -0,0 +1,37 @@ +package camp.woowak.lab.payaccount.exception; + +import org.springframework.http.HttpStatus; + +import camp.woowak.lab.common.exception.ErrorCode; + +public enum PayAccountErrorCode implements ErrorCode { + INVALID_TRANSACTION_AMOUNT(HttpStatus.BAD_REQUEST, "a_1_1", "금액은 0보다 커야합니다."), + DAILY_LIMIT_EXCEED(HttpStatus.BAD_REQUEST, "a_1_2", "일일 충전 한도 금액을 초과했습니다."), + INSUFFICIENT_BALANCE(HttpStatus.BAD_REQUEST,"a_1_3","금액이 부족합니다."), + ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "a_1_4", "계좌를 찾을 수 없습니다."); + + private final int status; + private final String errorCode; + private final String message; + + PayAccountErrorCode(HttpStatus status, String errorCode, String message) { + this.status = status.value(); + this.errorCode = errorCode; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getMessage() { + return message; + } +} From 454fee14b6f7d623e723cc2e6155bf32f5bfff08 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 01:01:31 +0900 Subject: [PATCH 57/62] =?UTF-8?q?[fix]=20PayAccount=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20Exception=EC=9D=84=20common=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94=20exception=EC=9D=84=20?= =?UTF-8?q?=EC=83=81=EC=86=8D=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 exception에 포함된 error message는 log로 대체 --- .../lab/payaccount/domain/PayAccount.java | 17 ++++++++++++----- .../exception/DailyLimitExceededException.java | 8 +++++--- .../exception/InsufficientBalanceException.java | 8 +++++--- .../InvalidTransactionAmountException.java | 8 +++++--- .../exception/NotFoundAccountException.java | 8 +++++--- .../service/PayAccountChargeService.java | 5 ++++- 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java index 415b9cf9..a21fe768 100644 --- a/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java +++ b/src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java @@ -17,8 +17,10 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import lombok.extern.slf4j.Slf4j; @Entity +@Slf4j public class PayAccount { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -84,17 +86,22 @@ private void validateDailyChargeLimit(long amount) { .sum(); if (todayTotalCharge + amount > 1_000_000) { - throw new DailyLimitExceededException("Daily charge limit of " + 1_000_000 + " exceeded."); + log.warn("Daily charge limit of {} exceeded.", 1_000_000); + throw new DailyLimitExceededException(); } } private void validateTransactionAmount(long amount) { - if (amount <= 0) - throw new InvalidTransactionAmountException("Transaction amount must be greater than zero."); + if (amount <= 0) { + log.warn("Transaction amount must be greater than zero."); + throw new InvalidTransactionAmountException(); + } } private void validateInsufficientBalance(long amount) { - if (this.balance - amount < 0) - throw new InsufficientBalanceException("Insufficient balance for this transaction."); + if (this.balance - amount < 0) { + log.warn("Insufficient balance for this transaction."); + throw new InsufficientBalanceException(); + } } } diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java b/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java index 1366d961..88022557 100644 --- a/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java +++ b/src/main/java/camp/woowak/lab/payaccount/exception/DailyLimitExceededException.java @@ -1,7 +1,9 @@ package camp.woowak.lab.payaccount.exception; -public class DailyLimitExceededException extends RuntimeException { - public DailyLimitExceededException(String message) { - super(message); +import camp.woowak.lab.common.exception.BadRequestException; + +public class DailyLimitExceededException extends BadRequestException { + public DailyLimitExceededException() { + super(PayAccountErrorCode.DAILY_LIMIT_EXCEED); } } diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java b/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java index c6ba2695..cecba19b 100644 --- a/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java +++ b/src/main/java/camp/woowak/lab/payaccount/exception/InsufficientBalanceException.java @@ -1,7 +1,9 @@ package camp.woowak.lab.payaccount.exception; -public class InsufficientBalanceException extends RuntimeException { - public InsufficientBalanceException(String message) { - super(message); +import camp.woowak.lab.common.exception.BadRequestException; + +public class InsufficientBalanceException extends BadRequestException { + public InsufficientBalanceException() { + super(PayAccountErrorCode.INSUFFICIENT_BALANCE); } } diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java b/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java index 519dae12..68f4642e 100644 --- a/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java +++ b/src/main/java/camp/woowak/lab/payaccount/exception/InvalidTransactionAmountException.java @@ -1,7 +1,9 @@ package camp.woowak.lab.payaccount.exception; -public class InvalidTransactionAmountException extends RuntimeException { - public InvalidTransactionAmountException(String message) { - super(message); +import camp.woowak.lab.common.exception.BadRequestException; + +public class InvalidTransactionAmountException extends BadRequestException { + public InvalidTransactionAmountException() { + super(PayAccountErrorCode.INVALID_TRANSACTION_AMOUNT); } } diff --git a/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java b/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java index f7f72806..37323673 100644 --- a/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java +++ b/src/main/java/camp/woowak/lab/payaccount/exception/NotFoundAccountException.java @@ -1,7 +1,9 @@ package camp.woowak.lab.payaccount.exception; -public class NotFoundAccountException extends RuntimeException { - public NotFoundAccountException(String message) { - super(message); +import camp.woowak.lab.common.exception.NotFoundException; + +public class NotFoundAccountException extends NotFoundException { + public NotFoundAccountException() { + super(PayAccountErrorCode.ACCOUNT_NOT_FOUND); } } diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java index 6bdc6b78..37d25fe6 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java @@ -22,7 +22,10 @@ public PayAccountChargeService(PayAccountRepository payAccountRepository) { @Transactional public long chargeAccount(PayAccountChargeCommand command) { PayAccount payAccount = payAccountRepository.findByCustomerIdForUpdate(command.customerId()) - .orElseThrow(() -> new NotFoundAccountException("Invalid account id with " + command.customerId())); + .orElseThrow(() -> { + log.warn("Invalid account id with {}", command.customerId()); + throw new NotFoundAccountException(); + }); PayAccountHistory chargeHistory = payAccount.charge(command.amount()); log.info("A Charge of {} has been completed from Account ID {}", command.amount(), payAccount.getId()); From 519c98bdd03d988a828bbc037f93aadd7454ee16 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 01:16:09 +0900 Subject: [PATCH 58/62] =?UTF-8?q?[fix]=20PayAccount=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20Exception=EC=9D=84=20=ED=9A=8C=EC=9D=98=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=97=90=20=EB=A7=9E=EA=B2=8C=20format=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProblemDetails를 이용해서 response해주도록 수정 --- .../PayAccountApiControllerAdvice.java | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java index 71eee9eb..1f10d3a6 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java @@ -1,17 +1,14 @@ package camp.woowak.lab.web.api.payaccount; -import java.util.HashMap; -import java.util.Map; - +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import camp.woowak.lab.common.advice.DomainExceptionHandler; -import camp.woowak.lab.payaccount.exception.DailyLimitExceededException; -import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; -import camp.woowak.lab.payaccount.exception.InvalidTransactionAmountException; +import camp.woowak.lab.common.exception.BadRequestException; +import camp.woowak.lab.common.exception.ErrorCode; +import camp.woowak.lab.common.exception.NotFoundException; import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import lombok.extern.slf4j.Slf4j; @@ -19,34 +16,21 @@ @DomainExceptionHandler(basePackageClasses = PayAccountApiController.class) @Slf4j public class PayAccountApiControllerAdvice { - @ExceptionHandler(value = MethodArgumentNotValidException.class) - public ResponseEntity bindingException(MethodArgumentNotValidException ex) { - Map errors = new HashMap<>(); - ex.getBindingResult().getAllErrors().forEach((error) -> { - String fieldName = ((FieldError)error).getField(); - String errorMessage = error.getDefaultMessage(); - errors.put(fieldName, errorMessage); - }); - log.warn("Bad Request with parameter binding : {}", errors); - return ResponseEntity.badRequest().build(); - } - - @ExceptionHandler(value = {DailyLimitExceededException.class, InsufficientBalanceException.class, - InvalidTransactionAmountException.class}) - public ResponseEntity badRequestException(Exception e) { + @ExceptionHandler(value = {BadRequestException.class}) + public ResponseEntity badRequestException(BadRequestException e) { log.warn("Bad Request", e); - return ResponseEntity.badRequest().build(); + ErrorCode errorCode = e.errorCode(); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, errorCode.getMessage()); + problemDetail.setProperty("errorCode", errorCode.getErrorCode()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); } @ExceptionHandler(value = {NotFoundAccountException.class}) - public ResponseEntity notFoundAccountException(Exception e) { + public ResponseEntity notFoundAccountException(NotFoundException e) { log.warn("Not Found Request", e); - return ResponseEntity.notFound().build(); - } - - @ExceptionHandler(value = {Exception.class}) - public ResponseEntity internalServerException(Exception e) { - log.error("Internal Server Error", e); - return ResponseEntity.internalServerError().build(); + ErrorCode errorCode = e.errorCode(); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, errorCode.getMessage()); + problemDetail.setProperty("errorCode", errorCode.getErrorCode()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); } } From c04dfc8c74b337ac5ec3cec6a03c7a727d5e6852 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 01:32:18 +0900 Subject: [PATCH 59/62] =?UTF-8?q?[feat]=20BindingException=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PayAccountApiControllerAdvice.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java index 1f10d3a6..21db169e 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiControllerAdvice.java @@ -1,8 +1,13 @@ package camp.woowak.lab.web.api.payaccount; +import java.util.HashMap; +import java.util.Map; + import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import camp.woowak.lab.common.advice.DomainExceptionHandler; @@ -12,10 +17,23 @@ import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import lombok.extern.slf4j.Slf4j; -//TODO : exception구체화 및 error code 정의 +//TODO : BindingException에 대한 처리 @DomainExceptionHandler(basePackageClasses = PayAccountApiController.class) @Slf4j public class PayAccountApiControllerAdvice { + @ExceptionHandler(value = MethodArgumentNotValidException.class) + public ResponseEntity bindingException(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError)error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + log.warn("Bad Request with parameter binding : {}", errors); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, errors.toString()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); + } + @ExceptionHandler(value = {BadRequestException.class}) public ResponseEntity badRequestException(BadRequestException e) { log.warn("Bad Request", e); From 27a5f20c470d93122cd05c7c935a8ac88863d1d9 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 01:32:41 +0900 Subject: [PATCH 60/62] =?UTF-8?q?[fix]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A5=BC=20error=20response=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/api/PayAccountApiControllerTest.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index 70d16a76..e333b776 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.Optional; @@ -18,12 +19,15 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import com.fasterxml.jackson.databind.ObjectMapper; +import camp.woowak.lab.common.exception.ErrorCode; import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.customer.repository.CustomerRepository; import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.PayAccountErrorCode; import camp.woowak.lab.payaccount.repository.PayAccountRepository; import camp.woowak.lab.payaccount.service.PayAccountChargeService; import camp.woowak.lab.payaccount.service.command.PayAccountChargeCommand; @@ -112,11 +116,14 @@ void dailyLimitExceededTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL) + ResultActions actions = mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command)) .session(session)) + .andDo(print()) .andExpect(status().isBadRequest()); + + validateErrorResponseWithErrorCode(actions, PayAccountErrorCode.DAILY_LIMIT_EXCEED); } //TODO : 아직 API Response Format이 정해지지 않았으므로, 논의 후 추가 @@ -131,11 +138,13 @@ void notExistsAccountIdTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL) + ResultActions actions = mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command)) .session(notExistsSession)) .andExpect(status().isNotFound()); + + validateErrorResponseWithErrorCode(actions, PayAccountErrorCode.ACCOUNT_NOT_FOUND); } @Test @@ -145,7 +154,7 @@ void nullAmountTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(null); //when & then - mvc.perform(post(BASE_URL) + ResultActions actions = mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command)) .session(session)) @@ -162,13 +171,21 @@ void negativeAmountTest() throws Exception { PayAccountChargeRequest command = new PayAccountChargeRequest(amount); //when & then - mvc.perform(post(BASE_URL) + ResultActions actions = mvc.perform(post(BASE_URL) .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsBytes(command)) .session(session)) .andExpect(status().isBadRequest()); + validateErrorResponseWithErrorCode(actions, PayAccountErrorCode.INVALID_TRANSACTION_AMOUNT); verificationPersistedBalance(payAccount.getId(), originBalance); } } + + private ResultActions validateErrorResponseWithErrorCode(ResultActions actions, ErrorCode errorCode) throws + Exception { + return actions.andExpect(jsonPath("$.status").value(errorCode.getStatus())) + .andExpect(jsonPath("$.detail").value(errorCode.getMessage())) + .andExpect(jsonPath("$.errorCode").value(errorCode.getErrorCode())); + } } From ff215953416c39d2049e409a4a0c070047910804 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 01:38:34 +0900 Subject: [PATCH 61/62] =?UTF-8?q?[chore]=20PayAccountChargeService?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8D=98=EC=A0=B8=EC=A7=80=EB=8A=94=20exc?= =?UTF-8?q?eption=EC=9D=84=20java=20doc=EC=9D=84=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=20=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lab/payaccount/service/PayAccountChargeService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java index 37d25fe6..695d7593 100644 --- a/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java +++ b/src/main/java/camp/woowak/lab/payaccount/service/PayAccountChargeService.java @@ -19,6 +19,10 @@ public PayAccountChargeService(PayAccountRepository payAccountRepository) { this.payAccountRepository = payAccountRepository; } + /** + * @throws camp.woowak.lab.payaccount.exception.NotFoundAccountException 계좌를 찾지 못함. 존재하지 않는 계좌 + * @throws camp.woowak.lab.payaccount.exception.DailyLimitExceededException 일일 충전 한도를 초과함 + */ @Transactional public long chargeAccount(PayAccountChargeCommand command) { PayAccount payAccount = payAccountRepository.findByCustomerIdForUpdate(command.customerId()) From cefbb879c39d9179297e17d3fc41dd15a0726283 Mon Sep 17 00:00:00 2001 From: Hyeon-Uk Date: Wed, 14 Aug 2024 01:40:54 +0900 Subject: [PATCH 62/62] =?UTF-8?q?[chore]=20=EC=99=84=EB=A3=8C=ED=95=9C=20T?= =?UTF-8?q?ODO=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../woowak/lab/web/api/payaccount/PayAccountApiController.java | 2 -- .../camp/woowak/lab/web/api/PayAccountApiControllerTest.java | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java index 72f6c43f..a81e55b0 100644 --- a/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/payaccount/PayAccountApiController.java @@ -30,8 +30,6 @@ public PayAccountApiController(PayAccountChargeService payAccountChargeService) /** * TODO 1. api end-point 설계 논의 - * TODO 2. 인증 방법에 대한 유저 구분값 가져오는 방법 논의 - * TODO 3. API Response Format에 대한 논의 후 적용 필요 */ @PostMapping("/charge") public ResponseEntity> payAccountCharge( diff --git a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java index e333b776..538d5594 100644 --- a/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/PayAccountApiControllerTest.java @@ -126,7 +126,6 @@ void dailyLimitExceededTest() throws Exception { validateErrorResponseWithErrorCode(actions, PayAccountErrorCode.DAILY_LIMIT_EXCEED); } - //TODO : 아직 API Response Format이 정해지지 않았으므로, 논의 후 추가 @Test @DisplayName("존재하지 않는 계정 ID를 입력하면 404를 return한다.") void notExistsAccountIdTest() throws Exception {