Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,100 @@
package com.commerce.platform.bootstrap.customer;

import com.commerce.platform.bootstrap.dto.payment.PaymentCancelRequest;
import com.commerce.platform.bootstrap.dto.payment.PaymentRequest;
import com.commerce.platform.core.application.in.PaymentUseCase;
import com.commerce.platform.core.application.in.dto.PayCancelCommand;
import com.commerce.platform.core.application.in.dto.PayOrderCommand;
import com.commerce.platform.core.application.in.dto.PayResult;
import com.commerce.platform.core.domain.enums.PaymentStatus;
import com.commerce.platform.core.domain.vo.OrderId;
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;

@PostMapping("/approval")
public ResponseEntity<PayResult> createPayment(@Valid @RequestBody PaymentRequest paymentRequest) {
PayResult result = null;

try {
PayOrderCommand command = new PayOrderCommand(
OrderId.of(paymentRequest.orderId()),
null,
paymentRequest.installment(),
paymentRequest.payMethod(),
paymentRequest.payProvider()
);

paymentUseCase.doApproval(command);
} catch (BusinessException e) {
result = new PayResult.Failed(e.getCode(), e.getMessage());
} catch (Exception e) {
result = new PayResult.Failed("9999", "승인 처리 중 오류가 발생했습니다");
}

return ResponseEntity.ok(result);
}

@PatchMapping("/cancel")
public ResponseEntity<PayResult> fullCancel(@Valid @RequestBody PaymentCancelRequest cancelRequest) {
PayResult result = null;

try {
PayCancelCommand cancelCommand = PayCancelCommand.builder()
.orderId(cancelRequest.orderId())
.paymentStatus(PaymentStatus.FULL_CANCELED)
.build();

paymentUseCase.doCancel(cancelCommand);
} catch (BusinessException e) {
result = new PayResult.Failed(e.getCode(), e.getMessage());
} catch (Exception e) {
result = new PayResult.Failed("9999", "전체취소 처리 중 오류가 발생했습니다");
}

return ResponseEntity.ok(result);
}

@PatchMapping("/partial-cancel")
public ResponseEntity<PayResult> partialCancel(@Valid @RequestBody PaymentCancelRequest cancelRequest) {
PayResult result = null;

try {
PayCancelCommand cancelCommand = PayCancelCommand.builder()
.orderId(cancelRequest.orderId())
.orderItemId(cancelRequest.orderItemId())
.canceledQuantity(cancelRequest.canceledQuantity())
.paymentStatus(PaymentStatus.FULL_CANCELED)
.build();

paymentUseCase.doPartCancel(cancelCommand);
} 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<String> createPaymentWithRegistryCard(
@PathVariable Long cardId,
@RequestBody Map<String, String> body) {

// todo
paymentUseCase.doApprovalWithCardId(cardId);

return ResponseEntity.ok("성공");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.commerce.platform.bootstrap.dto.payment;

import com.commerce.platform.core.domain.enums.PaymentStatus;
import com.commerce.platform.core.domain.vo.OrderId;
import com.commerce.platform.core.domain.vo.Quantity;
import jakarta.validation.constraints.NotBlank;

/**
* 전채/부분 취소 요청 dto
*/
public record PaymentCancelRequest(
@NotBlank
OrderId orderId,

Long orderItemId, // 취소할 orderItem

Quantity canceledQuantity, // 해당 orderItem의 취소 개수

@NotBlank
PaymentStatus paymentStatus
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.commerce.platform.bootstrap.dto.payment;

import com.commerce.platform.core.domain.enums.PayMethod;
import com.commerce.platform.core.domain.enums.PayProvider;
import jakarta.validation.constraints.NotBlank;

/**
* 승인 요청 dto
*/
public record PaymentRequest(
@NotBlank
String orderId,

@NotBlank
PayMethod payMethod,

@NotBlank
PayProvider payProvider,

@NotBlank
String installment
) { }
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public OrderDetailResponse getOrder(OrderId orderId) {
List<OrderItem> orderItems = orderItemOutPort.findByOrderId(order.getOrderId());

List<ProductId> productIds = orderItems.stream()
.map(oi -> oi.getOrderItemId().productId())
.map(oi -> oi.getProductId())
.toList();

Map<ProductId, Product> productMap = productOutputPort.findByIdIn(productIds).stream()
Expand Down Expand Up @@ -156,15 +156,15 @@ private Money applyCoupon(Order order, CouponId couponId) {
*/
private Money calculateTotalAmountFromProducts(List<OrderItem> orderItems) {
List<ProductId> productIds = orderItems.stream()
.map(oi -> oi.getOrderItemId().productId())
.map(oi -> oi.getProductId())
.toList();

Map<ProductId, Product> productMap = productOutputPort.findByIdIn(productIds).stream()
.collect(Collectors.toMap(Product::getProductId, Function.identity()));

return orderItems.stream()
.map(item -> {
Product product = productMap.get(item.getOrderItemId().productId());
Product product = productMap.get(item.getProductId());
product.decreaseStock(item.getQuantity()); // todo 재고 소진 테스트
return product.getPrice()
.multiply(item.getQuantity());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.commerce.platform.core.application.in;

import com.commerce.platform.core.application.in.dto.PayCancelCommand;
import com.commerce.platform.core.application.in.dto.PayOrderCommand;

public interface PaymentUseCase {
void doApproval(PayOrderCommand command);
void doCancel(PayCancelCommand cancelCommand);
Long doPartCancel(PayCancelCommand cancelCommand);
void doApprovalWithCardId(Long cardId); // 등록된 카드로 결제
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package com.commerce.platform.core.application.in;

import com.commerce.platform.core.application.in.dto.PayCancelCommand;
import com.commerce.platform.core.application.in.dto.PayOrderCommand;
import com.commerce.platform.core.application.out.*;
import com.commerce.platform.core.application.out.dto.PgPayResponse;
import com.commerce.platform.core.domain.aggreate.*;
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 java.util.List;
import java.util.Optional;

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 OrderItemOutPort orderItemOutPort;
private final ProductOutputPort productOutputPort;
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(PayCancelCommand cancelCommand) {

// 주문 검증
Order orderEntity = orderOutputPort.findById(cancelCommand.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);
}

cancelCommand.setCanceledAmount(orderEntity.getResultAmt());
cancelCommand.setPayProvider(paymentEntity.getPayProvider());
cancelCommand.setPayMethod(paymentEntity.getPayMethod());
cancelCommand.setPgProvider(paymentEntity.getPgProvider());

PgStrategy pgStrategy = pgRouter.getPgStrategyByProvider(paymentEntity.getPgProvider());
PgPayResponse pgResponse = pgStrategy.processCancel(cancelCommand);

// PG 응답 반영
if (!pgResponse.isSuccess()) {
throw new BusinessException(PG_RESPONSE_FAILED);
}

orderEntity.refund();
paymentEntity.canceled(pgResponse);
paymentOutPort.savePayment(paymentEntity);
}

/**
* 부분취소
*/
@Override
@Transactional
public Long doPartCancel(PayCancelCommand cancelCommand) {
// 주문 검정
Order orderEntity = orderOutputPort.findById(cancelCommand.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();
}

// 취소가능금액 검증
if(remainAmt.value() == 0) {
throw new BusinessException(PAYMENT_CANCEL_AMOUNT_EXCEEDED);
}

// 부분취소 가능수량 검증
OrderItem orderItemEntity = orderItemOutPort.findById(cancelCommand.getOrderItemId())
.orElseThrow(() -> new BusinessException(INVALID_ORDER_ITEM_ID));
// 해당 건 삭제처리
orderItemEntity.canceledItem(cancelCommand.getCanceledQuantity());
// 새롭게 행 생성한다.
OrderItem refreshOrderItem = OrderItem.create(cancelCommand.getOrderId(),
orderItemEntity.getProductId(),
orderItemEntity.getQuantity().minus(cancelCommand.getCanceledQuantity()));
orderItemOutPort.saveAll(List.of(refreshOrderItem));

// 취소금액 계산
Money canceledAmt = productOutputPort.findById(orderItemEntity.getProductId())
.get()
.getPrice().multiply(cancelCommand.getCanceledQuantity());

cancelCommand.setCanceledAmount(canceledAmt);
cancelCommand.setPayProvider(paymentEntity.getPayProvider());
cancelCommand.setPayMethod(paymentEntity.getPayMethod());
cancelCommand.setPgProvider(paymentEntity.getPgProvider());

// 최종 남은금액
Money refreshRemainAmt = remainAmt.subtract(canceledAmt);

// 취소 요청
cancelCommand.setCanceledAmount(paymentEntity.getApprovedAmt());
cancelCommand.setPayProvider(paymentEntity.getPayProvider());
cancelCommand.setPayMethod(paymentEntity.getPayMethod());
cancelCommand.setPgProvider(paymentEntity.getPgProvider());

PgStrategy pgStrategy = pgRouter.getPgStrategyByProvider(paymentEntity.getPgProvider());
PgPayResponse pgResponse = pgStrategy.processCancel(cancelCommand);

if (!pgResponse.isSuccess()) {
throw new BusinessException(PG_RESPONSE_FAILED);
}

// 부분취소 내역 저장
PaymentPartCancel partCancel = PaymentPartCancel.create(
paymentEntity.getPaymentId(),
canceledAmt,
refreshRemainAmt
);
partCancel.completed(pgResponse.pgTid());
paymentOutPort.savePartCancel(partCancel);

return partCancel.getId();
}

/**
* 등록된 카드로 결제
* @param cardId
*/
@Override
public void doApprovalWithCardId(Long cardId) {
CustomerCard customerCard = customerCardOutPort.findActiveById(cardId)
.orElseThrow(() -> new BusinessException(INVALID_ORDER_ID));

}

}
Loading
Loading