# 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 >> adjusts the move-in date of a scheduled booking to today
- Location: src/manager/tests/e2e/booking/booking-adjust-move-in-date.spec.ts:30:7

# 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("Payments"))').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("Payments"))').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=f2e1]:
  - generic [ref=f2e6]:
    - navigation [ref=f2e7]:
      - generic [ref=f2e9]:
        - link [ref=f2e10] [cursor=pointer]:
          - /url: /
        - generic [ref=f2e11]:
          - generic [ref=f2e12]: KINNOVIS
          - generic [ref=f2e13]: Manager
        - button [ref=f2e15] [cursor=pointer]
      - list [ref=f2e19]:
        - generic [ref=f2e21]: Dashboard
        - link "Tasks" [ref=f2e23] [cursor=pointer]:
          - /url: /tasks
        - link "Facility maps" [ref=f2e30] [cursor=pointer]:
          - /url: /facility-map
          - generic [ref=f2e31]: 󰧾
        - link "Analytics" [ref=f2e35] [cursor=pointer]:
          - /url: /dashboard
          - generic [ref=f2e36]: 󱖶
        - generic [ref=f2e40]: Sales
        - link "Bookings" [ref=f2e42] [cursor=pointer]:
          - /url: /bookings
          - generic [ref=f2e43]: 󰇡
        - link "Customers" [ref=f2e47] [cursor=pointer]:
          - /url: /customers
          - generic [ref=f2e48]: 󰀏
        - link "Invoices" [ref=f2e52] [cursor=pointer]:
          - /url: /invoices
          - generic [ref=f2e53]: 󰷉
        - link "Credit notes" [ref=f2e57] [cursor=pointer]:
          - /url: /credit-notes
        - link "Units" [ref=f2e64] [cursor=pointer]:
          - /url: /units
          - generic [ref=f2e65]: 󰍀
        - generic [ref=f2e69]: Site management
        - link "Locations" [ref=f2e71] [cursor=pointer]:
          - /url: /locations
          - generic [ref=f2e72]: 󰟙
        - link "Unit types" [ref=f2e76] [cursor=pointer]:
          - /url: /unit-types
          - generic [ref=f2e77]: 󰆧
        - link "Protection Plans" [ref=f2e81] [cursor=pointer]:
          - /url: /insurances
          - generic [ref=f2e82]: 󰳌
        - link "Deposits" [ref=f2e86] [cursor=pointer]:
          - /url: /deposits
          - generic [ref=f2e87]: 󱙆
        - link "Products" [ref=f2e91] [cursor=pointer]:
          - /url: /products
          - generic [ref=f2e92]: 󰄑
        - link "Discounts" [ref=f2e96] [cursor=pointer]:
          - /url: /discounts
          - generic [ref=f2e97]: 󰓼
        - generic [ref=f2e101]:
          - option "Emails" [ref=f2e102] [cursor=pointer]:
            - generic [ref=f2e103]: 󰻨
            - generic [ref=f2e107]: 󰅀
          - text: 󱡰 󰁥
        - link "Document templates" [ref=f2e111] [cursor=pointer]:
          - /url: /document-templates
          - generic [ref=f2e112]: 󰷉
        - generic [ref=f2e116]: Admin
        - link "Integrations" [ref=f2e118] [cursor=pointer]:
          - /url: /connected-apps
          - generic [ref=f2e119]: 󱘖
        - link "User & Roles" [ref=f2e123] [cursor=pointer]:
          - /url: /users
          - generic [ref=f2e124]: 󰭘
        - generic [ref=f2e128]:
          - option "Booking Portal" [ref=f2e129] [cursor=pointer]:
            - generic [ref=f2e130]: 󱃁
            - generic [ref=f2e134]: 󰅀
          - text: 󰖟 󰟙
        - generic [ref=f2e138]:
          - option "JaneAI" [ref=f2e139] [cursor=pointer]:
            - generic [ref=f2e140]: 󱙺
            - generic [ref=f2e144]: 󰅀
          - text: 󱜹
        - generic [ref=f2e148]: Feedback
        - link "Voting Portal" [ref=f2e150] [cursor=pointer]:
          - /url: /voting-portal
          - generic [ref=f2e151]: 󰔔
      - generic [ref=f2e157]:
        - button "AS Andreas Stadler andreas.stadler@storeroom.at" [ref=f2e160] [cursor=pointer]:
          - generic [ref=f2e161]: AS
          - generic [ref=f2e164]:
            - generic [ref=f2e165]: Andreas Stadler
            - generic "andreas.stadler@storeroom.at" [ref=f2e166]
          - generic [ref=f2e167]: 󰇙
          - generic: 󰗊 󰅀 󰷖 󰍃
        - button [ref=f2e168] [cursor=pointer]:
          - generic [ref=f2e169]: 󰋽
    - main [ref=f2e171]:
      - generic [ref=f2e173]:
        - generic [ref=f2e175]:
          - generic [ref=f2e176]:
            - link "Bookings" [ref=f2e177] [cursor=pointer]:
              - /url: /bookings
            - generic [ref=f2e179]: 󰅂
            - generic [ref=f2e180]: Booking 1466-3762-43
            - generic [ref=f2e181]: Active
          - button [ref=f2e186] [cursor=pointer]:
            - generic [ref=f2e187]:
              - text: Actions
              - generic [ref=f2e188]: 󱨉
            - generic: 󱕂
        - generic [ref=f2e189]:
          - generic [ref=f2e190]:
            - tablist [ref=f2e191]:
              - generic [ref=f2e193]:
                - tab "Details" [selected] [ref=f2e194] [cursor=pointer]
                - tab "Timeline" [ref=f2e196] [cursor=pointer]
            - separator [ref=f2e198]
          - generic [ref=f2e199]:
            - generic [ref=f2e200]:
              - generic [ref=f2e202]:
                - heading "General" [level=3] [ref=f2e206]
                - table [ref=f2e209]:
                  - rowgroup [ref=f2e210]:
                    - row [ref=f2e211]:
                      - rowheader "Customer" [ref=f2e212]
                      - cell [ref=f2e213]:
                        - link "Fay Shields" [ref=f2e216] [cursor=pointer]:
                          - /url: /customers/1931953252
                    - row [ref=f2e219]:
                      - rowheader "Location" [ref=f2e220]
                      - cell [ref=f2e221]:
                        - link "Vienna South" [ref=f2e224] [cursor=pointer]:
                          - /url: /locations/5000500050
                    - row [ref=f2e227]:
                      - rowheader "Created at" [ref=f2e228]
                      - cell "19 Aug 2026" [ref=f2e229]
                    - row [ref=f2e232]:
                      - rowheader "Move-in" [ref=f2e233]
                      - cell "19 Aug 2026" [ref=f2e234]
                    - row [ref=f2e237]:
                      - rowheader "Move-out" [ref=f2e238]
                      - cell "-" [ref=f2e239]
                    - row [ref=f2e242]:
                      - rowheader "Cancelled at" [ref=f2e243]
                      - cell "-" [ref=f2e244]
                    - row [ref=f2e247]:
                      - rowheader "Deposit excl. VAT" [ref=f2e248]
                      - cell "-" [ref=f2e249]
                    - row [ref=f2e252]:
                      - rowheader "Discount" [ref=f2e253]
                      - cell "-" [ref=f2e254]
                    - row [ref=f2e257]:
                      - rowheader "Contract" [ref=f2e258]
                      - cell "-" [ref=f2e259]
              - generic [ref=f2e262]:
                - generic [ref=f2e263]:
                  - heading "Billing" [level=3] [ref=f2e267]
                  - table [ref=f2e270]:
                    - rowgroup [ref=f2e271]:
                      - row [ref=f2e272]:
                        - rowheader "Booking Plan" [ref=f2e273]
                        - cell "Basic (1 week)" [ref=f2e274]
                      - row [ref=f2e277]:
                        - rowheader "Current period" [ref=f2e278]
                        - cell "19 - 26 Aug 2026" [ref=f2e279]
                      - row [ref=f2e282]:
                        - rowheader "Amount excl. VAT" [ref=f2e283]
                        - cell "€50.00" [ref=f2e284]
                      - row [ref=f2e287]:
                        - rowheader "Price adjustment" [ref=f2e288]
                        - cell "-" [ref=f2e289]
                      - row [ref=f2e292]:
                        - rowheader "Next invoice" [ref=f2e293]
                        - cell "26 Aug 2026" [ref=f2e294]
                      - row [ref=f2e297]:
                        - rowheader "Payment method" [ref=f2e298]
                        - cell "-" [ref=f2e299]
                      - row [ref=f2e302]:
                        - rowheader "Payment option" [ref=f2e303]
                        - cell "Automatic payment through Stripe" [ref=f2e304]
                - generic [ref=f2e307]:
                  - generic [ref=f2e309]:
                    - heading "Notes" [level=3] [ref=f2e311]
                    - link [ref=f2e313] [cursor=pointer]:
                      - /url: /bookings/1466376243/edit/notes
                      - generic [ref=f2e314]: 󰲶
                  - table [ref=f2e318]:
                    - rowgroup [ref=f2e319]:
                      - row [ref=f2e320]:
                        - rowheader "Note" [ref=f2e321]
                        - cell "-" [ref=f2e322]
            - generic [ref=f2e325]:
              - heading "Items" [level=3] [ref=f2e329]
              - generic [ref=f2e332]:
                - generic [ref=f2e333]:
                  - generic [ref=f2e334]:
                    - link "Sleek Box VbHBZ5" [ref=f2e335] [cursor=pointer]:
                      - /url: /unit-types/285421349
                    - text: /
                    - link "Unit HMO6HO" [ref=f2e338] [cursor=pointer]:
                      - /url: /units/1569201569
                  - generic [ref=f2e341]: €50.00
                - generic [ref=f2e342]:
                  - generic [ref=f2e343]:
                    - strong [ref=f2e344]: Total excl. VAT
                    - strong [ref=f2e345]: €50.00
                  - generic [ref=f2e346]:
                    - generic [ref=f2e347]: VAT
                    - generic [ref=f2e348]: €8.50
                  - generic [ref=f2e349]:
                    - strong [ref=f2e350]: Total incl. VAT
                    - strong [ref=f2e351]: €58.50
            - generic [ref=f2e352]:
              - heading "Invoices" [level=3] [ref=f2e356]
              - generic [ref=f2e360]:
                - generic [ref=f2e362]:
                  - generic [ref=f2e364]:
                    - button "Select all" [ref=f2e372] [cursor=pointer]:
                      - generic [ref=f2e373]: 󰄬
                    - generic [ref=f2e376]:
                      - button "Mark as paid" [disabled] [ref=f2e379]:
                        - generic [ref=f2e380]: 󰗡
                      - button "SEPA XML actions" [disabled] [ref=f2e385]:
                        - generic [ref=f2e386]: 󱨉
                        - generic: 󰗡 󰅚 󰗖
                      - generic [ref=f2e391]:
                        - button [ref=f2e392] [cursor=pointer]
                        - generic: 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲
                  - table [ref=f2e398]:
                    - rowgroup [ref=f2e399]:
                      - row [ref=f2e400]:
                        - columnheader [ref=f2e401]:
                          - generic [ref=f2e402]:
                            - checkbox [ref=f2e403]
                            - generic [ref=f2e404] [cursor=pointer]: 󰄱
                        - columnheader [ref=f2e406]:
                          - button "Number" [ref=f2e407] [cursor=pointer]:
                            - generic [ref=f2e410]: 󰁝
                        - columnheader [ref=f2e413]:
                          - button "Status" [ref=f2e414] [cursor=pointer]:
                            - generic [ref=f2e417]: 󰁝
                        - columnheader [ref=f2e420]:
                          - button "Payment option" [ref=f2e421] [cursor=pointer]:
                            - generic [ref=f2e424]: 󰁝
                        - columnheader [ref=f2e427]:
                          - button "Due date" [ref=f2e428] [cursor=pointer]:
                            - generic [ref=f2e431]: 󰁝
                        - columnheader [ref=f2e434]:
                          - button "Unit" [ref=f2e435] [cursor=pointer]:
                            - generic [ref=f2e438]: 󰁝
                        - columnheader "Discount" [ref=f2e441]
                        - columnheader [ref=f2e445]:
                          - button "Total (incl. VAT)" [ref=f2e446] [cursor=pointer]:
                            - generic [ref=f2e447]:
                              - generic [ref=f2e448]: Total (incl. VAT)
                              - generic [ref=f2e449]: 󰋽
                            - generic [ref=f2e451]: 󰁝
                        - columnheader [ref=f2e454]:
                          - button "Amount due" [ref=f2e455] [cursor=pointer]:
                            - generic [ref=f2e458]: 󰁝
                        - columnheader [ref=f2e461]:
                          - button "Issue date" [ref=f2e462] [cursor=pointer]:
                            - generic [ref=f2e465]: 󰁝
                        - columnheader "Actions" [ref=f2e468]
                      - row [ref=f2e472]:
                        - columnheader [ref=f2e473]
                    - rowgroup [ref=f2e474]:
                      - row [ref=f2e475]:
                        - cell [ref=f2e476]:
                          - generic [ref=f2e477]:
                            - checkbox [ref=f2e478]
                            - generic [ref=f2e479] [cursor=pointer]: 󰄱
                        - cell [ref=f2e481]:
                          - link "INV-2105951" [ref=f2e482] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e484]:
                          - link "Open" [ref=f2e485] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e490]:
                          - link "Manual" [ref=f2e491] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e493]:
                          - link "21 Aug 2026" [ref=f2e494] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e496]:
                          - link "Unit HMO6HO" [ref=f2e497] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell "-" [ref=f2e499]
                        - cell [ref=f2e501]:
                          - link "€58.50" [ref=f2e502] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e504]:
                          - link "€58.50" [ref=f2e505] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e507]:
                          - link "19 Aug 2026" [ref=f2e508] [cursor=pointer]:
                            - /url: /invoices/55467548
                        - cell [ref=f2e510]:
                          - button [ref=f2e513] [cursor=pointer]:
                            - generic [ref=f2e514]: 󰇘
                            - generic: 󰇚 󰯝
                - generic [ref=f2e518]:
                  - generic [ref=f2e519]:
                    - generic [ref=f2e520]: "Items per page:"
                    - combobox [ref=f2e523]:
                      - generic [ref=f2e525] [cursor=pointer]:
                        - generic [ref=f2e526]: "10"
                        - combobox "Items per page:": "10"
                      - generic [ref=f2e528]: 󰍝
                  - generic [ref=f2e530]: 1-1 of 1
                  - generic [ref=f2e531]:
                    - button [disabled]:
                      - generic: 󰘀
                    - button [disabled]:
                      - generic: 󰅁
                    - button [disabled]:
                      - generic: 󰅂
                    - button [disabled]:
                      - generic: 󰘁
            - generic [ref=f2e532]:
              - heading "Payments" [level=3] [ref=f2e536]
              - generic [ref=f2e540]:
                - table [ref=f2e544]:
                  - rowgroup [ref=f2e545]:
                    - row [ref=f2e546]:
                      - columnheader [ref=f2e547]:
                        - button "Total" [ref=f2e548] [cursor=pointer]:
                          - generic [ref=f2e551]: 󰁝
                      - columnheader [ref=f2e554]:
                        - button "Status" [ref=f2e555] [cursor=pointer]:
                          - generic [ref=f2e558]: 󰁝
                      - columnheader [ref=f2e561]:
                        - button "Invoice no." [ref=f2e562] [cursor=pointer]:
                          - generic [ref=f2e565]: 󰁝
                      - columnheader [ref=f2e568]:
                        - button "Date" [ref=f2e569] [cursor=pointer]:
                          - generic [ref=f2e572]: 󰁝
                      - columnheader [ref=f2e575]:
                        - button "Payment method" [ref=f2e576] [cursor=pointer]:
                          - generic [ref=f2e579]: 󰁝
                    - row [ref=f2e582]:
                      - columnheader [ref=f2e583]
                  - rowgroup [ref=f2e584]:
                    - row [ref=f2e585]:
                      - cell "No data yet" [ref=f2e586]
                - generic [ref=f2e590]:
                  - generic [ref=f2e591]:
                    - generic [ref=f2e592]: "Items per page:"
                    - combobox [ref=f2e595]:
                      - generic [ref=f2e597] [cursor=pointer]:
                        - generic [ref=f2e598]: "10"
                        - combobox "Items per page:": "10"
                      - generic [ref=f2e600]: 󰍝
                  - generic [ref=f2e602]: 0-0 of 0
                  - generic [ref=f2e603]:
                    - 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
    - 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 });
  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 });
      |                                                              ^ Error: expect(locator).toHaveCount(expected) failed
  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,
  175 |   depositAmount: string
  176 | ): Promise<void> {
  177 |   const invoiceNumber = await getBookingDepositInvoiceNumber(detailsPage.invoicesCard.dataTable);
  178 | 
  179 |   await detailsPage.reload();
  180 | 
  181 |   await expectSingleLocatorToHaveText(detailsPage.generalCard.depositLink, invoiceNumber);
  182 |   await expectSingleLocatorToHaveText(detailsPage.generalCard.deposit, `${depositAmount} (${invoiceNumber})`);
  183 | }
```