diff --git a/src/main/java/com/hcommerce/heecommerce/order/OrderController.java b/src/main/java/com/hcommerce/heecommerce/order/OrderController.java index 2e51c8ce..1726143b 100644 --- a/src/main/java/com/hcommerce/heecommerce/order/OrderController.java +++ b/src/main/java/com/hcommerce/heecommerce/order/OrderController.java @@ -55,6 +55,8 @@ public ResponseDto placeOrder(@Valid @RequestBody OrderForm orderForm) { * - 재고 부족으로 주문이 불가능한 경우 409 error 예외 : 재고가 0이어서 불가능한 경우가 있을 수 있고, 재고는 2개인데, 주문량이 3개여서 주문이 불가능한 경우도 있을 수 있는데, 이 경우는 어떻게 처리할 것인가? */ + orderService.placeOrder(orderForm); + return ResponseDto.builder() .code(HttpStatus.CREATED.name()) .message("주문 접수가 완료되었습니다.") diff --git a/src/main/java/com/hcommerce/heecommerce/order/OrderService.java b/src/main/java/com/hcommerce/heecommerce/order/OrderService.java index 29759178..f213d2e2 100644 --- a/src/main/java/com/hcommerce/heecommerce/order/OrderService.java +++ b/src/main/java/com/hcommerce/heecommerce/order/OrderService.java @@ -1,7 +1,5 @@ package com.hcommerce.heecommerce.order; -import com.hcommerce.heecommerce.inventory.InventoryCommandRepository; -import com.hcommerce.heecommerce.inventory.InventoryQueryRepository; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -44,18 +42,22 @@ public void placeOrder(OrderForm orderForm) { int inventory = inventoryQueryRepository.get(key); // key에 해당 하는 값 없으면 Null 나옴 -> TODO : Null 처리 어떻게? Optional 활용? - if(inventory <= 0) { + int orderQuantity = orderForm.getOrderQuantity(); + + int realOrderQuantity = 0; + + if(inventory <= 0 || (inventory < orderQuantity && orderForm.getOutOfStockHandlingOption() == OutOfStockHandlingOption.ALL_CANCEL)) { throw new OrderOverStockException(); } - int orderQuantity = orderForm.getOrderQuantity(); - - if(orderQuantity > inventory) { - // TODO : OrderForm에 OutOfStockHandlingOption 추가 PR Merge 되면 로직 추가하기 + if (inventory < orderQuantity && orderForm.getOutOfStockHandlingOption() == OutOfStockHandlingOption.PARTIAL_ORDER) { + realOrderQuantity = inventory; // 재고량 만큼만 주문 + } else { + realOrderQuantity = orderQuantity; } // 2. 재고량 감소 - int currentInventory = inventoryCommandRepository.decreaseByAmount(key, orderQuantity); // TODO : 결제가 실패하여 재고량 다시 원상복귀해야할 때도, 분산락 걸어줘야 되나? + int currentInventory = inventoryCommandRepository.decreaseByAmount(key, realOrderQuantity); // TODO : 결제가 실패하여 재고량 다시 원상복귀해야할 때도, 분산락 걸어줘야 되나? if(currentInventory == 0) { // TODO : [품절 처리] Redis에 저장된 dealproducts 목록에서 딜 상품 상태 오픈 -> 품절로 변경 @@ -68,11 +70,10 @@ public void placeOrder(OrderForm orderForm) { boolean isSuccessPayment = false; // TODO : 임시 데이터 if(!isSuccessPayment) { - inventoryCommandRepository.increaseByAmount(key, orderQuantity); + inventoryCommandRepository.increaseByAmount(key, realOrderQuantity); return; } - /** * 바로 MySQL을 사용한 것과 AWS SQS를 사용한 것과 어떤 차이가 있을까? * 주문, 배송, 결제 가 각각 다른 Table 또는 다른 DB에 있을 때 이 작업 단위를 원자 단위로 하고 싶거나 또는 diff --git a/src/test/java/com/hcommerce/heecommerce/order/OrderControllerTest.java b/src/test/java/com/hcommerce/heecommerce/order/OrderControllerTest.java index 4a0d40db..95e7662d 100644 --- a/src/test/java/com/hcommerce/heecommerce/order/OrderControllerTest.java +++ b/src/test/java/com/hcommerce/heecommerce/order/OrderControllerTest.java @@ -173,11 +173,11 @@ void it_returns_401_Error() throws Exception { @DisplayName("POST /orders") class Describe_PlaceOrder_API { @Nested - @DisplayName("when order is successful") + @DisplayName("with orderQuantity < inventory") class Context_When_Order_Is_Successful { @Test @DisplayName("returns 201") - void it_returns_201() throws Exception { + void It_returns_201() throws Exception { // when OrderForm orderForm = OrderForm.builder() .userId(1) @@ -212,11 +212,106 @@ void it_returns_201() throws Exception { } @Nested - @DisplayName("when order fails due to invalid orderForm with orderQuantity > maxOrderQuantityPerOrder") - class Context_When_Order_Fails { + @DisplayName("with orderQuantity > inventory and outOfStockHandlingOption is `PARTIAL_ORDER`") + class Context_With_OrderQuantity_Exceeds_Inventory_And_OutOfStockHandlingOption_Is_PARTIAL_ORDER { + @Test + @DisplayName("returns 201") + void It_returns_201() throws Exception { + // given + UUID UUID_WITH_ORDER_QUANTITY_EXCEEDING_INVENTORY = UUID.randomUUID(); + + int ORDER_QUANTITY_EXCEEDING_INVENTORY = 5; + + OutOfStockHandlingOption PARTIAL_ORDER = OutOfStockHandlingOption.PARTIAL_ORDER; + + // when + OrderForm orderForm = OrderForm.builder() + .userId(1) + .recipientInfoForm( + RecipientInfoForm.builder() + .recipientName("leecommerce") + .recipientPhoneNumber("01087654321") + .recipientAddress("서울시 ") + .recipientDetailAddress("101호") + .shippingRequest("빠른 배송 부탁드려요!") + .build() + ) + .outOfStockHandlingOption(PARTIAL_ORDER) + .dealProductUuid(UUID_WITH_ORDER_QUANTITY_EXCEEDING_INVENTORY) + .orderQuantity(ORDER_QUANTITY_EXCEEDING_INVENTORY) + .paymentType(PaymentType.CREDIT_CARD) + .build(); + + String content = objectMapper.writeValueAsString(orderForm); + + ResultActions resultActions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + ); + + // then + resultActions.andExpect(status().isCreated()); + } + } + + @Nested + @DisplayName("with orderQuantity > inventory and outOfStockHandlingOption is `ALL_CANCEL`") + class Context_With_OrderQuantity_Exceeds_Inventory_And_OutOfStockHandlingOption_Is_ALL_CANCEL { + @Test + @DisplayName("returns 409 error") + void It_returns_409() throws Exception { + // given + UUID UUID_WITH_ORDER_QUANTITY_EXCEEDING_INVENTORY = UUID.randomUUID(); + + int ORDER_QUANTITY_EXCEEDING_INVENTORY = 5; + + OutOfStockHandlingOption ALL_CANCEL = OutOfStockHandlingOption.ALL_CANCEL; + + // when + OrderForm orderForm = OrderForm.builder() + .userId(1) + .recipientInfoForm( + RecipientInfoForm.builder() + .recipientName("leecommerce") + .recipientPhoneNumber("01087654321") + .recipientAddress("서울시 ") + .recipientDetailAddress("101호") + .shippingRequest("빠른 배송 부탁드려요!") + .build() + ) + .outOfStockHandlingOption(ALL_CANCEL) + .dealProductUuid(UUID_WITH_ORDER_QUANTITY_EXCEEDING_INVENTORY) + .orderQuantity(ORDER_QUANTITY_EXCEEDING_INVENTORY) + .paymentType(PaymentType.CREDIT_CARD) + .build(); + + String content = objectMapper.writeValueAsString(orderForm); + + ResultActions resultActions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + ); + + // then + resultActions.andExpect(status().isConflict()); + } + } + + @Nested + @DisplayName("with orderQuantity > maxOrderQuantityPerOrder") + class Context_With_OrderQuantity_Exceeds_MaxOrderQuantityPerOrder { @Test @DisplayName("returns 400 error") - void it_returns_400() throws Exception { + void It_returns_400() throws Exception { + // given + UUID UUID_WITH_MAX_ORDER_QUANTITY_PER_ORDER_OF_10 = UUID.randomUUID(); + + int ORDER_QUANTITY_EXCEEDING_MAX_ORDER_QUANTITY_PER_ORDER = 12; + // when OrderForm orderForm = OrderForm.builder() .userId(1) @@ -230,8 +325,8 @@ void it_returns_400() throws Exception { .build() ) .outOfStockHandlingOption(OutOfStockHandlingOption.ALL_CANCEL) - .dealProductUuid(UUID.randomUUID()) - .orderQuantity(12) + .dealProductUuid(UUID_WITH_MAX_ORDER_QUANTITY_PER_ORDER_OF_10) + .orderQuantity(ORDER_QUANTITY_EXCEEDING_MAX_ORDER_QUANTITY_PER_ORDER) .paymentType(PaymentType.CREDIT_CARD) .build(); @@ -247,7 +342,6 @@ void it_returns_400() throws Exception { // then resultActions.andExpect(status().isBadRequest()); } - } } }