Skip to content
Closed
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,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<PayResult> 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<String> createPaymentWithRegistryCard(
@PathVariable Long cardId,
@RequestBody Map<String, String> body) {

paymentUseCase.doApprovalWithCardId(cardId);

return ResponseEntity.ok("성공");
}
}
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -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); // 등록된 카드로 결제
}
Original file line number Diff line number Diff line change
@@ -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) {

}

}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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{}

}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomerCard> findActiveById(Long cardId);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<Payment> findByOrderId(OrderId orderId);

// 부분취소 관련
void savePartCancel(PaymentPartCancel partCancel);
boolean existsPartCancelByPaymentId(PaymentId paymentId);
Money getRemainAmount(PaymentId paymentId);
}
Loading
Loading