Skip to content

Commit

Permalink
Merge pull request #110 from daadaadaah/feat/add-approveOrder
Browse files Browse the repository at this point in the history
토스 페이먼츠 결제 승인 로직 전의 주문 승인 로직 추가
  • Loading branch information
daadaadaah authored Jul 8, 2023
2 parents aaf4f46 + 7e8e479 commit 62c6420
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 1 deletion.
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은 아니지만, 사용자가 주문한 수량에 비해 재고량이 없는 경우가 있다.
* 이때, 재고량만큼만 주문하도록 할 수 있도록 "부문 주문"이 가능한데, 사용자가 주문한 수량과 혼동되지 않도록 실제 주문하는 수량이라는 의미를 내포하기 위해서 필요하다.
*/
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);

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

0 comments on commit 62c6420

Please sign in to comment.