# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: src/manager/tests/e2e/booking/booking-adjust-move-in-date.spec.ts >> auto-charges the first invoice when the adjusted move-in date activates the booking
- Location: src/manager/tests/e2e/booking/booking-adjust-move-in-date.spec.ts:102:5

# Error details

```
Error: expect(locator).toHaveCount(expected) failed

Locator:  getByTestId('app-page').getByTestId('app-page-content').locator('[data-test-id=app-details-card]:has([data-test-id=title]:has-text("Invoices"))').locator('.ui-data-grid').locator('[data-test-id=ui-data-grid-table]:not(.disabled):not(.loading):not(.fetching)').getByTestId('ui-data-grid-table-with-data').locator('tr')
Expected: 1
Received: 0
Timeout:  60000ms

Call log:
  - Expect "toHaveCount" with timeout 60000ms
  - waiting for getByTestId('app-page').getByTestId('app-page-content').locator('[data-test-id=app-details-card]:has([data-test-id=title]:has-text("Invoices"))').locator('.ui-data-grid').locator('[data-test-id=ui-data-grid-table]:not(.disabled):not(.loading):not(.fetching)').getByTestId('ui-data-grid-table-with-data').locator('tr')
    123 × locator resolved to 0 elements
        - unexpected value "0"

```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - generic [ref=e6]:
    - navigation [ref=e7]:
      - generic [ref=e9]:
        - link [ref=e10] [cursor=pointer]:
          - /url: /
        - generic [ref=e11]:
          - generic [ref=e12]: KINNOVIS
          - generic [ref=e13]: Manager
        - button [ref=e15] [cursor=pointer]
      - list [ref=e19]:
        - generic [ref=e21]: Dashboard
        - link "Tasks" [ref=e23] [cursor=pointer]:
          - /url: /tasks
        - link "Facility maps" [ref=e30] [cursor=pointer]:
          - /url: /facility-map
          - generic [ref=e31]: 󰧾
        - link "Analytics" [ref=e35] [cursor=pointer]:
          - /url: /dashboard
          - generic [ref=e36]: 󱖶
        - generic [ref=e40]: Sales
        - link "Bookings" [ref=e42] [cursor=pointer]:
          - /url: /bookings
          - generic [ref=e43]: 󰇡
        - link "Customers" [ref=e47] [cursor=pointer]:
          - /url: /customers
          - generic [ref=e48]: 󰀏
        - link "Invoices" [ref=e52] [cursor=pointer]:
          - /url: /invoices
          - generic [ref=e53]: 󰷉
        - link "Credit notes" [ref=e57] [cursor=pointer]:
          - /url: /credit-notes
        - link "Units" [ref=e64] [cursor=pointer]:
          - /url: /units
          - generic [ref=e65]: 󰍀
        - generic [ref=e69]: Site management
        - link "Locations" [ref=e71] [cursor=pointer]:
          - /url: /locations
          - generic [ref=e72]: 󰟙
        - link "Unit types" [ref=e76] [cursor=pointer]:
          - /url: /unit-types
          - generic [ref=e77]: 󰆧
        - link "Protection Plans" [ref=e81] [cursor=pointer]:
          - /url: /insurances
          - generic [ref=e82]: 󰳌
        - link "Deposits" [ref=e86] [cursor=pointer]:
          - /url: /deposits
          - generic [ref=e87]: 󱙆
        - link "Products" [ref=e91] [cursor=pointer]:
          - /url: /products
          - generic [ref=e92]: 󰄑
        - link "Discounts" [ref=e96] [cursor=pointer]:
          - /url: /discounts
          - generic [ref=e97]: 󰓼
        - generic [ref=e101]:
          - option "Emails" [ref=e102] [cursor=pointer]:
            - generic [ref=e103]: 󰻨
            - generic [ref=e107]: 󰅀
          - text: 󱡰 󰁥
        - link "Document templates" [ref=e111] [cursor=pointer]:
          - /url: /document-templates
          - generic [ref=e112]: 󰷉
        - generic [ref=e116]: Admin
        - link "Integrations" [ref=e118] [cursor=pointer]:
          - /url: /connected-apps
          - generic [ref=e119]: 󱘖
        - link "User & Roles" [ref=e123] [cursor=pointer]:
          - /url: /users
          - generic [ref=e124]: 󰭘
        - generic [ref=e128]:
          - option "Booking Portal" [ref=e129] [cursor=pointer]:
            - generic [ref=e130]: 󱃁
            - generic [ref=e134]: 󰅀
          - text: 󰖟 󰟙
        - generic [ref=e138]:
          - option "JaneAI" [ref=e139] [cursor=pointer]:
            - generic [ref=e140]: 󱙺
            - generic [ref=e144]: 󰅀
          - text: 󱜹
        - generic [ref=e148]: Feedback
        - link "Voting Portal" [ref=e150] [cursor=pointer]:
          - /url: /voting-portal
          - generic [ref=e151]: 󰔔
      - generic [ref=e157]:
        - button "AS Andreas Stadler andreas.stadler@storeroom.at" [ref=e160] [cursor=pointer]:
          - generic [ref=e161]: AS
          - generic [ref=e164]:
            - generic [ref=e165]: Andreas Stadler
            - generic "andreas.stadler@storeroom.at" [ref=e166]
          - generic [ref=e167]: 󰇙
          - generic: 󰗊 󰅀 󰷖 󰍃
        - button [ref=e168] [cursor=pointer]:
          - generic [ref=e169]: 󰋽
    - main [ref=e171]:
      - generic [ref=e173]:
        - generic [ref=e175]:
          - generic [ref=e176]:
            - link "Bookings" [ref=e177] [cursor=pointer]:
              - /url: /bookings
            - generic [ref=e179]: 󰅂
            - generic [ref=e180]: Booking 7103-0578-2
            - generic [ref=e181]: Active
          - button [ref=e186] [cursor=pointer]:
            - generic [ref=e187]:
              - text: Actions
              - generic [ref=e188]: 󱨉
            - generic: 󱕂
        - generic [ref=e189]:
          - generic [ref=e190]:
            - tablist [ref=e191]:
              - generic [ref=e193]:
                - tab "Details" [selected] [ref=e194] [cursor=pointer]
                - tab "Timeline" [ref=e196] [cursor=pointer]
            - separator [ref=e198]
          - generic [ref=e199]:
            - generic [ref=e200]:
              - generic [ref=e202]:
                - heading "General" [level=3] [ref=e206]
                - table [ref=e209]:
                  - rowgroup [ref=e210]:
                    - row [ref=e211]:
                      - rowheader "Customer" [ref=e212]
                      - cell [ref=e213]:
                        - link "Nichole O'Hara" [ref=e216] [cursor=pointer]:
                          - /url: /customers/1014450428
                    - row [ref=e219]:
                      - rowheader "Location" [ref=e220]
                      - cell [ref=e221]:
                        - link "Vienna South" [ref=e224] [cursor=pointer]:
                          - /url: /locations/5000500050
                    - row [ref=e227]:
                      - rowheader "Created at" [ref=e228]
                      - cell "16 Aug 2026" [ref=e229]
                    - row [ref=e232]:
                      - rowheader "Move-in" [ref=e233]
                      - cell "9 Aug 2026" [ref=e234]
                    - row [ref=e237]:
                      - rowheader "Move-out" [ref=e238]
                      - cell "-" [ref=e239]
                    - row [ref=e242]:
                      - rowheader "Cancelled at" [ref=e243]
                      - cell "-" [ref=e244]
                    - row [ref=e247]:
                      - rowheader "Deposit excl. VAT" [ref=e248]
                      - cell "-" [ref=e249]
                    - row [ref=e252]:
                      - rowheader "Discount" [ref=e253]
                      - cell "-" [ref=e254]
                    - row [ref=e257]:
                      - rowheader "Contract" [ref=e258]
                      - cell "-" [ref=e259]
              - generic [ref=e262]:
                - generic [ref=e263]:
                  - heading "Billing" [level=3] [ref=e267]
                  - table [ref=e270]:
                    - rowgroup [ref=e271]:
                      - row [ref=e272]:
                        - rowheader "Booking Plan" [ref=e273]
                        - cell "Basic (1 week)" [ref=e274]
                      - row [ref=e277]:
                        - rowheader "Current period" [ref=e278]
                        - cell "16 - 23 Aug 2026" [ref=e279]
                      - row [ref=e282]:
                        - rowheader "Amount excl. VAT" [ref=e283]
                        - cell "€50.00" [ref=e284]
                      - row [ref=e287]:
                        - rowheader "Price adjustment" [ref=e288]
                        - cell "-" [ref=e289]
                      - row [ref=e292]:
                        - rowheader "Next invoice" [ref=e293]
                        - cell "23 Aug 2026" [ref=e294]
                      - row [ref=e297]:
                        - rowheader "Payment method" [ref=e298]
                        - cell "•••• 4242 Card" [ref=e299]:
                          - generic [ref=e305]:
                            - generic [ref=e307]: •••• 4242
                            - generic [ref=e308]: Card
                      - row [ref=e309]:
                        - rowheader "Payment option" [ref=e310]
                        - cell "Automatic payment through Stripe" [ref=e311]
                - generic [ref=e314]:
                  - generic [ref=e316]:
                    - heading "Notes" [level=3] [ref=e318]
                    - link [ref=e320] [cursor=pointer]:
                      - /url: /bookings/710305782/edit/notes
                      - generic [ref=e321]: 󰲶
                  - table [ref=e325]:
                    - rowgroup [ref=e326]:
                      - row [ref=e327]:
                        - rowheader "Note" [ref=e328]
                        - cell "-" [ref=e329]
            - generic [ref=e332]:
              - heading "Items" [level=3] [ref=e336]
              - generic [ref=e339]:
                - generic [ref=e340]:
                  - generic [ref=e341]:
                    - link "Fresh Box xAeKmn" [ref=e342] [cursor=pointer]:
                      - /url: /unit-types/607461362
                    - text: /
                    - link "Unit TyII9S" [ref=e345] [cursor=pointer]:
                      - /url: /units/829978768
                  - generic [ref=e348]: €50.00
                - generic [ref=e349]:
                  - generic [ref=e350]:
                    - strong [ref=e351]: Total excl. VAT
                    - strong [ref=e352]: €50.00
                  - generic [ref=e353]:
                    - generic [ref=e354]: VAT
                    - generic [ref=e355]: €8.50
                  - generic [ref=e356]:
                    - strong [ref=e357]: Total incl. VAT
                    - strong [ref=e358]: €58.50
            - generic [ref=e359]:
              - heading "Invoices" [level=3] [ref=e363]
              - generic [ref=e367]:
                - generic [ref=e369]:
                  - generic [ref=e371]:
                    - button "Select all" [ref=e379] [cursor=pointer]:
                      - generic [ref=e380]: 󰄬
                    - generic [ref=e383]:
                      - button "Mark as paid" [disabled] [ref=e386]:
                        - generic [ref=e387]: 󰗡
                      - button "SEPA XML actions" [disabled] [ref=e392]:
                        - generic [ref=e393]: 󱨉
                        - generic: 󰗡 󰅚 󰗖
                      - generic [ref=e398]:
                        - button [ref=e399] [cursor=pointer]
                        - generic: 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲
                  - table [ref=e405]:
                    - rowgroup [ref=e406]:
                      - row [ref=e407]:
                        - columnheader [ref=e408]:
                          - button "Number" [ref=e409] [cursor=pointer]:
                            - generic [ref=e412]: 󰁝
                        - columnheader [ref=e415]:
                          - button "Status" [ref=e416] [cursor=pointer]:
                            - generic [ref=e419]: 󰁝
                        - columnheader [ref=e422]:
                          - button "Payment option" [ref=e423] [cursor=pointer]:
                            - generic [ref=e426]: 󰁝
                        - columnheader [ref=e429]:
                          - button "Due date" [ref=e430] [cursor=pointer]:
                            - generic [ref=e433]: 󰁝
                        - columnheader [ref=e436]:
                          - button "Unit" [ref=e437] [cursor=pointer]:
                            - generic [ref=e440]: 󰁝
                        - columnheader "Discount" [ref=e443]
                        - columnheader [ref=e447]:
                          - button "Total (incl. VAT)" [ref=e448] [cursor=pointer]:
                            - generic [ref=e449]:
                              - generic [ref=e450]: Total (incl. VAT)
                              - generic [ref=e451]: 󰋽
                            - generic [ref=e453]: 󰁝
                        - columnheader [ref=e456]:
                          - button "Amount due" [ref=e457] [cursor=pointer]:
                            - generic [ref=e460]: 󰁝
                        - columnheader [ref=e463]:
                          - button "Issue date" [ref=e464] [cursor=pointer]:
                            - generic [ref=e467]: 󰁝
                        - columnheader "Actions" [ref=e470]
                      - row [ref=e474]:
                        - columnheader [ref=e475]
                    - rowgroup [ref=e476]:
                      - row [ref=e477]:
                        - cell "No data yet" [ref=e478]
                - generic [ref=e482]:
                  - generic [ref=e483]:
                    - generic [ref=e484]: "Items per page:"
                    - combobox [ref=e487]:
                      - generic [ref=e489] [cursor=pointer]:
                        - generic [ref=e490]: "10"
                        - combobox "Items per page:": "10"
                      - generic [ref=e492]: 󰍝
                  - generic [ref=e494]: 0-0 of 0
                  - generic [ref=e495]:
                    - button [disabled]:
                      - generic: 󰘀
                    - button [disabled]:
                      - generic: 󰅁
                    - button [disabled]:
                      - generic: 󰅂
                    - button [disabled]:
                      - generic: 󰘁
            - generic [ref=e496]:
              - heading "Payments" [level=3] [ref=e500]
              - generic [ref=e504]:
                - table [ref=e508]:
                  - rowgroup [ref=e509]:
                    - row [ref=e510]:
                      - columnheader [ref=e511]:
                        - button "Total" [ref=e512] [cursor=pointer]:
                          - generic [ref=e515]: 󰁝
                      - columnheader [ref=e518]:
                        - button "Status" [ref=e519] [cursor=pointer]:
                          - generic [ref=e522]: 󰁝
                      - columnheader [ref=e525]:
                        - button "Invoice no." [ref=e526] [cursor=pointer]:
                          - generic [ref=e529]: 󰁝
                      - columnheader [ref=e532]:
                        - button "Date" [ref=e533] [cursor=pointer]:
                          - generic [ref=e536]: 󰁝
                      - columnheader [ref=e539]:
                        - button "Payment method" [ref=e540] [cursor=pointer]:
                          - generic [ref=e543]: 󰁝
                    - row [ref=e546]:
                      - columnheader [ref=e547]
                  - rowgroup [ref=e548]:
                    - row [ref=e549]:
                      - cell "No data yet" [ref=e550]
                - generic [ref=e554]:
                  - generic [ref=e555]:
                    - generic [ref=e556]: "Items per page:"
                    - combobox [ref=e559]:
                      - generic [ref=e561] [cursor=pointer]:
                        - generic [ref=e562]: "10"
                        - combobox "Items per page:": "10"
                      - generic [ref=e564]: 󰍝
                  - generic [ref=e566]: 0-0 of 0
                  - generic [ref=e567]:
                    - button [disabled]:
                      - generic: 󰘀
                    - button [disabled]:
                      - generic: 󰅁
                    - button [disabled]:
                      - generic: 󰅂
                    - button [disabled]:
                      - generic: 󰘁
  - generic:
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
    - tooltip
```

# Test source

```ts
  1   | import { BookingItemsDetails, BookingPriceChangeDetails } from '@/manager/modules/booking/booking-types';
  2   | import { getBookingDepositInvoiceNumber } from '@/manager/modules/booking/booking-utils';
  3   | import { BookingDetailsItemsSection } from '@/manager/modules/booking/components/BookingDetailsItemsSection';
  4   | import { BookingDetailsPage } from '@/manager/modules/booking/views/BookingDetailsPage';
  5   | import {
  6   |   bookingPaymentTableColumnTestIds,
  7   |   invoiceTableColumnTestIds,
  8   | } from '@/manager/modules/ui/data-table/data/data-table-column-test-ids';
  9   | import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
  10  | import { formatCurrency, formatDate } from '@/shared/utils/formatters';
  11  | import { expect } from '@/shared/utils/matchers';
  12  | 
  13  | export async function verifyBookingDetailsItemsSection(
  14  |   section: BookingDetailsItemsSection,
  15  |   data: BookingItemsDetails,
  16  |   currency: string
  17  | ): Promise<void> {
  18  |   await expectSingleLocatorToHaveText(section.unitTypeLabel, data.unitType.name);
  19  |   await expectSingleLocatorToHaveText(section.unitLabel, data.unitType.unitName);
  20  |   await expectSingleLocatorToHaveText(
  21  |     section.unitTypePriceExclVat,
  22  |     formatCurrency(data.unitType.priceExclVat, currency)
  23  |   );
  24  | 
  25  |   if (data.insurance) {
  26  |     await expectSingleLocatorToHaveText(section.insuranceLabel, data.insurance.name);
  27  |     await expectSingleLocatorToHaveText(
  28  |       section.insurancePriceExclVat,
  29  |       formatCurrency(data.insurance.priceExclVat, currency)
  30  |     );
  31  |   } else {
  32  |     await expect(section.insuranceLabel).toBeHidden();
  33  |     await expect(section.insurancePriceExclVat).toBeHidden();
  34  |   }
  35  | 
  36  |   // The SPA renders recurring product rows in the order they were added to the booking (the API
  37  |   // orders by the booking_has_recurring_products pivot id — see orderByPivot('id')). That insertion
  38  |   // order matches data.recurringProducts, so iterate it as-is; sorting by product id is wrong because
  39  |   // parallel test setup assigns product ids in a non-deterministic order. The per-piece price line is
  40  |   // only shown for multi-piece items, so it is tracked with its own running index.
  41  |   const recurringProducts = data.recurringProducts;
  42  |   let perPieceIndex = 0;
  43  | 
  44  |   for (const [index, product] of recurringProducts.entries()) {
  45  |     // The label drops the "{quantity} x" prefix when only a single piece is booked.
  46  |     const label = product.quantity > 1 ? `${product.quantity} x ${product.name}` : product.name;
  47  |     await expectSingleLocatorToHaveText(section.recurringProductLabel.nth(index), label);
  48  |     await expectSingleLocatorToHaveText(
  49  |       section.recurringProductPriceExclVat.nth(index),
  50  |       formatCurrency(product.priceExclVat, currency)
  51  |     );
  52  | 
  53  |     if (product.quantity > 1) {
  54  |       await expectSingleLocatorToHaveText(
  55  |         section.recurringProductPricePerPiece.nth(perPieceIndex),
  56  |         `${formatCurrency(product.perPieceExclVat, currency)} per piece`
  57  |       );
  58  |       perPieceIndex++;
  59  |     }
  60  |   }
  61  | 
  62  |   await expectSingleLocatorToHaveText(section.totalExclVatValue, formatCurrency(data.totals.exclVat, currency));
  63  |   await expectSingleLocatorToHaveText(section.vatValue, formatCurrency(data.totals.vat, currency));
  64  |   await expectSingleLocatorToHaveText(section.totalInclVatValue, formatCurrency(data.totals.inclVat, currency));
  65  | }
  66  | 
  67  | // Invoices are issued by an asynchronous backend job after the booking is created, and the invoices
  68  | // card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a
  69  | // settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the issued
  70  | // invoices render. The window is deliberately bounded: if issuance doesn't land within it (e.g. the
  71  | // billing job lagging under load), that is a real backend-latency signal — don't paper over it by
  72  | // bumping the timeout.
  73  | export async function verifyBookingInvoiceCount(detailsPage: BookingDetailsPage, expected: number): Promise<void> {
> 74  |   await expect(detailsPage.invoicesCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 });
      |                                                              ^ Error: expect(locator).toHaveCount(expected) failed
  75  | }
  76  | 
  77  | // Payments are recorded by the same asynchronous backend job that issues the invoices, and the payments
  78  | // card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a
  79  | // settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the payments
  80  | // render. The window is deliberately bounded: if the payment doesn't land within it (e.g. the billing
  81  | // job lagging under load), that is a real backend-latency signal — don't paper over it by bumping the timeout.
  82  | export async function verifyBookingPaymentCount(detailsPage: BookingDetailsPage, expected: number): Promise<void> {
  83  |   await expect(detailsPage.paymentsCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 });
  84  | }
  85  | 
  86  | // The whole price adjustment scene in the billing card: chip, new subscription amount, targets, and the
  87  | // snapped apply-from date. The SPA polls the booking while has_pending_changes is set, so the section
  88  | // appears without a reload once the backend has persisted the change.
  89  | export async function verifyBookingPriceChangeSection(
  90  |   detailsPage: BookingDetailsPage,
  91  |   expected: BookingPriceChangeDetails,
  92  |   currency: string,
  93  |   timezone: string
  94  | ): Promise<void> {
  95  |   const section = detailsPage.billingCard.priceChangeSection;
  96  | 
  97  |   await expectSingleLocatorToHaveText(section.chip, expected.chip);
  98  |   await expectSingleLocatorToHaveText(
  99  |     section.newAmount,
  100 |     `New amount excl. VAT: ${formatCurrency(expected.newAmountExclVat, currency)}`
  101 |   );
  102 |   await expectSingleLocatorToHaveText(section.appliedOn, expected.appliedOn);
  103 |   await expectSingleLocatorToHaveText(
  104 |     section.appliedFrom,
  105 |     `applied from ${formatDate(expected.appliedFrom, timezone)}`
  106 |   );
  107 | }
  108 | 
  109 | // Asserts one invoice row in full. Pass `numberColumnText` to locate the row in the number column — an
  110 | // invoice number for a regular invoice, or the "Deposit" chip label for the deposit invoice (whose number
  111 | // cell also holds the chip, so it can't be matched by number) — needed when a booking has several invoices.
  112 | // Omit it for a booking with a single invoice, where it falls back to the first row. Covers every column
  113 | // the row carries besides the number itself. `unit` is optional — omit it for an invoice not tied to a
  114 | // unit (e.g. the upfront-deposit invoice), where the unit cell asserts empty.
  115 | export async function verifyBookingInvoice(
  116 |   detailsPage: BookingDetailsPage,
  117 |   expected: {
  118 |     status: string;
  119 |     paymentOption: string;
  120 |     unit?: string;
  121 |     amount: string;
  122 |     amountDue: string;
  123 |     issueDate: string;
  124 |     dueDate: string;
  125 |   },
  126 |   numberColumnText?: string
  127 | ): Promise<void> {
  128 |   const invoices = detailsPage.invoicesCard.dataTable;
  129 |   const cell = (columnId: string) =>
  130 |     numberColumnText
  131 |       ? invoices.getRowColumnBy([{ columnId: invoiceTableColumnTestIds.id, text: numberColumnText }], columnId)
  132 |       : invoices.getRowColumnByIndex(0, columnId);
  133 | 
  134 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.status), expected.status);
  135 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.paymentOption), expected.paymentOption);
  136 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.unit), expected.unit);
  137 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amount), expected.amount);
  138 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amountDue), expected.amountDue);
  139 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.issueDate), expected.issueDate);
  140 |   await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.dueDate), expected.dueDate);
  141 | }
  142 | 
  143 | // A booking has one payment row per invoice in the payments grid, keyed by invoice number. Pass an
  144 | // invoiceNumber to scope the check to that invoice's payment — needed when a booking has several invoices
  145 | // (e.g. an upfront deposit's invoice alongside the first-period one), each with its own payment; omit it
  146 | // for a booking with a single payment. Asserts the row's gross amount, status, payment method, and date.
  147 | // Omit paymentMethod for an unpaid payment (e.g. a manual invoice's "Not paid yet" row): no method has
  148 | // been charged yet, so the cell is empty and the assertion checks it renders blank.
  149 | export async function verifyBookingPayment(
  150 |   detailsPage: BookingDetailsPage,
  151 |   expected: { amount: string; status: string; paymentMethod?: string; date: string },
  152 |   invoiceNumber?: string
  153 | ): Promise<void> {
  154 |   const payments = detailsPage.paymentsCard.dataTable;
  155 |   const cell = (columnId: string) =>
  156 |     invoiceNumber
  157 |       ? payments.getRowColumnBy(
  158 |           [{ columnId: bookingPaymentTableColumnTestIds.invoiceId, text: invoiceNumber }],
  159 |           columnId
  160 |         )
  161 |       : payments.getRowColumnByIndex(0, columnId);
  162 | 
  163 |   await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.amountInclVat), expected.amount);
  164 |   await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.status), expected.status);
  165 |   await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.paymentMethod), expected.paymentMethod);
  166 |   await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.date), expected.date);
  167 | }
  168 | 
  169 | // The deposit field renders its amount as a link to the deposit invoice, but the link is only hydrated
  170 | // on a fresh load, not right after the booking is created. So grep the invoice's number from the grid,
  171 | // reload to mount the link, then assert the field reads "{depositAmount} ({number})" with the number
  172 | // rendered as the invoice link.
  173 | export async function verifyBookingDepositInvoiceLink(
  174 |   detailsPage: BookingDetailsPage,
```