Skip to content

Commit

Permalink
Merge pull request #24 from leung018/create-order-api
Browse files Browse the repository at this point in the history
Create-order-api
  • Loading branch information
leung018 authored Dec 12, 2024
2 parents 544a67d + 9cec48a commit 59bccb7
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.leungcheng.spring_simple_backend.controller;

import com.leungcheng.spring_simple_backend.validation.MyIllegalArgumentException;
import com.leungcheng.spring_simple_backend.validation.ObjectValidator.ObjectValidationException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
Expand All @@ -23,6 +24,12 @@ String usernameAlreadyExistsHandler(UsernameAlreadyExistsException ex) {
return ex.getMessage();
}

@ExceptionHandler(MyIllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
String myIllegalArgumentExceptionHandler(MyIllegalArgumentException ex) {
return ex.getMessage();
}

@ExceptionHandler(ObjectValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
String objectValidationHandler(ObjectValidationException ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.leungcheng.spring_simple_backend.controller;

import com.leungcheng.spring_simple_backend.auth.UserAuthenticatedInfoToken;
import com.leungcheng.spring_simple_backend.domain.order.Order;
import com.leungcheng.spring_simple_backend.domain.order.OrderService;
import com.leungcheng.spring_simple_backend.domain.order.PurchaseItems;
import jakarta.validation.Valid;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
@Autowired private OrderService orderService;

@PostMapping("/orders")
@ResponseStatus(HttpStatus.CREATED)
Order newOrder(
@Valid @RequestBody CreateOrderRequest createOrderRequest,
UserAuthenticatedInfoToken authToken) {
String userId = authToken.getPrincipal();
PurchaseItems purchaseItems = new PurchaseItems();
for (var entry : createOrderRequest.productIdToQuantity().entrySet()) {
purchaseItems.setPurchaseItem(entry.getKey(), entry.getValue());
}
return orderService.createOrder(userId, purchaseItems, createOrderRequest.requestId());
}

public record CreateOrderRequest(String requestId, Map<String, Integer> productIdToQuantity) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.leungcheng.spring_simple_backend.domain.ProductRepository;
import com.leungcheng.spring_simple_backend.domain.User;
import com.leungcheng.spring_simple_backend.domain.UserRepository;
import com.leungcheng.spring_simple_backend.validation.MyIllegalArgumentException;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
Expand All @@ -20,7 +21,14 @@ public class OrderService {
private @Autowired ProductRepository productRepository;
private @Autowired OrderRepository orderRepository;

public static class CreateOrderException extends IllegalArgumentException {
public static class CreateOrderException extends MyIllegalArgumentException {
// Add this static method to reduce duplication because one test in api level is interested in
// this message. But it may not be necessary to move other error messages to this class until we
// need them.
public static String insufficientStockMsg(String productId) {
return "Insufficient stock for product: " + productId;
}

public CreateOrderException(String message) {
super(message);
}
Expand Down Expand Up @@ -84,7 +92,7 @@ private BigDecimal processPurchaseItems(PurchaseItems purchaseItems) {

private void reduceProductStock(Product product, int purchaseQuantity) {
if (purchaseQuantity > product.getQuantity()) {
throw new CreateOrderException("Insufficient stock for product: " + product.getId());
throw new CreateOrderException(CreateOrderException.insufficientStockMsg(product.getId()));
}
int newQuantity = product.getQuantity() - purchaseQuantity;
Product updatedProduct = product.toBuilder().quantity(newQuantity).build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.leungcheng.spring_simple_backend.domain.order;

import com.google.common.collect.ImmutableMap;
import com.leungcheng.spring_simple_backend.validation.MyIllegalArgumentException;
import jakarta.persistence.*;
import java.util.Map;

@Embeddable
public class PurchaseItems {
public static String INVALID_QUANTITY_MSG = "Quantity must be greater than 0";

@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(
name = "purchase_items",
Expand All @@ -16,7 +19,7 @@ public class PurchaseItems {

public void setPurchaseItem(String productId, int quantity) {
if (quantity < 1) {
throw new IllegalArgumentException("Quantity must be greater than 0");
throw new MyIllegalArgumentException(INVALID_QUANTITY_MSG);
}
productIdToQuantity.put(productId, quantity);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.leungcheng.spring_simple_backend.validation;

public class MyIllegalArgumentException extends IllegalArgumentException {
public MyIllegalArgumentException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package com.leungcheng.spring_simple_backend;

import static com.leungcheng.spring_simple_backend.domain.order.PurchaseItems.INVALID_QUANTITY_MSG;
import static com.leungcheng.spring_simple_backend.testutil.CustomAssertions.assertBigDecimalEquals;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import com.google.common.collect.ImmutableMap;
import com.jayway.jsonpath.JsonPath;
import com.leungcheng.spring_simple_backend.domain.Product;
import com.leungcheng.spring_simple_backend.domain.ProductRepository;
import com.leungcheng.spring_simple_backend.domain.User;
import com.leungcheng.spring_simple_backend.domain.UserRepository;
import com.leungcheng.spring_simple_backend.domain.order.OrderService;
import com.leungcheng.spring_simple_backend.validation.ObjectValidator.ObjectValidationException;
import java.math.BigDecimal;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -253,6 +257,70 @@ void shouldGetAccountInfo() throws Exception {
assertBigDecimalEquals(User.INITIAL_BALANCE, actualBalance);
}

@Test
void shouldCreateOrder() throws Exception {
String userId = useNewUserAccessToken();

CreateProductParams productParams = CreateProductParams.sample();

// Product 1
productParams.price = "1";
productParams.quantity = 99;
MvcResult result = createProduct(productParams).andReturn();
String product1Id = JsonPath.read(result.getResponse().getContentAsString(), "$.id");

// Product 2
result = createProduct(productParams).andReturn();
String product2Id = JsonPath.read(result.getResponse().getContentAsString(), "$.id");

// Create Order
CreateOrderParams createOrderParams =
new CreateOrderParams("request-001", ImmutableMap.of(product1Id, 4, product2Id, 5));

createOrder(createOrderParams)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.purchaseItems").exists())
.andExpect(jsonPath("$.purchaseItems.productIdToQuantity." + product1Id).value(4))
.andExpect(jsonPath("$.purchaseItems.productIdToQuantity." + product2Id).value(5))
.andExpect(jsonPath("$.requestId").value("request-001"))
.andExpect(jsonPath("$.buyerUserId").value(userId));
}

@Test
void shouldCreateOrderApiHandleExceptionDueToNegativeQuantity() throws Exception {
useNewUserAccessToken();

CreateProductParams productParams = CreateProductParams.sample();
MvcResult result = createProduct(productParams).andReturn();
String productId = JsonPath.read(result.getResponse().getContentAsString(), "$.id");

CreateOrderParams createOrderParams =
new CreateOrderParams("request-001", ImmutableMap.of(productId, -1));

createOrder(createOrderParams)
.andExpect(status().isBadRequest())
.andExpect(content().string(INVALID_QUANTITY_MSG));
}

@Test
void shouldCreateOrderApiHandleCreateOrderExceptionFromOrderService() throws Exception {
useNewUserAccessToken();

CreateProductParams productParams = CreateProductParams.sample();
productParams.quantity = 0;
MvcResult result = createProduct(productParams).andReturn();
String productId = JsonPath.read(result.getResponse().getContentAsString(), "$.id");

CreateOrderParams createOrderParams =
new CreateOrderParams("request-001", ImmutableMap.of(productId, 1));

createOrder(createOrderParams)
.andExpect(status().isBadRequest())
.andExpect(
content().string(OrderService.CreateOrderException.insufficientStockMsg(productId)));
}

private static class CreateProductParams {
String name;
String price;
Expand Down Expand Up @@ -326,6 +394,37 @@ private ResultActions getAccountInfo() throws Exception {
return mockMvc.perform(builder);
}

private ResultActions createOrder(CreateOrderParams createOrderParams) throws Exception {
MockHttpServletRequestBuilder builder =
post("/orders").contentType("application/json").content(createOrderParams.toContent());
addAuthHeader(builder);
return mockMvc.perform(builder);
}

private static class CreateOrderParams {
String requestId;
ImmutableMap<String, Integer> productIdToQuantity;

CreateOrderParams(String requestId, ImmutableMap<String, Integer> productIdToQuantity) {
this.requestId = requestId;
this.productIdToQuantity = productIdToQuantity;
}

String toContent() {
return "{\"requestId\": \""
+ this.requestId
+ "\", \"productIdToQuantity\": "
+ productIdToQuantityContent()
+ "}";
}

private String productIdToQuantityContent() {
return productIdToQuantity.entrySet().stream()
.map(entry -> "\"" + entry.getKey() + "\": " + entry.getValue())
.collect(Collectors.joining(", ", "{", "}"));
}
}

private void addAuthHeader(MockHttpServletRequestBuilder builder) {
if (isAccessTokenSet()) {
builder.header("Authorization", "Bearer " + this.accessToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.google.common.collect.ImmutableMap;
import com.leungcheng.spring_simple_backend.validation.MyIllegalArgumentException;
import org.junit.jupiter.api.Test;

class PurchaseItemsTest {
Expand Down Expand Up @@ -32,9 +33,9 @@ void shouldNotAllowLessThanZeroQuantity() {
PurchaseItems purchaseItems = new PurchaseItems();

assertThrows(
IllegalArgumentException.class, () -> purchaseItems.setPurchaseItem("product_id", 0));
MyIllegalArgumentException.class, () -> purchaseItems.setPurchaseItem("product_id", 0));
assertThrows(
IllegalArgumentException.class, () -> purchaseItems.setPurchaseItem("product_id", -1));
MyIllegalArgumentException.class, () -> purchaseItems.setPurchaseItem("product_id", -1));

ImmutableMap<String, Integer> map = purchaseItems.getProductIdToQuantity();
assertEquals(0, map.size());
Expand Down

0 comments on commit 59bccb7

Please sign in to comment.