Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

구매자 페이먼트 충전 #15

Merged
merged 65 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
54ae838
[feat] PayAccount 도메인에 balance 멤버변수 추가
Hyeon-Uk Aug 10, 2024
4c9c117
[feat] InsufficientBalanceException 생성
Hyeon-Uk Aug 10, 2024
d8444fe
[feat] InvalidTransactionAmountException 생성
Hyeon-Uk Aug 10, 2024
bc82d4d
[test] PayAccount 도메인의 생성자 및 입/출금 테스트 작성
Hyeon-Uk Aug 10, 2024
3a9cf9a
[feat] PayAccount 도메인의 생성자 및 입/출금 기능 구현
Hyeon-Uk Aug 10, 2024
56b9970
[feat] PayAccount 도메인의 getId 메서드 생성
Hyeon-Uk Aug 11, 2024
f994af5
[feat] PayAccount Repository 생성
Hyeon-Uk Aug 11, 2024
33f2522
[feat] PayAccount에 x-lock을 위한 메서드 추가
Hyeon-Uk Aug 11, 2024
1c99da6
[feat] PayAccount를 찾지 못한 경우에 대한 exception 추가
Hyeon-Uk Aug 11, 2024
5ebac92
[feat] AccountTransactionCommand 추가
Hyeon-Uk Aug 11, 2024
4d87bad
[feat] 계좌 출금 서비스 구현
Hyeon-Uk Aug 11, 2024
0371bde
[test] 출금 서비스에 대한 테스트코드 작성
Hyeon-Uk Aug 11, 2024
2e34cea
[test] 출금 서비스에 대한 동시성 테스트코드 작성
Hyeon-Uk Aug 11, 2024
48e0658
[feat] 계좌 입금 서비스 구현
Hyeon-Uk Aug 11, 2024
3ed17b3
[test] 입금 서비스에 대한 테스트코드 작성
Hyeon-Uk Aug 11, 2024
c957024
[test] 입금 서비스에 대한 동시성 테스트코드 작성
Hyeon-Uk Aug 11, 2024
0048f61
[fix] deposit service 테스트의 @DisplayName 수정
Hyeon-Uk Aug 11, 2024
30f78dd
[feat] 계좌 입출금의 히스토리에 저장할 상태값을 enum으로 관리
Hyeon-Uk Aug 11, 2024
a61c64d
[feat] 계좌 입출금의 히스토리에 대한 도메인 엔티티 생성
Hyeon-Uk Aug 11, 2024
079a842
[feat] 계좌에서 입/출금을 진행할 때 History를 생성해서 return 해주도록 구현 추가
Hyeon-Uk Aug 11, 2024
e5f2023
[test] 계좌에서 입/출금을 진행할 때 History를 생성해서 return 해주는것을 검증하는 테스트로직 추가
Hyeon-Uk Aug 11, 2024
212c23b
[feat] account history를 관리할 수 있는 repository 생성
Hyeon-Uk Aug 11, 2024
8b68382
[feat] deposit과 withdraw service에 history를 받아 저장할 수 있는 기능 추가 구현
Hyeon-Uk Aug 11, 2024
7014f86
[feat] 계좌에 충전할 수 있는 API Controller 추가
Hyeon-Uk Aug 11, 2024
f1cdf4f
[feat] 계좌에 충전할 수 있는 API Controller에서 발생할 수 있는 exception에 대해 처리할 수 있는 …
Hyeon-Uk Aug 11, 2024
f6ad720
[test] 계좌에 충전할 수 있는 API Controller에 대한 통합 테스트 추가
Hyeon-Uk Aug 11, 2024
54b0820
[chore] .gitkeep 삭제
Hyeon-Uk Aug 11, 2024
f34aed0
[chore] PayAccountChargeRequest의 format 수정
Hyeon-Uk Aug 11, 2024
30c80ab
[chore] chaining method를 보기 편하도록 수정
Hyeon-Uk Aug 11, 2024
7e4a8aa
Merge remote-tracking branch 'origin/main' into feature/3_Hyeon-Uk_구매…
Hyeon-Uk Aug 12, 2024
20f5755
[feat] AccountTransactionType에 CHARGE 타입 추가
Hyeon-Uk Aug 12, 2024
77d9c14
[feat] PayAccount 도메인에 charge 메서드 추가
Hyeon-Uk Aug 12, 2024
15a6278
[feat] 일일 한도 초과에 대한 Exception 생성
Hyeon-Uk Aug 12, 2024
b1cda7f
[feat] PayAccountHistory 도메인 엔티티에 createdAt의 defaultValue를 now()로 설정 …
Hyeon-Uk Aug 12, 2024
5273aa8
[feat] PayAccount -> PayAccountHistory로 향하는 단방향 매핑 추가 후 비즈니스 로직 리팩토링
Hyeon-Uk Aug 12, 2024
9d52041
[feat] charge메서드에 일일 한도를 검증하는 로직 추가
Hyeon-Uk Aug 12, 2024
581f4c5
[test] PayAccount 도메인 엔티티의 charge 메서드에 대한 테스트코드 추가
Hyeon-Uk Aug 12, 2024
ec96339
[refactor] domain에서 history를 저장하기 때문에 service 레이어에서 history를 저장하는 로직 삭제
Hyeon-Uk Aug 12, 2024
e4cb60d
[feat] PayAccountChargeService 구현
Hyeon-Uk Aug 12, 2024
30be760
[test] PayAccountChargeService 테스트코드 작성
Hyeon-Uk Aug 12, 2024
60dcd8a
[feat] PayAccountAPIController의 PayAccountDepositService를 PayAccountC…
Hyeon-Uk Aug 12, 2024
3949d65
[feat] @DomainExceptionHandler 어노테이션으로 변경 및 DailyLimitExceededExcepti…
Hyeon-Uk Aug 12, 2024
3b4bf54
[fix] PayAccount 도메인 엔티티의 테스트코드 오류 수정
Hyeon-Uk Aug 12, 2024
d73de81
[test] PayAccountApiController의 충전 로직에 대한 테스트코드 추가
Hyeon-Uk Aug 12, 2024
73ac49f
Merge remote-tracking branch 'origin/main' into feature/3_Hyeon-Uk_구매…
Hyeon-Uk Aug 12, 2024
9fd2f2b
[fix] 현재 이슈의 범위를 넘어선 클래스 및 테스트 제거
Hyeon-Uk Aug 13, 2024
fc2fa5e
[fix] API Response format에 맞게 반환하도록 수정 및 테스트코드 수정
Hyeon-Uk Aug 13, 2024
c7a576e
[fix] AccountTransactionCommand 에서 PayAccountChargeCommand로 이름 수정
Hyeon-Uk Aug 13, 2024
9364157
[fix] ChargeCommand에서 payAccountId 대신 customerId를 받도록 수정
Hyeon-Uk Aug 13, 2024
c0faca6
[fix] CustomerId를 이용해서 PayAccount를 찾을 수 있도록 메서드 이름 및 파라미터 이름 수정
Hyeon-Uk Aug 13, 2024
d2794b8
[fix] CustomerId를 이용해서 PayAccount를 찾은 뒤 충전할 수 있도록 수정
Hyeon-Uk Aug 13, 2024
c592ccb
[fix] PayAccountRepository에서 CustomerId를 이용해서 계좌를 찾는 Query 오류 수정
Hyeon-Uk Aug 13, 2024
13301d7
[feat] 테스트를 위해 Customer의 생성자 및 Repository생성
Hyeon-Uk Aug 13, 2024
a74c1e1
[fix] Command의 인자가 PayAccountId -> CustomerId로 변경된 요구사항에 맞춰 테스트코드 수정
Hyeon-Uk Aug 13, 2024
6c6e338
[fix] 세션에 저장된 인증 객체의 CustomerId를 이용해서 포인트를 충전할 수 있도록 수정
Hyeon-Uk Aug 13, 2024
281f6b2
[feat] 현재 브랜치에서 임시로 ArgumentResolver를 등록
Hyeon-Uk Aug 13, 2024
f519ed6
[fix] 세션에 저장된 인증 객체로 포인트를 충전할 수 있도록 수정한 사항에 대해 테스트코드도 수정
Hyeon-Uk Aug 13, 2024
bdf7452
[feat] PayAccount에 대한 ErrorCode enum 생성
Hyeon-Uk Aug 13, 2024
454fee1
[fix] PayAccount에 대한 Exception을 common에서 관리하는 exception을 상속하도록 수정
Hyeon-Uk Aug 13, 2024
519c98b
[fix] PayAccount에 대한 Exception을 회의 결과에 맞게 format 수정
Hyeon-Uk Aug 13, 2024
c04dfc8
[feat] BindingException에 대한 핸들링 추가
Hyeon-Uk Aug 13, 2024
27a5f20
[fix] 테스트코드를 error response 형식에 맞게 검증 수정
Hyeon-Uk Aug 13, 2024
ff21595
[chore] PayAccountChargeService에서 던져지는 exception을 java doc을 이용해 설명 추가
Hyeon-Uk Aug 13, 2024
cefbb87
[chore] 완료한 TODO 제거
Hyeon-Uk Aug 13, 2024
1a491d2
[merge] resolve merge conflict
Hyeon-Uk Aug 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/camp/woowak/lab/LabApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package camp.woowak.lab.payaccount.domain;

public enum AccountTransactionType {
DEPOSIT, WITHDRAW, CHARGE;
}
93 changes: 90 additions & 3 deletions src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,100 @@
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;
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 {
@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;

@OneToMany(mappedBy = "payAccount", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private List<PayAccountHistory> history = new ArrayList<>();

public PayAccount() {
this.balance = 0;
}

public Long getId() {
return id;
}

public long getBalance() {
return this.balance;
}

public PayAccountHistory withdraw(long amount) {
validateTransactionAmount(amount);
validateInsufficientBalance(amount);
this.balance -= amount;

return issueAndSavePayAccountHistory(amount, AccountTransactionType.WITHDRAW);
}

public PayAccountHistory deposit(long amount) {
validateTransactionAmount(amount);
this.balance += amount;

return issueAndSavePayAccountHistory(amount, AccountTransactionType.DEPOSIT);
}

public PayAccountHistory charge(long amount) {
validateTransactionAmount(amount);
validateDailyChargeLimit(amount);
this.balance += amount;

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 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.");
}

private void validateInsufficientBalance(long amount) {
if (this.balance - amount < 0)
throw new InsufficientBalanceException("Insufficient balance for this transaction.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 = LocalDateTime.now();

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;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package camp.woowak.lab.payaccount.exception;

public class DailyLimitExceededException extends RuntimeException {
public DailyLimitExceededException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package camp.woowak.lab.payaccount.exception;

public class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package camp.woowak.lab.payaccount.exception;

public class InvalidTransactionAmountException extends RuntimeException {
public InvalidTransactionAmountException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package camp.woowak.lab.payaccount.exception;

public class NotFoundAccountException extends RuntimeException {
public NotFoundAccountException(String message) {
super(message);
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -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<PayAccountHistory, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +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<PayAccount, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT pa FROM PayAccount pa where pa.id = :id")
Optional<PayAccount> findByIdForUpdate(@Param("id") Long id);
}
Hyeon-Uk marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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 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();
}
}
Original file line number Diff line number Diff line change
@@ -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 PayAccountWithdrawService {
Hyeon-Uk marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package camp.woowak.lab.payaccount.service.command;

public record AccountTransactionCommand(
Long payAccountId,
long amount) {
}
Original file line number Diff line number Diff line change
@@ -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.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;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/account")
@Slf4j
public class PayAccountApiController {
private final PayAccountChargeService payAccountChargeService;

public PayAccountApiController(PayAccountChargeService payAccountChargeService) {
this.payAccountChargeService = payAccountChargeService;
}

/**
* TODO 1. api end-point 설계 논의
* TODO 2. 인증 방법에 대한 유저 구분값 가져오는 방법 논의
* TODO 3. API Response Format에 대한 논의 후 적용 필요
*/
@PostMapping("/{payAccountId}/charge")
public ResponseEntity<PayAccountChargeResponse> 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 = payAccountChargeService.chargeAccount(command);
log.info("Charge successful. Account ID: {}, New Balance: {}", payAccountId, remainBalance);

return ResponseEntity.ok(new PayAccountChargeResponse(remainBalance));
}
}
Loading