From ebab892e93f5098f2767070b242fb58b551e5d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20M=C3=B6rling?= Date: Fri, 15 May 2026 19:58:33 +0200 Subject: [PATCH] feat: add PATCH /api/admin/orders/{id} for manual tracking entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UpdateTrackingRequest DTO: optional trackingId string (nullable — allows clearing a tracking ID entered incorrectly) - OrderService.updateTracking(orderId, trackingId): finds order, sets trackingId via setter, saves entity — @PreUpdate fires to update the updated_at timestamp automatically - AdminController.PATCH /api/admin/orders/{id}: admin-only endpoint, validates request body with @Valid, returns updated AdminOrderResponse via the existing toAdminResponse() mapper - AdminControllerTest: 5 new tests — shouldReturn403WhenPatchingTrackingWithoutAuth, shouldReturn403WhenPatchingTrackingAsNonAdmin, shouldUpdateTrackingSuccessfully (verifies response id and trackingId), shouldClearTrackingWhenNull (removes trackingId), shouldReturn404WhenOrderNotFoundForTracking --- .../controller/AdminController.java | 9 +++ .../dto/UpdateTrackingRequest.java | 5 ++ .../se/bilhalsning/service/OrderService.java | 8 +++ .../controller/AdminControllerTest.java | 65 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 backend/src/main/java/se/bilhalsning/dto/UpdateTrackingRequest.java diff --git a/backend/src/main/java/se/bilhalsning/controller/AdminController.java b/backend/src/main/java/se/bilhalsning/controller/AdminController.java index 5b3f2a8..3bc1726 100644 --- a/backend/src/main/java/se/bilhalsning/controller/AdminController.java +++ b/backend/src/main/java/se/bilhalsning/controller/AdminController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import se.bilhalsning.dto.AdminOrderResponse; import se.bilhalsning.dto.UpdateStatusRequest; +import se.bilhalsning.dto.UpdateTrackingRequest; import se.bilhalsning.entity.Order; import se.bilhalsning.service.OrderService; @@ -40,6 +41,14 @@ public class AdminController { return ResponseEntity.ok(toAdminResponse(order)); } + @PatchMapping("/orders/{id}") + public ResponseEntity updateTracking( + @PathVariable UUID id, + @Valid @RequestBody UpdateTrackingRequest request) { + Order order = orderService.updateTracking(id, request.trackingId()); + return ResponseEntity.ok(toAdminResponse(order)); + } + private AdminOrderResponse toAdminResponse(Order order) { String email = order.getUser() != null ? order.getUser().getEmail() : ""; return new AdminOrderResponse( diff --git a/backend/src/main/java/se/bilhalsning/dto/UpdateTrackingRequest.java b/backend/src/main/java/se/bilhalsning/dto/UpdateTrackingRequest.java new file mode 100644 index 0000000..1eabfbe --- /dev/null +++ b/backend/src/main/java/se/bilhalsning/dto/UpdateTrackingRequest.java @@ -0,0 +1,5 @@ +package se.bilhalsning.dto; + +public record UpdateTrackingRequest( + String trackingId +) {} diff --git a/backend/src/main/java/se/bilhalsning/service/OrderService.java b/backend/src/main/java/se/bilhalsning/service/OrderService.java index 2591fe4..5e9f7df 100644 --- a/backend/src/main/java/se/bilhalsning/service/OrderService.java +++ b/backend/src/main/java/se/bilhalsning/service/OrderService.java @@ -46,4 +46,12 @@ public class OrderService { order.setStatus(newStatus); return orderRepository.save(order); } + + public Order updateTracking(UUID orderId, String trackingId) { + Order order = orderRepository.findById(orderId) + .orElseThrow(() -> new OrderNotFoundException(orderId)); + + order.setTrackingId(trackingId); + return orderRepository.save(order); + } } diff --git a/backend/src/test/java/se/bilhalsning/controller/AdminControllerTest.java b/backend/src/test/java/se/bilhalsning/controller/AdminControllerTest.java index 0b6a5f6..ec76e08 100644 --- a/backend/src/test/java/se/bilhalsning/controller/AdminControllerTest.java +++ b/backend/src/test/java/se/bilhalsning/controller/AdminControllerTest.java @@ -144,6 +144,71 @@ class AdminControllerTest { .andExpect(status().isNotFound()); } + @Test + void shouldReturn403WhenPatchingTrackingWithoutAuth() throws Exception { + mockMvc.perform(patch("/api/admin/orders/{id}", + "c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"trackingId\":\"PN123456789\"}")) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(username = "test@bilhalsning.se", roles = "USER") + void shouldReturn403WhenPatchingTrackingAsNonAdmin() throws Exception { + mockMvc.perform(patch("/api/admin/orders/{id}", + "c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"trackingId\":\"PN123456789\"}")) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN") + void shouldUpdateTrackingSuccessfully() throws Exception { + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.SENT); + order.setTrackingId("PN123456789"); + + when(orderService.updateTracking(eq(orderId), eq("PN123456789"))).thenReturn(order); + + mockMvc.perform(patch("/api/admin/orders/{id}", orderId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"trackingId\":\"PN123456789\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(orderId.toString())) + .andExpect(jsonPath("$.trackingId").value("PN123456789")); + } + + @Test + @WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN") + void shouldClearTrackingWhenNull() throws Exception { + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + Order order = createOrder(orderId, "ABC123", "test@bilhalsning.se", OrderStatus.SENT); + order.setTrackingId(null); + + when(orderService.updateTracking(eq(orderId), eq(null))).thenReturn(order); + + mockMvc.perform(patch("/api/admin/orders/{id}", orderId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"trackingId\":null}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.trackingId").doesNotExist()); + } + + @Test + @WithMockUser(username = "admin@bilhalsning.se", roles = "ADMIN") + void shouldReturn404WhenOrderNotFoundForTracking() throws Exception { + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + when(orderService.updateTracking(eq(orderId), eq("PN123456789"))) + .thenThrow(new OrderNotFoundException(orderId)); + + mockMvc.perform(patch("/api/admin/orders/{id}", orderId) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"trackingId\":\"PN123456789\"}")) + .andExpect(status().isNotFound()); + } + private Order createOrder(UUID orderId, String plate, String email, OrderStatus status) { User user = new User(); user.setEmail(email);