Project
[Toss] 잔액 사용(1)
sangyunpark
2023. 8. 23. 12:37
잔액 사용
POST /transaction/use
파라미터 : 사용자 아이디, 계좌 번호, 거래 금액
정책 : 사용자 없는 경우, 사용자 아이디와 계좌 소유주가 다른 경우, 계좌가 이미 해지 상태인 경우, 거래금액이 잔액보다 큰 경우, 거래금액이 너무 작거나 큰 경우, 해당 계좌에서 거래(사용, 사용 취소)가 진행 중일 때, 다른 거래 요청이 오는 경우 해당 거래가 동시에 잘못 처리되는 것을 방지해야 한다.
성공 응답 : 계좌번호, 거래 결과 코드(성공/실패), 거래 아이디, 거래금액, 거래 일시
저장 필요 정보
컬럼명 | 데이터 타입 | 설명 |
id | pk | primary key |
transactionType | TransactionType | 거래의 종류(사용, 사용취소) |
transactionResultType | TransactionResultType | 거래 결과(성공, 실패) |
account | Account | 거래가 발생한 계좌(N:1연결) |
amount | Long | 거래 금액 |
balanceSnapshot | Long | 거래 후 계좌 잔액 |
transactionId | String | 계좌 해지일시 |
transactionAt | LocalDateTime | 거래 일시 |
createdAt | LocalDateTime | 생성 일시 |
updateAt | LocalDateTime | 최종 수정일시 |
요청
{
"userId":1,
"accountNumber":"1000000000",
"amount" : 1000
}
응답
{
"accountNumber' : "123456789",
"transactionResult":"5",
"transactionId: "asdf12312dsfsd59u0uce",
"amount":1000,
"transactedAt" : "2023-01-25"
}
controller/TransactionController
@Slf4j
@RestController
@RequiredArgsConstructor
public class TransactionController {
private final TransactionService transactionService;
@PostMapping("/transaction/use")
public UseBalance.Response useBalance(@Valid @RequestBody UseBalance.Request request){
try{
return UseBalance.Response.from(
transactionService.useBalance(request.getId(), request.getAccountNumber(), request.getAmount())
);
}catch(AccountException e){ // 에러가 발생한 경우 거래결과 실패를 주어야한다.
log.error("Failed to use balance.");
transactionService.saveFailedUseTransaction(
request.getAccountNumber(),
request.getAmount()
);
throw e;
}
}
거래가 실패한 경우도 생각해 주어야 한다.
dto/UseBalance
package com.example.account.dto;
import com.example.account.type.TransactionResultType;
import jakarta.validation.constraints.*;
import lombok.*;
import java.time.LocalDateTime;
public class UseBalance {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Request {
@NotNull
@Min(1)
private Long id;
@NotBlank
@Size(min = 10, max = 10)
private String accountNumber;
@NotNull
@Min(10)
@Max(1000_000_000)
private Long amount;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Response {
private String accountNumber;
private TransactionResultType transactionResult;
private String transactionId;
private Long amount;
private LocalDateTime transactedAt;
public static Response from(TransactionDto transactionDto){
return Response.builder()
.accountNumber(transactionDto.getAccountNumber())
.transactionResult(transactionDto.getTransactionResultType())
.transactionId(transactionDto.getTransactionId())
.amount(transactionDto.getAmount())
.transactedAt(transactionDto.getTransactedAt())
.build();
}
}
}
service/TransactionService
@Slf4j
@Service
@RequiredArgsConstructor
public class TransactionService {
private final TransactionRepository transactionRepository;
private final AccountUserRepository accountUserRepository;
private final AccountRepository accountRepository;
@Transactional
public TransactionDto useBalance(Long userId, String accountNumber, Long amount){
AccountUser user = accountUserRepository.findById(userId)
.orElseThrow(() -> new AccountException(ErrorCode.USER_NOT_FOUND));
Account account = accountRepository.findByAccountNumber(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
validationUseBalance(user, account, amount); // 유효성 검사
account.useBalance(amount); // 보안성이 필요한 정보는 메소드를 따로 선언해주어야한다.
return TransactionDto.fromEntity(getSave(SUCCESS, account, amount));
}
public void validationUseBalance(AccountUser user, Account account, Long amount){
if(!user.getId().equals(account.getUserId())){ // 계좌 소유주가 다른 경우
throw new AccountException(ErrorCode.USER_ACCOUNT_UN_MATCH);
}
if(AccountStatus.UNREGISTERED == account.getAccountStatus()){ // 계좌가 이미 해지 상태인 경우
throw new AccountException(ErrorCode.ACCOUNT_ALREADY_UNREGISTRED);
}
if(account.getBalance() < amount){ // 거래금액이 잔액보다 큰 경우
throw new AccountException(ErrorCode.AMOUNT_EXCEED_BALANCE);
}
}
@Transactional
public void saveFailedUseTransaction(String accountNumber, Long amount) { // 실패한 경우
Account account = accountRepository.findByAccountNumber(accountNumber)
.orElseThrow(() -> new AccountException(ErrorCode.ACCOUNT_NOT_FOUND));
getSave(FAIL, account, amount);
}
// 코드 중복 방지
private Transaction getSave(TransactionResultType transactionResultType, Account account, Long amount) {
return transactionRepository.save( // 성공한 경우
Transaction.builder()
.transactionType(USE)
.transactionResultType(transactionResultType)
.account(account)
.amount(amount)
.balanceSnapshot(account.getBalance())
.transactionId(UUID.randomUUID().toString().replace("-", ""))
.transactedAt(LocalDateTime.now())
.build()
);
}
}
dto/TransactionDto
package com.example.account.dto;
import com.example.account.type.TransactionResultType;
import jakarta.validation.constraints.*;
import lombok.*;
import java.time.LocalDateTime;
public class UseBalance {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Request {
@NotNull
@Min(1)
private Long id;
@NotBlank
@Size(min = 10, max = 10)
private String accountNumber;
@NotNull
@Min(10)
@Max(1000_000_000)
private Long amount;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Response {
private String accountNumber;
private TransactionResultType transactionResult;
private String transactionId;
private Long amount;
private LocalDateTime transactedAt;
public static Response from(TransactionDto transactionDto){
return Response.builder()
.accountNumber(transactionDto.getAccountNumber())
.transactionResult(transactionDto.getTransactionResultType())
.transactionId(transactionDto.getTransactionId())
.amount(transactionDto.getAmount())
.transactedAt(transactionDto.getTransactedAt())
.build();
}
}
}
domain/Transaction
package com.example.account.domain;
import com.example.account.type.TransactionResultType;
import com.example.account.type.TransactionType;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class) // 시간에 대해 자동으로 값을 넣어주는 기능
public class Transaction {
// primary key
@Id
@GeneratedValue
private Long id;
@Enumerated(EnumType.STRING)
private TransactionType transactionType;
@Enumerated(EnumType.STRING)
private TransactionResultType transactionResultType;
@ManyToOne
private Account account;
private Long amount;
private Long balanceSnapshot;
private String transactionId;
private LocalDateTime transactedAt;
@CreatedDate // 자동 저장
private LocalDateTime createdAt; // 계좌 생성 일시
@LastModifiedDate // 자동 저장
private LocalDateTime updatedAt; // 계좌 수정 일시
}
repository/TransactionRepository
package com.example.account.repository;
import com.example.account.domain.Account;
import com.example.account.domain.AccountUser;
import com.example.account.domain.Transaction;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
}