Let customers cancel their own orders
Java · Spring Boot · beginner · modification
Adds self-service cancellation for orders that have not been confirmed yet. `OrderCancellationService.cancel` loads the order, checks that it belongs to the signed-in customer (404 otherwise, the same answer `GET` gives, so order ids are not confirmed), refuses anything but `PENDING` with a 409, and flips the status inside a transaction. The existing `@Version` column turns a race with the warehouse confirming the order into a 409. Unit tests cover the three outcomes and a missing order.
Shop backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security. The existing security configuration requires a signed-in session for every `/api/**` request, checks CSRF tokens (the SPA sends them), and at login stores a `ShopUser(Long customerId, String email)` record as the principal, which `@AuthenticationPrincipal` resolves; `customerId` is the customer's `customers.id`, a `bigint` identity starting at 1, never null. The shop has about 40,000 customers. Existing and unchanged: `OrderRepository extends JpaRepository<Order, Long>`; `OrderStatus` is `PENDING`, `CONFIRMED`, `SHIPPED`, `DELIVERED` or `CANCELLED`; the record `OrderView.from(order)` copies the order's id, status and total. Every writer of `orders` goes through the `Order` entity, so its `@Version` column catches concurrent changes, and an existing `@RestControllerAdvice` answers `ObjectOptimisticLockingFailureException` with 409.
Requirements
- `POST /api/orders/{id}/cancel` lets a signed-in customer cancel one of their own orders while its status is `PENDING`: the status becomes `CANCELLED`, and the response is 200 with the updated `OrderView`.
- An order that does not exist or belongs to another customer answers 404 and changes nothing, the same answer `GET /api/orders/{id}` gives, so the endpoint never confirms that someone else's order exists. An order of their own in any other status answers 409 and changes nothing.
- A pending order holds no payment and no reserved stock yet (both happen when it is confirmed), so cancelling changes only its status.
Files touched
- src/main/java/com/acme/shop/orders/Order.java
- src/main/java/com/acme/shop/orders/OrderController.java
- src/main/java/com/acme/shop/orders/OrderCancellationService.java
- src/test/java/com/acme/shop/orders/OrderCancellationServiceTest.java
--- src/main/java/com/acme/shop/orders/Order.java
return totalCents;
}
+
+ public void cancel() {
+ this.status = OrderStatus.CANCELLED;
+ }
}
--- src/main/java/com/acme/shop/orders/OrderController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
private final OrderRepository orders;
+ private final OrderCancellationService cancellations;
- public OrderController(OrderRepository orders) {
+ public OrderController(OrderRepository orders, OrderCancellationService cancellations) {
this.orders = orders;
+ this.cancellations = cancellations;
}
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
+
+ @PostMapping("/{id}/cancel")
+ public OrderView cancel(@PathVariable long id, @AuthenticationPrincipal ShopUser user) {