From 0a859816fe637fd429780b00f2062a25a5f19f10 Mon Sep 17 00:00:00 2001 From: Leung Cheng Date: Wed, 11 Dec 2024 18:22:25 +0800 Subject: [PATCH 1/3] Add requestId to OrderService interface --- .../domain/order/OrderService.java | 2 +- .../domain/order/OrderServiceTest.java | 39 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) 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..1b88e68 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,7 @@ 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) { User buyer = getUser(buyerUserId).orElseThrow(() -> new CreateOrderException("Buyer does not exist")); 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..dc1fca7 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,8 +225,8 @@ 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); + Order order2 = createOrder(buyer.getId(), purchaseItems); assertNotEquals(order1.getId(), order2.getId()); } @@ -254,8 +245,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)); + Thread thread2 = new Thread(() -> createOrder(buyer.getId(), purchaseItems)); thread1.start(); thread2.start(); @@ -270,6 +261,10 @@ 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 void assertOrderEquals(Order expected, Order actual) { assertEquals(expected.getId(), actual.getId()); assertEquals(expected.getBuyerUserId(), actual.getBuyerUserId()); From c51cbd8e50027b80d57e283edeaca2f25a4ed20a Mon Sep 17 00:00:00 2001 From: Leung Cheng Date: Wed, 11 Dec 2024 19:05:19 +0800 Subject: [PATCH 2/3] Implement requestId --- .../domain/order/Order.java | 11 ++++++- .../domain/order/OrderRepository.java | 6 +++- .../domain/order/OrderService.java | 11 +++++-- .../domain/order/OrderServiceTest.java | 33 ++++++++++++++++--- 4 files changed, 52 insertions(+), 9 deletions(-) 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..6f620f7 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 @@ -2,4 +2,8 @@ import org.springframework.data.repository.CrudRepository; -public interface OrderRepository extends CrudRepository {} +import java.util.Optional; + +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 1b88e68..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 @@ -29,6 +29,11 @@ public CreateOrderException(String message) { @Retryable(noRetryFor = CreateOrderException.class) @Transactional(isolation = Isolation.SERIALIZABLE) 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, String } 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 dc1fca7..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 @@ -225,12 +225,32 @@ void shouldEachCreatedOrderHasDifferentId() { PurchaseItems purchaseItems = new PurchaseItems(); purchaseItems.setPurchaseItem(product.getId(), 1); - Order order1 = createOrder(buyer.getId(), purchaseItems); - Order order2 = 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(); @@ -245,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(() -> createOrder(buyer.getId(), purchaseItems)); - Thread thread2 = new Thread(() -> 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(); @@ -265,11 +285,16 @@ 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()); } } From c31a21321f36e5eda23ef56dab0e1804f83a6e24 Mon Sep 17 00:00:00 2001 From: Leung Cheng Date: Wed, 11 Dec 2024 19:07:45 +0800 Subject: [PATCH 3/3] Fix formatting --- .../spring_simple_backend/domain/order/OrderRepository.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 6f620f7..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,8 +1,7 @@ package com.leungcheng.spring_simple_backend.domain.order; -import org.springframework.data.repository.CrudRepository; - import java.util.Optional; +import org.springframework.data.repository.CrudRepository; public interface OrderRepository extends CrudRepository { Optional findByRequestId(String requestId);