import { expect, test } from "@playwright/test"; import { DEFAULT_BOOKING_ID, DEFAULT_DEPARTMENT_ID, REGULAR_CUSTOMER_ID, createAttachmentFile, createMobilePosFixture, getStoredPosSnapshot, setupMobilePosPage, suppressVueDevtoolsOverlay, waitForMobileNextStepCooldown, } from "./support/mobilePos.js"; function buildRegularOrder(orderId, overrides = {}) { return { id: orderId, customer_id: REGULAR_CUSTOMER_ID, department_id: DEFAULT_DEPARTMENT_ID, reference: "STEP2-REF", notes: "", po: "", reg_1: "AB12345", reg_2: "", reg_3: "", invoice_collection_id: null, booking_id: null, completed_at: null, created_at: "2026-01-01T10:00:00.000Z", ...overrides, }; } async function selectCustomerFromPopup(page, customerNumber = REGULAR_CUSTOMER_ID) { await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-customer-search-input").fill(String(customerNumber)); await expect(page.getByTestId("pos-mobile-customer-search-result-0")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-customer-search-result-0").click(); await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 }); } async function waitForStepReset(page) { await expect.poll(() => { const currentUrl = new URL(page.url()); return currentUrl.searchParams.get("step") || "1"; }, { timeout: 12_000 }).toBe("1"); await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 }); } async function expectHorizontallyCentered(container, target, tolerance = 8) { const [containerBox, targetBox] = await Promise.all([ container.boundingBox(), target.boundingBox(), ]); expect(containerBox).not.toBeNull(); expect(targetBox).not.toBeNull(); const containerCenter = containerBox.x + (containerBox.width / 2); const targetCenter = targetBox.x + (targetBox.width / 2); expect(Math.abs(containerCenter - targetCenter)).toBeLessThanOrEqual(tolerance); } async function expectSameRow(left, right, tolerance = 8) { const [leftBox, rightBox] = await Promise.all([ left.boundingBox(), right.boundingBox(), ]); expect(leftBox).not.toBeNull(); expect(rightBox).not.toBeNull(); const leftCenterY = leftBox.y + (leftBox.height / 2); const rightCenterY = rightBox.y + (rightBox.height / 2); expect(Math.abs(leftCenterY - rightCenterY)).toBeLessThanOrEqual(tolerance); } async function expectAboveFixedActions(page, target, tolerance = 8) { const [targetBox, actionsBox] = await Promise.all([ target.boundingBox(), page.getByTestId("pos-mobile-fixed-actions").boundingBox(), ]); expect(targetBox).not.toBeNull(); expect(actionsBox).not.toBeNull(); expect(targetBox.y + targetBox.height).toBeLessThanOrEqual(actionsBox.y - tolerance); } async function selectPrimaryProduct(page, productId = 53) { await expect(page.getByTestId("pos-mobile-vehicle-selection")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-category-4")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-category-4").click(); await page.waitForTimeout(800); await expect(page.getByTestId(`pos-mobile-product-${productId}`)).toBeVisible({ timeout: 10_000 }); await page.getByTestId(`pos-mobile-product-${productId}`).click(); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); } async function waitForBookingHydration(page, { primaryId, addonProductIds = [], } = {}) { await expect.poll(async () => { const snapshot = await getStoredPosSnapshot(page); const addonIds = (snapshot?.transactionItems?.primaryItem?.addons || []) .map((addon) => Number(addon?.product?.id ?? addon?.id)) .sort((left, right) => left - right); return { primaryId: snapshot?.transactionItems?.primaryItem?.id ?? null, addonIds, }; }, { timeout: 10_000 }).toEqual({ primaryId, addonIds: [...addonProductIds].sort((left, right) => left - right), }); } async function createOrderFromStep1(page, fixture, { reg = "FREE123", reference = "STEP1-REF", manualInput = false, customerNumber = REGULAR_CUSTOMER_ID, extraSeedState = {}, } = {}) { await setupMobilePosPage(page, fixture, { token: `step1-${reg.toLowerCase()}-token`, seedState: { customerId: null, reg, reference, includePrimaryItem: false, vehicleType: null, lastOrderId: null, ...extraSeedState, }, route: { step: 1, }, }); if (manualInput) { await page.getByTestId("pos-mobile-manual-input-toggle").click(); await expect(page.getByTestId("pos-mobile-manual-input")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-reg-input-1").fill(reg.toLowerCase()); await expect(page.getByTestId("pos-mobile-reg-input-1")).toHaveValue(reg.toUpperCase()); await page.getByTestId("pos-mobile-manual-input-close").click(); await expect(page.getByTestId("pos-mobile-manual-input")).toBeHidden({ timeout: 10_000 }); } await page.getByTestId("pos-mobile-next-step").click(); await selectCustomerFromPopup(page, customerNumber); await page.getByTestId("pos-mobile-next-step").click(); } test.describe("POS mobile order flow", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(testInfo.project.name !== "chromium-mobile", "Mobile POS order suite is scoped to chromium-mobile."); await suppressVueDevtoolsOverlay(page); }); test("scanner shell renders and prompts for customer selection", async ({ page }) => { const fixture = createMobilePosFixture(); await setupMobilePosPage(page, fixture, { token: "mobile-shell-token", seedState: { customerId: null, reg: "FREE123", reference: "SHELL-REF", includePrimaryItem: false, vehicleType: null, lastOrderId: null, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-step-1-title")).toContainText("Scan nummerplader"); await expect(page.getByTestId("pos-mobile-step-1-subtitle")).toContainText("Hold kameraet op mod nummerpladerne."); await expectHorizontallyCentered(page.getByTestId("pos-mobile-step-1-shell"), page.getByTestId("pos-mobile-step-1-title")); await expectHorizontallyCentered(page.getByTestId("pos-mobile-step-1-shell"), page.getByTestId("pos-mobile-step-1-subtitle")); await expectHorizontallyCentered(page.getByTestId("pos-mobile-step-1-shell"), page.getByTestId("pos-mobile-manual-input-toggle")); const [registrationRowBox, referenceTriggerBox] = await Promise.all([ page.getByTestId("pos-mobile-step-1-registration-row").boundingBox(), page.getByTestId("pos-mobile-step-1-reference-trigger").boundingBox(), ]); expect(registrationRowBox).not.toBeNull(); expect(referenceTriggerBox).not.toBeNull(); expect(Math.abs((registrationRowBox?.width ?? 0) - (referenceTriggerBox?.width ?? 0))).toBeLessThanOrEqual(4); expect(Math.abs((registrationRowBox?.x ?? 0) - (referenceTriggerBox?.x ?? 0))).toBeLessThanOrEqual(4); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 }); }); test("transaction history keeps each wash visually separated with a border", async ({ page }) => { const fixture = createMobilePosFixture({ ordersById: { 9201: buildRegularOrder(9201, { customer_name: "VOGNMAND JIMMY CHRISTENSEN ApS", total_net_amount: 746, pending_handheld: true, created_at: "2026-04-09T12:08:00.000Z", }), 9202: buildRegularOrder(9202, { customer_name: "BYGMA Roskilde A/S", total_net_amount: 507, pending_handheld: true, created_at: "2026-04-09T11:51:00.000Z", }), 9203: buildRegularOrder(9203, { customer_name: "VOLVO ENTREPRENØRMASKINER A/S", total_net_amount: 512, pending_handheld: true, created_at: "2026-04-09T10:10:00.000Z", }), }, }); await setupMobilePosPage(page, fixture, { token: "mobile-transaction-history-token", seedState: { customerId: null, reg: "FREE123", reference: "HISTORY-REF", includePrimaryItem: false, vehicleType: null, lastOrderId: null, transactionHistoryView: true, }, route: { step: 1, }, }); const cards = page.locator('[data-testid^="pos-mobile-transaction-history-card-"]'); await expect(cards.first()).toBeVisible({ timeout: 10_000 }); expect(await cards.count()).toBe(3); await expect(cards.first()).toHaveCSS("border-top-style", "solid"); await expect(cards.first()).toHaveCSS("border-top-width", "1px"); }); test("customer notes popup shows the translated created-by label", async ({ page }) => { const fixture = createMobilePosFixture({ customerNotesByNumber: { [REGULAR_CUSTOMER_ID]: [ { id: 401, note: "test123", created_at: "2026-04-09T06:17:00.000Z", cashier_id: 7, }, ], }, employees: [ { id: 7, display_name: "Jeppe", }, ], }); await setupMobilePosPage(page, fixture, { token: "mobile-customer-notes-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "NOTES-REF", includePrimaryItem: false, vehicleType: null, lastOrderId: null, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-customer-notes-trigger")).toBeVisible({ timeout: 10_000 }); const [registrationRowBox, notesTriggerBox] = await Promise.all([ page.getByTestId("pos-mobile-step-1-registration-row").boundingBox(), page.getByTestId("pos-mobile-customer-notes-trigger").boundingBox(), ]); expect(registrationRowBox).not.toBeNull(); expect(notesTriggerBox).not.toBeNull(); expect(Math.abs((registrationRowBox?.width ?? 0) - (notesTriggerBox?.width ?? 0))).toBeLessThanOrEqual(4); expect(Math.abs((registrationRowBox?.x ?? 0) - (notesTriggerBox?.x ?? 0))).toBeLessThanOrEqual(4); await page.getByTestId("pos-mobile-customer-notes-trigger").click(); const popup = page.locator('[data-testid="pos-mobile-popup"][data-popup-id="customer_notes"]'); await expect(popup).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).toContainText("Oprettet af Jeppe"); await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).not.toContainText("pos.created_by"); await popup.getByText("Tilføj note", { exact: true }).click(); await expect(page.locator("#input_field")).toBeVisible({ timeout: 10_000 }); await expect(page.locator("#input_field")).toHaveAttribute("placeholder", "Tilføj note..."); }); test("@smoke manual input happy path creates, completes, and resets", async ({ page }) => { const fixture = createMobilePosFixture(); await createOrderFromStep1(page, fixture, { reg: "ZZ00000", reference: "MOBILE-SMOKE-REF", manualInput: true, }); await selectPrimaryProduct(page, 53); await waitForMobileNextStepCooldown(page); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1); await waitForStepReset(page); }); test("required reference prompt retries and creates the order after confirmation", async ({ page }) => { const fixture = createMobilePosFixture({ customerAttributesByNumber: { [REGULAR_CUSTOMER_ID]: [ { id: 9, customer_number: REGULAR_CUSTOMER_ID, attribute: "requiresReferenceNumber", }, ], }, }); await createOrderFromStep1(page, fixture, { reg: "FREE123", reference: "", extraSeedState: { reference: "", }, }); await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10_000 }); await page.locator(".swal2-input").fill("REQ-REF-001"); await page.locator(".swal2-confirm").click(); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); const orderId = Number(new URL(page.url()).searchParams.get("id")); await expect.poll(() => fixture.ordersById[orderId]?.reference ?? "").toBe("REQ-REF-001"); expect(fixture.requestCounters.orderCreate).toBe(1); }); test("invalid mobile POS route params fall back to step 1", async ({ page }) => { const fixture = createMobilePosFixture(); await setupMobilePosPage(page, fixture, { token: "mobile-invalid-route-token", seedState: { customerId: null, reg: "FREE123", reference: "INVALID-ROUTE-REF", includePrimaryItem: false, vehicleType: null, lastOrderId: null, }, route: { step: 1, }, }); await page.goto(`/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=999&id=abc&customer_id=def`); await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0); }); test("step 2 without an order id falls back to step 1", async ({ page }) => { const fixture = createMobilePosFixture(); await setupMobilePosPage(page, fixture, { token: "mobile-missing-order-id-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "FREE123", reference: "MISSING-ORDER-ID-REF", includePrimaryItem: false, vehicleType: null, lastOrderId: null, }, route: { step: 1, }, }); await page.goto(`/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=2&customer_id=${REGULAR_CUSTOMER_ID}`); await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0); }); test("step 1 attachments upload after order creation completes", async ({ page }) => { const fixture = createMobilePosFixture(); await createOrderFromStep1(page, fixture, { reg: "ATT1234", reference: "ATTACH-SUCCESS-REF", extraSeedState: { attachmentsBase64: [createAttachmentFile("damage-success.jpg")], }, }); await selectPrimaryProduct(page, 53); await waitForMobileNextStepCooldown(page); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.attachmentUpload, { timeout: 10_000 }).toBe(1); await expect.poll(() => fixture.requestLog.attachmentUploads.length, { timeout: 10_000 }).toBe(1); await waitForStepReset(page); }); test("stored pos_order_id restores step 2 without creating a duplicate order", async ({ page }) => { const orderId = 9305; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reference: "RESTORE-REF", reg_1: "AB12345", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-restore-token", storedOrderId: orderId, seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "RESTORE-REF", includePrimaryItem: false, vehicleType: 53, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 }); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); expect(fixture.requestCounters.orderCreate).toBe(0); expect(fixture.requestCounters.orderGet).toBeGreaterThan(0); }); test("step 2 query bootstrap auto-loads the primary product and persists notes/reference", async ({ page }) => { const orderId = 9401; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reference: "BOOTSTRAP-REF", reg_1: "AB12345", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-step2-bootstrap-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "BOOTSTRAP-REF", includePrimaryItem: false, vehicleType: 53, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-last-order")).toBeVisible({ timeout: 10_000 }); await expect.poll(async () => { const snapshot = await getStoredPosSnapshot(page); return snapshot?.transactionItems?.primaryItem?.id ?? null; }, { timeout: 10_000 }).toBe(53); await page.getByTestId("pos-mobile-notes-input").scrollIntoViewIfNeeded(); await expectAboveFixedActions(page, page.getByTestId("pos-mobile-notes-input")); await page.getByTestId("pos-mobile-notes-input").fill("mobile-pos-notes"); await page.getByTestId("pos-mobile-notes-input").press("Tab"); await page.getByTestId("pos-mobile-reference-step-2-input").scrollIntoViewIfNeeded(); await expectAboveFixedActions(page, page.getByTestId("pos-mobile-reference-step-2-input")); await page.getByTestId("pos-mobile-reference-step-2-input").fill("mobile-pos-reference"); await page.getByTestId("pos-mobile-reference-step-2-input").press("Tab"); await expect.poll(() => fixture.ordersById[orderId]?.notes ?? "").toBe("mobile-pos-notes"); await expect.poll(() => fixture.ordersById[orderId]?.reference ?? "").toBe("mobile-pos-reference"); }); test("manual step 2 selection supports addons and additional items", async ({ page }) => { const orderId = 9402; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reference: "STEP2-MANUAL-REF", reg_1: "ZZ00000", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-step2-manual-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "ZZ00000", reference: "STEP2-MANUAL-REF", includePrimaryItem: false, vehicleType: null, lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await selectPrimaryProduct(page, 53); await expect(page.getByTestId("pos-mobile-additional-items-header-title")).toBeVisible({ timeout: 10_000 }); await expectSameRow( page.getByTestId("pos-mobile-additional-items-header-title"), page.getByTestId("pos-mobile-additional-items-header-icon"), ); await expect(page.getByTestId("pos-mobile-addon-71")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-addon-71").click(); await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("1", { timeout: 10_000 }); await page.getByTestId("pos-mobile-addon-71-increase").click(); await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("2", { timeout: 10_000 }); await page.getByTestId("pos-mobile-additional-items-open").click(); await expect(page.getByTestId("pos-mobile-additional-items-selection")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-category-8").click(); await page.waitForTimeout(2_000); await page.getByTestId("pos-mobile-product-91").click(); await expect.poll(async () => { const snapshot = await getStoredPosSnapshot(page); return (snapshot?.transactionItems?.additionalItems || []).map((item) => Number(item?.id)).sort((left, right) => left - right); }, { timeout: 10_000 }).toEqual([91]); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("2", { timeout: 10_000 }); await expect.poll(async () => { const snapshot = await getStoredPosSnapshot(page); return (snapshot?.transactionItems?.additionalItems || []).map((item) => ({ id: Number(item?.id), quantity: Number(item?.quantity ?? 0), })); }, { timeout: 10_000 }).toEqual([ { id: 91, quantity: 1, }, ]); await page.waitForTimeout(500); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(3); await expect.poll(() => (fixture.orderItemsByOrderId[orderId] || []).length, { timeout: 10_000 }).toBe(3); const createdProductIds = fixture.requestLog.orderItemCreates.map((entry) => Number(entry.product_id)); expect(createdProductIds).toEqual([53, 71, 91]); }); test("clear all deletes the order and returns to the scanner", async ({ page }) => { const orderId = 9403; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-clear-all-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "CLEAR-ALL-REF", includePrimaryItem: true, primaryItemId: 53, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-clear-all-button")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-clear-all-button").click({ force: true }); await expect.poll(() => fixture.deletedOrderIds.includes(orderId), { timeout: 10_000 }).toBe(true); await waitForStepReset(page); }); test("step 2 sync is idempotent when the order already matches the local transaction", async ({ page }) => { const orderId = 9404; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId), }, orderItemsByOrderId: { [orderId]: [ { id: 9901, order_id: orderId, product_id: 53, product: fixtureProduct(53), quantity: 1, notes: "", reference: "", related_item_id: null, price: 599, }, ], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-idempotent-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "IDEMPOTENT-REF", includePrimaryItem: true, primaryItemId: 53, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 2_500 }).toBe(0); await expect.poll(() => fixture.requestCounters.orderItemsDelete, { timeout: 2_500 }).toBe(0); await waitForStepReset(page); }); test("booking hydration applies booking items, reference, notes, and po", async ({ page }) => { const orderId = 9405; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reg_1: "BOOK123", reference: "", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-booking-hydration-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "BOOK123", reference: "", includePrimaryItem: false, vehicleType: null, bookingId: DEFAULT_BOOKING_ID, vehicleStatus: "booked", lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await waitForBookingHydration(page, { primaryId: 53, addonProductIds: [71], }); await expect.poll(() => fixture.ordersById[orderId]?.reference ?? "").toBe("BOOKING-REF-8101"); await expect.poll(() => fixture.ordersById[orderId]?.notes ?? "").toBe("Booking notes from planner"); await expect.poll(() => fixture.ordersById[orderId]?.po ?? "").toBe("PO-8101"); }); test("non-wash booking falls back to the first wash product", async ({ page }) => { const orderId = 9406; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reg_1: "NOWASH1", reference: "", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-booking-fallback-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "NOWASH1", reference: "", includePrimaryItem: false, vehicleType: null, bookingId: 8102, vehicleStatus: "booked", lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect.poll(async () => { const snapshot = await getStoredPosSnapshot(page); return snapshot?.transactionItems?.primaryItem?.id ?? null; }, { timeout: 10_000 }).toBe(53); }); test("booking completion without safety seal completes and resets", async ({ page }) => { const orderId = 9407; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reg_1: "BOOK123", reference: "", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-booking-complete-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "BOOK123", reference: "", includePrimaryItem: false, vehicleType: null, bookingId: DEFAULT_BOOKING_ID, vehicleStatus: "booked", lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await waitForBookingHydration(page, { primaryId: 53, addonProductIds: [71], }); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.bookingSetOrderId, { timeout: 10_000 }).toBe(1); await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1); await expect.poll(() => fixture.bookingsById[DEFAULT_BOOKING_ID]?.status ?? "").toBe("completed"); await waitForStepReset(page); }); test("booking completion prompts when a safety seal is added to the basket", async ({ page }) => { const orderId = 9409; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reg_1: "BOOK123", reference: "", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-booking-basket-seal-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "BOOK123", reference: "", includePrimaryItem: false, vehicleType: null, bookingId: DEFAULT_BOOKING_ID, vehicleStatus: "booked", lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await waitForBookingHydration(page, { primaryId: 53, addonProductIds: [71], }); await page.getByTestId("pos-mobile-additional-items-open").click(); await expect(page.getByTestId("pos-mobile-additional-items-selection")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-category-8").click(); await page.waitForTimeout(2_000); await page.getByTestId("pos-mobile-product-41").click(); await expect.poll(async () => { const snapshot = await getStoredPosSnapshot(page); return (snapshot?.transactionItems?.additionalItems || []).map((item) => Number(item?.id)).sort((left, right) => left - right); }, { timeout: 10_000 }).toEqual([41]); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await page.waitForTimeout(500); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("9090"); await page.getByTestId("pos-mobile-booking-complete-with-certificate").click(); await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1); await expect.poll(() => fixture.bookingsById[DEFAULT_BOOKING_ID]?.status ?? "").toBe("completed"); await expect.poll(() => String(fixture.bookingsById[DEFAULT_BOOKING_ID]?.safety_seal ?? "")).toBe("9090"); await waitForStepReset(page); }); test("booking completion with safety seal popup submits the explicit seal", async ({ page }) => { const orderId = 9408; const fixture = createMobilePosFixture({ ordersById: { [orderId]: buildRegularOrder(orderId, { reg_1: "SEAL123", reference: "", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-booking-seal-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "SEAL123", reference: "", includePrimaryItem: false, vehicleType: null, bookingId: 8103, vehicleStatus: "booked", lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await waitForBookingHydration(page, { primaryId: 53, addonProductIds: [41], }); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("4242"); await page.getByTestId("pos-mobile-booking-complete-with-certificate").click(); await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1); await expect.poll(() => fixture.bookingsById[8103]?.status ?? "").toBe("completed"); await expect.poll(() => String(fixture.bookingsById[8103]?.safety_seal ?? "")).toBe("4242"); await waitForStepReset(page); }); test("booking completion failure keeps the mobile booking popup open for retry", async ({ page }) => { const orderId = 9410; const fixture = createMobilePosFixture({ failureBudget: { bookingComplete: 1, }, ordersById: { [orderId]: buildRegularOrder(orderId, { reg_1: "SEAL123", reference: "", }), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-booking-failure-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "SEAL123", reference: "", includePrimaryItem: false, vehicleType: null, bookingId: 8103, vehicleStatus: "booked", lastOrderId: null, }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 }); await waitForBookingHydration(page, { primaryId: 53, addonProductIds: [41], }); await page.getByTestId("pos-mobile-next-step").click(); await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 }); await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("1111"); await page.getByTestId("pos-mobile-booking-complete-with-certificate").click(); await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1); await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-step-1-shell")).toHaveCount(0); await expect.poll(() => fixture.bookingsById[8103]?.status ?? "").toBe("pending"); }); test("order creation failure leaves the flow on step 1", async ({ page }) => { const fixture = createMobilePosFixture({ failureBudget: { orderCreate: 1, }, }); await setupMobilePosPage(page, fixture, { token: "mobile-order-create-failure-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "FAIL-CREATE-REF", includePrimaryItem: false, vehicleType: 53, }, route: { step: 1, }, }); await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 }); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.orderCreate, { timeout: 10_000 }).toBe(1); await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 }); await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0); }); test("attachment upload failure is recorded but does not block completion", async ({ page }) => { const orderId = 9409; const fixture = createMobilePosFixture({ failureBudget: { attachmentUpload: 1, }, ordersById: { [orderId]: buildRegularOrder(orderId), }, orderItemsByOrderId: { [orderId]: [], }, }); await setupMobilePosPage(page, fixture, { token: "mobile-attachment-failure-token", seedState: { customerId: REGULAR_CUSTOMER_ID, reg: "AB12345", reference: "ATTACHMENT-REF", includePrimaryItem: true, primaryItemId: 53, attachmentsBase64: [createAttachmentFile("damage.jpg")], }, route: { step: 2, orderId, customerId: REGULAR_CUSTOMER_ID, }, }); await page.getByTestId("pos-mobile-next-step").click(); await expect.poll(() => fixture.requestCounters.attachmentUpload, { timeout: 10_000 }).toBe(1); await waitForStepReset(page); }); }); function fixtureProduct(productId) { const fixture = createMobilePosFixture(); return fixture.products.find((product) => product.id === productId); }