From 78d6f1a98c774243822e5145c1732b9cdd120657 Mon Sep 17 00:00:00 2001 From: dongHyo Date: Fri, 15 Jul 2022 17:15:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=B8=EC=9D=B8=EA=B2=B0=EC=A0=9C?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=ED=99=98=EB=B6=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/exception/ErrorCode.java | 10 +++ .../movie/application/TicketController.java | 2 +- .../ticketing/server/movie/domain/Ticket.java | 21 +++++ .../movie/service/TicketServiceImpl.java | 17 ++++ .../movie/service/dto/TicketRefundDTO.java | 20 +++++ .../movie/service/dto/TicketsRefundDTO.java | 16 ++++ .../service/dto/TicketsRefundResponse.java | 21 +++++ .../service/interfaces/TicketService.java | 2 + .../server/payment/api/KakaoPayClient.java | 6 ++ .../server/payment/api/MovieClient.java | 4 + .../dto/requset/KakaoPayCancelRequest.java | 47 ++++++++++ .../dto/response/ApprovedCancelAmount.java | 16 ++++ .../dto/response/CancelAvailableAmount.java | 16 ++++ .../api/dto/response/CanceledAmount.java | 16 ++++ .../dto/response/KakaoPayCancelResponse.java | 50 +++++++++++ .../payment/api/impl/MovieClientImpl.java | 8 ++ .../api/impl/TicketsRefundRequest.java | 12 +++ .../application/PaymentController.java | 33 ++++--- .../request/PaymentRefundRequest.java | 12 +++ .../response/PaymentRefundResponse.java | 18 ++++ .../server/payment/domain/Payment.java | 11 +++ .../service/PaymentApisServiceImpl.java | 29 +++++++ .../payment/service/dto/PaymentRefundDTO.java | 41 +++++++++ .../interfaces/PaymentApisService.java | 2 + .../server/movie/domain/TicketTest.java | 87 ++++++++++++++++++- 25 files changed, 505 insertions(+), 12 deletions(-) create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketRefundDTO.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundDTO.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundResponse.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayCancelRequest.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/ApprovedCancelAmount.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/CancelAvailableAmount.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/CanceledAmount.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayCancelResponse.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/impl/TicketsRefundRequest.java create mode 100644 server/src/main/java/com/ticketing/server/payment/application/request/PaymentRefundRequest.java create mode 100644 server/src/main/java/com/ticketing/server/payment/application/response/PaymentRefundResponse.java create mode 100644 server/src/main/java/com/ticketing/server/payment/service/dto/PaymentRefundDTO.java diff --git a/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java b/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java index fc8e00b..cff84fb 100644 --- a/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java +++ b/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java @@ -24,6 +24,8 @@ public enum ErrorCode { BAD_REQUEST_PAYMENT_COMPLETE(BAD_REQUEST, "처리할 결제 정보가 존재하지 않습니다."), BAD_REQUEST_PAYMENT_READY(BAD_REQUEST, "이미 진행 중인 결제가 존재합니다."), BAD_REQUEST_PAYMENT_CANCEL(BAD_REQUEST, "취소할 티켓이 존재하지 않습니다."), + NOT_REFUNDABLE_TIME(BAD_REQUEST, "환불이 가능한 시간이 지났습니다."), + NOT_REFUNDABLE_SEAT(BAD_REQUEST, "환불할 수 있는 좌석이 아닙니다."), /* 403 FORBIDDEN : 접근 권한 제한 */ VALID_USER_ID(FORBIDDEN, "해당 정보에 접근 권한이 존재하지 않습니다."), @@ -85,6 +87,14 @@ public enum ErrorCode { throw new TicketingException(BAD_REQUEST_PAYMENT_CANCEL); } + public static TicketingException throwNotRefundableTime() { + throw new TicketingException(NOT_REFUNDABLE_TIME); + } + + public static TicketingException throwNotRefundableSeat() { + throw new TicketingException(NOT_REFUNDABLE_SEAT); + } + /* 403 FORBIDDEN : 접근 권한 제한 */ public static TicketingException throwValidUserId() { throw new TicketingException(VALID_USER_ID); diff --git a/server/src/main/java/com/ticketing/server/movie/application/TicketController.java b/server/src/main/java/com/ticketing/server/movie/application/TicketController.java index f4ce22c..0032855 100644 --- a/server/src/main/java/com/ticketing/server/movie/application/TicketController.java +++ b/server/src/main/java/com/ticketing/server/movie/application/TicketController.java @@ -40,7 +40,7 @@ public class TicketController { ); } - @GetMapping("payments/{paymentId}") + @GetMapping("/payments/{paymentId}") @Secured(USER) public ResponseEntity findTicketsByPaymentId( @PathVariable("paymentId") @NotNull Long paymentId) { diff --git a/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java b/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java index 57efc83..8f12867 100644 --- a/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java +++ b/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java @@ -3,6 +3,7 @@ package com.ticketing.server.movie.domain; import com.ticketing.server.global.dto.repository.AbstractEntity; import com.ticketing.server.global.exception.ErrorCode; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -78,6 +79,26 @@ public class Ticket extends AbstractEntity { } status = TicketStatus.SALE; + paymentId = null; + return this; + } + + public Ticket refund(LocalDateTime dateTime) { + long seconds = ChronoUnit.SECONDS.between(dateTime, getStartAt()); + if (600L > seconds) { + throw ErrorCode.throwNotRefundableTime(); + } + + return refund(); + } + + public Ticket refund() { + if (!TicketStatus.SOLD.equals(status)) { + throw ErrorCode.throwNotRefundableSeat(); + } + + status = TicketStatus.SALE; + paymentId = null; return this; } diff --git a/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java b/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java index f3f622f..58bd582 100644 --- a/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java @@ -9,13 +9,16 @@ import com.ticketing.server.movie.domain.repository.TicketRepository; import com.ticketing.server.movie.service.dto.TicketDTO; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; import com.ticketing.server.movie.service.dto.TicketListDTO; +import com.ticketing.server.movie.service.dto.TicketRefundDTO; import com.ticketing.server.movie.service.dto.TicketReservationDTO; import com.ticketing.server.movie.service.dto.TicketSoldDTO; import com.ticketing.server.movie.service.dto.TicketsCancelDTO; +import com.ticketing.server.movie.service.dto.TicketsRefundDTO; import com.ticketing.server.movie.service.dto.TicketsReservationDTO; import com.ticketing.server.movie.service.dto.TicketsSoldDTO; import com.ticketing.server.movie.service.interfaces.TicketService; import com.ticketing.server.payment.service.dto.TicketDetailDTO; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; import javax.validation.constraints.NotNull; @@ -104,6 +107,20 @@ public class TicketServiceImpl implements TicketService { return new TicketsCancelDTO(ticketIds); } + @Override + @Transactional + public TicketsRefundDTO myTicketsRefund(@NotNull Long paymentId) { + List tickets = ticketRepository.findTicketFetchJoinByPaymentId(paymentId); + LocalDateTime now = LocalDateTime.now(); + + List refundDtoList = tickets.stream() + .map(ticket -> ticket.refund(now)) + .map(TicketRefundDTO::new) + .collect(Collectors.toList()); + + return new TicketsRefundDTO(refundDtoList); + } + private List getTicketsByInTicketIds(List ticketIds) { List tickets = ticketRepository.findTicketFetchJoinByTicketIds(ticketIds); diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketRefundDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketRefundDTO.java new file mode 100644 index 0000000..9b3cf67 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketRefundDTO.java @@ -0,0 +1,20 @@ +package com.ticketing.server.movie.service.dto; + +import com.ticketing.server.movie.domain.Ticket; +import com.ticketing.server.movie.domain.TicketStatus; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TicketRefundDTO { + + private Long ticketId; + private TicketStatus ticketStatus; + + public TicketRefundDTO(Ticket ticket) { + this(ticket.getId(), ticket.getStatus()); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundDTO.java new file mode 100644 index 0000000..8faeb72 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundDTO.java @@ -0,0 +1,16 @@ +package com.ticketing.server.movie.service.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketsRefundDTO { + + private List tickets; + + public TicketsRefundResponse toResponse() { + return new TicketsRefundResponse(tickets); + } +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundResponse.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundResponse.java new file mode 100644 index 0000000..e4fdd27 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsRefundResponse.java @@ -0,0 +1,21 @@ +package com.ticketing.server.movie.service.dto; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketsRefundResponse { + + private final List tickets; + + public List getTicketIds() { + return tickets.stream() + .mapToLong(TicketRefundDTO::getTicketId) + .boxed() + .collect(Collectors.toList()); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java b/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java index 0d1c9b9..91d8538 100644 --- a/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java +++ b/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java @@ -4,6 +4,7 @@ import com.ticketing.server.global.validator.constraints.NotEmptyCollection; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; import com.ticketing.server.movie.service.dto.TicketListDTO; import com.ticketing.server.movie.service.dto.TicketsCancelDTO; +import com.ticketing.server.movie.service.dto.TicketsRefundDTO; import com.ticketing.server.movie.service.dto.TicketsReservationDTO; import com.ticketing.server.movie.service.dto.TicketsSoldDTO; import java.util.List; @@ -21,4 +22,5 @@ public interface TicketService { TicketsCancelDTO ticketCancel(@NotEmptyCollection List ticketIds); + TicketsRefundDTO myTicketsRefund(@NotNull Long paymentId); } diff --git a/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java b/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java index 8371740..b1ba595 100644 --- a/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java +++ b/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java @@ -3,8 +3,10 @@ package com.ticketing.server.payment.api; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; import com.ticketing.server.payment.api.dto.requset.KakaoPayApproveRequest; +import com.ticketing.server.payment.api.dto.requset.KakaoPayCancelRequest; import com.ticketing.server.payment.api.dto.requset.KakaoPayReadyRequest; import com.ticketing.server.payment.api.dto.response.KakaoPayApproveResponse; +import com.ticketing.server.payment.api.dto.response.KakaoPayCancelResponse; import com.ticketing.server.payment.api.dto.response.KakaoPayReadyResponse; import feign.Headers; import org.springframework.cloud.openfeign.FeignClient; @@ -23,4 +25,8 @@ public interface KakaoPayClient { @Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8") KakaoPayApproveResponse approve(@RequestHeader(value = "Authorization") String authorization, KakaoPayApproveRequest request); + @PostMapping(value = "/cancel", consumes = APPLICATION_FORM_URLENCODED_VALUE) + @Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8") + KakaoPayCancelResponse cancel(@RequestHeader(value = "Authorization") String authorization, KakaoPayCancelRequest request); + } diff --git a/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java b/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java index 55fd333..ea7bca0 100644 --- a/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java +++ b/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java @@ -7,6 +7,8 @@ import com.ticketing.server.movie.application.response.TicketDetailsResponse; import com.ticketing.server.movie.application.response.TicketReservationResponse; import com.ticketing.server.movie.application.request.TicketSoldRequest; import com.ticketing.server.movie.application.response.TicketSoldResponse; +import com.ticketing.server.movie.service.dto.TicketsRefundResponse; +import com.ticketing.server.payment.api.impl.TicketsRefundRequest; import javax.validation.constraints.NotNull; public interface MovieClient { @@ -18,4 +20,6 @@ public interface MovieClient { TicketSoldResponse ticketSold(@NotNull TicketSoldRequest request); TicketCancelResponse ticketCancel(@NotNull TicketCancelRequest request); + + TicketsRefundResponse myTicketRefund(TicketsRefundRequest request); } diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayCancelRequest.java b/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayCancelRequest.java new file mode 100644 index 0000000..d3ac509 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayCancelRequest.java @@ -0,0 +1,47 @@ +package com.ticketing.server.payment.api.dto.requset; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.validation.constraints.NotEmpty; +import lombok.Getter; + +@Getter +@JsonInclude(Include.NON_NULL) +public class KakaoPayCancelRequest { + + @NotEmpty + private String cid = "TC0ONETIME"; + + @feign.form.FormProperty("cid_secret") + private String cidSecret; + + @NotEmpty + private String tid; + + @NotEmpty + @feign.form.FormProperty("cancel_amount") + private Integer cancelAmount; + + @NotEmpty + @feign.form.FormProperty("cancel_tax_free_amount") + private Integer cancelTaxFreeAmount; + + @feign.form.FormProperty("cancel_vat_amount") + private Integer cancelVatAmount; + + @feign.form.FormProperty("cancel_available_amount") + private Integer cancelAvailableAmount; + + private String payload; + + public KakaoPayCancelRequest(String tid, Integer cancelAmount) { + this(tid, cancelAmount, 0); + } + + public KakaoPayCancelRequest(String tid, Integer cancelAmount, int cancelTaxFreeAmount) { + this.tid = tid; + this.cancelAmount = cancelAmount; + this.cancelTaxFreeAmount = cancelTaxFreeAmount; + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/ApprovedCancelAmount.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/ApprovedCancelAmount.java new file mode 100644 index 0000000..6a85df6 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/ApprovedCancelAmount.java @@ -0,0 +1,16 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import lombok.Getter; + +@Getter +public class ApprovedCancelAmount extends SnakeCaseStrategy { + + private Integer total; + private Integer taxFree; + private Integer vat; + private Integer point; + private Integer discount; + private Integer greenDeposit; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/CancelAvailableAmount.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/CancelAvailableAmount.java new file mode 100644 index 0000000..e19ce72 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/CancelAvailableAmount.java @@ -0,0 +1,16 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import lombok.Getter; + +@Getter +public class CancelAvailableAmount extends SnakeCaseStrategy { + + private Integer total; + private Integer taxFree; + private Integer vat; + private Integer point; + private Integer discount; + private Integer greenDeposit; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/CanceledAmount.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/CanceledAmount.java new file mode 100644 index 0000000..c0e0623 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/CanceledAmount.java @@ -0,0 +1,16 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import lombok.Getter; + +@Getter +public class CanceledAmount extends SnakeCaseStrategy { + + private Integer total; + private Integer taxFree; + private Integer vat; + private Integer point; + private Integer discount; + private Integer greenDeposit; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayCancelResponse.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayCancelResponse.java new file mode 100644 index 0000000..03be105 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayCancelResponse.java @@ -0,0 +1,50 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class KakaoPayCancelResponse extends SnakeCaseStrategy { + + private String aid; + + private String tid; + + private String cid; + + private String status; + + private String partnerOrderId; + + private String partnerUserId; + + private String paymentMethodType; + + private Amount amount; + + private ApprovedCancelAmount approvedCancelAmount; + + private CanceledAmount canceledAmount; + + private CancelAvailableAmount cancelAvailableAmount; + + private String itemName; + + private String itemCode; + + private Integer quantity; + + private LocalDateTime createdAt; + + private LocalDateTime approvedAt; + + private LocalDateTime canceledAt; + + private String payload; + + public Integer getTotalAmount() { + return amount.getTotal(); + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java b/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java index e598b4c..3e0b69b 100644 --- a/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java +++ b/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java @@ -9,6 +9,8 @@ import com.ticketing.server.movie.application.response.TicketReservationResponse import com.ticketing.server.movie.application.response.TicketSoldResponse; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; import com.ticketing.server.movie.service.dto.TicketsCancelDTO; +import com.ticketing.server.movie.service.dto.TicketsRefundDTO; +import com.ticketing.server.movie.service.dto.TicketsRefundResponse; import com.ticketing.server.movie.service.dto.TicketsReservationDTO; import com.ticketing.server.movie.service.dto.TicketsSoldDTO; import com.ticketing.server.movie.service.interfaces.TicketService; @@ -49,4 +51,10 @@ public class MovieClientImpl implements MovieClient { return ticketsCancelDto.toResponse(); } + @Override + public TicketsRefundResponse myTicketRefund(TicketsRefundRequest request) { + TicketsRefundDTO ticketsRefundDto = ticketService.myTicketsRefund(request.getPaymentId()); + return ticketsRefundDto.toResponse(); + } + } diff --git a/server/src/main/java/com/ticketing/server/payment/api/impl/TicketsRefundRequest.java b/server/src/main/java/com/ticketing/server/payment/api/impl/TicketsRefundRequest.java new file mode 100644 index 0000000..41df234 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/impl/TicketsRefundRequest.java @@ -0,0 +1,12 @@ +package com.ticketing.server.payment.api.impl; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketsRefundRequest { + + private final Long paymentId; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java b/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java index 820c71f..af308f3 100644 --- a/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java +++ b/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java @@ -1,18 +1,22 @@ package com.ticketing.server.payment.application; +import static com.ticketing.server.user.domain.UserGrade.ROLES.USER; + import com.ticketing.server.payment.application.request.PaymentReadyRequest; -import com.ticketing.server.payment.application.response.PaymentDetailResponse; -import com.ticketing.server.payment.application.response.SimplePaymentsResponse; -import com.ticketing.server.payment.service.dto.PaymentCancelDTO; +import com.ticketing.server.payment.application.request.PaymentRefundRequest; import com.ticketing.server.payment.application.response.PaymentCancelResponse; -import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; import com.ticketing.server.payment.application.response.PaymentCompleteResponse; +import com.ticketing.server.payment.application.response.PaymentDetailResponse; +import com.ticketing.server.payment.application.response.PaymentRefundResponse; +import com.ticketing.server.payment.application.response.SimplePaymentsResponse; +import com.ticketing.server.payment.service.dto.PaymentRefundDTO; +import com.ticketing.server.payment.service.dto.PaymentCancelDTO; +import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; import com.ticketing.server.payment.service.dto.PaymentDetailDTO; import com.ticketing.server.payment.service.dto.PaymentReadyDTO; import com.ticketing.server.payment.service.dto.SimplePaymentsDTO; import com.ticketing.server.payment.service.interfaces.PaymentApisService; import com.ticketing.server.payment.service.interfaces.PaymentService; -import com.ticketing.server.user.domain.UserGrade; import javax.validation.Valid; import javax.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; @@ -39,7 +43,7 @@ public class PaymentController { private final PaymentService paymentService; @GetMapping - @Secured(UserGrade.ROLES.USER) + @Secured(USER) public ResponseEntity simplePayments(@NotNull Long userAlternateId) { SimplePaymentsDTO simplePayments = paymentService.findSimplePayments(userAlternateId); @@ -48,7 +52,7 @@ public class PaymentController { } @GetMapping("/detail") - @Secured(UserGrade.ROLES.USER) + @Secured(USER) public ResponseEntity detail(@NotNull Long paymentId) { PaymentDetailDTO paymentDetail = paymentApisService.findPaymentDetail(paymentId); @@ -57,7 +61,7 @@ public class PaymentController { } @PostMapping("/ready") - @Secured(UserGrade.ROLES.USER) + @Secured(USER) public ResponseEntity ready(@RequestBody @Valid PaymentReadyRequest request) { PaymentReadyDTO paymentReadyDto = paymentApisService.ready(request.getTicketIds()); @@ -66,7 +70,7 @@ public class PaymentController { } @GetMapping("/complete") - @Secured(UserGrade.ROLES.USER) + @Secured(USER) public ResponseEntity complete( @AuthenticationPrincipal UserDetails userRequest, @RequestParam("pg_token") String pgToken) { @@ -77,7 +81,7 @@ public class PaymentController { } @GetMapping("/cancel") - @Secured(UserGrade.ROLES.USER) + @Secured(USER) public ResponseEntity cancel(@AuthenticationPrincipal UserDetails userRequest) { PaymentCancelDTO paymentCancelDto = paymentApisService.cancel(userRequest.getUsername()); @@ -85,4 +89,13 @@ public class PaymentController { .body(paymentCancelDto.toResponse()); } + @PostMapping("/refund") + @Secured(USER) + public ResponseEntity refund(@RequestBody @Valid PaymentRefundRequest request) { + PaymentRefundDTO paymentRefundDto = paymentApisService.myPaymentRefund(request.getPaymentId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(paymentRefundDto.toResponse()); + } + } diff --git a/server/src/main/java/com/ticketing/server/payment/application/request/PaymentRefundRequest.java b/server/src/main/java/com/ticketing/server/payment/application/request/PaymentRefundRequest.java new file mode 100644 index 0000000..d08435e --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/application/request/PaymentRefundRequest.java @@ -0,0 +1,12 @@ +package com.ticketing.server.payment.application.request; + +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class PaymentRefundRequest { + + @NotNull + private Long paymentId; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/application/response/PaymentRefundResponse.java b/server/src/main/java/com/ticketing/server/payment/application/response/PaymentRefundResponse.java new file mode 100644 index 0000000..a6ee061 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/application/response/PaymentRefundResponse.java @@ -0,0 +1,18 @@ +package com.ticketing.server.payment.application.response; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentRefundResponse { + + private Long paymentId; + private List ticketIds; + private String movieTitle; + private Integer cancelAmount; + private LocalDateTime canceledAt; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/domain/Payment.java b/server/src/main/java/com/ticketing/server/payment/domain/Payment.java index 85c1f60..fdf7c2e 100644 --- a/server/src/main/java/com/ticketing/server/payment/domain/Payment.java +++ b/server/src/main/java/com/ticketing/server/payment/domain/Payment.java @@ -2,6 +2,7 @@ package com.ticketing.server.payment.domain; import com.ticketing.server.global.dto.repository.AbstractEntity; import com.ticketing.server.global.redis.PaymentCache; +import com.ticketing.server.payment.api.dto.response.UserDetailResponse; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -82,4 +83,14 @@ public class Payment extends AbstractEntity { this.totalPrice = totalPrice; } + public boolean validUser(UserDetailResponse userDetail) { + if (!userDetail.hasUserAlternateId(this)) { + return false; + } + return true; + } + + public void refund() { + status = PaymentStatus.REFUNDED; + } } diff --git a/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java b/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java index 3786e21..515f09c 100644 --- a/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java @@ -10,6 +10,8 @@ import com.ticketing.server.movie.application.request.TicketReservationRequest; import com.ticketing.server.movie.application.request.TicketSoldRequest; import com.ticketing.server.movie.application.response.TicketDetailsResponse; import com.ticketing.server.movie.application.response.TicketReservationResponse; +import com.ticketing.server.movie.service.dto.TicketsRefundResponse; +import com.ticketing.server.payment.api.dto.requset.KakaoPayCancelRequest; import com.ticketing.server.payment.api.KakaoPayClient; import com.ticketing.server.payment.api.KakaoPayProperties; import com.ticketing.server.payment.api.MovieClient; @@ -17,8 +19,10 @@ import com.ticketing.server.payment.api.UserClient; import com.ticketing.server.payment.api.dto.requset.KakaoPayApproveRequest; import com.ticketing.server.payment.api.dto.requset.KakaoPayReadyRequest; import com.ticketing.server.payment.api.dto.response.KakaoPayApproveResponse; +import com.ticketing.server.payment.api.dto.response.KakaoPayCancelResponse; import com.ticketing.server.payment.api.dto.response.KakaoPayReadyResponse; import com.ticketing.server.payment.api.dto.response.UserDetailResponse; +import com.ticketing.server.payment.api.impl.TicketsRefundRequest; import com.ticketing.server.payment.domain.Payment; import com.ticketing.server.payment.domain.PaymentStatus; import com.ticketing.server.payment.domain.PaymentType; @@ -27,6 +31,7 @@ import com.ticketing.server.payment.service.dto.PaymentCancelDTO; import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; import com.ticketing.server.payment.service.dto.PaymentDetailDTO; import com.ticketing.server.payment.service.dto.PaymentReadyDTO; +import com.ticketing.server.payment.service.dto.PaymentRefundDTO; import com.ticketing.server.payment.service.interfaces.PaymentApisService; import java.util.List; import javax.validation.constraints.NotEmpty; @@ -153,4 +158,28 @@ public class PaymentApisServiceImpl implements PaymentApisService { return new PaymentCancelDTO(paymentCache); } + @Override + @Transactional + public PaymentRefundDTO myPaymentRefund(@NotNull Long paymentId) { + UserDetailResponse userDetail = userClient.detail(); + Payment payment = paymentRepository.findById(paymentId) + .orElseThrow(ErrorCode::throwPaymentIdNotFound); + + if (!payment.validUser(userDetail)) { + throw ErrorCode.throwValidUserId(); + } + + // 카카오페이 환불 + KakaoPayCancelResponse kakaoPayCancelResponse = kakaoPayClient.cancel( + kakaoPayProperties.getAuthorization(), + new KakaoPayCancelRequest(payment.getTid(), payment.getTotalPrice()) + ); + + // 내부 환불진행 + TicketsRefundResponse refundResponse = movieClient.myTicketRefund(new TicketsRefundRequest(payment.getId())); + payment.refund(); + + return new PaymentRefundDTO(payment, kakaoPayCancelResponse, refundResponse); + } + } diff --git a/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentRefundDTO.java b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentRefundDTO.java new file mode 100644 index 0000000..8dae7f6 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentRefundDTO.java @@ -0,0 +1,41 @@ +package com.ticketing.server.payment.service.dto; + +import com.ticketing.server.movie.service.dto.TicketsRefundResponse; +import com.ticketing.server.payment.api.dto.response.KakaoPayCancelResponse; +import com.ticketing.server.payment.application.response.PaymentRefundResponse; +import com.ticketing.server.payment.domain.Payment; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentRefundDTO { + + private Long paymentId; + private List ticketIds; + private String movieTitle; + private Integer cancelAmount; + private LocalDateTime canceledAt; + + public PaymentRefundDTO(Payment payment, KakaoPayCancelResponse kakaoPayCancelResponse, TicketsRefundResponse refundResponse) { + this( + payment.getId(), + refundResponse.getTicketIds(), + kakaoPayCancelResponse.getItemName(), + kakaoPayCancelResponse.getTotalAmount(), + kakaoPayCancelResponse.getCanceledAt() + ); + } + + public PaymentRefundResponse toResponse() { + return new PaymentRefundResponse( + paymentId, + ticketIds, + movieTitle, + cancelAmount, + canceledAt + ); + } +} diff --git a/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java b/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java index 50afb7a..bbba397 100644 --- a/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java +++ b/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java @@ -1,6 +1,7 @@ package com.ticketing.server.payment.service.interfaces; import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import com.ticketing.server.payment.service.dto.PaymentRefundDTO; import com.ticketing.server.payment.service.dto.PaymentCancelDTO; import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; import com.ticketing.server.payment.service.dto.PaymentDetailDTO; @@ -19,4 +20,5 @@ public interface PaymentApisService { PaymentCancelDTO cancel(@NotEmpty String email); + PaymentRefundDTO myPaymentRefund(@NotNull Long paymentId); } diff --git a/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java b/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java index 8c608f7..6c44f8d 100644 --- a/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java +++ b/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.ticketing.server.global.exception.ErrorCode; import com.ticketing.server.global.exception.TicketingException; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -165,10 +166,94 @@ public class TicketTest { ticket.makeSold(123L); // then - assertThatThrownBy(() -> ticket.cancel()) + assertThatThrownBy(ticket::cancel) .isInstanceOf(TicketingException.class) .extracting("errorCode") .isEqualTo(ErrorCode.BAD_REQUEST_PAYMENT_CANCEL); } + @Test + @DisplayName("시간 비교 환불 성공") + void refundByDateTimeSuccess() { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime dateTime = LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 7, 50); + Ticket ticket = tickets.get(0); + + // when + ticket.makeSold(123L); + ticket.refund(dateTime); + + // then + assertAll( + () -> assertThat(ticket.getStatus()).isEqualTo(TicketStatus.SALE), + () -> assertThat(ticket.getPaymentId()).isNull() + ); + } + + @Test + @DisplayName("시간 비교 환불 실패 - 상영시작 시간 10분보다 작을경우") + void refundByDateTimeFail() { + // given + LocalDateTime now = LocalDateTime.now(); + LocalDateTime dateTime = LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 7, 51); + Ticket ticket = tickets.get(0); + + // when + ticket.makeSold(123L); + + // then + assertThatThrownBy(() -> ticket.refund(dateTime)) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.NOT_REFUNDABLE_TIME); + } + + @Test + @DisplayName("관리자 환불 성공") + void refundSuccess() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket.makeSold(123L); + ticket.refund(); + + // then + assertAll( + () -> assertThat(ticket.getStatus()).isEqualTo(TicketStatus.SALE), + () -> assertThat(ticket.getPaymentId()).isNull() + ); + } + + @Test + @DisplayName("관리자 환불 실패 - 상태 SALE") + void refundFail_SALE() { + // given + Ticket ticket = tickets.get(0); + + // when + // then + assertThatThrownBy(ticket::refund) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.NOT_REFUNDABLE_SEAT); + } + + @Test + @DisplayName("관리자 환불 실패 - 상태 RESERVATION") + void refundFail_RESERVATION() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket.makeReservation(); + + // then + assertThatThrownBy(ticket::refund) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.NOT_REFUNDABLE_SEAT); + } + }