우주먼지

💡 결제 기능

 

요구사항

Open-Session-in-view 옵션: false (트랜잭션 내부의 프록시객체 초기화 필요)
연관관계 : Member 1   < - >    Post N

@ManyToOne 단방향 매핑으로 진행
FetchType : Lazy

1. 결제 등록
2. 결제 취소 요청 (유저 마이페이지)
3. 결제 취소 승인 (관리자)
4. 프로그램 결제내역 단건 조회 (유저 마이페이지)
5. 프로그램 결제내역 전체 조회 (유저 마이페이지)
6. 특정 회원 상담내역 전체 조회 (관리자 마이페이지)
7. 결제 상태별 내역 조회 (관리자)

💡 소스코드

 

Entity

@Entity
@Getter
@NoArgsConstructor
public class Pay extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long payId;

    @Column(nullable = false)
    private String cardOwner;

    @Column(length = 16, nullable = false)
    private String cardNum;

    @Column(length = 3, nullable = false)
    private String cvvNum;

    @Column(nullable = false)
    private String expirationTime;

    @Enumerated(EnumType.STRING)
    private Status status;

    @ManyToOne(optional = false)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    public void setMember(Member member) {
        this.member = member;
    }

    @ManyToOne(optional = false)
    @JoinColumn(name = "PROGRAM_ID")
    private Program program;

    public void setProgram(Program program) {
        this.program = program;
    }

    public enum Status {
        COMPLETE_PAYMENT("결제 완료"),
        WAITING_CANCEL_PAYMENT("취소 대기중"),
        CANCEL_PAYMENT("결제 취소");

        @Getter
        private String statusMessage;

        Status(String statusMessage) {
            this.statusMessage = statusMessage;
        }
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    @Builder
    public Pay(Long payId, String cardOwner, String cardNum, String cvvNum, String expirationTime,
        Status status, Member member, Program program) {
        this.payId = payId;
        this.cardOwner = cardOwner;
        this.cardNum = cardNum;
        this.cvvNum = cvvNum;
        this.expirationTime = expirationTime;
        this.status = status;
        this.member = member;
        this.program = program;
    }

    public static Pay of(Pay pay) {
        return Pay.builder()
            .payId(pay.getPayId())
            .cardOwner(pay.getCardOwner())
            .cardNum(pay.getCardNum())
            .cvvNum(pay.getCvvNum())
            .expirationTime(pay.getExpirationTime())
            .status(pay.getStatus())
            .member(pay.getMember())
            .program(pay.getProgram())
            .build();
    }
}

 

DTO

public class PayDto {
    @Getter
    @Builder
    public static class Post {
        @NotBlank
        private String cardOwner;

        @NotBlank
        private String cardNum;

        @NotBlank
        private String cvvNum;

        @NotBlank
        @Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "월/년 순서로 입력하셔야 합니다.")
        private String expirationTime;

        public static PayDto.Post of(PayDto.Post requestBody) {
            return PayDto.Post.builder()
                .cardOwner(requestBody.getCardOwner())
                .cardNum(requestBody.getCardNum())
                .cvvNum(requestBody.getCvvNum())
                .expirationTime(requestBody.getExpirationTime())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class PostResponse {
        private Long programId;
        private String programTitle;
        private String dateStart;
        private String dateEnd;
        private int cost;
        private String counselorName;
        private String status;

        public static PayDto.PostResponse of(Pay pay) {
            return PayDto.PostResponse.builder()
                .programId(pay.getProgram().getProgramId())
                .programTitle(pay.getProgram().getTitle())
                .dateStart(pay.getProgram().getDateStart())
                .dateEnd(pay.getProgram().getDateEnd())
                .cost(pay.getProgram().getCost())
                .counselorName(pay.getProgram().getCounselor().getCounselorName())
                .status(pay.getStatus().getStatusMessage())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class PayPatchResponse {
        private String status;

        public static PayDto.PayPatchResponse of(Pay pay) {
            return PayPatchResponse.builder()
                .status(pay.getStatus().getStatusMessage())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class GetResponse {
        private Long payId;
        private LocalDateTime createdAt;
        private String status;
        private Long programId;
        private String title;
        private String dateStart;
        private String dateEnd;
        private int userMax;
        private int cost;
        private String zoomLink;
        private String announce;
        private String counselorName;

        public static PayDto.GetResponse of(Pay pay) {
            return GetResponse.builder()
                .payId(pay.getPayId())
                .createdAt(pay.getCreateTime())
                .status(pay.getStatus().getStatusMessage())
                .programId(pay.getProgram().getProgramId())
                .title(pay.getProgram().getTitle())
                .dateStart(pay.getProgram().getDateStart())
                .dateEnd(pay.getProgram().getDateEnd())
                .userMax(pay.getProgram().getUserMax())
                .cost(pay.getProgram().getCost())
                .zoomLink(pay.getProgram().getZoomLink())
                .announce(pay.getProgram().getAnnounce())
                .counselorName(pay.getProgram().getCounselor().getCounselorName())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class UserReservePageResponse {
        private Long payId;
        private Long memberId;
        private String title;
        private String dateStart;
        private String dateEnd;
        private int userMax;
        private LocalDateTime createdAt;
        private String counselorName;
        private String status;

        public static PayDto.UserReservePageResponse of(Pay pay) {
            return UserReservePageResponse.builder()
                .payId(pay.getPayId())
                .memberId(pay.getMember().getMemberId())
                .title(pay.getProgram().getTitle())
                .dateStart(pay.getProgram().getDateStart())
                .dateEnd(pay.getProgram().getDateEnd())
                .userMax(pay.getProgram().getUserMax())
                .createdAt(pay.getCreateTime())
                .counselorName(pay.getProgram().getCounselor().getCounselorName())
                .status(pay.getStatus().getStatusMessage())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AdminReservePageResponse {
        private Long payId;
        private Long memberId;
        private String title;
        private String dateStart;
        private String dateEnd;
        private int userMax;
        private String counselorName;

        public static PayDto.AdminReservePageResponse of(Pay pay) {
            return AdminReservePageResponse.builder()
                .payId(pay.getPayId())
                .memberId(pay.getMember().getMemberId())
                .title(pay.getProgram().getTitle())
                .dateStart(pay.getProgram().getDateStart())
                .dateEnd(pay.getProgram().getDateEnd())
                .userMax(pay.getProgram().getUserMax())
                .counselorName(pay.getProgram().getCounselor().getCounselorName())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class MemberInPayList {
        private String nickName;
        private String birth;

        public static PayDto.MemberInPayList of(Pay pay) {
            return MemberInPayList.builder()
                .nickName(pay.getMember().getNickName())
                .birth(pay.getMember().getBirth())
                .build();
        }
    }

    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class AdminPayStatusPageResponse{
        private long memberId;

        private String memberName;

        private long payId;

        private String status;

        private String title;

        private int cost;

        public static PayDto.AdminPayStatusPageResponse of(Pay pay){
            return AdminPayStatusPageResponse.builder()
                    .memberId(pay.getMember().getMemberId())
                    .memberName(pay.getMember().getMemberName())
                    .payId(pay.getPayId())
                    .status(pay.getStatus().getStatusMessage())
                    .title(pay.getProgram().getTitle())
                    .cost(pay.getProgram().getCost())
                    .build();
        }
    }
}

 

Mapper

@Mapper(componentModel = "spring")
public interface PayMapper {
    Pay PayPostDtoToPay(PayDto.Post requestBody);

    default PayDto.PayPatchResponse PayToPayPatchResponse(Pay pay) {
        PayDto.PayPatchResponse payPatchResponse = PayDto.PayPatchResponse.of(pay);
        return payPatchResponse;
    }

    default PayDto.PostResponse PayToPayPostResponseDto(Pay pay) {
        PayDto.PostResponse postResponse = PayDto.PostResponse.of(pay);
        return postResponse;
    }

    default PayDto.GetResponse PayToPayGetResponseDto(Pay pay) {
        PayDto.GetResponse getResponse = PayDto.GetResponse.of(pay);
        return getResponse;
    }
    default List<PayDto.UserReservePageResponse> ReserveProgramToUserPageProgramResponse(List<Pay> pays) {
        List<PayDto.UserReservePageResponse> list = new ArrayList<UserReservePageResponse>(pays.size());

        for(Pay pay : pays) {
            list.add(PayDto.UserReservePageResponse.of(pay));
        }
        return list;
    }

    default List<PayDto.AdminReservePageResponse> ReserveProgramToAdminPageProgramResponse(List<Pay> pays) {
        List<PayDto.AdminReservePageResponse> list = new ArrayList<AdminReservePageResponse>(pays.size());

        for(Pay pay : pays) {
            list.add(PayDto.AdminReservePageResponse.of(pay));
        }
        return list;
    }
    default List<PayDto.AdminPayStatusPageResponse> ReserveProgramToAdminPayStatusPageResponse(List<Pay> pays) {
        List<PayDto.AdminPayStatusPageResponse> list = new ArrayList<PayDto.AdminPayStatusPageResponse>(pays.size());

        for(Pay pay : pays) {
            list.add(PayDto.AdminPayStatusPageResponse.of(pay));
        }
        return list;
    }
}

 

Repository

public interface PayRepository extends JpaRepository<Pay, Long> {
    @Query(value = "select * from pay where member_id = :memberId", nativeQuery = true)
    Page<Pay> findAllByMember(long memberId, Pageable pageable);

    @Query(value = "select * from pay where status = :status", nativeQuery = true)
    Page<Pay> findAllByStatus(String status, Pageable pageable);
}

 

Service

@Service
@RequiredArgsConstructor
public class PayService {
    private final PayRepository payRepository;
    private final ProgramService programService;

    public Pay createPay(Pay pay, Long programId, Member member) {
        Program program = programService.findVerifiedExistsReserveProgram(
                member.getMemberId(), programId);

        // 결제 시 프로그램 참여자 수 증가
        if (program.getUserCount() >= program.getUserMax()) {
            throw new BusinessException(ErrorCode.PROGRAM_CAPACITY_EXCEEDED);
        } else {
            int findUserCount = program.getUserCount() + 1;
            program.setUserCount(findUserCount);
        }

        createPayStatusSave(pay);

        pay.setMember(member);
        pay.setProgram(program);


        return payRepository.save(pay);
    }

    @Transactional(readOnly = true)
    public Pay findReservation(Long payId) {
        Pay findPay = findVerifiedPay(payId);
        return findPay;
    }

    public Pay findVerifiedPay(Long payId) {
        Optional<Pay> optionalPay = payRepository.findById(payId);
        Pay findPay = optionalPay.orElseThrow(
                () -> new BusinessException(ErrorCode.RESERVATION_NOT_FOUND)
        );

        return findPay;
    }

    @Transactional(readOnly = true)
    public Page<Pay> searchUserReserveProgram(Long memberId, int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("pay_id").descending());

        Page<Pay> payPage = payRepository.findAllByMember(memberId, pageable);

        return payPage;
    }

    public Pay createPayStatusSave(Pay pay) {
        pay.setStatus(Status.COMPLETE_PAYMENT);
        return pay;
    }


    /**
     * 결제 상태별 조회
     */
    public Page<Pay> searchCompletePayment(int page, int size, String status) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("pay_id").descending());

        if (Status.COMPLETE_PAYMENT.equals(Status.valueOf(status))) {
            Page<Pay> pays = payRepository.findAllByStatus(status, pageable);
            return pays;
        } else if (Status.WAITING_CANCEL_PAYMENT.equals(Status.valueOf(status))) {
            Page<Pay> pays = payRepository.findAllByStatus(status, pageable);
            return pays;
        } else if (Status.CANCEL_PAYMENT.equals(Status.valueOf(status))) {
            Page<Pay> pays = payRepository.findAllByStatus(status, pageable);
            return pays;
        } else {
            throw new EntityNotFoundException(ErrorCode.STATUS_NOT_FOUND);
        }
    }

    /**
     * 결제 상태 변경 : 결제 완료 -> 결제 취소 요청
     * */
    public Pay updatePayStatus(Long payId) {
        Pay pay = findVerifiedPay(payId);

        if(Status.WAITING_CANCEL_PAYMENT.equals(pay.getStatus())) {
            throw new BusinessException(ErrorCode.ALREADY_CANCELLATION_REQUESTED);
        }

        pay.setStatus(Status.WAITING_CANCEL_PAYMENT);

        return payRepository.save(pay);
    }

    /**
     * 결제 상태 변경 : 결제 취소 요청 -> 결제 취소
     */
    public Pay confirmPayStatus(Long payId){
        Pay pay = findVerifiedPay(payId);

        if(Status.CANCEL_PAYMENT.equals(pay.getStatus())) {
            throw new BusinessException(ErrorCode.CANCEL_RESERVATION);
        } else if(Status.COMPLETE_PAYMENT.equals(pay.getStatus())) {
            throw  new BusinessException(ErrorCode.NO_CANCELLATION_REQUEST);
        }

        pay.setStatus(Status.CANCEL_PAYMENT);

        Program program = programService.findVerifiedProgram(pay.getProgram().getProgramId());

        if(program.getUserCount() > 0) {
            int user = program.getUserCount() - 1;
            program.setUserCount(user);
        }

        return payRepository.save(pay);
    }
}

 

Controller

@RestController
@RequestMapping("/api/pays")
@RequiredArgsConstructor
public class PayController {
    private final PayMapper payMapper;
    private final PayService payService;
    private final MemberService memberService;

    // 화면정의서 9p
    // 유저 - 결제(예약)하기
    @PostMapping("/{program-id}/post")
    public ResponseEntity postPay(@PathVariable("program-id") @Positive Long programId,
        HttpServletRequest httpServletRequest,
        @RequestBody PayDto.Post requestBody) {
        Pay pay = payMapper.PayPostDtoToPay(requestBody);
        Pay completedPay = payService.createPay(pay, programId, memberService.getLoginMember(httpServletRequest));

        PayDto.PostResponse response = payMapper.PayToPayPostResponseDto(completedPay);

        return new ResponseEntity<>(
            new SingleResponseDto<>(response), HttpStatus.CREATED);
    }

    // 화면정의서 23p
    // 유저 - 마이페이지 나의 프로그램 예약내역 개별조회
    @GetMapping("/lookup/{pay-id}")
    public ResponseEntity getUserReserveProgram(@PathVariable("pay-id") @Positive Long payId) {
        Pay findReserveProgram = payService.findReservation(payId);
        PayDto.GetResponse response = payMapper.PayToPayGetResponseDto(findReserveProgram);
        return new ResponseEntity<>(
            new SingleResponseDto<>(response), HttpStatus.OK);
    }

//    // 화면정의서 22p
//    // 유저 - 마이페이지 나의 프로그램 예약 내역 전체 조회
//    @GetMapping("/{member-id}/lookup/list")
//    public ResponseEntity getUserReservePrograms(@PathVariable("member-id") @Positive Long memberId,
//        @Positive @RequestParam(defaultValue = "1") int page,
//        @Positive @RequestParam(defaultValue = "10") int size) {
//        Page<Pay> myReserveProgramPage = payService.searchUserReserveProgram(memberId, page-1, size);
//        List<Pay> payList = myReserveProgramPage.getContent();
//        List<PayDto.UserReservePageResponse> response = payMapper.ReserveProgramToUserPageProgramResponse(payList);
//
//
//        return new ResponseEntity(
//            new MultiResponseDto<>(
//                response, myReserveProgramPage), HttpStatus.OK);
//    }

    // 화면정의서 22p
    // 유저 - 마이페이지 나의 프로그램 예약 내역 전체 조회
    @GetMapping("/lookup/list")
    public ResponseEntity getUserReservePrograms(HttpServletRequest httpServletRequest,
                                                 @Positive @RequestParam(defaultValue = "1") int page,
                                                 @Positive @RequestParam(defaultValue = "10") int size) {
        Member member = memberService.getLoginMember(httpServletRequest);
        Long memberId = member.getMemberId();
        Page<Pay> myReserveProgramPage = payService.searchUserReserveProgram(memberId, page-1, size);
        List<Pay> payList = myReserveProgramPage.getContent();
        List<PayDto.UserReservePageResponse> response = payMapper.ReserveProgramToUserPageProgramResponse(payList);


        return new ResponseEntity(
                new MultiResponseDto<>(
                        response, myReserveProgramPage), HttpStatus.OK);
    }

    // 화면정의서 23p
    // 유저 - 마이페이지 결제 취소 요청
    @PatchMapping("/lookup/{pay-id}/edit")
    public ResponseEntity cancelPay(@PathVariable("pay-id") @Positive Long payId) {
        Pay pay = payService.updatePayStatus(payId);

        PayDto.PayPatchResponse response = payMapper.PayToPayPatchResponse(pay);

        return new ResponseEntity(
                new SingleResponseDto<>(response), HttpStatus.OK
        );
    }

    // 화면정의서 33p
    // 관리자 - 마이페이지 특정 유저 상담 내역 전체 조회
    @GetMapping("/admin/{member-id}/lookup/list")
    public ResponseEntity getAdminReservePrograms(@PathVariable("member-id") @Positive Long memberId,
        @Positive @RequestParam(defaultValue = "1") int page,
        @Positive @RequestParam(defaultValue = "10") int size) {
        Page<Pay> myReserveProgramPage = payService.searchUserReserveProgram(memberId, page-1, size);
        List<Pay> payList = myReserveProgramPage.getContent();
        List<PayDto.AdminReservePageResponse> response = payMapper.ReserveProgramToAdminPageProgramResponse(payList);


        return new ResponseEntity(
            new MultiResponseDto<>(
                response, myReserveProgramPage), HttpStatus.OK);
    }


    // 화면정의서 38p
    // 관리자 - 결제 완료 내역 조회
    // 관리자 - 결제 취소 대기중 내역 조회
    // 관리자 - 결제 취소 내역 조회
    @GetMapping("/admin/payment/list")
    public ResponseEntity getAdminCompletePayment(@Positive @RequestParam("page") int page,
                                                  @Positive @RequestParam("size") int size,
                                                  @RequestParam("status") String status){
        Page<Pay> searchResult = payService.searchCompletePayment(page-1, size, status);
        List<Pay> pays = searchResult.getContent();

        return new ResponseEntity<>(new MultiResponseDto<>(
                payMapper.ReserveProgramToAdminPayStatusPageResponse(pays), searchResult), HttpStatus.OK
        );
    }

    // 화면정의서 38p
    // 관리자 - 결제 취소 요청 승인
    @PatchMapping("/admin/{pay-id}/edit")
    public ResponseEntity cancelPayConfirm(@PathVariable("pay-id") @Positive Long payId) {
        Pay pay = payService.confirmPayStatus(payId);

        PayDto.PayPatchResponse response = payMapper.PayToPayPatchResponse(pay);

        return new ResponseEntity(
                new SingleResponseDto<>(response), HttpStatus.OK
        );
    }
}

 

'Project > Main Project' 카테고리의 다른 글

💻 배포 - Github Actions CI/CD Pipeline  (0) 2023.01.27
💻 기능 개발 - 프로그램  (0) 2023.01.27
💻 기능 개발 - 상담사  (0) 2023.01.27
💻 기능 개발 - 공지사항  (0) 2023.01.26
💻 기능 개발 - 게시물  (0) 2023.01.26
profile

우주먼지

@o귤o

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그