diff --git a/frontend/src/__tests__/GuestCheckoutPage.spec.ts b/frontend/src/__tests__/GuestCheckoutPage.spec.ts
new file mode 100644
index 0000000..0bce7d2
--- /dev/null
+++ b/frontend/src/__tests__/GuestCheckoutPage.spec.ts
@@ -0,0 +1,185 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { createPinia, setActivePinia } from 'pinia'
+import { createRouter, createMemoryHistory } from 'vue-router'
+
+const mocks = vi.hoisted(() => ({
+ mockCreateGuestOrder: vi.fn(),
+}))
+
+vi.mock('@/api/guestOrders', () => ({
+ createGuestOrder: mocks.mockCreateGuestOrder,
+}))
+
+import GuestCheckoutPage from '@/pages/GuestCheckoutPage.vue'
+import { createGuestOrder } from '@/api/guestOrders'
+const mockCreateGuestOrder = vi.mocked(createGuestOrder)
+
+function createTestRouter() {
+ return createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ { path: '/', name: 'home', component: { template: '
Home
' } },
+ {
+ path: '/gast-kassa',
+ name: 'guest-checkout',
+ component: GuestCheckoutPage,
+ },
+ {
+ path: '/gast-betalning/:orderId',
+ name: 'guest-payment',
+ component: { template: 'Payment
' },
+ },
+ {
+ path: '/logga-in',
+ name: 'login',
+ component: { template: 'Login
' },
+ },
+ ],
+ })
+}
+
+async function mountPage() {
+ const pinia = createPinia()
+ setActivePinia(pinia)
+ const router = createTestRouter()
+ await router.push({ name: 'guest-checkout' })
+ await router.isReady()
+
+ const wrapper = mount(GuestCheckoutPage, {
+ global: { plugins: [router, pinia] },
+ })
+
+ return { wrapper, router }
+}
+
+describe('GuestCheckoutPage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders heading and price', async () => {
+ const { wrapper } = await mountPage()
+ expect(wrapper.text()).toContain('Skicka ett brev')
+ expect(wrapper.text()).toContain('49 kr')
+ expect(wrapper.text()).toContain('Inget konto behövs')
+ })
+
+ it('disables submit button when fields are empty', async () => {
+ const { wrapper } = await mountPage()
+ const submit = wrapper.find('.guest-checkout__submit')
+ expect(submit.attributes('disabled')).toBeDefined()
+ })
+
+ it('enables submit button when plate, letter, and email are valid', async () => {
+ const { wrapper } = await mountPage()
+ const inputs = wrapper.findAll('input')
+ const textarea = wrapper.find('textarea')
+
+ await inputs[0].setValue('ABC123')
+ await textarea.setValue('Hej, din bil står fel!')
+ await inputs[1].setValue('test@example.se')
+
+ const submit = wrapper.find('.guest-checkout__submit')
+ expect(submit.attributes('disabled')).toBeUndefined()
+ })
+
+ it('keeps submit disabled with an invalid plate', async () => {
+ const { wrapper } = await mountPage()
+ const inputs = wrapper.findAll('input')
+ const textarea = wrapper.find('textarea')
+
+ await inputs[0].setValue('ABC') // too short
+ await textarea.setValue('Hej')
+ await inputs[1].setValue('test@example.se')
+
+ const submit = wrapper.find('.guest-checkout__submit')
+ expect(submit.attributes('disabled')).toBeDefined()
+ })
+
+ it('keeps submit disabled with an invalid email', async () => {
+ const { wrapper } = await mountPage()
+ const inputs = wrapper.findAll('input')
+ const textarea = wrapper.find('textarea')
+
+ await inputs[0].setValue('ABC123')
+ await textarea.setValue('Hej')
+ await inputs[1].setValue('not-an-email')
+
+ const submit = wrapper.find('.guest-checkout__submit')
+ expect(submit.attributes('disabled')).toBeDefined()
+ })
+
+ it('shows character count that updates with input', async () => {
+ const { wrapper } = await mountPage()
+ const textarea = wrapper.find('textarea')
+
+ await textarea.setValue('Hej')
+
+ expect(wrapper.text()).toContain('3 / 1000 tecken')
+ })
+
+ it('shows link to login page', async () => {
+ const { wrapper } = await mountPage()
+ expect(wrapper.text()).toContain('Har du redan ett konto?')
+ expect(wrapper.text()).toContain('Logga in')
+ })
+
+ it('calls createGuestOrder and navigates on submit', async () => {
+ mockCreateGuestOrder.mockResolvedValue({
+ id: 'order-123',
+ plate: 'ABC123',
+ letterText: 'Hej',
+ status: 'pending_payment',
+ trackingId: null,
+ amountPaid: null,
+ createdAt: '2025-01-01T00:00:00Z',
+ guestToken: 'token-abc',
+ })
+
+ const { wrapper, router } = await mountPage()
+ const inputs = wrapper.findAll('input')
+ const textarea = wrapper.find('textarea')
+
+ await inputs[0].setValue('ABC123')
+ await textarea.setValue('Hej, din bil står fel!')
+ await inputs[1].setValue('test@example.se')
+
+ await wrapper.find('form').trigger('submit.prevent')
+
+ await vi.waitFor(() => {
+ expect(mockCreateGuestOrder).toHaveBeenCalledWith(
+ 'ABC123',
+ 'Hej, din bil står fel!',
+ 'test@example.se',
+ )
+ })
+
+ await vi.waitFor(() => {
+ expect(router.currentRoute.value.name).toBe('guest-payment')
+ expect(router.currentRoute.value.params.orderId).toBe('order-123')
+ expect(router.currentRoute.value.query.token).toBe('token-abc')
+ expect(router.currentRoute.value.query.plate).toBe('ABC123')
+ })
+ })
+
+ it('shows error message when order creation fails', async () => {
+ mockCreateGuestOrder.mockRejectedValue(new Error('Network error'))
+
+ const { wrapper } = await mountPage()
+ const inputs = wrapper.findAll('input')
+ const textarea = wrapper.find('textarea')
+
+ await inputs[0].setValue('ABC123')
+ await textarea.setValue('Hej')
+ await inputs[1].setValue('test@example.se')
+
+ await wrapper.find('form').trigger('submit.prevent')
+
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain(
+ 'Kunde inte skapa beställningen. Försök igen senare.',
+ )
+ })
+ })
+})
diff --git a/frontend/src/__tests__/GuestOrderPage.spec.ts b/frontend/src/__tests__/GuestOrderPage.spec.ts
new file mode 100644
index 0000000..d5f62b6
--- /dev/null
+++ b/frontend/src/__tests__/GuestOrderPage.spec.ts
@@ -0,0 +1,191 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { createPinia, setActivePinia } from 'pinia'
+import { createRouter, createMemoryHistory } from 'vue-router'
+
+const mocks = vi.hoisted(() => ({
+ mockFetchGuestOrder: vi.fn(),
+}))
+
+vi.mock('@/api/guestOrders', () => ({
+ fetchGuestOrder: mocks.mockFetchGuestOrder,
+}))
+
+import GuestOrderPage from '@/pages/GuestOrderPage.vue'
+
+const mockOrder = {
+ id: 'order-123',
+ plate: 'ABC123',
+ letterText: 'Hej, din bil står fel!',
+ status: 'pending_payment',
+ trackingId: null,
+ amountPaid: null,
+ createdAt: '2025-01-01T12:00:00Z',
+ guestToken: 'token-abc',
+}
+
+function createTestRouter() {
+ return createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ { path: '/', name: 'home', component: { template: 'Home
' } },
+ {
+ path: '/gast-order/:token',
+ name: 'guest-order',
+ component: GuestOrderPage,
+ },
+ {
+ path: '/gast-betalning/:orderId',
+ name: 'guest-payment',
+ component: { template: 'Payment
' },
+ },
+ ],
+ })
+}
+
+async function mountPage(token = 'token-abc') {
+ const pinia = createPinia()
+ setActivePinia(pinia)
+ const router = createTestRouter()
+ await router.push({ name: 'guest-order', params: { token } })
+ await router.isReady()
+
+ const wrapper = mount(GuestOrderPage, {
+ global: { plugins: [router, pinia] },
+ })
+
+ return { wrapper, router }
+}
+
+describe('GuestOrderPage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mocks.mockFetchGuestOrder.mockResolvedValue(mockOrder)
+ })
+
+ it('shows loading state initially', async () => {
+ // Never resolve so we can see the loading state
+ mocks.mockFetchGuestOrder.mockReturnValue(new Promise(() => {}))
+
+ const { wrapper } = await mountPage()
+ expect(wrapper.text()).toContain('Laddar')
+ })
+
+ it('renders order details after loading', async () => {
+ const { wrapper } = await mountPage()
+
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Din beställning')
+ })
+
+ expect(wrapper.text()).toContain('ABC123')
+ expect(wrapper.text()).toContain('order-123')
+ expect(wrapper.text()).toContain('Hej, din bil står fel!')
+ })
+
+ it('displays human-readable status labels', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'pending_payment',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Väntar på betalning')
+ })
+ })
+
+ it('shows "Behandlas" for paid status', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'paid',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Behandlas')
+ })
+ })
+
+ it('shows "Skickat" for sent status', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'sent',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Skickat')
+ })
+ })
+
+ it('shows "Levererat" for delivered status', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'delivered',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Levererat')
+ })
+ })
+
+ it('shows "Misslyckades" for failed status', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'failed',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Misslyckades')
+ })
+ })
+
+ it('shows "Avbrutet" for cancelled status', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'cancelled',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Avbrutet')
+ })
+ })
+
+ it('shows payment link when status is pending_payment', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Gå till betalningssidan')
+ })
+ })
+
+ it('hides payment link when order is not pending_payment', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'sent',
+ })
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).not.toContain('Gå till betalningssidan')
+ })
+ })
+
+ it('shows error when fetch fails', async () => {
+ mocks.mockFetchGuestOrder.mockRejectedValue(new Error('Not found'))
+
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain(
+ 'Kunde inte hitta beställningen. Kontrollera länken.',
+ )
+ })
+ })
+
+ it('does not fetch order when token is missing in params', async () => {
+ // The component guards against an empty token in onMounted.
+ // Vue Router requires the :token param so we simulate by checking
+ // that fetchGuestOrder is only called once per mount with a token.
+ const { wrapper } = await mountPage('token-abc')
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Din beställning')
+ })
+ expect(mocks.mockFetchGuestOrder).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/frontend/src/__tests__/GuestPaymentRedirect.spec.ts b/frontend/src/__tests__/GuestPaymentRedirect.spec.ts
new file mode 100644
index 0000000..7e31b4b
--- /dev/null
+++ b/frontend/src/__tests__/GuestPaymentRedirect.spec.ts
@@ -0,0 +1,307 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { createPinia, setActivePinia } from 'pinia'
+import { createRouter, createMemoryHistory } from 'vue-router'
+
+const mocks = vi.hoisted(() => ({
+ mockFetchGuestOrder: vi.fn(),
+ mockPayGuestOrder: vi.fn(),
+ mockFetchSwishInfo: vi.fn(),
+ mockBuildSwishPaymentUrl: vi.fn(),
+ mockToDataURL: vi.fn(),
+}))
+
+vi.mock('@/api/guestOrders', () => ({
+ fetchGuestOrder: mocks.mockFetchGuestOrder,
+ payGuestOrder: mocks.mockPayGuestOrder,
+}))
+
+vi.mock('@/api/payment', () => ({
+ fetchSwishInfo: mocks.mockFetchSwishInfo,
+ buildSwishPaymentUrl: mocks.mockBuildSwishPaymentUrl,
+}))
+
+vi.mock('qrcode', () => ({
+ default: {
+ toDataURL: mocks.mockToDataURL,
+ },
+}))
+
+import GuestPaymentRedirect from '@/pages/GuestPaymentRedirect.vue'
+import QRCode from 'qrcode'
+
+const mockToDataURL = vi.mocked(QRCode.toDataURL)
+
+const mockOrder = {
+ id: 'order-123',
+ plate: 'ABC123',
+ letterText: 'Hej',
+ status: 'pending_payment',
+ trackingId: null,
+ amountPaid: null,
+ createdAt: '2025-01-01T00:00:00Z',
+ guestToken: 'token-abc',
+}
+
+function createTestRouter() {
+ return createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ { path: '/', name: 'home', component: { template: 'Home
' } },
+ {
+ path: '/gast-betalning/:orderId',
+ name: 'guest-payment',
+ component: GuestPaymentRedirect,
+ },
+ {
+ path: '/gast-order/:token',
+ name: 'guest-order',
+ component: { template: 'Order
' },
+ },
+ ],
+ })
+}
+
+async function mountPage(
+ orderId = 'order-123',
+ token = 'token-abc',
+ plate = 'ABC123',
+) {
+ const pinia = createPinia()
+ setActivePinia(pinia)
+ const router = createTestRouter()
+ await router.push({
+ name: 'guest-payment',
+ params: { orderId },
+ query: { token, plate },
+ })
+ await router.isReady()
+
+ const wrapper = mount(GuestPaymentRedirect, {
+ global: { plugins: [router, pinia] },
+ })
+
+ return { wrapper, router }
+}
+
+function setupDefaultMocks() {
+ mocks.mockFetchSwishInfo.mockResolvedValue({
+ number: '0701234567',
+ amount: 49,
+ })
+ mocks.mockFetchGuestOrder.mockResolvedValue(mockOrder)
+ mocks.mockBuildSwishPaymentUrl.mockReturnValue(
+ 'https://app.swish.nu/1/p/sw/?sw=46701234567&amt=49.00&msg=order-123',
+ )
+ mocks.mockToDataURL.mockResolvedValue('data:image/png;base64,mock-qr')
+}
+
+describe('GuestPaymentRedirect', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ setupDefaultMocks()
+ })
+
+ it('renders heading and plate', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Betalning')
+ })
+ expect(wrapper.text()).toContain('ABC123')
+ })
+
+ it('displays order id', async () => {
+ const { wrapper } = await mountPage('order-456')
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('order-456')
+ })
+ })
+
+ it('shows the amount to pay', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('49 kr')
+ })
+ })
+
+ it('renders QR code after loading swish info', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__qr-img').exists()).toBe(true)
+ })
+ expect(mockToDataURL).toHaveBeenCalledTimes(1)
+ })
+
+ it('renders Swish payment link', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ const link = wrapper.find('.payment__swish-link')
+ expect(link.exists()).toBe(true)
+ expect(link.attributes('href')).toContain('app.swish.nu')
+ })
+ })
+
+ it('displays Swish number from API', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('0701234567')
+ })
+ })
+
+ it('shows the "Jag har betalat" button', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__submit').exists()).toBe(true)
+ expect(wrapper.text()).toContain('Jag har betalat')
+ })
+ })
+
+ it('shows confirmation dialog when clicking "Jag har betalat"', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__submit').exists()).toBe(true)
+ })
+
+ await wrapper.find('.payment__submit').trigger('click')
+
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Jag bekräftar att jag har Swishat')
+ expect(wrapper.text()).toContain('0701234567')
+ })
+ })
+
+ it('can cancel the confirmation dialog', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__submit').exists()).toBe(true)
+ })
+
+ await wrapper.find('.payment__submit').trigger('click')
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__confirm-cancel').exists()).toBe(true)
+ })
+
+ await wrapper.find('.payment__confirm-cancel').trigger('click')
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Jag har betalat')
+ expect(wrapper.text()).not.toContain('Jag bekräftar')
+ })
+ })
+
+ it('calls payGuestOrder on confirmation', async () => {
+ mocks.mockPayGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'processing',
+ })
+
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__submit').exists()).toBe(true)
+ })
+
+ await wrapper.find('.payment__submit').trigger('click')
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__confirm .btn--primary').exists()).toBe(
+ true,
+ )
+ })
+
+ await wrapper.find('.payment__confirm .btn--primary').trigger('click')
+
+ expect(mocks.mockPayGuestOrder).toHaveBeenCalledWith('token-abc')
+ })
+
+ it('navigates to guest-order after successful payment', async () => {
+ mocks.mockPayGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'processing',
+ })
+
+ const { wrapper, router } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__submit').exists()).toBe(true)
+ })
+
+ await wrapper.find('.payment__submit').trigger('click')
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__confirm .btn--primary').exists()).toBe(
+ true,
+ )
+ })
+
+ await wrapper.find('.payment__confirm .btn--primary').trigger('click')
+
+ await vi.waitFor(() => {
+ expect(router.currentRoute.value.name).toBe('guest-order')
+ expect(router.currentRoute.value.params.token).toBe('token-abc')
+ })
+ })
+
+ it('shows error when payment confirmation fails', async () => {
+ mocks.mockPayGuestOrder.mockRejectedValue(new Error('Network error'))
+
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__submit').exists()).toBe(true)
+ })
+
+ await wrapper.find('.payment__submit').trigger('click')
+ await vi.waitFor(() => {
+ expect(wrapper.find('.payment__confirm .btn--primary').exists()).toBe(
+ true,
+ )
+ })
+
+ await wrapper.find('.payment__confirm .btn--primary').trigger('click')
+
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain(
+ 'Kunde inte bekräfta betalningen. Försök igen.',
+ )
+ })
+ })
+
+ it('shows error when swish info fetch fails', async () => {
+ mocks.mockFetchSwishInfo.mockRejectedValue(new Error('Network error'))
+
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain(
+ 'Kunde inte ladda betalningsinformation. Försök igen senare.',
+ )
+ })
+ })
+
+ it('shows error when token is missing', async () => {
+ const { wrapper } = await mountPage('order-123', '', 'ABC123')
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain(
+ 'Saknar order-token. Gå tillbaka och försök igen.',
+ )
+ })
+ expect(mocks.mockFetchSwishInfo).not.toHaveBeenCalled()
+ })
+
+ it('redirects to guest-order when order is already paid', async () => {
+ mocks.mockFetchGuestOrder.mockResolvedValue({
+ ...mockOrder,
+ status: 'processing',
+ })
+
+ const { router } = await mountPage()
+ await vi.waitFor(() => {
+ expect(router.currentRoute.value.name).toBe('guest-order')
+ expect(router.currentRoute.value.params.token).toBe('token-abc')
+ })
+ })
+
+ it('shows the magic order link', async () => {
+ const { wrapper } = await mountPage()
+ await vi.waitFor(() => {
+ expect(wrapper.text()).toContain('Din orderlänk')
+ })
+ expect(wrapper.find('.guest-payment__magic-url').text()).toContain(
+ 'token-abc',
+ )
+ })
+})
diff --git a/frontend/src/__tests__/guestOrders.spec.ts b/frontend/src/__tests__/guestOrders.spec.ts
new file mode 100644
index 0000000..05215ed
--- /dev/null
+++ b/frontend/src/__tests__/guestOrders.spec.ts
@@ -0,0 +1,104 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { createPinia, setActivePinia } from 'pinia'
+
+const mocks = vi.hoisted(() => ({
+ mockRequest: vi.fn(),
+}))
+
+vi.mock('@/api/client', () => ({
+ request: mocks.mockRequest,
+}))
+
+import {
+ createGuestOrder,
+ fetchGuestOrder,
+ payGuestOrder,
+} from '@/api/guestOrders'
+
+describe('guestOrders API', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ mocks.mockRequest.mockReset()
+ })
+
+ describe('createGuestOrder', () => {
+ it('sends a POST to /guest-orders with plate, letterText, and email', async () => {
+ const mockOrder = {
+ id: 'order-1',
+ plate: 'ABC123',
+ letterText: 'Hej',
+ status: 'pending_payment',
+ trackingId: null,
+ amountPaid: null,
+ createdAt: '2025-01-01T00:00:00Z',
+ guestToken: 'token-abc',
+ }
+ mocks.mockRequest.mockResolvedValue(mockOrder)
+
+ const result = await createGuestOrder('ABC123', 'Hej', 'test@example.se')
+
+ expect(mocks.mockRequest).toHaveBeenCalledWith('/guest-orders', {
+ method: 'POST',
+ body: JSON.stringify({
+ plate: 'ABC123',
+ letterText: 'Hej',
+ email: 'test@example.se',
+ }),
+ })
+ expect(result).toEqual(mockOrder)
+ })
+
+ it('propagates errors from the API client', async () => {
+ mocks.mockRequest.mockRejectedValue(new Error('Server error'))
+
+ await expect(
+ createGuestOrder('ABC123', 'Hej', 'test@example.se'),
+ ).rejects.toThrow('Server error')
+ })
+ })
+
+ describe('fetchGuestOrder', () => {
+ it('sends a GET to /guest-orders/:token', async () => {
+ const mockOrder = {
+ id: 'order-1',
+ plate: 'ABC123',
+ letterText: 'Hej',
+ status: 'pending_payment',
+ trackingId: null,
+ amountPaid: null,
+ createdAt: '2025-01-01T00:00:00Z',
+ guestToken: 'token-abc',
+ }
+ mocks.mockRequest.mockResolvedValue(mockOrder)
+
+ const result = await fetchGuestOrder('token-abc')
+
+ expect(mocks.mockRequest).toHaveBeenCalledWith('/guest-orders/token-abc')
+ expect(result).toEqual(mockOrder)
+ })
+ })
+
+ describe('payGuestOrder', () => {
+ it('sends a POST to /guest-orders/:token/pay', async () => {
+ const mockOrder = {
+ id: 'order-1',
+ plate: 'ABC123',
+ letterText: 'Hej',
+ status: 'processing',
+ trackingId: null,
+ amountPaid: 49,
+ createdAt: '2025-01-01T00:00:00Z',
+ guestToken: 'token-abc',
+ }
+ mocks.mockRequest.mockResolvedValue(mockOrder)
+
+ const result = await payGuestOrder('token-abc')
+
+ expect(mocks.mockRequest).toHaveBeenCalledWith(
+ '/guest-orders/token-abc/pay',
+ { method: 'POST' },
+ )
+ expect(result).toEqual(mockOrder)
+ })
+ })
+})