diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java index b9daf39..675ca5f 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java @@ -1,5 +1,6 @@ package com.leungcheng.spring_simple_backend.domain.order; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; @@ -11,11 +12,15 @@ public class Order { private String buyerUserId; private PurchaseItems purchaseItems; + @Column(unique = true) + private String requestId; + private Order() {} - Order(String buyerUserId, PurchaseItems purchaseItems) { + Order(String buyerUserId, PurchaseItems purchaseItems, String requestId) { this.buyerUserId = buyerUserId; this.purchaseItems = purchaseItems; + this.requestId = requestId; } public String getId() { @@ -29,4 +34,8 @@ public String getBuyerUserId() { public PurchaseItems getPurchaseItems() { return purchaseItems; } + + public String getRequestId() { + return requestId; + } } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java index 14765ce..d35a779 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java @@ -1,5 +1,8 @@ package com.leungcheng.spring_simple_backend.domain.order; +import java.util.Optional; import org.springframework.data.repository.CrudRepository; -public interface OrderRepository extends CrudRepository {} +public interface OrderRepository extends CrudRepository { + Optional findByRequestId(String requestId); +} diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java index baafaed..e768853 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java @@ -28,7 +28,12 @@ public CreateOrderException(String message) { @Retryable(noRetryFor = CreateOrderException.class) @Transactional(isolation = Isolation.SERIALIZABLE) - public Order createOrder(String buyerUserId, PurchaseItems purchaseItems) { + public Order createOrder(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + Optional order = orderRepository.findByRequestId(requestId); + if (order.isPresent()) { + return order.get(); + } + User buyer = getUser(buyerUserId).orElseThrow(() -> new CreateOrderException("Buyer does not exist")); @@ -39,7 +44,7 @@ public Order createOrder(String buyerUserId, PurchaseItems purchaseItems) { } saveNewBalance(buyer, buyer.getBalance().subtract(totalCost)); - return addNewOrder(buyerUserId, purchaseItems); + return addNewOrder(buyerUserId, purchaseItems, requestId); } private Optional getUser(String userId) { @@ -51,8 +56,8 @@ private void saveNewBalance(User buyer, BigDecimal newBalance) { userRepository.save(updatedBuyer); } - private Order addNewOrder(String buyerUserId, PurchaseItems purchaseItems) { - Order order = new Order(buyerUserId, purchaseItems); + private Order addNewOrder(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + Order order = new Order(buyerUserId, purchaseItems, requestId); return orderRepository.save(order); } diff --git a/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java b/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java index 6b21dc0..13ede40 100644 --- a/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java +++ b/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java @@ -57,8 +57,7 @@ void shouldRejectCreateOrderWithNonExistingBuyer() { CreateOrderException exception = assertThrows( - CreateOrderException.class, - () -> orderService.createOrder("non_existing_buyer_id", purchaseItems)); + CreateOrderException.class, () -> createOrder("non_existing_buyer_id", purchaseItems)); assertEquals("Buyer does not exist", exception.getMessage()); } @@ -70,9 +69,7 @@ void shouldRejectCreateOrderWithEmptyPurchaseItems() { PurchaseItems purchaseItems = new PurchaseItems(); CreateOrderException exception = - assertThrows( - CreateOrderException.class, - () -> orderService.createOrder(buyer.getId(), purchaseItems)); + assertThrows(CreateOrderException.class, () -> createOrder(buyer.getId(), purchaseItems)); assertEquals("Purchase items cannot be empty", exception.getMessage()); } @@ -85,9 +82,7 @@ void shouldRejectCreateOrderWithNonExistingProduct() { purchaseItems.setPurchaseItem("non_existing_product_id", 1); CreateOrderException exception = - assertThrows( - CreateOrderException.class, - () -> orderService.createOrder(buyer.getId(), purchaseItems)); + assertThrows(CreateOrderException.class, () -> createOrder(buyer.getId(), purchaseItems)); assertEquals("Product: non_existing_product_id does not exist", exception.getMessage()); } @@ -103,9 +98,7 @@ void shouldRejectCreateOrderWithInsufficientBalance() { purchaseItems.setPurchaseItem(product.getId(), 2); CreateOrderException exception = - assertThrows( - CreateOrderException.class, - () -> orderService.createOrder(buyer.getId(), purchaseItems)); + assertThrows(CreateOrderException.class, () -> createOrder(buyer.getId(), purchaseItems)); assertEquals("Insufficient balance", exception.getMessage()); // product quantity should not be reduced @@ -124,9 +117,7 @@ void shouldRejectOrderWithInsufficientProductQuantity() { purchaseItems.setPurchaseItem(product.getId(), 2); CreateOrderException exception = - assertThrows( - CreateOrderException.class, - () -> orderService.createOrder(buyer.getId(), purchaseItems)); + assertThrows(CreateOrderException.class, () -> createOrder(buyer.getId(), purchaseItems)); assertEquals("Insufficient stock for product: " + product.getId(), exception.getMessage()); // buyer balance should not be reduced @@ -145,7 +136,7 @@ void shouldNotThrowExceptionIfStockAndBuyerBalanceIsJustEnough() { PurchaseItems purchaseItems = new PurchaseItems(); purchaseItems.setPurchaseItem(product.getId(), 1); - orderService.createOrder(buyer.getId(), purchaseItems); // should not throw exception + createOrder(buyer.getId(), purchaseItems); // should not throw exception } @Test @@ -162,7 +153,7 @@ void shouldReduceProductQuantityAndBuyerBalanceWhenOrderIsSuccessful() { purchaseItems.setPurchaseItem(product1.getId(), 2); purchaseItems.setPurchaseItem(product2.getId(), 3); - orderService.createOrder(buyer.getId(), purchaseItems); + createOrder(buyer.getId(), purchaseItems); assertEquals(8, productRepository.findById(product1.getId()).orElseThrow().getQuantity()); assertEquals(7, productRepository.findById(product2.getId()).orElseThrow().getQuantity()); @@ -192,7 +183,7 @@ void shouldIncreaseSellersBalance() { purchaseItems.setPurchaseItem(product1.getId(), 2); purchaseItems.setPurchaseItem(product2.getId(), 3); - orderService.createOrder(buyer.getId(), purchaseItems); + createOrder(buyer.getId(), purchaseItems); assertBigDecimalEquals( new BigDecimal(15), userRepository.findById(seller1.getId()).orElseThrow().getBalance()); @@ -213,7 +204,7 @@ void shouldCreateOrder() { purchaseItems.setPurchaseItem(product1.getId(), 2); purchaseItems.setPurchaseItem(product2.getId(), 5); - Order order = orderService.createOrder(buyer.getId(), purchaseItems); + Order order = createOrder(buyer.getId(), purchaseItems); assertEquals(buyer.getId(), order.getBuyerUserId()); assertEquals( @@ -234,12 +225,32 @@ void shouldEachCreatedOrderHasDifferentId() { PurchaseItems purchaseItems = new PurchaseItems(); purchaseItems.setPurchaseItem(product.getId(), 1); - Order order1 = orderService.createOrder(buyer.getId(), purchaseItems); - Order order2 = orderService.createOrder(buyer.getId(), purchaseItems); + Order order1 = createOrder(buyer.getId(), purchaseItems, "request-01"); + Order order2 = createOrder(buyer.getId(), purchaseItems, "request-02"); assertNotEquals(order1.getId(), order2.getId()); } + @Test + void shouldOneOrderBeingCreatedOnly_IfCreateOrderWithSameRequestIdTwice() { + User buyer = randomUsernameUserBuilder().balance(new BigDecimal(10)).build(); + userRepository.save(buyer); + + Product product = productBuilder().quantity(5).price(new BigDecimal(1)).build(); + productRepository.save(product); + + PurchaseItems purchaseItems = new PurchaseItems(); + purchaseItems.setPurchaseItem(product.getId(), 1); + + Order order1 = createOrder(buyer.getId(), purchaseItems, "request_id"); + Order order2 = createOrder(buyer.getId(), purchaseItems, "request_id"); + + assertOrderEquals(order2, order1); + assertEquals(4, productRepository.findById(product.getId()).orElseThrow().getQuantity()); + assertBigDecimalEquals( + new BigDecimal(9), userRepository.findById(buyer.getId()).orElseThrow().getBalance()); + } + @Test void shouldAutoRetry_WhenOneThreadMayFailJustDueToRacing() { User seller = randomUsernameUserBuilder().balance(new BigDecimal(999)).build(); @@ -254,8 +265,8 @@ void shouldAutoRetry_WhenOneThreadMayFailJustDueToRacing() { purchaseItems.setPurchaseItem(product.getId(), 1); // 2 threads try to buy the same product at the same time - Thread thread1 = new Thread(() -> orderService.createOrder(buyer.getId(), purchaseItems)); - Thread thread2 = new Thread(() -> orderService.createOrder(buyer.getId(), purchaseItems)); + Thread thread1 = new Thread(() -> createOrder(buyer.getId(), purchaseItems, "request-01")); + Thread thread2 = new Thread(() -> createOrder(buyer.getId(), purchaseItems, "request-02")); thread1.start(); thread2.start(); @@ -270,11 +281,20 @@ void shouldAutoRetry_WhenOneThreadMayFailJustDueToRacing() { assertEquals(0, productRepository.findById(product.getId()).orElseThrow().getQuantity()); } + private Order createOrder(String buyerUserId, PurchaseItems purchaseItems) { + return orderService.createOrder(buyerUserId, purchaseItems, "dummy_request_id"); + } + + private Order createOrder(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + return orderService.createOrder(buyerUserId, purchaseItems, requestId); + } + private void assertOrderEquals(Order expected, Order actual) { assertEquals(expected.getId(), actual.getId()); assertEquals(expected.getBuyerUserId(), actual.getBuyerUserId()); assertEquals( expected.getPurchaseItems().getProductIdToQuantity(), actual.getPurchaseItems().getProductIdToQuantity()); + assertEquals(expected.getRequestId(), actual.getRequestId()); } }