diff --git a/platform/src/main/java/com/commerce/platform/bootstrap/customer/PaymentController.java b/platform/src/main/java/com/commerce/platform/bootstrap/customer/PaymentController.java index 6079843..af09b70 100644 --- a/platform/src/main/java/com/commerce/platform/bootstrap/customer/PaymentController.java +++ b/platform/src/main/java/com/commerce/platform/bootstrap/customer/PaymentController.java @@ -1,11 +1,55 @@ package com.commerce.platform.bootstrap.customer; +import com.commerce.platform.bootstrap.dto.payment.PaymentRequest; +import com.commerce.platform.core.application.in.PaymentUseCase; +import com.commerce.platform.core.application.in.dto.PayResult; +import com.commerce.platform.shared.exception.BusinessException; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; @RequiredArgsConstructor -@RequestMapping("/payment") +@RequestMapping("/payments") @RestController public class PaymentController { + private final PaymentUseCase paymentUseCase; + + /** + * 카드, 휴대폰, 간편결제의 + * 승인, 취소(부분취소 포함) 처리 + * @param paymentRequest + * @return + */ + @PostMapping + public ResponseEntity createPayment(@Valid @RequestBody PaymentRequest paymentRequest) { + PayResult result = null; + + try { + switch (paymentRequest.paymentStatus()) { + case APPROVED -> paymentUseCase.doApproval(paymentRequest.toApproval()); + case FULL_CANCELED -> paymentUseCase.doCancel(paymentRequest.toCancel()); + case PARTIAL_CANCELED -> paymentUseCase.doPartCancel(paymentRequest.toCancel()); + default -> throw new IllegalStateException("Unexpected value: " + paymentRequest.paymentStatus()); + }; + } catch (BusinessException e) { + result = new PayResult.Failed(e.getCode(), e.getMessage()); + } catch (Exception e) { + result = new PayResult.Failed("9999", "전체취소 처리 중 오류가 발생했습니다"); + } + + return ResponseEntity.ok(result); + } + + @PostMapping("/registry-card/{cardId}") + public ResponseEntity createPaymentWithRegistryCard( + @PathVariable Long cardId, + @RequestBody Map body) { + + paymentUseCase.doApprovalWithCardId(cardId); + + return ResponseEntity.ok("성공"); + } } diff --git a/platform/src/main/java/com/commerce/platform/bootstrap/dto/payment/PaymentRequest.java b/platform/src/main/java/com/commerce/platform/bootstrap/dto/payment/PaymentRequest.java new file mode 100644 index 0000000..6a33316 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/bootstrap/dto/payment/PaymentRequest.java @@ -0,0 +1,53 @@ +package com.commerce.platform.bootstrap.dto.payment; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.domain.enums.PayMethod; +import com.commerce.platform.core.domain.enums.PayProvider; +import com.commerce.platform.core.domain.enums.PaymentStatus; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.core.domain.vo.OrderId; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public record PaymentRequest( + @NotBlank + String orderId, + + @Min(value = 0) + long amount, + + @NotBlank + PayMethod payMethod, + + @NotBlank + PayProvider payProvider, + + @NotBlank + PaymentStatus paymentStatus, + + String installment +) { + public PayOrderCommand toApproval() { + return new PayOrderCommand( + OrderId.of(this.orderId), + null, + null, + installment, + payMethod, + payProvider, + PaymentStatus.APPROVED + ); + } + + public PayOrderCommand toCancel() { + return new PayOrderCommand( + OrderId.of(this.orderId), + null, + Money.create(amount), + null, + payMethod, + null, + paymentStatus + ); + } +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/in/PaymentUseCase.java b/platform/src/main/java/com/commerce/platform/core/application/in/PaymentUseCase.java new file mode 100644 index 0000000..1560656 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/in/PaymentUseCase.java @@ -0,0 +1,10 @@ +package com.commerce.platform.core.application.in; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; + +public interface PaymentUseCase { + void doApproval(PayOrderCommand command); + void doCancel(PayOrderCommand command); + void doPartCancel(PayOrderCommand cancel); + void doApprovalWithCardId(Long cardId); // 등록된 카드로 결제 +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/in/PaymentUseCaseImpl.java b/platform/src/main/java/com/commerce/platform/core/application/in/PaymentUseCaseImpl.java new file mode 100644 index 0000000..c5c964b --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/in/PaymentUseCaseImpl.java @@ -0,0 +1,173 @@ +package com.commerce.platform.core.application.in; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.CustomerCardOutPort; +import com.commerce.platform.core.application.out.OrderOutputPort; +import com.commerce.platform.core.application.out.PaymentOutPort; +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.application.out.dto.PgPayResponse; +import com.commerce.platform.core.domain.aggreate.CustomerCard; +import com.commerce.platform.core.domain.aggreate.Order; +import com.commerce.platform.core.domain.aggreate.Payment; +import com.commerce.platform.core.domain.aggreate.PaymentPartCancel; +import com.commerce.platform.core.domain.service.PaymentPgRouter; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.shared.exception.BusinessError; +import com.commerce.platform.shared.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.commerce.platform.shared.exception.BusinessError.*; + +@Log4j2 +@RequiredArgsConstructor +@Service +public class PaymentUseCaseImpl implements PaymentUseCase{ + private final PaymentPgRouter pgRouter; + private final PaymentOutPort paymentOutPort; + private final OrderOutputPort orderOutputPort; + private final CustomerCardOutPort customerCardOutPort; + + @Override + @Transactional + public void doApproval(PayOrderCommand payOrdercommand) { + // 주문 결제처리 + Order orderEntity = orderOutputPort.findById(payOrdercommand.getOrderId()) + .orElseThrow(() -> new BusinessException(INVALID_ORDER_ID)); + orderEntity.validForPay(); + + // pg사 라우팅 + PgStrategy pgStrategy = pgRouter.routPg(payOrdercommand.getPayMethod()); + + // 결재 entity 생성 + payOrdercommand.setApprovedAmount(orderEntity.getResultAmt()); + Payment paymentEntity = Payment.create(payOrdercommand, pgStrategy.getPgProvider()); + + // pg 결제 응답 수신 + PgPayResponse pgResponse = pgStrategy.processApproval(payOrdercommand); + + // 결제 결과에 따른 주문/결제 상태 변경 + orderEntity.changeStatusAfterPay(pgResponse); + paymentEntity.approved(pgResponse); + + paymentOutPort.savePayment(paymentEntity); + + // todo pg사 응답데이터/ 시간정보 저장? + } + + /** + * 전체취소 + */ + @Override + @Transactional + public void doCancel(PayOrderCommand payOrderCommand) { + + // 주문 검증 + Order orderEntity = orderOutputPort.findById(payOrderCommand.getOrderId()) + .orElseThrow(() -> new BusinessException(INVALID_ORDER_ID)); + orderEntity.validateForCancel(); + + // 결제 검증 + Payment paymentEntity = paymentOutPort.findByOrderId(orderEntity.getOrderId()) + .orElseThrow(() -> new BusinessException(INVALID_PAYMENT)); + paymentEntity.validateForCancel(); + + // 부분취소 존재여부 확인 + boolean hasPartialCancel = paymentOutPort.existsPartCancelByPaymentId(paymentEntity.getPaymentId()); + if (hasPartialCancel) { + throw new BusinessException(BusinessError.PAYMENT_HAS_PARTIAL_CANCEL); + } + + // 취소요청 + PgStrategy pgStrategy = pgRouter.getPgStrategyByProvider(paymentEntity.getPgProvider()); + payOrderCommand.setCancelAmount(paymentEntity.getApprovedAmt()); + PgPayResponse pgResponse = pgStrategy.processCancel(payOrderCommand); + + // PG 응답 반영 + if (!pgResponse.isSuccess()) { + throw new BusinessException(PG_RESPONSE_FAILED); + } + + orderEntity.refund(); + paymentEntity.canceled(pgResponse); + paymentOutPort.savePayment(paymentEntity); + + } + + /** + * 부분취소 + */ + @Override + @Transactional + public void doPartCancel(PayOrderCommand payOrderCommand) { + // 주문 검정 + Order orderEntity = orderOutputPort.findById(payOrderCommand.getOrderId()) + .orElseThrow(() -> new BusinessException(INVALID_ORDER_ID)); + orderEntity.validateForCancel(); + + // 결제 검증 + Payment paymentEntity = paymentOutPort.findByOrderId(orderEntity.getOrderId()) + .orElseThrow(() -> new BusinessException(INVALID_PAYMENT)); + paymentEntity.validateForCancel(); + + // 부분취소 내역 조회 + Money remainAmt = null; + boolean hasPartialCancel = paymentOutPort.existsPartCancelByPaymentId(paymentEntity.getPaymentId()); + if (hasPartialCancel) { + remainAmt = paymentOutPort.getRemainAmount(paymentEntity.getPaymentId()); + } else { + remainAmt = paymentEntity.getApprovedAmt(); + } + + // 취소 요청 금액 검증 + Money requestCancelAmt = payOrderCommand.getCancelAmount(); + if (requestCancelAmt.value() > remainAmt.value()) { + throw new BusinessException(BusinessError.PAYMENT_CANCEL_AMOUNT_EXCEEDED); + } else if (!hasPartialCancel && requestCancelAmt.value() == remainAmt.value()) { + throw new BusinessException(INVALID_PARTIAL_CANCEL_AMOUNT); + } + + // 최종 남은금액 + Money new_remainAmt = remainAmt.subtract(requestCancelAmt); + + // 취소 요청 + PgStrategy pgStrategy = pgRouter.routPg(paymentEntity.getPayMethod()); + payOrderCommand.setCancelAmount(new_remainAmt); + PgPayResponse pgResponse = pgStrategy.processCancel(payOrderCommand); + + if (!pgResponse.isSuccess()) { + throw new BusinessException(PG_RESPONSE_FAILED); + } + + // 부분취소 내역 저장 + PaymentPartCancel partCancel = PaymentPartCancel.create( + paymentEntity.getPaymentId(), // Payment 엔티티 전달 + requestCancelAmt, + new_remainAmt + ); + partCancel.completed(pgResponse.pgTid()); + paymentOutPort.savePartCancel(partCancel); + + } + + /** + * 등록된 카드로 결제 + * @param cardId + */ + @Override + public void doApprovalWithCardId(Long cardId) { + CustomerCard customerCard = customerCardOutPort.findActiveById(cardId) + .orElseThrow(() -> new BusinessException(INVALID_ORDER_ID)); + + } + + /** + * 실패거래건 저장 + */ + private void saveFailedPayment(Payment payment, PgPayResponse pgResponse) { + + } + +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/in/dto/PayOrderCommand.java b/platform/src/main/java/com/commerce/platform/core/application/in/dto/PayOrderCommand.java new file mode 100644 index 0000000..a5989bd --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/in/dto/PayOrderCommand.java @@ -0,0 +1,21 @@ +package com.commerce.platform.core.application.in.dto; + +import com.commerce.platform.core.domain.enums.PayMethod; +import com.commerce.platform.core.domain.enums.PayProvider; +import com.commerce.platform.core.domain.enums.PaymentStatus; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.core.domain.vo.OrderId; +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor +@Data +public class PayOrderCommand { + private OrderId orderId; + private Money approvedAmount; + private Money cancelAmount; + private String installment; + private PayMethod payMethod; + private PayProvider payProvider; + private PaymentStatus paymentStatus; +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/in/dto/PayResult.java b/platform/src/main/java/com/commerce/platform/core/application/in/dto/PayResult.java new file mode 100644 index 0000000..25ced15 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/in/dto/PayResult.java @@ -0,0 +1,18 @@ +package com.commerce.platform.core.application.in.dto; + +/** + * 결제 응답 + */ +public interface PayResult { + + record Success( + String paymentId, + String message + ) implements PayResult{} + + record Failed( + String errorCode, + String message + ) implements PayResult{} + +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/CardPay.java b/platform/src/main/java/com/commerce/platform/core/application/out/CardPay.java new file mode 100644 index 0000000..c32472c --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/CardPay.java @@ -0,0 +1,9 @@ +package com.commerce.platform.core.application.out; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.dto.PgPayResponse; + +public interface CardPay { + PgPayResponse approveCard(PayOrderCommand command); + PgPayResponse cancelCard(PayOrderCommand command); +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/CustomerCardOutPort.java b/platform/src/main/java/com/commerce/platform/core/application/out/CustomerCardOutPort.java index 188d402..cce339e 100644 --- a/platform/src/main/java/com/commerce/platform/core/application/out/CustomerCardOutPort.java +++ b/platform/src/main/java/com/commerce/platform/core/application/out/CustomerCardOutPort.java @@ -3,7 +3,10 @@ import com.commerce.platform.core.domain.aggreate.CustomerCard; import com.commerce.platform.core.domain.vo.CustomerId; +import java.util.Optional; + public interface CustomerCardOutPort { void save(CustomerCard customerCard); int countActiveCardByCustomerId(CustomerId customerId); + Optional findActiveById(Long cardId); } diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/EasyPay.java b/platform/src/main/java/com/commerce/platform/core/application/out/EasyPay.java new file mode 100644 index 0000000..a261031 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/EasyPay.java @@ -0,0 +1,9 @@ +package com.commerce.platform.core.application.out; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.dto.PgPayResponse; + +public interface EasyPay { + PgPayResponse approveEasyPay(PayOrderCommand command); + PgPayResponse cancelEasyPay(PayOrderCommand command); +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/PaymentOutPort.java b/platform/src/main/java/com/commerce/platform/core/application/out/PaymentOutPort.java new file mode 100644 index 0000000..8b04ad8 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/PaymentOutPort.java @@ -0,0 +1,19 @@ +package com.commerce.platform.core.application.out; + +import com.commerce.platform.core.domain.aggreate.Payment; +import com.commerce.platform.core.domain.aggreate.PaymentPartCancel; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.core.domain.vo.OrderId; +import com.commerce.platform.core.domain.vo.PaymentId; + +import java.util.Optional; + +public interface PaymentOutPort { + void savePayment(Payment payment); + Optional findByOrderId(OrderId orderId); + + // 부분취소 관련 + void savePartCancel(PaymentPartCancel partCancel); + boolean existsPartCancelByPaymentId(PaymentId paymentId); + Money getRemainAmount(PaymentId paymentId); +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/PgStrategy.java b/platform/src/main/java/com/commerce/platform/core/application/out/PgStrategy.java new file mode 100644 index 0000000..508aaf6 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/PgStrategy.java @@ -0,0 +1,58 @@ +package com.commerce.platform.core.application.out; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.dto.PgPayResponse; +import com.commerce.platform.core.domain.enums.PgProvider; + +public abstract class PgStrategy { + + public abstract PgProvider getPgProvider(); + + /** + * 라우팅된 pg사에서 해당 결제유형으로 승인 요청 + * @param command + * @return pg 응답 + */ + public final PgPayResponse processApproval(PayOrderCommand command) { + return switch (command.getPayMethod()) { + case CARD -> getCardPay().approveCard(command); + case EASY_PAY -> getEasyPay().approveEasyPay(command); + case PHONE -> getPhonePay().approvePhone(command); + }; + } + + /** + * 라우팅된 pg사에서 해당 결제유형으로 취소 요청 + * @param command + * @return pg 응답 + */ + public final PgPayResponse processCancel(PayOrderCommand command) { + return switch (command.getPayMethod()) { + case CARD -> getCardPay().cancelCard(command); + case EASY_PAY -> getEasyPay().cancelEasyPay(command); + case PHONE -> getPhonePay().cancelPhone(command); + }; + } + + private CardPay getCardPay() { + if (this instanceof CardPay cardPay) { + return cardPay; + } + throw new UnsupportedOperationException("카드결제 미지원 pg사"); + } + + private PhonePay getPhonePay() { + if (this instanceof PhonePay phonePay) { + return phonePay; + } + throw new UnsupportedOperationException("휴대폰결제 미지원 pg사"); + } + + private EasyPay getEasyPay() { + if (this instanceof EasyPay easyPay) { + return easyPay; + } + throw new UnsupportedOperationException("간편결제 미지원 pg사"); + } + +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/PhonePay.java b/platform/src/main/java/com/commerce/platform/core/application/out/PhonePay.java new file mode 100644 index 0000000..7e96364 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/PhonePay.java @@ -0,0 +1,9 @@ +package com.commerce.platform.core.application.out; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.dto.PgPayResponse; + +public interface PhonePay { + PgPayResponse approvePhone(PayOrderCommand command); + PgPayResponse cancelPhone(PayOrderCommand command); +} diff --git a/platform/src/main/java/com/commerce/platform/core/application/out/dto/PgPayResponse.java b/platform/src/main/java/com/commerce/platform/core/application/out/dto/PgPayResponse.java new file mode 100644 index 0000000..7d6ea9c --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/application/out/dto/PgPayResponse.java @@ -0,0 +1,8 @@ +package com.commerce.platform.core.application.out.dto; + +public record PgPayResponse ( + String pgTid, + String responseCode, // pg사 응답코드 + String responseMessage, // pg사 응답메시지 + boolean isSuccess +) {} diff --git a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Order.java b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Order.java index 9007242..1d758b0 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Order.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Order.java @@ -1,5 +1,6 @@ package com.commerce.platform.core.domain.aggreate; +import com.commerce.platform.core.application.out.dto.PgPayResponse; import com.commerce.platform.core.domain.enums.OrderStatus; import com.commerce.platform.core.domain.vo.CouponId; import com.commerce.platform.core.domain.vo.CustomerId; @@ -125,16 +126,31 @@ public void cancel() { /** 주문 환불 **/ public void refund() { + updateOrderStatus(REFUND); + } + + /** 환불가능여부 확인 **/ + public void validateForCancel() { if(this.status != OrderStatus.PAID) { - throw new RuntimeException("환불처리 불가"); + throw new RuntimeException("환불 불가능한 주문 상태입니다"); } + } - updateOrderStatus(REFUND); + /** 주문 결제 **/ + public void validForPay() { + if(this.status != OrderStatus.CONFIRMED) { + throw new RuntimeException("결제처리 불가"); + } + } + + public void changeStatusAfterPay(PgPayResponse pgPayResponse) { + if(!pgPayResponse.isSuccess()) return; + + updateOrderStatus(PAID); } /** * 주문상태, 수정시간 변경 - * @param updateStatus */ private void updateOrderStatus(OrderStatus updateStatus) { this.updatedAt = LocalDateTime.now(); diff --git a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Payment.java b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Payment.java index 60b2540..1f431fa 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Payment.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/Payment.java @@ -1,5 +1,7 @@ package com.commerce.platform.core.domain.aggreate; +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.dto.PgPayResponse; import com.commerce.platform.core.domain.enums.PayMethod; import com.commerce.platform.core.domain.enums.PayProvider; import com.commerce.platform.core.domain.enums.PaymentStatus; @@ -8,14 +10,14 @@ import com.commerce.platform.core.domain.vo.OrderId; import com.commerce.platform.core.domain.vo.PaymentId; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.LocalDateTime; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Builder(access = AccessLevel.PRIVATE) @Table(name = "payment") @Entity public class Payment { @@ -37,7 +39,7 @@ public class Payment { private PayMethod payMethod; @Column(name = "pay_provider", length = 10) - private PayProvider cardProvider; + private PayProvider payProvider; @Column(name = "installment", length = 2) private String installment; @@ -67,4 +69,50 @@ public class Payment { @Column(name = "canceled_at") private LocalDateTime canceledAt; + + public static Payment create(PayOrderCommand command, PgProvider targetPg) { + return Payment.builder() + .paymentId(PaymentId.create()) + .orderId(command.getOrderId()) + .approvedAmt(command.getApprovedAmount()) + .payMethod(command.getPayMethod()) + .payProvider(command.getPayProvider()) + .installment(command.getInstallment()) + .pgProvider(targetPg) + .paymentStatus(command.getPaymentStatus()) + .requestedAt(LocalDateTime.now()) + .build(); + } + + public void approved(PgPayResponse pgResponse) { + this.pgTid = pgResponse.pgTid(); + + if(!pgResponse.isSuccess()) { + this.paymentStatus = PaymentStatus.FAILED; + return; + } + + this.approvedAt = LocalDateTime.now(); + this.paymentStatus = PaymentStatus.APPROVED; + } + + public void canceled(PgPayResponse pgResponse) { + this.pgCancelTid = pgResponse.pgTid(); + + if(!pgResponse.isSuccess()) { + this.paymentStatus = PaymentStatus.FAILED; + return; + } + this.canceledAt = LocalDateTime.now(); + this.paymentStatus = PaymentStatus.FULL_CANCELED; + } + + /** + * 전체/부분 취소 가능 여부 검증 + */ + public void validateForCancel() { + if(this.paymentStatus != PaymentStatus.APPROVED) { + throw new RuntimeException("취소가 불가능한 결제 상태입니다"); + } + } } diff --git a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/PaymentPartCancel.java b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/PaymentPartCancel.java index 22b4234..d92f87c 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/aggreate/PaymentPartCancel.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/aggreate/PaymentPartCancel.java @@ -15,7 +15,12 @@ @Entity public class PaymentPartCancel { - @EmbeddedId + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + @AttributeOverride(name = "id", column = @Column(name = "part_canceled_payment_id", nullable = false)) private PaymentId paymentPartCancelId; @Embedded @@ -38,4 +43,27 @@ public class PaymentPartCancel { @Column(name = "canceled_at") private LocalDateTime canceledAt; + + /** + * 부분 취소 생성 + */ + public static PaymentPartCancel create( + PaymentId approvedPaymentId, + Money canceledAmt, + Money remainAmt + ) { + PaymentPartCancel partCancel = new PaymentPartCancel(); + partCancel.paymentPartCancelId = PaymentId.create(); + partCancel.approvedPaymentId = approvedPaymentId; + partCancel.canceledAmt = canceledAmt; + partCancel.remainAmt = remainAmt; + partCancel.requestedAt = LocalDateTime.now(); + return partCancel; + } + + /**부분취소 완료 처리*/ + public void completed(String pgCancelTid) { + this.pgCancelTid = pgCancelTid; + this.canceledAt = LocalDateTime.now(); + } } diff --git a/platform/src/main/java/com/commerce/platform/core/domain/enums/OrderStatus.java b/platform/src/main/java/com/commerce/platform/core/domain/enums/OrderStatus.java index 356ce2a..2f8d33d 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/enums/OrderStatus.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/enums/OrderStatus.java @@ -6,15 +6,11 @@ @Getter @AllArgsConstructor public enum OrderStatus { - CANCELED("취소완료"), - REFUND("환불완료"), PENDING("주문대기"), CONFIRMED("주문완료"), - PAID("결제완료"); -// PREPARING("상품준비중"), -// READY_TO_SHIP("배송준비중"), -// SHIPPING("배송중"), -// DELIVERED("배송완료"); + CANCELED("취소완료"), + PAID("결제완료"), + REFUND("환불완료"); private final String value; } diff --git a/platform/src/main/java/com/commerce/platform/core/domain/enums/PayMethod.java b/platform/src/main/java/com/commerce/platform/core/domain/enums/PayMethod.java index 52d00b9..c6b9f29 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/enums/PayMethod.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/enums/PayMethod.java @@ -7,6 +7,7 @@ @AllArgsConstructor public enum PayMethod { CARD("카드결제"), + EASY_PAY("간편결제"), PHONE("휴대폰결제"); private final String value; diff --git a/platform/src/main/java/com/commerce/platform/core/domain/enums/PaymentStatus.java b/platform/src/main/java/com/commerce/platform/core/domain/enums/PaymentStatus.java index 3c338b0..2a2fcc2 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/enums/PaymentStatus.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/enums/PaymentStatus.java @@ -8,7 +8,8 @@ public enum PaymentStatus { APPROVED("approved"), FULL_CANCELED("fullCanceled"), - PARTIAL_CANCELED("partialCanceled"); + PARTIAL_CANCELED("partialCanceled"), + FAILED("failed"); private final String value; } diff --git a/platform/src/main/java/com/commerce/platform/core/domain/enums/PgProvider.java b/platform/src/main/java/com/commerce/platform/core/domain/enums/PgProvider.java index 5410d31..3d2ace0 100644 --- a/platform/src/main/java/com/commerce/platform/core/domain/enums/PgProvider.java +++ b/platform/src/main/java/com/commerce/platform/core/domain/enums/PgProvider.java @@ -3,16 +3,29 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Arrays; +import java.util.List; + @Getter @AllArgsConstructor public enum PgProvider { - // todo 여기서 pg사별 지원하는 결제유형을 관리해야되는지 - // todo 카드-수기, 인증, sms 위한 값을 만들지 이 부분은 생략할지 CardAuthType - TOSS("토스"), - OLIVE_NETWORKS("올리브네트웍스"), - NHN("NHN"), - NICE_PAYMENTS("나이스페이먼츠"); - - private final String value; + TOSS(List.of(PayMethod.CARD, PayMethod.EASY_PAY)), + NHN(List.of(PayMethod.CARD, PayMethod.EASY_PAY)), + NICE_PAYMENTS(List.of(PayMethod.CARD, PayMethod.EASY_PAY)), + + DANAL(List.of(PayMethod.PHONE)), + PAYLETTER(List.of(PayMethod.PHONE)) + ; + + private final List payMethods; + + public static List getByPayMethod(PayMethod payMethod) { + List pgProviders = Arrays.stream(PgProvider.values()) + .filter(pg -> pg.getPayMethods().contains(payMethod)) + .toList(); + + if(pgProviders.isEmpty()) throw new IllegalArgumentException("지원 PG사 없음"); + return pgProviders; + } } diff --git a/platform/src/main/java/com/commerce/platform/core/domain/service/PaymentPgRouter.java b/platform/src/main/java/com/commerce/platform/core/domain/service/PaymentPgRouter.java new file mode 100644 index 0000000..f33a1a8 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/core/domain/service/PaymentPgRouter.java @@ -0,0 +1,34 @@ +package com.commerce.platform.core.domain.service; + +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.domain.enums.PayMethod; +import com.commerce.platform.core.domain.enums.PgProvider; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class PaymentPgRouter { + private final Map pgStrategies; + + public PaymentPgRouter(List list) { + this.pgStrategies = list.stream() + .collect(Collectors.toMap(PgStrategy::getPgProvider, pg -> pg)); + } + + public PgStrategy routPg(PayMethod payMethod) { + List availablePgList = PgProvider.getByPayMethod(payMethod); + + // todo 조건에 따른 pg사 선택 + PgProvider targetPg = availablePgList.get(0); + + return pgStrategies.get(targetPg); + } + + public PgStrategy getPgStrategyByProvider(PgProvider pgProvider) { + return pgStrategies.get(pgProvider); + } + +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CustomerCardAdaptor.java b/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CustomerCardAdaptor.java index 9c03d11..7a27fed 100644 --- a/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CustomerCardAdaptor.java +++ b/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/CustomerCardAdaptor.java @@ -7,6 +7,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.Optional; + @RequiredArgsConstructor @Component public class CustomerCardAdaptor implements CustomerCardOutPort { @@ -21,4 +23,9 @@ public void save(CustomerCard customerCard) { public int countActiveCardByCustomerId(CustomerId customerId) { return repository.countByActiveCustomerId(customerId); } + + @Override + public Optional findActiveById(Long cardId) { + return repository.findByIdAndActive(cardId); + } } diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/PaymentAdaptor.java b/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/PaymentAdaptor.java new file mode 100644 index 0000000..3d6d544 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/adaptor/PaymentAdaptor.java @@ -0,0 +1,47 @@ +package com.commerce.platform.infrastructure.adaptor; + +import com.commerce.platform.core.application.out.PaymentOutPort; +import com.commerce.platform.core.domain.aggreate.Payment; +import com.commerce.platform.core.domain.aggreate.PaymentPartCancel; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.core.domain.vo.OrderId; +import com.commerce.platform.core.domain.vo.PaymentId; +import com.commerce.platform.infrastructure.persistence.PaymentPartCancelRepository; +import com.commerce.platform.infrastructure.persistence.PaymentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class PaymentAdaptor implements PaymentOutPort { + private final PaymentRepository paymentRepository; + private final PaymentPartCancelRepository paymentPartCancelRepository; + + @Override + public void savePayment(Payment payment) { + paymentRepository.save(payment); + } + + @Override + public Optional findByOrderId(OrderId orderId) { + return paymentRepository.findByOrderId(orderId); + } + + @Override + public void savePartCancel(PaymentPartCancel partCancel) { + paymentPartCancelRepository.save(partCancel); + } + + @Override + public boolean existsPartCancelByPaymentId(PaymentId paymentId) { + return paymentPartCancelRepository.existsPaymentPartCancelByApprovedPaymentId(paymentId); + } + + @Override + public Money getRemainAmount(PaymentId paymentId) { + return paymentPartCancelRepository.selectRemainAmountByPaymentId(paymentId); + } +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CustomerCardRepository.java b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CustomerCardRepository.java index 3fb76db..c904a47 100644 --- a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CustomerCardRepository.java +++ b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/CustomerCardRepository.java @@ -6,9 +6,15 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface CustomerCardRepository extends JpaRepository { - @Query("SELECT COUNT(c) FROM CustomerCard c WHERE c.isActive = true AND c.customerId = :customerId") + @Query("SELECT COUNT(c) FROM CustomerCard c WHERE c.customerId = :customerId AND c.isActive = true") int countByActiveCustomerId(CustomerId customerId); + + @Query("SELECT c FROM CustomerCard c WHERE c.id = :cardId AND c.isActive = true") + Optional findByIdAndActive(Long cardId); + } diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/PaymentPartCancelRepository.java b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/PaymentPartCancelRepository.java new file mode 100644 index 0000000..f5f25bb --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/PaymentPartCancelRepository.java @@ -0,0 +1,19 @@ +package com.commerce.platform.infrastructure.persistence; + +import com.commerce.platform.core.domain.aggreate.PaymentPartCancel; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.core.domain.vo.PaymentId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PaymentPartCancelRepository extends JpaRepository { + + boolean existsPaymentPartCancelByApprovedPaymentId(PaymentId paymentId); + + /** + * 최신 남은 금액 조회 + */ + @Query("SELECT p.remainAmt FROM PaymentPartCancel p WHERE p.approvedPaymentId = :paymentId ORDER BY p.id DESC LIMIT 1") + Money selectRemainAmountByPaymentId(@Param("payment") PaymentId paymentId); +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/persistence/PaymentRepository.java b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/PaymentRepository.java new file mode 100644 index 0000000..cedcf31 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/persistence/PaymentRepository.java @@ -0,0 +1,17 @@ +package com.commerce.platform.infrastructure.persistence; + +import com.commerce.platform.core.domain.aggreate.Payment; +import com.commerce.platform.core.domain.vo.OrderId; +import com.commerce.platform.core.domain.vo.PaymentId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PaymentRepository extends JpaRepository { + + @Query("SELECT p FROM Payment p WHERE p.orderId = :orderId") + Optional findByOrderId(OrderId orderId); +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/pg/DanalStrategy.java b/platform/src/main/java/com/commerce/platform/infrastructure/pg/DanalStrategy.java new file mode 100644 index 0000000..f195b19 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/pg/DanalStrategy.java @@ -0,0 +1,27 @@ +package com.commerce.platform.infrastructure.pg; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.application.out.PhonePay; +import com.commerce.platform.core.application.out.dto.PgPayResponse; +import com.commerce.platform.core.domain.enums.PgProvider; +import org.springframework.stereotype.Component; + +@Component +public class DanalStrategy extends PgStrategy + implements PhonePay { + @Override + public PgProvider getPgProvider() { + return PgProvider.DANAL; + } + + @Override + public PgPayResponse approvePhone(PayOrderCommand command) { + return null; + } + + @Override + public PgPayResponse cancelPhone(PayOrderCommand command) { + return null; + } +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/pg/NHNStrategy.java b/platform/src/main/java/com/commerce/platform/infrastructure/pg/NHNStrategy.java new file mode 100644 index 0000000..731d8b3 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/pg/NHNStrategy.java @@ -0,0 +1,27 @@ +package com.commerce.platform.infrastructure.pg; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.CardPay; +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.application.out.dto.PgPayResponse; +import com.commerce.platform.core.domain.enums.PgProvider; +import org.springframework.stereotype.Component; + +@Component +public class NHNStrategy extends PgStrategy + implements CardPay { + @Override + public PgPayResponse approveCard(PayOrderCommand command) { + return null; + } + + @Override + public PgPayResponse cancelCard(PayOrderCommand command) { + return null; + } + + @Override + public PgProvider getPgProvider() { + return PgProvider.NHN; + } +} diff --git a/platform/src/main/java/com/commerce/platform/infrastructure/pg/TossStrategy.java b/platform/src/main/java/com/commerce/platform/infrastructure/pg/TossStrategy.java new file mode 100644 index 0000000..c5b9b04 --- /dev/null +++ b/platform/src/main/java/com/commerce/platform/infrastructure/pg/TossStrategy.java @@ -0,0 +1,40 @@ +package com.commerce.platform.infrastructure.pg; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.CardPay; +import com.commerce.platform.core.application.out.EasyPay; +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.application.out.dto.PgPayResponse; +import com.commerce.platform.core.domain.enums.PgProvider; +import org.springframework.stereotype.Component; + +@Component +public class TossStrategy extends PgStrategy + implements CardPay, EasyPay { + + @Override + public PgProvider getPgProvider() { + return PgProvider.TOSS; + } + + + @Override + public PgPayResponse approveCard(PayOrderCommand command) { + return null; + } + + @Override + public PgPayResponse cancelCard(PayOrderCommand command) { + return null; + } + + @Override + public PgPayResponse approveEasyPay(PayOrderCommand command) { + return null; + } + + @Override + public PgPayResponse cancelEasyPay(PayOrderCommand command) { + return null; + } +} diff --git a/platform/src/main/java/com/commerce/platform/shared/exception/BusinessError.java b/platform/src/main/java/com/commerce/platform/shared/exception/BusinessError.java index e4123b2..2622ac6 100644 --- a/platform/src/main/java/com/commerce/platform/shared/exception/BusinessError.java +++ b/platform/src/main/java/com/commerce/platform/shared/exception/BusinessError.java @@ -28,7 +28,8 @@ public enum BusinessError { // Customer INVALID_CUSTOMER("M001", "고객ID 확인요망"), DUPLICATED_REGISTRY_CARD("M002", "이미 등록된 카드 존재"), - EXCEED_REGISTRY_CARD("M002", "카드는 최대 5개 등록 가능합니다."), + EXCEED_REGISTRY_CARD("M003", "카드는 최대 5개 등록 가능합니다."), + NOT_FOUND_REGISTRY_CARD("M004", "해당 카드가 존재하지 않습니다."), // Coupon INVALID_COUPON("C001", "유효하지 않는 쿠폰 ID"), @@ -43,7 +44,13 @@ public enum BusinessError { DUPLICATE_ISSUED_COUPON("I004", "이미 발급된 쿠폰입니다."), // Payment - INVALID_PAYMENT("T", "유효하지 않는 결제ID"), + INVALID_PAYMENT("T001", "유효하지 않는 결제ID"), + PAYMENT_ALREADY_CANCELED("T002", "이미 전체 취소된 결제입니다"), + PAYMENT_HAS_PARTIAL_CANCEL("T003", "부분 취소 내역이 존재하여 전체 취소가 불가능합니다"), + PAYMENT_CANCEL_AMOUNT_EXCEEDED("T004", "취소 가능 금액을 초과했습니다"), + PAYMENT_INVALID_STATUS_FOR_CANCEL("T005", "취소 불가능한 결제 상태입니다"), + PG_RESPONSE_FAILED("T006", "결제처리 결과 실패"), + INVALID_PARTIAL_CANCEL_AMOUNT("T007", "부분취소 불가능한 금액입니다. 전체취소로 요청하세요."), // Money INVALID_MONEY("M001", "금액 오류"), diff --git a/platform/src/test/java/com/commerce/platform/core/application/in/PaymentUseCaseImplTest.java b/platform/src/test/java/com/commerce/platform/core/application/in/PaymentUseCaseImplTest.java new file mode 100644 index 0000000..c22d2d8 --- /dev/null +++ b/platform/src/test/java/com/commerce/platform/core/application/in/PaymentUseCaseImplTest.java @@ -0,0 +1,202 @@ +package com.commerce.platform.core.application.in; + +import com.commerce.platform.core.application.in.dto.PayOrderCommand; +import com.commerce.platform.core.application.out.PaymentOutPort; +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.application.out.dto.PgPayResponse; +import com.commerce.platform.core.domain.aggreate.Order; +import com.commerce.platform.core.domain.aggreate.Payment; +import com.commerce.platform.core.domain.enums.*; +import com.commerce.platform.core.domain.service.PaymentPgRouter; +import com.commerce.platform.core.domain.vo.CustomerId; +import com.commerce.platform.core.domain.vo.Money; +import com.commerce.platform.infrastructure.persistence.OrderRepository; +import com.commerce.platform.infrastructure.pg.TossStrategy; +import com.commerce.platform.shared.exception.BusinessError; +import com.commerce.platform.shared.exception.BusinessException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SpringBootTest +@Transactional +class PaymentUseCaseImplTest { + + @Autowired + private PaymentUseCase paymentUseCase; + + @Autowired + private PaymentOutPort paymentOutPort; + + @Autowired + private OrderRepository orderRepository; + + @MockBean + private PaymentPgRouter mockPaymentPgRouter; + + private Order testOrder; + + private PgPayResponse success_pgResponse = new PgPayResponse( + "PG_TID_12345", + "0000", + "성공", + true + ); + + private PgPayResponse fail_pgResponse = new PgPayResponse( + "PG_TID_12345", + "errorCode", + "실패", + false + ); + + @BeforeEach + void init() { + // 주문완료 상태의 신규order 저장 + testOrder = Order.create(CustomerId.of("test1"), null); + testOrder.confirm(Money.create(35000), Money.create(0)); + orderRepository.save(testOrder); + + orderRepository.flush(); + } + + @Test + void doApproval_successful() { + PayOrderCommand command = new PayOrderCommand( + testOrder.getOrderId(), + null, + null, + null, + PayMethod.CARD, + PayProvider.KB, + PaymentStatus.APPROVED + ); + + // mock pg + PgStrategy mockPgStrategy = mock(PgStrategy.class); + + when(mockPaymentPgRouter.routPg(PayMethod.CARD)) + .thenReturn(mockPgStrategy); + when(mockPgStrategy.getPgProvider()) + .thenReturn(PgProvider.TOSS); + when(mockPgStrategy.processApproval(any())) + .thenReturn(success_pgResponse); + + paymentUseCase.doApproval(command); + + // then 승인 성공 + Payment payment = paymentOutPort.findByOrderId(testOrder.getOrderId()) + .get(); + assertThat(payment).isNotNull(); + assertThat(payment.getPaymentStatus()).isEqualTo(PaymentStatus.APPROVED); + + // order 상태 검증 + assertThat(orderRepository.findById(testOrder.getOrderId()).get().getStatus()) + .isEqualTo(OrderStatus.PAID); + } + + @Test + void doCancel_successful() { + // 주문 결제 + doApproval_successful(); + + PayOrderCommand command = new PayOrderCommand( + testOrder.getOrderId(), + null, + null, + null, + PayMethod.CARD, + null, + PaymentStatus.FULL_CANCELED + ); + + PgStrategy mockPgStrategy = mock(PgStrategy.class); + + when(mockPaymentPgRouter.getPgStrategyByProvider(any())) + .thenReturn(mockPgStrategy); + when(mockPgStrategy.processCancel(any())) + .thenReturn(success_pgResponse); + + paymentUseCase.doCancel(command); + + // payment 상태 + assertThat(paymentOutPort.findByOrderId(testOrder.getOrderId())).isNotNull(); + assertThat(paymentOutPort.findByOrderId(testOrder.getOrderId()).get().getPaymentStatus()) + .isEqualTo(PaymentStatus.FULL_CANCELED); + + // order 상태 검증 + Order refundedOrder = orderRepository.findById(testOrder.getOrderId()).orElseThrow(); + assertThat(refundedOrder.getStatus()).isEqualTo(OrderStatus.REFUND); + } + + @DisplayName("전체취소실패 : 부분취소내역 존재") + @Test + void doCancel_failed() { + // 결제 승인 + doApproval_successful(); + + // 10,000원 부분취소 + PayOrderCommand partCancelCommand = new PayOrderCommand( + testOrder.getOrderId(), + null, + Money.create(10000), + null, + PayMethod.CARD, + null, + null + ); + + PgStrategy mockStrategy = mock(TossStrategy.class); + PgPayResponse partCancelResponse = new PgPayResponse( + "PART_CANCEL_TID_001", + "0000", + "부분취소 성공", + true + ); + + when(mockPaymentPgRouter.routPg(PayMethod.CARD)) + .thenReturn(mockStrategy); + when(mockStrategy.processCancel(any())) + .thenReturn(partCancelResponse); + + // 부분취소 + paymentUseCase.doPartCancel(partCancelCommand); + + // 전체취소 + PayOrderCommand fullCancelCommand = new PayOrderCommand( + testOrder.getOrderId(), + null, + null, + null, + null, + null, + PaymentStatus.FULL_CANCELED + ); + + // 전체취소 실패 + assertThatThrownBy(() ->paymentUseCase.doCancel(fullCancelCommand)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining(BusinessError.PAYMENT_HAS_PARTIAL_CANCEL.getMessage()); + + // payment 상태 검증 + Payment payment = paymentOutPort.findByOrderId(testOrder.getOrderId()).orElseThrow(); + assertThat(payment.getPaymentStatus()) + .isEqualTo(PaymentStatus.APPROVED); + + // order 상태 검증 + Order order = orderRepository.findById(testOrder.getOrderId()).orElseThrow(); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PAID); + } + + +} \ No newline at end of file diff --git a/platform/src/test/java/com/commerce/platform/core/domain/service/PaymentPgRouterTest.java b/platform/src/test/java/com/commerce/platform/core/domain/service/PaymentPgRouterTest.java new file mode 100644 index 0000000..6cc1b72 --- /dev/null +++ b/platform/src/test/java/com/commerce/platform/core/domain/service/PaymentPgRouterTest.java @@ -0,0 +1,29 @@ +package com.commerce.platform.core.domain.service; + +import com.commerce.platform.core.application.out.PgStrategy; +import com.commerce.platform.core.domain.enums.PayMethod; +import com.commerce.platform.infrastructure.pg.NHNStrategy; +import com.commerce.platform.infrastructure.pg.TossStrategy; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class PaymentPgRouterTest { + @Autowired + private PaymentPgRouter paymentPgRouter; + + @DisplayName("결제유형에 따른 라우팅") + @Test + void routPg() { + PgStrategy pgStrategy = paymentPgRouter.routPg(PayMethod.CARD); + + assertThat(pgStrategy) + .as("카드결제는 토스, NHN 지원").isInstanceOfAny(TossStrategy.class, NHNStrategy.class); + + } +} \ No newline at end of file