diff --git a/src/main/java/com/threestar/trainus/domain/comment/controller/CommentController.java b/src/main/java/com/threestar/trainus/domain/comment/controller/CommentController.java index 0c127e4..761d0f1 100644 --- a/src/main/java/com/threestar/trainus/domain/comment/controller/CommentController.java +++ b/src/main/java/com/threestar/trainus/domain/comment/controller/CommentController.java @@ -51,7 +51,7 @@ public ResponseEntity> readAll(@PathVariabl @RequestParam("page") int page, @RequestParam("pageSize") int pageSize) { int correctPage = Math.max(page, 1); - int correctPageSize = Math.max(1, Math.max(pageSize, pageSizeLimit)); + int correctPageSize = Math.max(1, Math.min(pageSize, pageSizeLimit)); CommentPageResponseDto commentsInfo = commentService.readAll(lessonId, correctPage, correctPageSize); CommentPageWrapperDto comments = CommentMapper.toCommentPageWrapperDto(commentsInfo); return PagedResponse.ok("댓글 조회 성공", comments, commentsInfo.getCount(), HttpStatus.OK); diff --git a/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java b/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java index 6f0551f..1bdfa21 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java +++ b/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java @@ -1,27 +1,41 @@ package com.threestar.trainus.domain.payment.controller; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.threestar.trainus.domain.payment.dto.CancelPaymentRequest; -import com.threestar.trainus.domain.payment.dto.ConfirmPaymentRequest; +import com.threestar.trainus.domain.payment.dto.CancelPaymentRequestDto; +import com.threestar.trainus.domain.payment.dto.ConfirmPaymentRequestDto; import com.threestar.trainus.domain.payment.dto.PaymentClient; import com.threestar.trainus.domain.payment.dto.PaymentRequestDto; import com.threestar.trainus.domain.payment.dto.PaymentResponseDto; -import com.threestar.trainus.domain.payment.dto.SaveAmountRequest; +import com.threestar.trainus.domain.payment.dto.SaveAmountRequestDto; import com.threestar.trainus.domain.payment.dto.TossPaymentResponseDto; +import com.threestar.trainus.domain.payment.dto.failure.FailurePaymentResponseDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailureHistoryPageDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailurePageWrapperDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessHistoryPageDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessPageWrapperDto; +import com.threestar.trainus.domain.payment.dto.success.SuccessfulPaymentResponseDto; +import com.threestar.trainus.domain.payment.mapper.PaymentMapper; import com.threestar.trainus.domain.payment.service.PaymentService; import com.threestar.trainus.global.unit.BaseResponse; +import com.threestar.trainus.global.unit.PagedResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +@Tag(name = "결제 API", description = "결제, 검증, 취소 API") @Slf4j @RestController @RequestMapping("/api/v1/payments") @@ -31,48 +45,94 @@ public class PaymentController { private final PaymentClient tossPaymentClient; private final PaymentService paymentService; + @Value("${spring.page.size.limit}") + private int pageSizeLimit; + @PostMapping("/prepare") + @Operation(summary = "결제 준비", description = "실제 결제 전 최종 가격 적용 후 결제 준비") public ResponseEntity> preparePayment(HttpSession session, @RequestBody PaymentRequestDto request) { Long userId = (Long)session.getAttribute("LOGIN_USER"); PaymentResponseDto response = paymentService.preparePayment(request, userId); - //여기에서 쿠폰 가격 정해서 답변 내보내고 이를 통해 아래 호출 return BaseResponse.ok("결제 정보 준비 완료", response, HttpStatus.OK); } @PostMapping("/saveAmount") - public ResponseEntity> saveAmount(HttpSession session, @RequestBody SaveAmountRequest request) { - session.setAttribute(request.getOrderId(), request.getAmount()); + @Operation(summary = "결제 검증 데이터 저장", description = "결제 무결성 검증을 위한 데이터 저장") + public ResponseEntity> saveAmount(HttpSession session, + @RequestBody SaveAmountRequestDto request) { + session.setAttribute(request.orderId(), request.amount()); return BaseResponse.ok("Payment temp save Successful", null, HttpStatus.OK); } @PostMapping("/verifyAmount") + @Operation(summary = "결제 검증 데이터 확인", description = "결제 무결성 검증을 위한 데이터 확인") public ResponseEntity> verifyAmount(HttpSession session, - @RequestBody SaveAmountRequest request) { - Integer amount = (Integer)session.getAttribute(request.getOrderId()); - log.info("amount = {}", amount); - + @RequestBody SaveAmountRequestDto request) { + Integer amount = (Integer)session.getAttribute(request.orderId()); try { - if (amount == null || !amount.equals(request.getAmount())) { + if (amount == null || !amount.equals(request.amount())) { return BaseResponse.error("결제 금액 정보가 유효하지 않습니다", null, HttpStatus.BAD_REQUEST); } return BaseResponse.ok("Payment is valid", null, HttpStatus.OK); } finally { - session.removeAttribute(request.getOrderId()); + session.removeAttribute(request.orderId()); } } @PostMapping("/confirm") - public ResponseEntity> confirm(@RequestBody ConfirmPaymentRequest request) { + @Operation(summary = "결제 진행", description = "결제 진행") + public ResponseEntity> confirm( + @RequestBody ConfirmPaymentRequestDto request) { TossPaymentResponseDto tossResponse = tossPaymentClient.confirmPayment(request); - paymentService.processConfirm(tossResponse); - return BaseResponse.ok("결제 성공", null, HttpStatus.OK); + SuccessfulPaymentResponseDto payResult = paymentService.processConfirm(tossResponse); + return BaseResponse.ok("결제 성공", payResult, HttpStatus.OK); } @PostMapping("/cancel") - public ResponseEntity> cancel(@RequestBody CancelPaymentRequest request) { + @Operation(summary = "결제 취소", description = "결제 취소") + public ResponseEntity> cancel( + @RequestBody CancelPaymentRequestDto request) { TossPaymentResponseDto tossResponse = tossPaymentClient.cancelPayment(request); - paymentService.processCancel(tossResponse); - return BaseResponse.ok("결제 취소 성공", null, HttpStatus.OK); + FailurePaymentResponseDto payResult = paymentService.processCancel(tossResponse, request.cancelReason()); + return BaseResponse.ok("결제 취소 성공", payResult, HttpStatus.OK); + } + + @GetMapping("/view/success") + @Operation(summary = "완료 결제 조회", description = "완료된 결제 내역 조회") + public ResponseEntity> readAll(HttpSession session, + @RequestParam("page") int page, + @RequestParam("pageSize") int pageSize) { + Long userId = (Long)session.getAttribute("LOGIN_USER"); + int correctPage = Math.max(page, 1); + int correctPageSize = Math.max(1, Math.min(pageSize, pageSizeLimit)); + PaymentSuccessHistoryPageDto paymentSuccessHistoryPageDto = paymentService.viewAllSuccessTransaction(userId, + correctPage, correctPageSize); + PaymentSuccessPageWrapperDto payments = PaymentMapper.toPaymentSuccessPageWrapperDto( + paymentSuccessHistoryPageDto); + return PagedResponse.ok("성공 결제 조회 성공", payments, paymentSuccessHistoryPageDto.count(), HttpStatus.OK); } + + @GetMapping("/view/cancel") + @Operation(summary = "취소 결제 조회", description = "취소된 결제 내역 조회") + public ResponseEntity> readAllFailure(HttpSession session, + @RequestParam("page") int page, + @RequestParam("pageSize") int pageSize) { + Long userId = (Long)session.getAttribute("LOGIN_USER"); + int correctPage = Math.max(page, 1); + int correctPageSize = Math.max(1, Math.min(pageSize, pageSizeLimit)); + PaymentFailureHistoryPageDto paymentFailureHistoryPageDto = paymentService.viewAllFailureTransaction(userId, + correctPage, correctPageSize); + PaymentFailurePageWrapperDto payments = PaymentMapper.toPaymentFailurePageWrapperDto( + paymentFailureHistoryPageDto); + return PagedResponse.ok("취소 결제 조회 성공", payments, paymentFailureHistoryPageDto.count(), HttpStatus.OK); + } + + @GetMapping("/{paymentKey}") + @Operation(summary = "상세 결제 조회", description = "상세 결제 내역 조회") + public ResponseEntity> readDetailPayment( + @PathVariable("paymentKey") String paymentKey) { + return BaseResponse.ok("상세 결제 조회 완료", tossPaymentClient.viewDetailPayment(paymentKey), HttpStatus.OK); + } + } diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequest.java b/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequest.java deleted file mode 100644 index 6d7fe05..0000000 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.threestar.trainus.domain.payment.dto; - -import lombok.Data; - -@Data -public class CancelPaymentRequest { - private String paymentKey; - private String cancelReason; -} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequestDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequestDto.java new file mode 100644 index 0000000..4fb134a --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequestDto.java @@ -0,0 +1,11 @@ +package com.threestar.trainus.domain.payment.dto; + +import jakarta.validation.constraints.NotNull; + +public record CancelPaymentRequestDto( + @NotNull(message = "필수 값입니다.") + String paymentKey, + @NotNull(message = "실패 원인은 필수입니다.") + String cancelReason +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequest.java b/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequest.java deleted file mode 100644 index 0022daa..0000000 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.threestar.trainus.domain.payment.dto; - -import lombok.Data; - -@Data -public class ConfirmPaymentRequest { - private int amount; - private String orderId; - private String paymentKey; -} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequestDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequestDto.java new file mode 100644 index 0000000..a3f312b --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequestDto.java @@ -0,0 +1,13 @@ +package com.threestar.trainus.domain.payment.dto; + +import jakarta.validation.constraints.NotNull; + +public record ConfirmPaymentRequestDto( + int amount, + @NotNull(message = "필수 값입니다.") + String orderId, + @NotNull(message = "필수 값입니다.") + String paymentKey +) { + +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java index ce8a9c7..937a728 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java @@ -32,18 +32,12 @@ public PaymentClient(PaymentProperties paymentProperties) { } private String createPaymentAuthHeader(PaymentProperties paymentProperties) { - log.info("secret-key : {}, url ={} baseUrl = {}, baseUrl2 = {}", paymentProperties.getSecretKey(), - paymentProperties.getBaseUrl(), paymentProperties.getConfirmEndPoint(), - paymentProperties.getCancelEndPoint()); byte[] encodedBytes = Base64.getEncoder() .encode((paymentProperties.getSecretKey() + BASIC_DELIMITER).getBytes(StandardCharsets.UTF_8)); - log.info("new Header = {}", AUTH_HEADER_PREFIX + new String(encodedBytes)); return AUTH_HEADER_PREFIX + new String(encodedBytes); } - public TossPaymentResponseDto confirmPayment(ConfirmPaymentRequest request) { - log.info("Sending Toss confirm request: orderId={}, amount={}, paymentKey={}", - request.getOrderId(), request.getAmount(), request.getPaymentKey()); + public TossPaymentResponseDto confirmPayment(ConfirmPaymentRequestDto request) { return restClient.post() .uri(paymentProperties.getConfirmEndPoint()) @@ -56,9 +50,9 @@ public TossPaymentResponseDto confirmPayment(ConfirmPaymentRequest request) { .body(TossPaymentResponseDto.class); } - public TossPaymentResponseDto cancelPayment(CancelPaymentRequest request) { + public TossPaymentResponseDto cancelPayment(CancelPaymentRequestDto request) { return restClient.post() - .uri(String.format(paymentProperties.getCancelEndPoint(), request.getPaymentKey())) + .uri(String.format(paymentProperties.getCancelEndPoint(), request.paymentKey())) .contentType(MediaType.APPLICATION_JSON) .body(request) .retrieve() @@ -68,4 +62,14 @@ public TossPaymentResponseDto cancelPayment(CancelPaymentRequest request) { .body(TossPaymentResponseDto.class); } + public TossPaymentResponseDto viewDetailPayment(String paymentKey) { + return restClient.get() + .uri(paymentProperties.getViewEndPoint() + "/" + paymentKey) + .retrieve() + .onStatus(HttpStatusCode::isError, (req, res) -> { + throw new BusinessException(ErrorCode.INVALID_PAYMENT); + }) + .body(TossPaymentResponseDto.class); + } + } diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java index a181e7e..b85a509 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java @@ -16,4 +16,5 @@ public class PaymentProperties { private String baseUrl; private String confirmEndPoint; private String cancelEndPoint; + private String viewEndPoint; } diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java index 4e39173..7c6d5df 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java @@ -1,12 +1,10 @@ package com.threestar.trainus.domain.payment.dto; -import com.threestar.trainus.domain.payment.entity.PaymentMethod; +import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Data -public class PaymentRequestDto { - private Long lessonId; - private Long userCouponId; - private PaymentMethod paymentMethod; +public record PaymentRequestDto( + @NotNull(message = "필수 값입니다.") + Long lessonId, + Long userCouponId +) { } diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java index bea9074..720dae0 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java @@ -5,15 +5,14 @@ import com.threestar.trainus.domain.payment.entity.PaymentMethod; import lombok.Builder; -import lombok.Data; @Builder -@Data -public class PaymentResponseDto { - private String orderId; - private String lessonTitle; - private Integer originPrice; - private Integer payPrice; - private PaymentMethod paymentMethod; - private LocalDateTime expiredAt; +public record PaymentResponseDto( + String orderId, + String lessonTitle, + Integer originPrice, + Integer payPrice, + PaymentMethod paymentMethod, + LocalDateTime expiredAt +) { } diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequest.java b/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequest.java deleted file mode 100644 index 39eb75f..0000000 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.threestar.trainus.domain.payment.dto; - -import lombok.Data; - -@Data -public class SaveAmountRequest { - private String orderId; - private Integer amount; -} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequestDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequestDto.java new file mode 100644 index 0000000..7fe5666 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequestDto.java @@ -0,0 +1,7 @@ +package com.threestar.trainus.domain.payment.dto; + +public record SaveAmountRequestDto( + String orderId, + int amount +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java index cb71f80..5ecd287 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java @@ -1,19 +1,13 @@ package com.threestar.trainus.domain.payment.dto; -import java.time.LocalDateTime; - -import com.threestar.trainus.domain.payment.entity.PaymentStatus; - -import lombok.Getter; - -@Getter -public class TossPaymentResponseDto { - private String paymentKey; - private String orderId; - private String orderName; - private String status; - private String requestedAt; - private String approvedAt; - private int totalAmount; - private String method; +public record TossPaymentResponseDto( + String paymentKey, + String orderId, + String orderName, + String status, + String requestedAt, + String approvedAt, + int totalAmount, + String method +) { } diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/failure/FailurePaymentResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/FailurePaymentResponseDto.java new file mode 100644 index 0000000..731d8df --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/FailurePaymentResponseDto.java @@ -0,0 +1,16 @@ +package com.threestar.trainus.domain.payment.dto.failure; + +import java.time.LocalDateTime; + +import lombok.Builder; + +@Builder +public record FailurePaymentResponseDto( + String lessonName, + String cancelReason, + LocalDateTime startAt, + LocalDateTime endAt, + LocalDateTime paymentCancelledAt, + int payPrice +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailureHistoryPageDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailureHistoryPageDto.java new file mode 100644 index 0000000..c501fa5 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailureHistoryPageDto.java @@ -0,0 +1,12 @@ +package com.threestar.trainus.domain.payment.dto.failure; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record PaymentFailureHistoryPageDto( + Integer count, + List failureHistory +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailureHistoryResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailureHistoryResponseDto.java new file mode 100644 index 0000000..4a886f6 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailureHistoryResponseDto.java @@ -0,0 +1,27 @@ +package com.threestar.trainus.domain.payment.dto.failure; + +import java.time.LocalDateTime; + +import com.threestar.trainus.domain.payment.entity.PaymentMethod; +import com.threestar.trainus.domain.payment.entity.PaymentStatus; + +import lombok.Builder; + +@Builder +public record PaymentFailureHistoryResponseDto( + String lessonTitle, + LocalDateTime paymentCancelledAt, + LocalDateTime lessonStartAt, + LocalDateTime lessonEndAt, + PaymentMethod paymentMethod, + String city, + String district, + String dong, + Integer originalPrice, + PaymentStatus paymentStatus, + String paymentKey, + String orderId, + String detailAddress, + String cancelReason +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailurePageWrapperDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailurePageWrapperDto.java new file mode 100644 index 0000000..c1f1481 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/failure/PaymentFailurePageWrapperDto.java @@ -0,0 +1,11 @@ +package com.threestar.trainus.domain.payment.dto.failure; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record PaymentFailurePageWrapperDto( + List failureHistory +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessHistoryPageDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessHistoryPageDto.java new file mode 100644 index 0000000..0715f3f --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessHistoryPageDto.java @@ -0,0 +1,12 @@ +package com.threestar.trainus.domain.payment.dto.success; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record PaymentSuccessHistoryPageDto( + Integer count, + List successHistory +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessHistoryResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessHistoryResponseDto.java new file mode 100644 index 0000000..275f44d --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessHistoryResponseDto.java @@ -0,0 +1,26 @@ +package com.threestar.trainus.domain.payment.dto.success; + +import java.time.LocalDateTime; + +import com.threestar.trainus.domain.payment.entity.PaymentMethod; +import com.threestar.trainus.domain.payment.entity.PaymentStatus; + +import lombok.Builder; + +@Builder +public record PaymentSuccessHistoryResponseDto( + String lessonTitle, + LocalDateTime paymentApprovedAt, + LocalDateTime lessonStartAt, + LocalDateTime lessonEndAt, + PaymentMethod paymentMethod, + String city, + String district, + String dong, + Integer originalPrice, + PaymentStatus paymentStatus, + String paymentKey, + String orderId, + String detailAddress +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessPageWrapperDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessPageWrapperDto.java new file mode 100644 index 0000000..fb3b6f3 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/success/PaymentSuccessPageWrapperDto.java @@ -0,0 +1,11 @@ +package com.threestar.trainus.domain.payment.dto.success; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record PaymentSuccessPageWrapperDto( + List successHistory +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/success/SuccessfulPaymentResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/success/SuccessfulPaymentResponseDto.java new file mode 100644 index 0000000..01a1005 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/success/SuccessfulPaymentResponseDto.java @@ -0,0 +1,20 @@ +package com.threestar.trainus.domain.payment.dto.success; + +import java.time.LocalDateTime; + +import com.threestar.trainus.domain.payment.entity.PaymentMethod; + +import lombok.Builder; + +@Builder +public record SuccessfulPaymentResponseDto( + String addressDetail, + LocalDateTime startAt, + LocalDateTime endAt, + int payPrice, + String city, + String district, + String dong, + PaymentMethod paymentMethod +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java b/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java index 7405be7..f8be8fd 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java +++ b/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java @@ -37,7 +37,7 @@ public class Payment extends BaseDateEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long paymentId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) diff --git a/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java b/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java index b12a8b2..ab66ecc 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java +++ b/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java @@ -27,7 +27,7 @@ public class TossPayment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long tossPaymentId; @Column(nullable = false, unique = true) private String paymentKey; @@ -57,18 +57,13 @@ public class TossPayment { private LocalDateTime approvedAt; - public void changeStatus(LocalDateTime requestedAt, LocalDateTime approvedAt, PaymentStatus paymentStatus) { + private String cancelReason; + + public void changeStatus(LocalDateTime requestedAt, LocalDateTime approvedAt, PaymentStatus paymentStatus, + String cancelReason) { this.requestedAt = requestedAt; this.approvedAt = approvedAt; this.paymentStatus = paymentStatus; + this.cancelReason = cancelReason; } - - public boolean isCancelStatus() { - return this.paymentStatus == PaymentStatus.CANCELED; - } - - public boolean isDoneStatus() { - return this.paymentStatus == PaymentStatus.DONE; - } - } diff --git a/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java b/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java index 43f3ef0..bdf9a86 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java +++ b/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java @@ -1,8 +1,114 @@ package com.threestar.trainus.domain.payment.mapper; +import java.time.LocalDateTime; +import java.util.List; + +import com.threestar.trainus.domain.payment.dto.failure.FailurePaymentResponseDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailureHistoryPageDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailureHistoryResponseDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailurePageWrapperDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessHistoryPageDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessHistoryResponseDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessPageWrapperDto; +import com.threestar.trainus.domain.payment.dto.success.SuccessfulPaymentResponseDto; +import com.threestar.trainus.domain.payment.entity.Payment; +import com.threestar.trainus.domain.payment.entity.TossPayment; + import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class PaymentMapper { + + public static SuccessfulPaymentResponseDto toSuccessfulPaymentResponseDto(Payment payment) { + return SuccessfulPaymentResponseDto.builder() + .payPrice(payment.getPayPrice()) + .paymentMethod(payment.getPaymentMethod()) + .city(payment.getLesson().getCity()) + .district(payment.getLesson().getDistrict()) + .dong(payment.getLesson().getDong()) + .addressDetail(payment.getLesson().getAddressDetail()) + .endAt(payment.getLesson().getEndAt()) + .startAt(payment.getLesson().getStartAt()) + .build(); + } + + public static FailurePaymentResponseDto toFailurePaymentResponseDto(Payment payment, String cancelReason) { + return FailurePaymentResponseDto.builder() + .lessonName(payment.getLesson().getLessonName()) + .cancelReason(cancelReason) + .payPrice(payment.getPayPrice()) + .startAt(payment.getLesson().getStartAt()) + .endAt(payment.getLesson().getEndAt()) + .paymentCancelledAt(payment.getCancelledAt()) + .build(); + } + + public static PaymentSuccessHistoryResponseDto toPaymentSuccessHistoryResponseDto(Payment payment, + TossPayment tossPayment) { + return PaymentSuccessHistoryResponseDto.builder() + .paymentKey(tossPayment.getPaymentKey()) + .paymentStatus(tossPayment.getPaymentStatus()) + .lessonTitle(payment.getLesson().getLessonName()) + .paymentMethod(payment.getPaymentMethod()) + .originalPrice(payment.getPayPrice()) + .dong(payment.getLesson().getDong()) + .district(payment.getLesson().getDistrict()) + .city(payment.getLesson().getCity()) + .detailAddress(payment.getLesson().getAddressDetail()) + .orderId(payment.getOrderId()) + .paymentApprovedAt(tossPayment.getApprovedAt()) + .lessonStartAt(payment.getLesson().getStartAt()) + .lessonEndAt(payment.getLesson().getEndAt()) + .build(); + } + + public static PaymentSuccessHistoryPageDto toPaymentSuccessHistoryPageDto( + List paymentHistory, Integer count) { + return PaymentSuccessHistoryPageDto.builder() + .successHistory(paymentHistory) + .count(count) + .build(); + } + + public static PaymentSuccessPageWrapperDto toPaymentSuccessPageWrapperDto(PaymentSuccessHistoryPageDto paymentDto) { + return PaymentSuccessPageWrapperDto.builder() + .successHistory(paymentDto.successHistory()) + .build(); + } + + public static PaymentFailureHistoryResponseDto toPaymentFailureHistoryResponseDto(Payment payment, + TossPayment tossPayment) { + return PaymentFailureHistoryResponseDto.builder() + .paymentKey(tossPayment.getPaymentKey()) + .paymentStatus(tossPayment.getPaymentStatus()) + .lessonTitle(payment.getLesson().getLessonName()) + .paymentMethod(payment.getPaymentMethod()) + .originalPrice(payment.getPayPrice()) + .dong(payment.getLesson().getDong()) + .district(payment.getLesson().getDistrict()) + .city(payment.getLesson().getCity()) + .detailAddress(payment.getLesson().getAddressDetail()) + .orderId(payment.getOrderId()) + .paymentCancelledAt(tossPayment.getApprovedAt()) + .lessonStartAt(payment.getLesson().getStartAt()) + .lessonEndAt(payment.getLesson().getEndAt()) + .cancelReason(tossPayment.getCancelReason()) + .paymentCancelledAt(payment.getCancelledAt()) + .build(); + } + + public static PaymentFailureHistoryPageDto toPaymentFailureHistoryPageDto( + List paymentHistory, Integer count) { + return PaymentFailureHistoryPageDto.builder() + .failureHistory(paymentHistory) + .count(count) + .build(); + } + + public static PaymentFailurePageWrapperDto toPaymentFailurePageWrapperDto(PaymentFailureHistoryPageDto paymentDto) { + return PaymentFailurePageWrapperDto.builder() + .failureHistory(paymentDto.failureHistory()) + .build(); + } } diff --git a/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java b/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java index bf42192..857fd58 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java +++ b/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java @@ -1,11 +1,43 @@ package com.threestar.trainus.domain.payment.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import com.threestar.trainus.domain.coupon.user.entity.UserCoupon; import com.threestar.trainus.domain.payment.entity.Payment; +import com.threestar.trainus.domain.payment.entity.PaymentStatus; public interface PaymentRepository extends JpaRepository { Optional findByOrderId(String orderId); + + Optional findByUserCouponAndStatus(UserCoupon coupon, PaymentStatus status); + + @Query(value = """ + select * from payments + where user_id = :userId + and status = :status + order by pay_date desc + limit :limit offset :offset + """, nativeQuery = true + ) + List findAllByUserAndStatus( + @Param("userId") Long userId, + @Param("status") String status, + @Param("offset") int offset, + @Param("limit") int limit + ); + + @Query(value = """ + select count(*) from (select payment_id from payments where user_id = :userId and status = :status limit :limit) t + """, nativeQuery = true + ) + Integer count( + @Param("userId") Long userId, + @Param("status") String status, + @Param("limit") int limit + ); } diff --git a/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java b/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java index c5fd45c..a61302c 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java +++ b/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java @@ -8,4 +8,6 @@ public interface TossPaymentRepository extends JpaRepository { Optional findByPaymentKey(String paymentKey); + + Optional findByOrderId(String orderId); } diff --git a/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java b/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java index be6ace2..de7909f 100644 --- a/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java +++ b/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java @@ -3,6 +3,8 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; import java.util.UUID; import org.springframework.stereotype.Service; @@ -16,16 +18,24 @@ import com.threestar.trainus.domain.payment.dto.PaymentRequestDto; import com.threestar.trainus.domain.payment.dto.PaymentResponseDto; import com.threestar.trainus.domain.payment.dto.TossPaymentResponseDto; +import com.threestar.trainus.domain.payment.dto.failure.FailurePaymentResponseDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailureHistoryPageDto; +import com.threestar.trainus.domain.payment.dto.failure.PaymentFailureHistoryResponseDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessHistoryPageDto; +import com.threestar.trainus.domain.payment.dto.success.PaymentSuccessHistoryResponseDto; +import com.threestar.trainus.domain.payment.dto.success.SuccessfulPaymentResponseDto; import com.threestar.trainus.domain.payment.entity.Payment; import com.threestar.trainus.domain.payment.entity.PaymentMethod; import com.threestar.trainus.domain.payment.entity.PaymentStatus; import com.threestar.trainus.domain.payment.entity.TossPayment; +import com.threestar.trainus.domain.payment.mapper.PaymentMapper; import com.threestar.trainus.domain.payment.repository.PaymentRepository; import com.threestar.trainus.domain.payment.repository.TossPaymentRepository; import com.threestar.trainus.domain.user.entity.User; import com.threestar.trainus.domain.user.service.UserService; import com.threestar.trainus.global.exception.domain.ErrorCode; import com.threestar.trainus.global.exception.handler.BusinessException; +import com.threestar.trainus.global.utils.PageLimitCalculator; import lombok.RequiredArgsConstructor; @@ -41,25 +51,36 @@ public class PaymentService { @Transactional public PaymentResponseDto preparePayment(PaymentRequestDto request, Long userId) { - Lesson lesson = lessonService.findLessonById(request.getLessonId()); + Lesson lesson = lessonService.findLessonById(request.lessonId()); User user = userService.getUserById(userId); int originPrice = lesson.getPrice(); - int discount = 0; + UserCoupon coupon = null; - if (request.getUserCouponId() != null) { - coupon = userCouponRepository.findById(request.getUserCouponId()) + if (request.userCouponId() != null) { + coupon = userCouponRepository.findById(request.userCouponId()) .filter(c -> c.getStatus() == CouponStatus.ACTIVE) .orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND)); - // discount = + + Optional existing = paymentRepository.findByUserCouponAndStatus(coupon, PaymentStatus.READY); + if (existing.isPresent()) { + return PaymentResponseDto.builder() + .originPrice(existing.get().getOriginPrice()) + .lessonTitle(lesson.getLessonName()) + .paymentMethod(existing.get().getPaymentMethod()) + .payPrice(existing.get().getPayPrice()) + .orderId(existing.get().getOrderId()) + .build(); + } + String discountPrice = coupon.getCoupon().getDiscountPrice(); if (discountPrice.contains("%")) { - discount = Integer.parseInt(discountPrice.substring(0, discountPrice.indexOf("%"))); + int discountPercentage = Integer.parseInt(discountPrice.substring(0, discountPrice.indexOf("%"))); + discount = (originPrice * discountPercentage) / 100; } else { - discount = Integer.parseInt(discountPrice); + discount = Integer.parseInt(discountPrice.substring(0, discountPrice.indexOf("원"))); } - coupon.use(); } int finalPrice = Math.max(0, originPrice - discount); @@ -75,7 +96,7 @@ public PaymentResponseDto preparePayment(PaymentRequestDto request, Long userId) .payDate(LocalDateTime.now()) .userCoupon(coupon) .status(PaymentStatus.READY) - .paymentMethod(PaymentMethod.TOSS_PAYMENT) + .paymentMethod(PaymentMethod.CREDIT_CARD) .build(); paymentRepository.save(payment); @@ -83,53 +104,61 @@ public PaymentResponseDto preparePayment(PaymentRequestDto request, Long userId) return PaymentResponseDto.builder() .originPrice(originPrice) .lessonTitle(lesson.getLessonName()) - .paymentMethod(PaymentMethod.TOSS_PAYMENT) + .paymentMethod(PaymentMethod.CREDIT_CARD) .payPrice(finalPrice) .orderId(orderId) .build(); - } @Transactional - public void processConfirm(TossPaymentResponseDto tossResponseDto) { - Payment payment = paymentRepository.findByOrderId(tossResponseDto.getOrderId()) + public SuccessfulPaymentResponseDto processConfirm(TossPaymentResponseDto tossResponseDto) { + Payment payment = paymentRepository.findByOrderId(tossResponseDto.orderId()) .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT)); - payment.setPayPrice(tossResponseDto.getTotalAmount()); + if (payment.getStatus() != PaymentStatus.READY) { + throw new BusinessException(ErrorCode.INVALID_PAYMENT); + } DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; - LocalDateTime paidAt = OffsetDateTime.parse(tossResponseDto.getApprovedAt(), formatter).toLocalDateTime(); - - LocalDateTime requestAt = OffsetDateTime.parse(tossResponseDto.getRequestedAt(), formatter).toLocalDateTime(); + LocalDateTime paidAt = OffsetDateTime.parse(tossResponseDto.approvedAt(), formatter).toLocalDateTime(); + LocalDateTime requestAt = OffsetDateTime.parse(tossResponseDto.requestedAt(), formatter).toLocalDateTime(); + payment.setPayPrice(tossResponseDto.totalAmount()); payment.setPayDate(paidAt); payment.setStatus(PaymentStatus.DONE); - payment.setPaymentMethod(PaymentMethod.fromTossMethod(tossResponseDto.getMethod())); + payment.setPaymentMethod(PaymentMethod.fromTossMethod(tossResponseDto.method())); TossPayment tossPayment = TossPayment.builder() .payment(payment) - .paymentKey(tossResponseDto.getPaymentKey()) - .orderId(tossResponseDto.getOrderId()) - .amount(tossResponseDto.getTotalAmount()) - .orderName(tossResponseDto.getOrderName()) + .paymentKey(tossResponseDto.paymentKey()) + .orderId(tossResponseDto.orderId()) + .amount(tossResponseDto.totalAmount()) + .orderName(tossResponseDto.orderName()) .paymentStatus(PaymentStatus.DONE) - .paymentMethod(PaymentMethod.fromTossMethod(tossResponseDto.getMethod())) + .paymentMethod(PaymentMethod.fromTossMethod(tossResponseDto.method())) .requestedAt(requestAt) .approvedAt(paidAt) .build(); + if (payment.getUserCoupon() != null && payment.getUserCoupon().getStatus() == CouponStatus.ACTIVE) { + UserCoupon coupon = payment.getUserCoupon(); + coupon.use(); + userCouponRepository.save(coupon); + } tossPaymentRepository.save(tossPayment); + + return PaymentMapper.toSuccessfulPaymentResponseDto(payment); } @Transactional - public void processCancel(TossPaymentResponseDto tossResponseDto) { - TossPayment tossPayment = tossPaymentRepository.findByPaymentKey(tossResponseDto.getPaymentKey()) + public FailurePaymentResponseDto processCancel(TossPaymentResponseDto tossResponseDto, String cancelReason) { + TossPayment tossPayment = tossPaymentRepository.findByPaymentKey(tossResponseDto.paymentKey()) .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT)); DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; - LocalDateTime requestAt = OffsetDateTime.parse(tossResponseDto.getRequestedAt(), formatter).toLocalDateTime(); + LocalDateTime requestAt = OffsetDateTime.parse(tossResponseDto.requestedAt(), formatter).toLocalDateTime(); - tossPayment.changeStatus(requestAt, null, PaymentStatus.CANCELED); + tossPayment.changeStatus(requestAt, null, PaymentStatus.CANCELED, cancelReason); tossPaymentRepository.save(tossPayment); @@ -145,5 +174,44 @@ public void processCancel(TossPaymentResponseDto tossResponseDto) { userCouponRepository.save(coupon); } } + + return PaymentMapper.toFailurePaymentResponseDto(payment, cancelReason); + } + + @Transactional(readOnly = true) + public PaymentSuccessHistoryPageDto viewAllSuccessTransaction(Long userId, int page, int pageSize) { + List allSuccessPayments = paymentRepository.findAllByUserAndStatus(userId, PaymentStatus.DONE.name(), + (page - 1) * pageSize, pageSize); + List dtoList = allSuccessPayments.stream() + .map(payment -> { + TossPayment tossPayment = tossPaymentRepository.findByOrderId(payment.getOrderId()) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT)); + return PaymentMapper.toPaymentSuccessHistoryResponseDto(payment, tossPayment); + }) + .toList(); + + return PaymentMapper.toPaymentSuccessHistoryPageDto( + dtoList, paymentRepository.count(userId, PaymentStatus.DONE.name(), + PageLimitCalculator.calculatePageLimit(page, pageSize, 5)) + ); + } + + @Transactional(readOnly = true) + public PaymentFailureHistoryPageDto viewAllFailureTransaction(Long userId, int page, int pageSize) { + List allFailurePayments = paymentRepository.findAllByUserAndStatus(userId, + PaymentStatus.CANCELED.name(), + (page - 1) * pageSize, pageSize); + List dtoList = allFailurePayments.stream() + .map(payment -> { + TossPayment tossPayment = tossPaymentRepository.findByOrderId(payment.getOrderId()) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT)); + return PaymentMapper.toPaymentFailureHistoryResponseDto(payment, tossPayment); + }) + .toList(); + + return PaymentMapper.toPaymentFailureHistoryPageDto( + dtoList, paymentRepository.count(userId, PaymentStatus.CANCELED.name(), + PageLimitCalculator.calculatePageLimit(page, pageSize, 5)) + ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/threestar/trainus/domain/review/controller/ReviewController.java b/src/main/java/com/threestar/trainus/domain/review/controller/ReviewController.java index d940d5e..d5e9d45 100644 --- a/src/main/java/com/threestar/trainus/domain/review/controller/ReviewController.java +++ b/src/main/java/com/threestar/trainus/domain/review/controller/ReviewController.java @@ -52,7 +52,7 @@ public ResponseEntity> readAll(@PathVariable @RequestParam("pageSize") int pageSize ) { int correctPage = Math.max(page, 1); - int correctPageSize = Math.max(1, Math.max(pageSize, pageSizeLimit)); + int correctPageSize = Math.max(1, Math.min(pageSize, pageSizeLimit)); ReviewPageResponseDto reviewsInfo = reviewService.readAll(userId, correctPage, correctPageSize); ReviewPageWrapperDto reviews = ReviewMapper.toReviewPageWrapperDto(reviewsInfo); return PagedResponse.ok("조회가 완료됐습니다.", reviews, reviewsInfo.getCount(), HttpStatus.OK); diff --git a/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java b/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java index d55d262..f4cd6f7 100644 --- a/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java +++ b/src/main/java/com/threestar/trainus/global/config/security/SecurityConfig.java @@ -27,7 +27,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/users/**", "/api/lessons/test-auth", "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/profiles/**", "/api/v1/lessons/**", "/api/v1/comments/**", "/api/v1/reviews/**" - , "/api/v1/rankings/**") + , "/api/v1/rankings/**", "/api/v1/payments/**") .permitAll() .anyRequest() .authenticated()