Files
pleno-vue/tests/e2e/pos-mobile-card-payments.spec.js
T
Jeppe B 4f26ddd2cc Remove manual Stripe payment actions (#233)
## Summary

- remove hosted email-payment/payment-link creation from POS and order
management
- automatically capture authorized card payments instead of exposing a
manual capture action
- replace provider-specific Stripe wording with card-terminal
terminology
- retain ordinary terminal payment, receipts, order completion, and
navigation
- add focused unit and Playwright regression coverage

Paired API change: https://github.com/copenhagentruckwash/api/pull/327

## Verification

- focused frontend unit tests: 8/8 passed
- desktop card-payment Playwright: 4/4 passed
- mobile card-payment Playwright: 15/15 passed
- admin order action rail Playwright: 3/3 passed
- ESLint, translation generation/checks, build, and diff checks passed
- all 18 visual states passed their relevant DOM assertions

## Visual change previews

### View: Regular POS card-payment view

**Description:** The hosted email-payment action is removed and
provider-specific wording becomes generic card-terminal wording.

#### Mobile

**Before:** ![POS card payment before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-pos-card-payment-mobile.png)

**After:** ![POS card payment after
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-pos-card-payment-mobile.png)

#### Tablet

**Before:** ![POS card payment before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-pos-card-payment-tablet.png)

**After:** ![POS card payment after
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-pos-card-payment-tablet.png)

#### Desktop

**Before:** ![POS card payment before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-pos-card-payment-desktop.png)

**After:** ![POS card payment after
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-pos-card-payment-desktop.png)

### View: Authorized payment capture

**Description:** The manual capture action is replaced by an
automatically initiated capture and its in-progress state.

#### Mobile

**Before:** ![Payment capture before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-payment-capture-mobile.png)

**After:** ![Payment capture after
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-payment-capture-mobile.png)

#### Tablet

**Before:** ![Payment capture before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-payment-capture-tablet.png)

**After:** ![Payment capture after
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-payment-capture-tablet.png)

#### Desktop

**Before:** ![Payment capture before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-payment-capture-desktop.png)

**After:** ![Payment capture after
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-payment-capture-desktop.png)

### View: Order-dashboard action rail

**Description:** The hosted Stripe invoice/payment-link action is
removed while normal order actions remain.

#### Mobile

**Before:** ![Order action rail before
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-order-dashboard-mobile.png)

**After:** ![Order action rail after
mobile](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-order-dashboard-mobile.png)

#### Tablet

**Before:** ![Order action rail before
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-order-dashboard-tablet.png)

**After:** ![Order action rail after
tablet](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-order-dashboard-tablet.png)

#### Desktop

**Before:** ![Order action rail before
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/before-order-dashboard-desktop.png)

**After:** ![Order action rail after
desktop](https://raw.githubusercontent.com/copenhagentruckwash/pleno-vue/6ea832ae507a81e010b599fb52bdb1b375b5431c/docs/pr-previews/stripe-cleanup/after-order-dashboard-desktop.png)

## Residual risk

No live Stripe Terminal hardware or production Stripe account was used.
Browser behavior is verified with repository-owned mocks; the API suite
verifies route and payment-intent lifecycle wiring.
2026-07-27 19:09:40 +02:00

581 lines
21 KiB
JavaScript

import { expect, test } from "@playwright/test";
import {
CARD_CUSTOMER_ID,
DEFAULT_ORDER_ID,
REGULAR_CUSTOMER_ID,
createMobilePosFixture,
createPaymentIntent,
getStoredPosSnapshot,
setupMobilePosPage,
suppressVueDevtoolsOverlay,
waitForMobileNextStepCooldown,
} from "./support/mobilePos.js";
test.describe("POS mobile card payments", () => {
test.beforeEach(async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-mobile", "Mobile card payment suite is scoped to chromium-mobile.");
await suppressVueDevtoolsOverlay(page);
});
test("card mode entry sets customer 999 and preserves order context", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-entry-token",
seedState: {
customerId: null,
reg: "ZZ00000",
reference: "CARD-CTX-REF",
includePrimaryItem: false,
vehicleType: 53,
},
route: {
step: 1,
},
});
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-direct-card-payment")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-direct-card-payment").click();
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 });
await expect
.poll(
async () => {
const snapshot = await getStoredPosSnapshot(page);
return snapshot?.metadata?.customerId ?? null;
},
{ timeout: 10_000 }
)
.toBe(CARD_CUSTOMER_ID);
const snapshot = await getStoredPosSnapshot(page);
expect(snapshot?.vehicles?.vehicle_1?.reg).toBe("ZZ00000");
expect(snapshot?.metadata?.reference).toBe("CARD-CTX-REF");
});
test("card mode stays disabled when no stripe readers are assigned and re-enables after polling", async ({
page,
}) => {
const fixture = createMobilePosFixture({
readers: [],
});
await setupMobilePosPage(page, fixture, {
token: "mobile-card-reader-polling-token",
seedState: {
customerId: null,
reg: "ZZ00000",
reference: "CARD-POLL-REF",
includePrimaryItem: false,
vehicleType: 53,
},
route: {
step: 1,
},
});
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-next-step").click();
const cardButton = page.getByTestId("pos-mobile-direct-card-payment");
await expect(cardButton).toBeVisible({ timeout: 10_000 });
await expect.poll(() => fixture.requestCounters.readersGet, { timeout: 10_000 }).toBeGreaterThan(0);
await expect(cardButton).toBeDisabled();
fixture.readers = [
{
id: "reader_online_2",
label: "Recovered Reader",
status: "online",
action: null,
},
];
await expect
.poll(
async () => {
return await cardButton.isEnabled();
},
{ timeout: 12_000 }
)
.toBe(true);
await cardButton.click();
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 });
await expect
.poll(
async () => {
const snapshot = await getStoredPosSnapshot(page);
return snapshot?.metadata?.customerId ?? null;
},
{ timeout: 10_000 }
)
.toBe(CARD_CUSTOMER_ID);
});
test("@smoke direct card flow bridges step 1 through automatic capture and resets to scanner", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-bridge-token",
seedState: {
customerId: null,
reg: "CD99999",
reference: "CARD-BRIDGE-REF",
includePrimaryItem: false,
vehicleType: 53,
},
route: {
step: 1,
},
});
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-direct-card-payment")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-direct-card-payment").click();
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await waitForMobileNextStepCooldown(page);
await page.getByTestId("pos-mobile-next-step").click();
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-create-intent").click();
await expect(page.getByTestId("pos-stripe-capture-intent")).toHaveCount(0);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
await expect
.poll(
() => {
const currentUrl = new URL(page.url());
return currentUrl.searchParams.get("step");
},
{ timeout: 12_000 }
)
.toBe("1");
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
});
test("step 2 transitions to mobile card payment step 3 for customer 999", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-step-transition-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
primaryItemId: 53,
},
route: {
step: 2,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_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: 10_000 }).toBe(1);
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await expect(page).toHaveURL(/step=3/);
expect(fixture.requestCounters.markAsCompleted).toBe(0);
});
test("reader unavailable state is recoverable and non-fatal", async ({ page }) => {
const fixture = createMobilePosFixture({
readers: [
{
id: "reader_in_use",
label: "Busy reader",
status: "online",
action: { type: "process_payment_intent" },
},
{
id: "reader_offline",
label: "Offline reader",
status: "offline",
action: null,
},
],
});
await setupMobilePosPage(page, fixture, {
token: "mobile-card-reader-unavailable-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-no-readers")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-reader-unavailable-message")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-create-intent")).toHaveCount(0);
await expect(page.getByTestId("pos-stripe-error-message")).toHaveCount(0);
await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveAttribute("data-status-key", "in_use");
await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveCSS(
"background-color",
"rgb(245, 158, 11)"
);
await page.getByTestId("pos-stripe-terminal-trigger").click();
await expect(page.getByTestId("pos-stripe-terminal-menu")).toBeVisible();
await expect(page.getByTestId("pos-stripe-terminal-group-in_use")).toBeVisible();
await expect(page.getByTestId("pos-stripe-terminal-group-offline")).toBeVisible();
await expect(page.getByTestId("pos-stripe-terminal-option-reader_in_use")).toHaveAttribute("aria-disabled", "true");
await expect(page.getByTestId("pos-stripe-terminal-option-reader_offline")).toHaveAttribute(
"aria-disabled",
"true"
);
await expect(page.getByTestId("pos-stripe-terminal-option-dot-reader_offline")).toHaveCSS(
"background-color",
"rgb(220, 38, 38)"
);
fixture.readers = [
{
id: "reader_in_use",
label: "Busy reader",
status: "online",
action: { type: "process_payment_intent" },
},
{
id: "reader_offline",
label: "Offline reader",
status: "offline",
action: null,
},
{
id: "reader_online_2",
label: "Recovered Reader",
status: "online",
action: null,
},
];
await page.getByTestId("pos-stripe-no-readers").click();
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-selected-reader")).toContainText("Recovered Reader");
await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveAttribute("data-status-key", "ready");
await expect(page.getByTestId("pos-stripe-terminal-trigger-dot")).toHaveCSS("background-color", "rgb(22, 163, 74)");
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
});
test("reader available creates intent once and automatically captures", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-create-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled();
await page.getByTestId("pos-stripe-create-intent").click();
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(1);
await expect(page.getByTestId("pos-stripe-capture-intent")).toHaveCount(0);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
});
test("email card payment action is not shown", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-email-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-stripe-email-action")).toHaveCount(0);
expect(fixture.requestCounters.stripeInvoiceSend).toBe(0);
});
test("step 3 back is disabled once card authorization auto-captures", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-cancel-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-create-intent").click();
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
await expect(page.locator(".swal2-popup")).toHaveCount(0);
await expect(page.getByTestId("pos-mobile-step-3-back")).toBeDisabled();
expect(fixture.requestCounters.paymentIntentDelete).toBe(0);
});
test("automatic capture success completes once and resets scanner flow", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-capture-success-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-create-intent").click();
await expect(page.getByTestId("pos-stripe-capture-intent")).toHaveCount(0);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
await expect
.poll(
() => {
const currentUrl = new URL(page.url());
return currentUrl.searchParams.get("step");
},
{ timeout: 12_000 }
)
.toBe("1");
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
});
test("guardrail: completion never runs before capture success", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-guardrail-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-create-intent").click();
await expect(page.getByTestId("pos-stripe-capture-intent")).toHaveCount(0);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
});
test("step 3 bootstrap recovery fetches existing requires_capture intent", async ({ page }) => {
const fixture = createMobilePosFixture({
paymentIntentsByOrderId: {
[DEFAULT_ORDER_ID]: createPaymentIntent(DEFAULT_ORDER_ID, {
status: "requires_capture",
}),
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-card-recovery-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await expect.poll(() => fixture.requestCounters.paymentIntentGet, { timeout: 10_000 }).toBeGreaterThan(0);
await expect(page.getByTestId("pos-stripe-capture-intent")).toHaveCount(0);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
});
test("step 3 bootstrap recovery completes already succeeded intent once", async ({ page }) => {
const fixture = createMobilePosFixture({
paymentIntentsByOrderId: {
[DEFAULT_ORDER_ID]: createPaymentIntent(DEFAULT_ORDER_ID, {
status: "succeeded",
}),
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-card-succeeded-recovery-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await expect.poll(() => fixture.requestCounters.paymentIntentGet, { timeout: 10_000 }).toBeGreaterThan(0);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
expect(fixture.requestCounters.paymentIntentCapture).toBe(0);
await expect
.poll(
() => {
const currentUrl = new URL(page.url());
return currentUrl.searchParams.get("step");
},
{ timeout: 12_000 }
)
.toBe("1");
});
test("returning to step 2 and re-entering step 3 does not duplicate order items", async ({ page }) => {
const fixture = createMobilePosFixture();
await setupMobilePosPage(page, fixture, {
token: "mobile-card-step-reentry-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 2,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_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: 10_000 }).toBe(1);
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-step-3-back").click();
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await page.waitForTimeout(2100);
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
await expect.poll(() => fixture.requestCounters.orderItemsGet, { timeout: 10_000 }).toBeGreaterThan(0);
});
test("payment-intent get/create/capture failures surface retryable error states", async ({ page }) => {
const fixture = createMobilePosFixture({
failureBudget: {
paymentIntentGet: 0,
paymentIntentCreate: 2,
paymentIntentCapture: 2,
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-card-error-recovery-token",
seedState: {
customerId: CARD_CUSTOMER_ID,
includePrimaryItem: true,
},
route: {
step: 3,
orderId: DEFAULT_ORDER_ID,
customerId: CARD_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-create-intent").click();
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(2);
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
fixture.failureBudget.paymentIntentGet = 2;
await page.getByTestId("pos-stripe-error").click();
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-error").click();
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-create-intent").click();
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(3);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(2);
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-stripe-error").click();
await expect(page.getByTestId("pos-stripe-capture-intent")).toHaveCount(0);
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(3);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
});
test("regression: non-card mobile customer still follows step-2 complete/reset path", async ({ page }) => {
const regularCustomerId = REGULAR_CUSTOMER_ID;
const fixture = createMobilePosFixture({
ordersById: {
[DEFAULT_ORDER_ID]: {
id: DEFAULT_ORDER_ID,
customer_id: regularCustomerId,
department_id: 1,
reference: "REF-9201",
notes: "",
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",
},
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-non-card-regression-token",
seedState: {
customerId: regularCustomerId,
includePrimaryItem: true,
},
route: {
step: 2,
orderId: DEFAULT_ORDER_ID,
customerId: regularCustomerId,
},
});
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: 10_000 }).toBe(1);
await expect
.poll(
() => {
const currentUrl = new URL(page.url());
return currentUrl.searchParams.get("step");
},
{ timeout: 12_000 }
)
.toBe("1");
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
await expect(page.getByTestId("pos-mobile-step-3")).toHaveCount(0);
});
});