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> {

}