import { expect, test } from "@playwright/test"; import { createPosFixture, mockApi, primeMockSession } from "./support/network.js"; const ORDER_ID = 54518; const DEPARTMENT_ID = 12; const CARD_CUSTOMER_ID = 999; const CARD_CUSTOMER_NAME = "Card Terminal Customer"; const CARD_ORDER_REFERENCE = "CARD-REF-54518"; const DESKTOP_POS_PERMISSIONS = [ "admin", `department_access_${DEPARTMENT_ID}`, "delete_order", "edit_order", "edit_order_items", "get_user", "list_customer_notes", "list_customer_attributes", "get_custom_prices_other", ]; const SUPERUSER_POS_PERMISSIONS = [...DESKTOP_POS_PERMISSIONS, "superuser"]; const SETUP_REQUIRED_MESSAGE = "Card payments are not ready for this department. Open Stripe setup and choose a terminal location."; function suppressVueDevtoolsOverlay(page) { return page.addInitScript(() => { window.__TW_POS_STRIPE_EMAIL_POLLING_INTERVAL_MS__ = 250; const injectStyle = () => { const root = document.documentElement; if (!root) { return; } const style = document.createElement("style"); style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }"; root.appendChild(style); }; if (document.documentElement) { injectStyle(); } else { document.addEventListener("DOMContentLoaded", injectStyle, { once: true }); } }); } function createCardOrderFixture(overrides = {}) { return createPosFixture({ ordersById: { [ORDER_ID]: { id: ORDER_ID, customer_id: CARD_CUSTOMER_ID, department_id: DEPARTMENT_ID, reference: "CARD-REF-54518", po: "", safety_seal: "", notes: "Paid at terminal", reg_1: "ZZ99999", reg_2: "", reg_3: "", invoice_collection_id: 200, booking_id: null, completed_at: null, closed_at: null, created_at: "2026-04-08 08:44:07", }, }, ...overrides, }); } function createStoredPaymentIntent(orderId, overrides = {}) { const status = overrides.status || "requires_capture"; const amount = Number(overrides.amount || 124800); const isCaptured = status === "succeeded"; return { id: overrides.id || `pi_${orderId}`, amount, amount_capturable: isCaptured ? 0 : amount, amount_received: isCaptured ? amount : 0, currency: overrides.currency || "dkk", status, metadata: { order_id: String(orderId), reader_id: String(overrides.readerId || "reader_online_1"), tax_percentage: String(overrides.taxPercentage || 25), ...(overrides.metadata || {}), }, }; } async function primeSession(page, token = "desktop-card-token") { await primeMockSession(page, { token }); } async function bootDesktopCardPayment(page, fixture, permissions = DESKTOP_POS_PERMISSIONS) { await mockApi(page, { authenticated: true, permissions, edgeGateways: false, pos: fixture, }); await primeSession(page, `desktop-card-${Date.now()}`); await page.goto(`/admin/${DEPARTMENT_ID}/modules/pos?id=${ORDER_ID}&customer_id=${CARD_CUSTOMER_ID}&step=3`); const stepThree = page.getByTestId("pos-step-3"); await expect(stepThree).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-panel-cart")).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-metadata-grid")).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-rail")).toBeVisible({ timeout: 10_000 }); await expect( stepThree .locator( '[data-testid="pos-stripe-setup-required"], [data-testid="pos-stripe-create-intent"], [data-testid="pos-stripe-capture-intent"], [data-testid="pos-stripe-no-readers"], [data-testid="pos-stripe-payment-in-progress"], [data-testid="pos-stripe-payment-succeeded"], [data-testid="pos-stripe-error"]' ) .first() ).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-customer-name")).toContainText(CARD_CUSTOMER_NAME, { timeout: 10_000 }); await expect(stepThree.getByTestId("pos-customer-card")).toContainText(CARD_CUSTOMER_NAME, { timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(CARD_ORDER_REFERENCE, { timeout: 10_000, }); return stepThree; } test.describe("POS desktop card payments", () => { test.beforeEach(async ({ page }, testInfo) => { test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only POS card payment coverage"); await suppressVueDevtoolsOverlay(page); }); test("misconfigured terminal location shows guided blocking state with setup CTA for superusers", async ({ page, }) => { const fixture = createCardOrderFixture({ stripeReadersError: { status: 409, code: "stripe_terminal_setup_required", message: SETUP_REQUIRED_MESSAGE, }, }); const stepThree = await bootDesktopCardPayment(page, fixture, SUPERUSER_POS_PERMISSIONS); await expect(stepThree.getByTestId("pos-stripe-setup-required")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-setup-required")).toContainText(SETUP_REQUIRED_MESSAGE); await expect(stepThree.getByTestId("pos-stripe-open-setup")).toHaveAttribute( "href", `/superuser/departments/${DEPARTMENT_ID}/stripe/setup` ); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-capture-intent")).toHaveCount(0); }); test("misconfigured terminal location falls back to contact-superuser guidance for department operators", async ({ page, }) => { const fixture = createCardOrderFixture({ stripeReadersError: { status: 409, code: "stripe_terminal_setup_required", message: SETUP_REQUIRED_MESSAGE, }, }); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-setup-required")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-contact-superuser")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-open-setup")).toHaveCount(0); }); test("setup-required desktop flow does not expose the email payment-link action", async ({ page }) => { const fixture = createCardOrderFixture({ stripeReadersError: { status: 409, code: "stripe_terminal_setup_required", message: SETUP_REQUIRED_MESSAGE, }, }); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-setup-required")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-email-action")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-email-input")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-setup-required")).toBeVisible(); }); test("configured department with zero readers keeps the unavailable-reader state", async ({ page }) => { const fixture = createCardOrderFixture({ readers: [], }); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-no-readers")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-setup-required")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toHaveCount(0); }); test("reloading step 3 keeps the customer summary hydrated", async ({ page }) => { const fixture = createCardOrderFixture(); await bootDesktopCardPayment(page, fixture); await page.reload(); const stepThree = page.getByTestId("pos-step-3"); await expect(stepThree).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-customer-name")).toContainText(CARD_CUSTOMER_NAME, { timeout: 10_000, }); await expect(stepThree.getByTestId("pos-customer-card")).toContainText(CARD_CUSTOMER_NAME, { timeout: 10_000, }); await expect(stepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(CARD_ORDER_REFERENCE, { timeout: 10_000, }); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 }); }); test("step 2 customer wishes stay visible after moving to card payment step 3", async ({ page }) => { const stepTwoReference = "CARD-STEP-2-REF"; const stepTwoPo = "PO-CARD-STEP-2"; const fixture = createCardOrderFixture({ ordersById: { [ORDER_ID]: { ...createCardOrderFixture().ordersById[ORDER_ID], reference: "", po: "", }, }, }); await mockApi(page, { authenticated: true, permissions: DESKTOP_POS_PERMISSIONS, edgeGateways: false, pos: fixture, }); await primeSession(page, `desktop-card-step2-${Date.now()}`); await page.goto(`/admin/${DEPARTMENT_ID}/modules/pos?id=${ORDER_ID}&customer_id=${CARD_CUSTOMER_ID}&step=2`); const stepTwo = page.getByTestId("pos-step-2"); await expect(stepTwo).toBeVisible({ timeout: 10_000 }); await expect(stepTwo.getByTestId("pos-order-metadata-grid")).toBeVisible({ timeout: 10_000 }); await stepTwo.getByTestId("pos-order-customer-wishes-reference").click(); await stepTwo.getByTestId("pos-order-customer-wishes-reference-input").fill(stepTwoReference); await stepTwo.getByTestId("pos-order-customer-wishes-po").click(); await stepTwo.getByTestId("pos-order-customer-wishes-po-input").fill(stepTwoPo); await stepTwo.getByTestId("pos-next-step").click(); const stepThree = page.getByTestId("pos-step-3"); await expect(stepThree).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(stepTwoReference, { timeout: 10_000, }); await expect(stepThree.getByTestId("pos-order-customer-wishes-po")).toContainText(stepTwoPo, { timeout: 10_000, }); await expect.poll(() => fixture.ordersById[ORDER_ID].reference, { timeout: 10_000 }).toBe(stepTwoReference); await expect.poll(() => fixture.ordersById[ORDER_ID].po, { timeout: 10_000 }).toBe(stepTwoPo); await page.reload(); const reloadedStepThree = page.getByTestId("pos-step-3"); await expect(reloadedStepThree).toBeVisible({ timeout: 10_000 }); await expect(reloadedStepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(stepTwoReference, { timeout: 10_000, }); await expect(reloadedStepThree.getByTestId("pos-order-customer-wishes-po")).toContainText(stepTwoPo, { timeout: 10_000, }); }); test("idle state renders grouped terminal statuses and only keeps ready terminals selectable", async ({ page }) => { const fixture = createCardOrderFixture({ readers: [ { id: "reader_ready", label: "Aabenraa", status: "online", action: null, }, { id: "reader_in_use", label: "Busy reader", status: "online", action: { type: "process_payment_intent" }, }, { id: "reader_offline", label: "Offline reader", status: "offline", action: null, }, ], }); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toContainText("Kortbetaling"); const desktopLayout = stepThree.getByTestId("pos-stripe-desktop-layout"); await expect(desktopLayout).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-terminal-trigger-label")).toContainText("Aabenraa"); await expect(stepThree.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveAttribute("data-status-key", "ready"); await expect(stepThree.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveCSS( "background-color", "rgb(22, 163, 74)" ); const taxSelect = stepThree.getByTestId("pos-stripe-tax-select"); await expect(taxSelect).toHaveValue("1"); await expect(taxSelect).not.toContainText("pos.stripe"); await expect(stepThree).not.toContainText("pos.pay_with_stripe"); const taxField = stepThree.getByTestId("pos-stripe-desktop-tax-field"); const readerField = stepThree.getByTestId("pos-stripe-desktop-reader-field"); const actionField = stepThree.getByTestId("pos-stripe-desktop-action-field"); const [taxBox, readerBox, actionBox] = await Promise.all([ taxField.boundingBox(), readerField.boundingBox(), actionField.boundingBox(), ]); expect(taxBox).not.toBeNull(); expect(readerBox).not.toBeNull(); expect(actionBox).not.toBeNull(); expect(taxBox.y).toBeLessThan(readerBox.y); expect(readerBox.y).toBeLessThan(actionBox.y); expect(Math.abs(taxBox.width - readerBox.width)).toBeLessThan(2); expect(Math.abs(readerBox.width - actionBox.width)).toBeLessThan(2); expect(Math.abs(taxBox.x - readerBox.x)).toBeLessThan(2); expect(Math.abs(readerBox.x - actionBox.x)).toBeLessThan(2); await stepThree.getByTestId("pos-stripe-terminal-trigger").click(); await expect(stepThree.getByTestId("pos-stripe-terminal-menu")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-terminal-group-ready")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-terminal-group-in_use")).toBeVisible(); await expect(stepThree.getByTestId("pos-stripe-terminal-group-offline")).toBeVisible(); const optionOrder = await stepThree .locator('[role="option"][data-testid^="pos-stripe-terminal-option-"]') .evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-testid"))); expect(optionOrder).toEqual([ "pos-stripe-terminal-option-reader_ready", "pos-stripe-terminal-option-reader_in_use", "pos-stripe-terminal-option-reader_offline", ]); await expect(stepThree.getByTestId("pos-stripe-terminal-option-reader_ready")).toHaveAttribute( "aria-disabled", "false" ); await expect(stepThree.getByTestId("pos-stripe-terminal-option-reader_in_use")).toHaveAttribute( "aria-disabled", "true" ); await expect(stepThree.getByTestId("pos-stripe-terminal-option-reader_offline")).toHaveAttribute( "aria-disabled", "true" ); await expect(stepThree.getByTestId("pos-stripe-terminal-option-dot-reader_in_use")).toHaveCSS( "background-color", "rgb(245, 158, 11)" ); await expect(stepThree.getByTestId("pos-stripe-terminal-option-dot-reader_offline")).toHaveCSS( "background-color", "rgb(220, 38, 38)" ); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toBeEnabled(); }); test("existing requires_capture intent on reload auto-captures without showing the capture action", async ({ page, }) => { const fixture = createCardOrderFixture({ paymentIntentsByOrderId: { [ORDER_ID]: createStoredPaymentIntent(ORDER_ID, { status: "requires_capture", }), }, }); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-capture-intent")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-payment-succeeded")).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toHaveCount(0); }); test("existing succeeded intent on reload restores the completion CTA", async ({ page }) => { const fixture = createCardOrderFixture({ paymentIntentsByOrderId: { [ORDER_ID]: createStoredPaymentIntent(ORDER_ID, { status: "succeeded", }), }, }); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-payment-succeeded")).toBeVisible(); await expect(stepThree.getByTestId("pos-next-step")).toBeVisible(); expect(fixture.ordersById[ORDER_ID].completed_at).toBeNull(); }); test("happy path create auto-captures and exposes completion without auto-completing the order", async ({ page }) => { const fixture = createCardOrderFixture(); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toBeVisible(); await stepThree.getByTestId("pos-stripe-create-intent").click(); await expect(stepThree.getByTestId("pos-stripe-capture-intent")).toHaveCount(0); expect(fixture.ordersById[ORDER_ID].completed_at).toBeNull(); await expect(stepThree.getByTestId("pos-stripe-payment-succeeded")).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-next-step")).toBeVisible({ timeout: 10_000 }); await page.waitForTimeout(750); expect(fixture.ordersById[ORDER_ID].completed_at).toBeNull(); }); test("email payment-link controls are hidden in desktop card payment", async ({ page }) => { const fixture = createCardOrderFixture(); const stepThree = await bootDesktopCardPayment(page, fixture); await expect(stepThree.getByTestId("pos-stripe-email-action")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-email-input")).toHaveCount(0); await expect(stepThree.getByTestId("pos-stripe-email-status")).toHaveCount(0); }); });