Skip to content

Commit

Permalink
Merge pull request #15 from woowa-techcamp-2024/feature/3_Hyeon-Uk_구매…
Browse files Browse the repository at this point in the history
…자_페이먼트_충전

구매자 페이먼트 충전 메인 머지
  • Loading branch information
Hyeon-Uk authored Aug 14, 2024
2 parents 763fabf + 1a491d2 commit 243607e
Show file tree
Hide file tree
Showing 26 changed files with 940 additions and 1 deletion.
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
Expand Up @@ -33,7 +33,7 @@ public Customer() {
}

public Customer(String name, String email, String password, String phone, PayAccount payAccount,
PasswordEncoder passwordEncoder) throws
PasswordEncoder passwordEncoder) throws
InvalidCreationException {
CustomerValidator.validateCreation(name, email, password, phone, payAccount);
this.name = name;
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;
}
94 changes: 94 additions & 0 deletions src/main/java/camp/woowak/lab/payaccount/domain/PayAccount.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,107 @@
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;
import lombok.extern.slf4j.Slf4j;

@Entity
@Slf4j
public class PayAccount {
@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) {
log.warn("Daily charge limit of {} exceeded.", 1_000_000);
throw new DailyLimitExceededException();
}
}

private void validateTransactionAmount(long amount) {
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) {
log.warn("Insufficient balance for this transaction.");
throw new InsufficientBalanceException();
}
}
}
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,9 @@
package camp.woowak.lab.payaccount.exception;

import camp.woowak.lab.common.exception.BadRequestException;

public class DailyLimitExceededException extends BadRequestException {
public DailyLimitExceededException() {
super(PayAccountErrorCode.DAILY_LIMIT_EXCEED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.payaccount.exception;

import camp.woowak.lab.common.exception.BadRequestException;

public class InsufficientBalanceException extends BadRequestException {
public InsufficientBalanceException() {
super(PayAccountErrorCode.INSUFFICIENT_BALANCE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.payaccount.exception;

import camp.woowak.lab.common.exception.BadRequestException;

public class InvalidTransactionAmountException extends BadRequestException {
public InvalidTransactionAmountException() {
super(PayAccountErrorCode.INVALID_TRANSACTION_AMOUNT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package camp.woowak.lab.payaccount.exception;

import camp.woowak.lab.common.exception.NotFoundException;

public class NotFoundAccountException extends NotFoundException {
public NotFoundAccountException() {
super(PayAccountErrorCode.ACCOUNT_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
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
@@ -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<PayAccount, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT pa FROM PayAccount pa LEFT JOIN Customer c on c.payAccount = pa where c.id = :customerId")
Optional<PayAccount> findByCustomerIdForUpdate(@Param("customerId") Long customerId);
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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.PayAccountChargeCommand;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class PayAccountChargeService {
private final PayAccountRepository payAccountRepository;

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())
.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());

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 PayAccountChargeCommand(
Long customerId,
long amount) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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.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.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;

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

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

/**
* TODO 1. api end-point 설계 논의
*/
@PostMapping("/charge")
public ResponseEntity<APIResponse<PayAccountChargeResponse>> payAccountCharge(
@AuthenticationPrincipal LoginCustomer loginCustomer,
@Validated @RequestBody PayAccountChargeRequest request) {
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 Owner ID: {}, New Balance: {}", loginCustomer.getId(), remainBalance);

return APIUtils.of(HttpStatus.OK, new PayAccountChargeResponse(remainBalance));
}
}
Loading

0 comments on commit 243607e

Please sign in to comment.