diff --git a/backend/src/main/java/se/bilhalsning/controller/OrderController.java b/backend/src/main/java/se/bilhalsning/controller/OrderController.java index 0c93aee..56b9fe7 100644 --- a/backend/src/main/java/se/bilhalsning/controller/OrderController.java +++ b/backend/src/main/java/se/bilhalsning/controller/OrderController.java @@ -7,12 +7,15 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import se.bilhalsning.dto.CreateOrderRequest; import se.bilhalsning.dto.OrderResponse; +import se.bilhalsning.dto.UpdateOrderRequest; import se.bilhalsning.entity.Order; import se.bilhalsning.entity.User; import se.bilhalsning.exception.InvalidCredentialsException; @@ -20,6 +23,7 @@ import se.bilhalsning.service.OrderService; import se.bilhalsning.service.UserService; import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/api/orders") @@ -41,6 +45,21 @@ public class OrderController { return ResponseEntity.ok(orders); } + @GetMapping("/{id}") + public ResponseEntity get(@PathVariable UUID id, + @AuthenticationPrincipal UserDetails userDetails) { + User user = userService.findByEmail(userDetails.getUsername()) + .orElseThrow(InvalidCredentialsException::new); + + Order order = orderService.getOrderById(id); + + if (!order.getUserId().equals(user.getId())) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(toResponse(order)); + } + @PostMapping public ResponseEntity create( @Valid @RequestBody CreateOrderRequest request, @@ -57,6 +76,31 @@ public class OrderController { return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(order)); } + @PatchMapping("/{id}") + public ResponseEntity update( + @PathVariable UUID id, + @Valid @RequestBody UpdateOrderRequest request, + @AuthenticationPrincipal UserDetails userDetails) { + User user = userService.findByEmail(userDetails.getUsername()) + .orElseThrow(InvalidCredentialsException::new); + + Order order = orderService.updatePendingOrder(id, user.getId(), request.letterText()); + + return ResponseEntity.ok(toResponse(order)); + } + + @PostMapping("/{id}/cancel") + public ResponseEntity cancel( + @PathVariable UUID id, + @AuthenticationPrincipal UserDetails userDetails) { + User user = userService.findByEmail(userDetails.getUsername()) + .orElseThrow(InvalidCredentialsException::new); + + Order order = orderService.cancelOrder(id, user.getId()); + + return ResponseEntity.ok(toResponse(order)); + } + private OrderResponse toResponse(Order order) { return new OrderResponse( order.getId(), diff --git a/backend/src/main/java/se/bilhalsning/controller/PaymentController.java b/backend/src/main/java/se/bilhalsning/controller/PaymentController.java index 63712e0..8b215d0 100644 --- a/backend/src/main/java/se/bilhalsning/controller/PaymentController.java +++ b/backend/src/main/java/se/bilhalsning/controller/PaymentController.java @@ -5,6 +5,8 @@ import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -12,28 +14,38 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import se.bilhalsning.dto.OrderResponse; import se.bilhalsning.entity.Order; +import se.bilhalsning.entity.User; +import se.bilhalsning.exception.InvalidCredentialsException; import se.bilhalsning.service.OrderService; +import se.bilhalsning.service.UserService; @RestController @RequestMapping("/api/payment") public class PaymentController { private final OrderService orderService; + private final UserService userService; private final String swishNumber; private final int letterPrice; public PaymentController( OrderService orderService, + UserService userService, @Value("${app.payment.swish-number}") String swishNumber, @Value("${app.payment.letter-price}") int letterPrice) { this.orderService = orderService; + this.userService = userService; this.swishNumber = swishNumber; this.letterPrice = letterPrice; } @PostMapping("/{orderId}/pay") - public ResponseEntity pay(@PathVariable UUID orderId) { - Order order = orderService.confirmPayment(orderId); + public ResponseEntity pay(@PathVariable UUID orderId, + @AuthenticationPrincipal UserDetails userDetails) { + User user = userService.findByEmail(userDetails.getUsername()) + .orElseThrow(InvalidCredentialsException::new); + + Order order = orderService.confirmPayment(orderId, user.getId()); return ResponseEntity.ok(toResponse(order)); } diff --git a/backend/src/main/java/se/bilhalsning/dto/UpdateOrderRequest.java b/backend/src/main/java/se/bilhalsning/dto/UpdateOrderRequest.java new file mode 100644 index 0000000..6b103d7 --- /dev/null +++ b/backend/src/main/java/se/bilhalsning/dto/UpdateOrderRequest.java @@ -0,0 +1,10 @@ +package se.bilhalsning.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UpdateOrderRequest( + @NotBlank(message = "Brevtext krävs") + @Size(min = 1, max = 1000, message = "Brevtexten måste vara mellan 1 och 1000 tecken") + String letterText +) {} diff --git a/backend/src/main/java/se/bilhalsning/entity/OrderStatus.java b/backend/src/main/java/se/bilhalsning/entity/OrderStatus.java index 092a9f5..221fa60 100644 --- a/backend/src/main/java/se/bilhalsning/entity/OrderStatus.java +++ b/backend/src/main/java/se/bilhalsning/entity/OrderStatus.java @@ -6,7 +6,8 @@ public enum OrderStatus { PROCESSING("processing"), SENT("sent"), DELIVERED("delivered"), - FAILED("failed"); + FAILED("failed"), + CANCELLED("cancelled"); private final String value; diff --git a/backend/src/main/java/se/bilhalsning/exception/GlobalExceptionHandler.java b/backend/src/main/java/se/bilhalsning/exception/GlobalExceptionHandler.java index 5fd4054..1e6d4e0 100644 --- a/backend/src/main/java/se/bilhalsning/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/se/bilhalsning/exception/GlobalExceptionHandler.java @@ -36,6 +36,13 @@ public class GlobalExceptionHandler { .body(new ErrorResponse("E-postadressen är redan registrerad")); } + @ExceptionHandler(InvalidOrderStateException.class) + public ResponseEntity handleInvalidOrderState(InvalidOrderStateException ex) { + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(new ErrorResponse(ex.getMessage())); + } + @ExceptionHandler(OrderNotFoundException.class) public ResponseEntity handleOrderNotFound(OrderNotFoundException ex) { return ResponseEntity diff --git a/backend/src/main/java/se/bilhalsning/exception/InvalidOrderStateException.java b/backend/src/main/java/se/bilhalsning/exception/InvalidOrderStateException.java new file mode 100644 index 0000000..3115a11 --- /dev/null +++ b/backend/src/main/java/se/bilhalsning/exception/InvalidOrderStateException.java @@ -0,0 +1,7 @@ +package se.bilhalsning.exception; + +public class InvalidOrderStateException extends RuntimeException { + public InvalidOrderStateException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/se/bilhalsning/service/OrderService.java b/backend/src/main/java/se/bilhalsning/service/OrderService.java index e0fc3b4..a4a6d68 100644 --- a/backend/src/main/java/se/bilhalsning/service/OrderService.java +++ b/backend/src/main/java/se/bilhalsning/service/OrderService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import se.bilhalsning.entity.Order; import se.bilhalsning.entity.OrderStatus; +import se.bilhalsning.exception.InvalidOrderStateException; import se.bilhalsning.exception.OrderNotFoundException; import se.bilhalsning.repository.OrderRepository; @@ -55,11 +56,37 @@ public class OrderService { return orderRepository.save(order); } - public Order confirmPayment(UUID orderId) { - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new OrderNotFoundException(orderId)); - + public Order confirmPayment(UUID orderId, UUID userId) { + Order order = requirePendingOwnedBy(orderId, userId); order.setStatus(OrderStatus.PROCESSING); return orderRepository.save(order); } + + public Order cancelOrder(UUID orderId, UUID userId) { + Order order = requirePendingOwnedBy(orderId, userId); + order.setStatus(OrderStatus.CANCELLED); + return orderRepository.save(order); + } + + public Order updatePendingOrder(UUID orderId, UUID userId, String letterText) { + Order order = requirePendingOwnedBy(orderId, userId); + order.setLetterText(letterText); + return orderRepository.save(order); + } + + private Order requirePendingOwnedBy(UUID orderId, UUID userId) { + Order order = orderRepository.findById(orderId) + .orElseThrow(() -> new OrderNotFoundException(orderId)); + + if (!order.getUserId().equals(userId)) { + throw new OrderNotFoundException(orderId); + } + + if (order.getStatus() != OrderStatus.PENDING_PAYMENT) { + throw new InvalidOrderStateException( + "Beställningen kan inte ändras i detta tillstånd"); + } + + return order; + } } diff --git a/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java b/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java index 53e9fe5..a42e9ab 100644 --- a/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java +++ b/backend/src/test/java/se/bilhalsning/controller/OrderControllerTest.java @@ -1,7 +1,9 @@ package se.bilhalsning.controller; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -163,4 +165,115 @@ class OrderControllerTest { .content("{\"plate\":\"ABC123\",\"letterText\":\"" + longText + "\"}")) .andExpect(status().isBadRequest()); } + + @Test + @WithMockUser(username = "test@bilhej.se") + void shouldGetSingleOrderForOwner() throws Exception { + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhej.se"); + + when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); + + se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); + order.setId(orderId); + order.setUserId(userId); + order.setPlate("ABC123"); + order.setLetterText("Test letter"); + order.setStatus(se.bilhalsning.entity.OrderStatus.PENDING_PAYMENT); + + when(orderService.getOrderById(orderId)).thenReturn(order); + + mockMvc.perform(get("/api/orders/" + orderId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(orderId.toString())) + .andExpect(jsonPath("$.plate").value("ABC123")) + .andExpect(jsonPath("$.status").value("pending_payment")); + } + + @Test + @WithMockUser(username = "test@bilhej.se") + void shouldReturn404WhenGettingOtherUsersOrder() throws Exception { + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhej.se"); + + when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); + + se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); + order.setId(orderId); + order.setUserId(UUID.randomUUID()); + + when(orderService.getOrderById(orderId)).thenReturn(order); + + mockMvc.perform(get("/api/orders/" + orderId)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(username = "test@bilhej.se") + void shouldPatchOrderSuccessfully() throws Exception { + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhej.se"); + + when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); + + se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); + order.setId(orderId); + order.setUserId(userId); + order.setPlate("ABC123"); + order.setLetterText("Updated text"); + order.setStatus(se.bilhalsning.entity.OrderStatus.PENDING_PAYMENT); + + when(orderService.updatePendingOrder(any(), any(), any())).thenReturn(order); + + mockMvc.perform(patch("/api/orders/" + orderId) + .contentType("application/json") + .content("{\"letterText\":\"Updated text\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.letterText").value("Updated text")); + } + + @Test + @WithMockUser(username = "test@bilhej.se") + void shouldRejectPatchWithEmptyLetterText() throws Exception { + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + + mockMvc.perform(patch("/api/orders/" + orderId) + .contentType("application/json") + .content("{\"letterText\":\"\"}")) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(username = "test@bilhej.se") + void shouldCancelOrderSuccessfully() throws Exception { + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhej.se"); + + when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); + + se.bilhalsning.entity.Order order = new se.bilhalsning.entity.Order(); + order.setId(orderId); + order.setUserId(userId); + order.setPlate("ABC123"); + order.setLetterText("Test letter"); + order.setStatus(se.bilhalsning.entity.OrderStatus.CANCELLED); + + when(orderService.cancelOrder(orderId, userId)).thenReturn(order); + + mockMvc.perform(post("/api/orders/" + orderId + "/cancel")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("cancelled")); + } } diff --git a/backend/src/test/java/se/bilhalsning/controller/PaymentControllerTest.java b/backend/src/test/java/se/bilhalsning/controller/PaymentControllerTest.java index 5d5dd22..be187ad 100644 --- a/backend/src/test/java/se/bilhalsning/controller/PaymentControllerTest.java +++ b/backend/src/test/java/se/bilhalsning/controller/PaymentControllerTest.java @@ -1,5 +1,6 @@ package se.bilhalsning.controller; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -7,6 +8,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,8 +20,10 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import se.bilhalsning.entity.Order; import se.bilhalsning.entity.OrderStatus; +import se.bilhalsning.entity.User; import se.bilhalsning.exception.OrderNotFoundException; import se.bilhalsning.service.OrderService; +import se.bilhalsning.service.UserService; @SpringBootTest @AutoConfigureMockMvc @@ -31,6 +35,9 @@ class PaymentControllerTest { @MockitoBean private OrderService orderService; + @MockitoBean + private UserService userService; + @Test void shouldReturn403WhenNotAuthenticated() throws Exception { mockMvc.perform(post("/api/payment/{orderId}/pay", @@ -42,12 +49,19 @@ class PaymentControllerTest { @WithMockUser(username = "test@bilhej.se") void shouldConfirmPaymentSuccessfully() throws Exception { UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhej.se"); + + when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); + Order order = new Order(); order.setId(orderId); order.setPlate("ABC123"); order.setStatus(OrderStatus.PROCESSING); - when(orderService.confirmPayment(eq(orderId))).thenReturn(order); + when(orderService.confirmPayment(eq(orderId), eq(userId))).thenReturn(order); mockMvc.perform(post("/api/payment/{orderId}/pay", orderId) .contentType(MediaType.APPLICATION_JSON)) @@ -60,7 +74,14 @@ class PaymentControllerTest { @WithMockUser(username = "test@bilhej.se") void shouldReturn404WhenOrderNotFound() throws Exception { UUID orderId = UUID.fromString("c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); - when(orderService.confirmPayment(eq(orderId))) + UUID userId = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"); + User user = new User(); + user.setId(userId); + user.setEmail("test@bilhej.se"); + + when(userService.findByEmail("test@bilhej.se")).thenReturn(Optional.of(user)); + + when(orderService.confirmPayment(eq(orderId), eq(userId))) .thenThrow(new OrderNotFoundException(orderId)); mockMvc.perform(post("/api/payment/{orderId}/pay", orderId) diff --git a/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java b/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java index 32fbb68..3a88972 100644 --- a/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java +++ b/backend/src/test/java/se/bilhalsning/service/OrderServiceTest.java @@ -9,6 +9,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import se.bilhalsning.entity.Order; import se.bilhalsning.entity.OrderStatus; +import se.bilhalsning.exception.InvalidOrderStateException; import se.bilhalsning.exception.OrderNotFoundException; import se.bilhalsning.repository.OrderRepository; @@ -127,4 +128,125 @@ class OrderServiceTest { assertThrows(OrderNotFoundException.class, () -> orderService.getOrderById(orderId)); } + + @Test + void shouldCancelOrderWhenPendingPayment() { + UUID orderId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(userId); + order.setStatus(OrderStatus.PENDING_PAYMENT); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.cancelOrder(orderId, userId); + + assertEquals(OrderStatus.CANCELLED, result.getStatus()); + } + + @Test + void shouldThrowWhenCancellingNonPendingOrder() { + UUID orderId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(userId); + order.setStatus(OrderStatus.PROCESSING); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + + assertThrows(InvalidOrderStateException.class, + () -> orderService.cancelOrder(orderId, userId)); + } + + @Test + void shouldThrowWhenCancellingOtherUsersOrder() { + UUID orderId = UUID.randomUUID(); + UUID ownerId = UUID.randomUUID(); + UUID otherUserId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(ownerId); + order.setStatus(OrderStatus.PENDING_PAYMENT); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + + assertThrows(OrderNotFoundException.class, + () -> orderService.cancelOrder(orderId, otherUserId)); + } + + @Test + void shouldUpdatePendingOrderLetterText() { + UUID orderId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(userId); + order.setStatus(OrderStatus.PENDING_PAYMENT); + order.setLetterText("Old text"); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.updatePendingOrder(orderId, userId, "New text"); + + assertEquals("New text", result.getLetterText()); + } + + @Test + void shouldThrowWhenUpdatingNonPendingOrder() { + UUID orderId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(userId); + order.setStatus(OrderStatus.PROCESSING); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + + assertThrows(InvalidOrderStateException.class, + () -> orderService.updatePendingOrder(orderId, userId, "New text")); + } + + @Test + void shouldConfirmPaymentForPendingOrder() { + UUID orderId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(userId); + order.setStatus(OrderStatus.PENDING_PAYMENT); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0)); + + Order result = orderService.confirmPayment(orderId, userId); + + assertEquals(OrderStatus.PROCESSING, result.getStatus()); + } + + @Test + void shouldThrowWhenConfirmingPaymentForNonPendingOrder() { + UUID orderId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(userId); + order.setStatus(OrderStatus.CANCELLED); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + + assertThrows(InvalidOrderStateException.class, + () -> orderService.confirmPayment(orderId, userId)); + } + + @Test + void shouldThrowWhenConfirmingPaymentForOtherUsersOrder() { + UUID orderId = UUID.randomUUID(); + UUID ownerId = UUID.randomUUID(); + UUID otherUserId = UUID.randomUUID(); + Order order = new Order(); + order.setId(orderId); + order.setUserId(ownerId); + order.setStatus(OrderStatus.PENDING_PAYMENT); + when(orderRepository.findById(orderId)).thenReturn(Optional.of(order)); + + assertThrows(OrderNotFoundException.class, + () -> orderService.confirmPayment(orderId, otherUserId)); + } } diff --git a/frontend/e2e/deferred-payment-admin.spec.ts b/frontend/e2e/deferred-payment-admin.spec.ts index de5aa64..29bb593 100644 --- a/frontend/e2e/deferred-payment-admin.spec.ts +++ b/frontend/e2e/deferred-payment-admin.spec.ts @@ -59,15 +59,15 @@ test.describe('Deferred payment and admin lookup', () => { const orderCard = page.locator('.orders__card', { hasText: orderId }) await expect(orderCard.getByText(plate)).toBeVisible() await expect(orderCard.locator('.badge')).toHaveText('Väntar på betalning') - await expect(orderCard.getByRole('link', { name: 'Betala nu' })).toBeVisible() + await expect(orderCard.getByRole('link', { name: 'Betala 49 kr' })).toBeVisible() - await orderCard.getByRole('link', { name: 'Betala nu' }).click() + await orderCard.getByRole('link', { name: 'Betala 49 kr' }).click() await expect(page).toHaveURL(new RegExp(`/betalning/${orderId}`)) await completeSwishPayment(page) await expect(page).toHaveURL('/orders') await expect(orderCard.locator('.badge')).toHaveText('Hanteras') - await expect(orderCard.getByRole('link', { name: 'Betala nu' })).not.toBeVisible() + await expect(orderCard.getByRole('link', { name: 'Betala 49 kr' })).not.toBeVisible() }) test('admin finds paid order under Att göra when searching partial order id', async ({ diff --git a/frontend/e2e/order-history.spec.ts b/frontend/e2e/order-history.spec.ts index c692752..89b6750 100644 --- a/frontend/e2e/order-history.spec.ts +++ b/frontend/e2e/order-history.spec.ts @@ -66,8 +66,8 @@ test.describe('Order history', () => { await page.goto('/orders') const unpaidCard = page.locator('.orders__card', { hasText: 'DEF456' }) - await expect(unpaidCard.getByRole('link', { name: 'Betala nu' })).toBeVisible() - await unpaidCard.getByRole('link', { name: 'Betala nu' }).click() + await expect(unpaidCard.getByRole('link', { name: 'Betala 49 kr' })).toBeVisible() + await unpaidCard.getByRole('link', { name: 'Betala 49 kr' }).click() await expect(page).toHaveURL(/\/betalning\/c2eebc99/) await expect(page.getByRole('heading', { name: 'Betalning' })).toBeVisible() diff --git a/frontend/src/__tests__/EditOrderPage.spec.ts b/frontend/src/__tests__/EditOrderPage.spec.ts new file mode 100644 index 0000000..2ae5e0c --- /dev/null +++ b/frontend/src/__tests__/EditOrderPage.spec.ts @@ -0,0 +1,149 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { createRouter, createMemoryHistory } from 'vue-router' +import EditOrderPage from '@/pages/EditOrderPage.vue' +import PaymentRedirect from '@/pages/PaymentRedirect.vue' + +vi.mock('@/api/orders', () => ({ + fetchOrder: vi.fn(), + updateOrder: vi.fn(), +})) + +import { fetchOrder, updateOrder } from '@/api/orders' + +const mockFetchOrder = vi.mocked(fetchOrder) +const mockUpdateOrder = vi.mocked(updateOrder) + +const pendingOrder = { + id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', + plate: 'DEF456', + letterText: 'Vill köpa din bil.', + status: 'pending_payment', + trackingId: null, + amountPaid: null, + createdAt: '2026-05-14T13:00:00Z', +} + +function createTestRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/orders', + name: 'orders', + component: { template: '
Orders
' }, + }, + { + path: '/bestallning/:orderId/redigera', + name: 'edit-order', + component: EditOrderPage, + }, + { + path: '/betalning/:orderId', + name: 'payment', + component: PaymentRedirect, + }, + ], + }) +} + +async function mountPage(orderId = pendingOrder.id) { + const pinia = createPinia() + setActivePinia(pinia) + + const router = createTestRouter() + await router.push({ name: 'edit-order', params: { orderId } }) + await router.isReady() + + const wrapper = mount(EditOrderPage, { + global: { + plugins: [router, pinia], + }, + }) + + return { wrapper, router } +} + +describe('EditOrderPage', () => { + beforeEach(() => { + vi.clearAllMocks() + mockFetchOrder.mockResolvedValue(pendingOrder) + mockUpdateOrder.mockResolvedValue(pendingOrder) + }) + + it('shows loading state while fetching', async () => { + mockFetchOrder.mockImplementation(() => new Promise(() => {})) + const { wrapper } = await mountPage() + expect(wrapper.text()).toContain('Laddar beställning...') + }) + + it('loads order and pre-fills textarea', async () => { + const { wrapper } = await mountPage() + await vi.waitFor(() => { + expect(mockFetchOrder).toHaveBeenCalledWith(pendingOrder.id) + }) + + const textarea = wrapper.find('textarea') + expect(textarea.element.value).toBe('Vill köpa din bil.') + expect(wrapper.text()).toContain('DEF456') + expect(wrapper.text()).toContain('Redigera brev') + }) + + it('shows error when order is not pending_payment', async () => { + mockFetchOrder.mockResolvedValue({ + ...pendingOrder, + status: 'sent', + }) + + const { wrapper } = await mountPage() + await vi.waitFor(() => { + expect(wrapper.text()).toContain( + 'Den här beställningen kan inte redigeras', + ) + }) + + expect(wrapper.find('textarea').exists()).toBe(false) + expect(wrapper.text()).toContain('Tillbaka till beställningar') + }) + + it('submit calls updateOrder and navigates to payment', async () => { + const { wrapper, router } = await mountPage() + await vi.waitFor(() => { + expect(wrapper.find('textarea').exists()).toBe(true) + }) + + const textarea = wrapper.find('textarea') + await textarea.setValue('Uppdaterat meddelande') + const button = wrapper.find('button[type="submit"]') + await button.trigger('submit') + + await vi.waitFor(() => { + expect(mockUpdateOrder).toHaveBeenCalledWith( + pendingOrder.id, + 'Uppdaterat meddelande', + ) + expect(router.currentRoute.value.name).toBe('payment') + expect(router.currentRoute.value.params.orderId).toBe(pendingOrder.id) + expect(router.currentRoute.value.query.plate).toBe('DEF456') + }) + }) + + it('shows error message on update failure', async () => { + mockUpdateOrder.mockRejectedValue(new Error('Network error')) + + const { wrapper } = await mountPage() + await vi.waitFor(() => { + expect(wrapper.find('textarea').exists()).toBe(true) + }) + + const textarea = wrapper.find('textarea') + await textarea.setValue('Uppdaterat meddelande') + const button = wrapper.find('button[type="submit"]') + await button.trigger('submit') + + await vi.waitFor(() => { + expect(wrapper.text()).toContain('Kunde inte spara ändringarna') + }) + }) +}) diff --git a/frontend/src/__tests__/OrdersPage.spec.ts b/frontend/src/__tests__/OrdersPage.spec.ts index a57a73c..1c87843 100644 --- a/frontend/src/__tests__/OrdersPage.spec.ts +++ b/frontend/src/__tests__/OrdersPage.spec.ts @@ -22,6 +22,11 @@ function createTestRouter() { name: 'payment', component: { template: '
Payment
' }, }, + { + path: '/bestallning/:orderId/redigera', + name: 'edit-order', + component: { template: '
Edit
' }, + }, { path: '/', name: 'home', component: { template: '
Home
' } }, ], }) @@ -58,13 +63,39 @@ const mockOrders = [ }, ] +function mockOrdersFetch(orders: unknown) { + vi.mocked(globalThis.fetch).mockImplementation((url, init) => { + const urlStr = String(url) + const method = init?.method ?? 'GET' + + if (urlStr.includes('/payment/swish-info')) { + return mockFetchResponse(200, { number: '1234567890', amount: 49 }) + } + + if (urlStr.includes('/cancel') && method === 'POST') { + return mockFetchResponse(200, { + id: 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', + plate: 'DEF456', + letterText: 'Vill köpa din bil.', + status: 'cancelled', + trackingId: null, + createdAt: '2026-05-14T13:00:00Z', + }) + } + + if (urlStr.includes('/orders')) { + return mockFetchResponse(200, orders) + } + + return mockFetchResponse(404, { message: 'Not found' }) + }) +} + describe('OrdersPage', () => { beforeEach(() => { localStorage.clear() globalThis.fetch = vi.fn() - vi.mocked(globalThis.fetch).mockResolvedValue( - mockFetchResponse(200, mockOrders), - ) + mockOrdersFetch(mockOrders) }) it('renders heading and subtitle', async () => { @@ -82,6 +113,13 @@ describe('OrdersPage', () => { expect(wrapper.text()).toContain('Laddar beställningar...') }) + it('shows section headings when pending and completed orders exist', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + expect(wrapper.text()).toContain('Obetalda beställningar') + expect(wrapper.text()).toContain('Tidigare beställningar') + }) + it('fetches orders from API on mount', async () => { mountPage() await new Promise((r) => setTimeout(r, 50)) @@ -110,7 +148,9 @@ describe('OrdersPage', () => { await new Promise((r) => setTimeout(r, 50)) const link = wrapper.find('a[href*="postnord"]') expect(link.exists()).toBe(true) + expect(link.classes()).toContain('orders__tracking-btn') expect(link.text()).toContain('PN123456789') + expect(link.text()).toContain('Spåra brev') expect(link.attributes('target')).toBe('_blank') }) @@ -125,9 +165,7 @@ describe('OrdersPage', () => { createdAt: '2026-05-14T13:00:00Z', }, ] - vi.mocked(globalThis.fetch).mockResolvedValue( - mockFetchResponse(200, ordersWithoutTracking), - ) + mockOrdersFetch(ordersWithoutTracking) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const link = wrapper.find('a[href*="postnord"]') @@ -137,9 +175,8 @@ describe('OrdersPage', () => { it('renders order id and message', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) - expect(wrapper.text()).toContain('Beställnings-ID') expect(wrapper.text()).toContain('c1eebc99-9c0b-4ef8-bb6d-6bb9bd380a11') - expect(wrapper.text()).toContain('Meddelande') + expect(wrapper.text()).toContain('Beställnings-ID') expect(wrapper.text()).toContain('Hej fin bil!') expect(wrapper.text()).toContain('Vill köpa din bil.') }) @@ -148,19 +185,24 @@ describe('OrdersPage', () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('2026') + expect(wrapper.text()).toContain('Skapad') }) it('shows empty state when no orders', async () => { - vi.mocked(globalThis.fetch).mockResolvedValue(mockFetchResponse(200, [])) + mockOrdersFetch([]) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Inga beställningar ännu') }) it('shows error state on API failure', async () => { - vi.mocked(globalThis.fetch).mockResolvedValue( - mockFetchResponse(500, { message: 'Internal server error' }), - ) + vi.mocked(globalThis.fetch).mockImplementation((url) => { + const urlStr = String(url) + if (urlStr.includes('/payment/swish-info')) { + return mockFetchResponse(200, { number: '1234567890', amount: 49 }) + } + return mockFetchResponse(500, { message: 'Internal server error' }) + }) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Kunde inte hämta beställningar') @@ -170,19 +212,35 @@ describe('OrdersPage', () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) const badges = wrapper.findAll('.badge') - expect(badges[0].classes()).toContain('badge--success') - expect(badges[1].classes()).toContain('badge--muted') + expect(badges[0].classes()).toContain('badge--warning') + expect(badges[1].classes()).toContain('badge--success') + }) + + it('shows order id on pending payment orders', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const pendingCard = wrapper + .findAll('.orders__card') + .find((card) => card.text().includes('DEF456')) + expect(pendingCard?.text()).toContain('Beställnings-ID') + expect(pendingCard?.text()).toContain( + 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', + ) }) it('shows pay button only for pending payment orders', async () => { const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) - const payLinks = wrapper.findAll('.orders__pay-btn') - expect(payLinks).toHaveLength(1) - expect(payLinks[0].text()).toBe('Betala nu') + const pendingCard = wrapper + .findAll('.orders__card') + .find((card) => card.text().includes('DEF456')) + const payLink = pendingCard?.find('a.orders__pay-btn') + expect(payLink?.exists()).toBe(true) + expect(payLink?.text()).toBe('Betala 49 kr') - const href = payLinks[0].attributes('href') + const href = payLink?.attributes('href') expect(href).toContain('c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12') expect(href).toContain('plate=DEF456') }) @@ -194,7 +252,71 @@ describe('OrdersPage', () => { const sentCard = wrapper .findAll('.orders__card') .find((card) => card.text().includes('ABC123')) - expect(sentCard?.find('.orders__pay-btn').exists()).toBe(false) + expect(sentCard?.find('a.orders__pay-btn').exists()).toBe(false) + }) + + it('shows edit link for pending payment orders', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const pendingCard = wrapper + .findAll('.orders__card') + .find((card) => card.text().includes('DEF456')) + const editLink = pendingCard?.find('a.orders__edit-btn') + expect(editLink?.exists()).toBe(true) + expect(editLink?.text()).toBe('Redigera brev') + + const href = editLink?.attributes('href') + expect(href).toContain('c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12') + expect(href).toContain('redigera') + }) + + it('shows cancel button for pending payment orders', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const pendingCard = wrapper + .findAll('.orders__card') + .find((card) => card.text().includes('DEF456')) + const cancelBtn = pendingCard?.find('.orders__cancel-btn') + expect(cancelBtn?.exists()).toBe(true) + expect(cancelBtn?.text()).toBe('Avbryt beställning') + }) + + it('calls cancel API and updates status to Avbruten', async () => { + vi.stubGlobal( + 'confirm', + vi.fn(() => true), + ) + + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const pendingCard = wrapper + .findAll('.orders__card') + .find((card) => card.text().includes('DEF456')) + await pendingCard?.find('.orders__cancel-btn').trigger('click') + await new Promise((r) => setTimeout(r, 50)) + + expect(globalThis.fetch).toHaveBeenCalledWith( + '/api/orders/c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a12/cancel', + expect.objectContaining({ method: 'POST' }), + ) + expect(wrapper.text()).toContain('Avbruten') + + vi.unstubAllGlobals() + }) + + it('does not show edit or cancel actions for non-pending orders', async () => { + const { wrapper } = mountPage() + await new Promise((r) => setTimeout(r, 50)) + + const sentCard = wrapper + .findAll('.orders__card') + .find((card) => card.text().includes('ABC123')) + expect(sentCard?.find('.orders__cancel-btn').exists()).toBe(false) + expect(sentCard?.text()).not.toContain('Redigera brev') + expect(sentCard?.text()).not.toContain('Avbryt beställning') }) it('renders processing status correctly', async () => { @@ -208,9 +330,7 @@ describe('OrdersPage', () => { createdAt: '2026-05-15T10:00:00Z', }, ] - vi.mocked(globalThis.fetch).mockResolvedValue( - mockFetchResponse(200, ordersWithProcessing), - ) + mockOrdersFetch(ordersWithProcessing) const { wrapper } = mountPage() await new Promise((r) => setTimeout(r, 50)) expect(wrapper.text()).toContain('Hanteras') diff --git a/frontend/src/api/orders.ts b/frontend/src/api/orders.ts index 2dd34d8..e160a8b 100644 --- a/frontend/src/api/orders.ts +++ b/frontend/src/api/orders.ts @@ -14,9 +14,26 @@ export function fetchOrders(): Promise { return request('/orders') } +export function fetchOrder(id: string): Promise { + return request(`/orders/${id}`) +} + export function createOrder(plate: string, letterText: string): Promise { return request('/orders', { method: 'POST', body: JSON.stringify({ plate, letterText }), }) } + +export function updateOrder(id: string, letterText: string): Promise { + return request(`/orders/${id}`, { + method: 'PATCH', + body: JSON.stringify({ letterText }), + }) +} + +export function cancelOrder(id: string): Promise { + return request(`/orders/${id}/cancel`, { + method: 'POST', + }) +} diff --git a/frontend/src/pages/AdminPage.vue b/frontend/src/pages/AdminPage.vue index 9fab0b0..902291a 100644 --- a/frontend/src/pages/AdminPage.vue +++ b/frontend/src/pages/AdminPage.vue @@ -27,6 +27,7 @@ const statusLabels: Record = { sent: 'Skickat', delivered: 'Levererat', failed: 'Misslyckad', + cancelled: 'Avbruten', } const statusBadge: Record = { @@ -36,6 +37,7 @@ const statusBadge: Record = { sent: 'badge--success', delivered: 'badge--success', failed: 'badge--danger', + cancelled: 'badge--muted', } const allStatuses = [ @@ -45,6 +47,7 @@ const allStatuses = [ 'sent', 'delivered', 'failed', + 'cancelled', ] const stats = computed(() => { diff --git a/frontend/src/pages/EditOrderPage.vue b/frontend/src/pages/EditOrderPage.vue new file mode 100644 index 0000000..66b5ade --- /dev/null +++ b/frontend/src/pages/EditOrderPage.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/frontend/src/pages/OrdersPage.vue b/frontend/src/pages/OrdersPage.vue index 7689165..b23bec4 100644 --- a/frontend/src/pages/OrdersPage.vue +++ b/frontend/src/pages/OrdersPage.vue @@ -1,11 +1,25 @@ @@ -153,6 +281,23 @@ onMounted(async () => { color: var(--color-muted); } +.orders__action-error { + margin-bottom: var(--space-md); +} + +.orders__section + .orders__section { + margin-top: var(--space-xl); +} + +.orders__section-title { + margin: 0 0 var(--space-md) 0; + font-size: 0.875rem; + font-weight: 600; + color: var(--color-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + .orders__list { display: flex; flex-direction: column; @@ -164,76 +309,191 @@ onMounted(async () => { border: 1px solid var(--color-border); border-radius: var(--radius-lg); overflow: hidden; - box-shadow: var(--shadow-card); + box-shadow: var(--shadow-md); } -.orders__card-top { +.orders__card-content { + padding: var(--space-lg); +} + +.orders__card--pending .orders__order-date { + margin-bottom: var(--space-md); +} + +.orders__card-head { display: flex; justify-content: space-between; align-items: center; - padding: var(--space-md) var(--space-lg); - background: var(--color-border-light); - border-bottom: 1px solid var(--color-border); + gap: var(--space-md); + margin-bottom: var(--space-md); } -.orders__plate { - font-size: 1.125rem; +.orders__plate-badge { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + background: var(--color-primary-soft); + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-full); + margin: 0; +} + +.orders__plate-label { + font-size: 0.75rem; font-weight: 600; + text-transform: uppercase; + color: var(--color-muted); +} + +.orders__plate-value { + font-size: 0.9375rem; + font-weight: 700; + letter-spacing: 0.08em; + color: var(--color-primary-dark); +} + +.orders__preview-box { + background: var(--color-border-light); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-md); + margin-bottom: var(--space-sm); +} + +.orders__preview { + margin: 0; + font-family: var(--font-serif); + font-size: 0.9375rem; + line-height: 1.6; color: var(--color-ink); + white-space: pre-wrap; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; +} + +.orders__order-date { + margin: 0 0 var(--space-sm) 0; + font-size: 0.8125rem; + color: var(--color-muted); +} + +.orders__tracking-btn { + width: 100%; + justify-content: center; + margin: var(--space-md) 0 var(--space-sm); + font-size: 0.9375rem; + font-weight: 600; + color: var(--color-primary-dark); + border-color: #ddd6fe; + background: var(--color-primary-soft); +} + +.orders__tracking-btn:hover { + background: #dbeafe; + border-color: #93c5fd; +} + +.orders__order-ref { + margin: var(--space-md) 0 0; + padding-top: var(--space-md); + border-top: 1px solid var(--color-border); +} + +.orders__order-ref--highlight { + margin: 0 0 var(--space-md); + padding: var(--space-md); + background: var(--color-border-light); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.orders__order-ref-label { + margin: 0 0 var(--space-xs) 0; + font-size: 0.75rem; + font-weight: 600; + color: var(--color-muted); + text-transform: uppercase; letter-spacing: 0.05em; } -.orders__card-meta { - padding: var(--space-md) var(--space-lg); - display: grid; - grid-template-columns: auto 1fr; - gap: var(--space-sm) var(--space-lg); - align-items: center; -} - -.orders__meta-label { - font-size: 0.8125rem; - color: var(--color-soft); - font-weight: 500; - white-space: nowrap; -} - -.orders__meta-value { - font-size: 0.875rem; - color: var(--color-ink); -} - .orders__order-id { + margin: 0; font-family: ui-monospace, monospace; font-size: 0.8125rem; + color: var(--color-ink); word-break: break-all; -} - -.orders__message { - white-space: pre-wrap; line-height: 1.5; } -.orders__tracking { +.orders__price-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-md); + padding-bottom: var(--space-md); + border-bottom: 1px solid var(--color-border); +} + +.orders__price-label { font-size: 0.875rem; - color: var(--color-primary); - text-decoration: none; - font-weight: 500; + color: var(--color-muted); } -.orders__tracking:hover { - text-decoration: underline; -} - -.orders__card-actions { - padding: var(--space-md) var(--space-lg); - border-top: 1px solid var(--color-border); - background: var(--color-border-light); +.orders__price-amount { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-ink); } .orders__pay-btn { width: 100%; justify-content: center; + margin-bottom: var(--space-md); +} + +.orders__links { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.orders__text-link { + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-primary); + text-decoration: none; + background: none; + border: none; + padding: 0; + cursor: pointer; +} + +.orders__text-link:hover:not(:disabled) { + text-decoration: underline; + text-underline-offset: 2px; +} + +.orders__text-link--danger { + color: var(--color-danger); +} + +.orders__text-link--danger:hover:not(:disabled) { + color: #991b1b; +} + +.orders__text-link:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.orders__link-sep { + color: var(--color-soft); + user-select: none; } .orders__empty { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1ac2844..6ef68a6 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -9,6 +9,7 @@ import ForgotPasswordPage from '@/pages/ForgotPasswordPage.vue' import ResetPasswordPage from '@/pages/ResetPasswordPage.vue' import ChangePasswordPage from '@/pages/ChangePasswordPage.vue' import OrdersPage from '@/pages/OrdersPage.vue' +import EditOrderPage from '@/pages/EditOrderPage.vue' import AdminPage from '@/pages/AdminPage.vue' import PaymentRedirect from '@/pages/PaymentRedirect.vue' import { useAuthStore } from '@/stores/authStore' @@ -34,6 +35,12 @@ const router = createRouter({ component: OrdersPage, meta: { requiresAuth: true }, }, + { + path: '/bestallning/:orderId/redigera', + name: 'edit-order', + component: EditOrderPage, + meta: { requiresAuth: true }, + }, { path: '/andra-losenord', name: 'change-password',