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

Order-service-request-id #22

Merged
merged 4 commits into from
Dec 11, 2024
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,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;
Expand All @@ -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() {
Expand All @@ -29,4 +34,8 @@ public String getBuyerUserId() {
public PurchaseItems getPurchaseItems() {
return purchaseItems;
}

public String getRequestId() {
return requestId;
}
}
Original file line number Diff line number Diff line change
@@ -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<Order, String> {}
public interface OrderRepository extends CrudRepository<Order, String> {
Optional<Order> findByRequestId(String requestId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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> order = orderRepository.findByRequestId(requestId);
if (order.isPresent()) {
return order.get();
}

User buyer =
getUser(buyerUserId).orElseThrow(() -> new CreateOrderException("Buyer does not exist"));

Expand All @@ -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<User> getUser(String userId) {
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand All @@ -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());
}

Expand All @@ -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());
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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());
Expand Down Expand Up @@ -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());
Expand All @@ -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(
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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());
}
}
Loading