Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

토스 페이먼츠 결제 승인 로직 전의 주문 승인 로직 추가 #110

Merged
merged 5 commits into from
Jul 8, 2023
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,6 +1,7 @@
package com.hcommerce.heecommerce.common;

import com.hcommerce.heecommerce.common.dto.ResponseDto;
import com.hcommerce.heecommerce.order.InvalidPaymentAmountException;
import com.hcommerce.heecommerce.order.MaxOrderQuantityExceededException;
import com.hcommerce.heecommerce.order.OrderNotFoundException;
import com.hcommerce.heecommerce.order.OrderOverStockException;
Expand Down Expand Up @@ -61,6 +62,15 @@ public ResponseDto methodArgumentNotValidExceptionHandler(MethodArgumentNotValid
.build();
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ResponseDto invalidPaymentAmountExceptionHandler(InvalidPaymentAmountException e) {
return ResponseDto.builder()
.code(HttpStatus.BAD_REQUEST.name())
.message(e.getMessage())
.build();
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ResponseDto fallbackExceptionHandler(Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.hcommerce.heecommerce.order;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidPaymentAmountException extends RuntimeException {
public InvalidPaymentAmountException() {
super("결제 금액이 유효하지 않습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.hcommerce.heecommerce.order;

import jakarta.validation.constraints.NotNull;
import java.beans.ConstructorProperties;
import lombok.Builder;
import lombok.Getter;

@Getter
public class OrderApproveForm {

@NotNull
private final String paymentKey; // 토스 키

@NotNull
private final String orderId; // 토스 주문 식별자

@NotNull
private final int amount; // 총 결제 금액

@Builder
@ConstructorProperties({
"paymentKey",
"orderId",
"amount"
})
public OrderApproveForm(String paymentKey, String orderId, int amount) {
this.paymentKey = paymentKey;
this.orderId = orderId;
this.amount = amount;
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/hcommerce/heecommerce/order/OrderController.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,20 @@ public ResponseDto placeOrderInAdvance(@Valid @RequestBody OrderForm orderForm)
.data(new OrderUuid(orderUuid))
.build();
}

/**
* 클라이언트에서 결제 완료가 되고, 주문 승인을 하는 경우에 대한 것
*/
@PostMapping("/orders/approve")
@ResponseStatus(HttpStatus.CREATED)
public ResponseDto approveOrder(@Valid @RequestBody OrderApproveForm orderApproveForm) {

UUID uuid = orderService.approveOrder(orderApproveForm);

return ResponseDto.builder()
.code(HttpStatus.CREATED.name())
.message("주문 접수가 완료되었습니다.")
.data(new OrderUuid(uuid))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.hcommerce.heecommerce.order;

import lombok.Builder;
import lombok.Getter;

@Getter
public class OrderEntityForOrderApproveValidation {

private final byte[] dealProductUuid;
private final int orderQuantity;
private final int totalPaymentAmount;
private final OutOfStockHandlingOption outOfStockHandlingOption;

@Builder
public OrderEntityForOrderApproveValidation(
byte[] dealProductUuid,
int orderQuantity,
int totalPaymentAmount,
OutOfStockHandlingOption outOfStockHandlingOption
) {
this.dealProductUuid = dealProductUuid;
this.orderQuantity = orderQuantity;
this.totalPaymentAmount = totalPaymentAmount;
this.outOfStockHandlingOption = outOfStockHandlingOption;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.hcommerce.heecommerce.order;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderQueryMapper {

OrderEntityForOrderApproveValidation findOrderEntityForOrderApproveValidation(@Param("orderUuid") byte[] orderUuid);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.hcommerce.heecommerce.order;

import com.hcommerce.heecommerce.common.utils.TypeConversionUtils;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class OrderQueryRepository {

private final OrderQueryMapper orderQueryMapper;

@Autowired
public OrderQueryRepository(OrderQueryMapper orderQueryMapper) {
this.orderQueryMapper = orderQueryMapper;
}

public OrderEntityForOrderApproveValidation findOrderEntityForOrderApproveValidation(String orderId) {

UUID orderUuid = UUID.fromString(orderId);

byte[] orderUuidByte = TypeConversionUtils.convertUuidToBinary(orderUuid);

return orderQueryMapper.findOrderEntityForOrderApproveValidation(orderUuidByte);
}
}
59 changes: 58 additions & 1 deletion src/main/java/com/hcommerce/heecommerce/order/OrderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
@Service
public class OrderService {

private final OrderQueryRepository orderQueryRepository;

private final OrderCommandRepository orderCommandRepository;

private final InventoryQueryRepository inventoryQueryRepository;
Expand All @@ -23,11 +25,13 @@ public class OrderService {

@Autowired
public OrderService(
OrderQueryRepository orderQueryRepository,
OrderCommandRepository orderCommandRepository,
InventoryQueryRepository inventoryQueryRepository,
InventoryCommandRepository inventoryCommandRepository,
DealProductQueryRepository dealProductQueryRepository
) {
this.orderQueryRepository = orderQueryRepository;
this.orderCommandRepository = orderCommandRepository;
this.inventoryQueryRepository = inventoryQueryRepository;
this.inventoryCommandRepository = inventoryCommandRepository;
Expand Down Expand Up @@ -93,6 +97,10 @@ public void placeOrder(OrderForm orderForm) {
* @param orderQuantity : 주문량
* @param outOfStockHandlingOption : 재고 부족 처리 옵션
* @return realOrderQuantity : 실제 주문량
*
* realOrderQuantity 이 필요한 이유는 "부분 주문" 때문이다.
* 재고량이 0은 아니지만, 사용자가 주문한 수량에 비해 재고량이 없는 경우가 있다.
* 이때, 재고량만큼만 주문하도록 할 수 있도록 "부문 주문"이 가능한데, 사용자가 주문한 수량과 혼동되지 않도록 실제 주문하는 수량이라는 의미를 내포하기 위해서 필요하다.
Comment on lines +100 to +103
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 추가

*/
private int calculateRealOrderQuantity(int inventoryAfterDecrease, int orderQuantity, OutOfStockHandlingOption outOfStockHandlingOption) {

Expand Down Expand Up @@ -243,4 +251,53 @@ private int calculateTotalPaymentAmount(int originPrice, DiscountType discountTy

return originPrice - discountValue; // 정률 할인
}
}

/**
* approveOrder 는 주문 승인을 하기 위한 함수이다.
*/
public UUID approveOrder(OrderApproveForm orderApproveForm) {
String orderId = orderApproveForm.getOrderId();

// 0. DB에서 검증에 필요한 데이터 가져오기
OrderEntityForOrderApproveValidation orderForm = orderQueryRepository.findOrderEntityForOrderApproveValidation(orderApproveForm.getOrderId());

// 1. orderApproveForm 검증
validateOrderApproveForm(orderApproveForm, orderForm);

// 2. 재고 감소
UUID dealProductUuid = TypeConversionUtils.convertBinaryToUuid(orderForm.getDealProductUuid());

int orderQuantity = orderForm.getOrderQuantity();

int inventoryAfterDecrease = inventoryCommandRepository.decreaseByAmount(dealProductUuid, orderQuantity);

try {
// 3. 실제 주문 수량 계산
int inventoryBeforeDecrease = orderQuantity + inventoryAfterDecrease;

OutOfStockHandlingOption outOfStockHandlingOption = orderForm.getOutOfStockHandlingOption();

int realOrderQuantity = calculateRealOrderQuantity(inventoryAfterDecrease, orderQuantity, outOfStockHandlingOption);
daadaadaah marked this conversation as resolved.
Show resolved Hide resolved

if (inventoryBeforeDecrease < orderQuantity && outOfStockHandlingOption == OutOfStockHandlingOption.PARTIAL_ORDER) {
inventoryCommandRepository.set(dealProductUuid, 0); // 데이터 일관성 맞춰주기 위해
}

// 4. 토스 페이먼트 결제 승인

// 5. 주문 관련 데이터 저장

} catch (OrderOverStockException orderOverStockException) {
rollbackReducedInventory(dealProductUuid, orderQuantity);
throw orderOverStockException;
}

return UUID.fromString(orderId); // TODO : 임시 데이터
}

public void validateOrderApproveForm(OrderApproveForm orderApproveForm, OrderEntityForOrderApproveValidation orderForm) {
if(orderApproveForm.getAmount() != orderForm.getTotalPaymentAmount()) {
throw new InvalidPaymentAmountException();
}
}
}
10 changes: 10 additions & 0 deletions src/main/resources/mappers/OrderQueryMapper.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hcommerce.heecommerce.order.OrderQueryMapper">

<!-- 주문 검증을 위해 주문 조회 -->
<select id="findOrderEntityForOrderApproveValidation" resultType="OrderEntityForOrderApproveValidation">
SELECT deal_product_uuid, order_quantity, total_payment_amount, out_of_stock_handling_option FROM `order` WHERE uuid = #{orderUuid}
</select>
</mapper>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ public static RestDocumentationResultHandler placeOrderInAdvance() {
);
}

public static RestDocumentationResultHandler approveOrder() {
return document("approve-order",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
responseFields(
fieldWithPath("code").type(JsonFieldType.STRING).description("코드"),
fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"),
fieldWithPath("data.orderUuid").type(JsonFieldType.STRING).description("주문 UUID")
)
);
}

public static RestDocumentationResultHandler placeOrder() {
return document("place-order",
preprocessRequest(prettyPrint()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,4 +595,36 @@ void It_returns_409_Error() throws Exception {
}
}
}

@Nested
@DisplayName("POST /orders/approve")
class Describe_ApproveOrder_API {
@Test
@DisplayName("returns 201")
void It_returns_201() throws Exception {
OrderApproveForm orderForm = OrderApproveForm.builder()
.paymentKey("5zJ4xY7m0kODnyRpQWGrN2xqGlNvLrKwv1M9ENjbeoPaZdL6")
.orderId(UUID.randomUUID().toString())
.amount(15000)
.build();

// given
given(orderService.approveOrder(any())).willReturn(UUID.randomUUID());

// when
String content = objectMapper.writeValueAsString(orderForm);

ResultActions resultActions = mockMvc.perform(
post("/orders/approve")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);

// then
resultActions.andExpect(status().isCreated())
.andDo(OrderControllerRestDocs.approveOrder());

}
}
}
Loading
Loading