# 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-adjustment-edit.spec.ts >> edits the adjustment from the notification banner - Location: src/manager/tests/e2e/booking/booking-adjustment-edit.spec.ts:17:5 # Error details ``` Error: Failed to fetch booking 982925893333518 via API: 404 { "message": "The route api/v1/booking/982925893333518 could not be found.", "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException", "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php", "line": 44, "trace": [ { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/CompiledRouteCollection.php", "line": 164, "function": "handleMatchedRoute", "class": "Illuminate\\Routing\\AbstractRouteCollection", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 777, "function": "match", "class": "Illuminate\\Routing\\CompiledRouteCollection", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 764, "function": "findRoute", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 753, "function": "dispatchToRoute", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", "line": 200, "function": "dispatch", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "{closure:Illuminate\\Foundation\\Http\\Kernel::dispatchToRouter():197}", "class": "Illuminate\\Foundation\\Http\\Kernel", "type": "->" }, { "file": "/var/www/vendor/livewire/livewire/src/Features/SupportDisablingBackButtonCache/DisableBackButtonCacheMiddleware.php", "line": 19, "function": "{closure:Illuminate\\Pipeline\\Pipeline::prepareDestination():178}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Livewire\\Features\\SupportDisablingBackButtonCache\\DisableBackButtonCacheMiddleware", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php", "line": 21, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php", "line": 31, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php", "line": 21, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php", "line": 51, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\TrimStrings", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePostSize.php", "line": 27, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Http\\Middleware\\ValidatePostSize", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php", "line": 110, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php", "line": 74, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Http\\Middleware\\HandleCors", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php", "line": 58, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Http\\Middleware\\TrustProxies", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php", "line": 22, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Http/Middleware/ValidatePathEncoding.php", "line": 28, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 219, "function": "handle", "class": "Illuminate\\Http\\Middleware\\ValidatePathEncoding", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 137, "function": "{closure:{closure:Illuminate\\Pipeline\\Pipeline::carry():194}:195}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", "line": 175, "function": "then", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", "line": 144, "function": "sendRequestThroughRouter", "class": "Illuminate\\Foundation\\Http\\Kernel", "type": "->" }, { "file": "/var/www/vendor/laravel/octane/src/ApplicationGateway.php", "line": 36, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Kernel", "type": "->" }, { "file": "/var/www/vendor/laravel/octane/src/Worker.php", "line": 84, "function": "handle", "class": "Laravel\\Octane\\ApplicationGateway", "type": "->" }, { "file": "/var/www/vendor/laravel/octane/bin/roadrunner-worker", "line": 56, "function": "handle", "class": "Laravel\\Octane\\Worker", "type": "->" }, { "file": "/var/www/vendor/bin/roadrunner-worker", "line": 119, "function": "include" } ] } ``` # Test source ```ts 1 | import { 2 | fromBookingCreateApiResponse, 3 | toBookingAdjustmentApiPayload, 4 | toBookingCreateApiPayload, 5 | toBookingPriceChangeApiPayload, 6 | } from '@/manager/modules/booking/booking-mappers'; 7 | import { 8 | BookingAdjustmentData, 9 | BookingCreateApiResponse, 10 | BookingCreateData, 11 | BookingPriceChange, 12 | BookingShowApiResponse, 13 | } from '@/manager/modules/booking/booking-types'; 14 | import { Booking } from '@/shared/types/booking-types'; 15 | import { isModernBillingMode } from '@/shared/utils/billing-utils'; 16 | import { APIRequestContext } from '@playwright/test'; 17 | 18 | // The manager API serves bookings at /v1/booking; the billing API (modern Kinnovis billing) serves them at 19 | // /api/v1/booking. A BookingApiClient bundles the request context with its matching base path so create and 20 | // cancel hit the right host without each call re-deriving the mode. 21 | export interface BookingApiClient { 22 | request: APIRequestContext; 23 | basePath: string; 24 | } 25 | 26 | const managerBookingBasePath = '/v1/booking'; 27 | const billingBookingBasePath = '/api/v1/booking'; 28 | 29 | export function resolveBookingApiClient( 30 | managerApiRequest: APIRequestContext, 31 | billingApiRequest: APIRequestContext 32 | ): BookingApiClient { 33 | return isModernBillingMode() 34 | ? { request: billingApiRequest, basePath: billingBookingBasePath } 35 | : { request: managerApiRequest, basePath: managerBookingBasePath }; 36 | } 37 | 38 | export async function createBookingViaApi(client: BookingApiClient, data: BookingCreateData): Promise { 39 | const response = await client.request.post(client.basePath, { 40 | data: toBookingCreateApiPayload(data), 41 | }); 42 | 43 | if (!response.ok()) { 44 | throw new Error(`Failed to create booking via API: ${response.status()} ${await response.text()}`); 45 | } 46 | 47 | const body = (await response.json()) as BookingCreateApiResponse; 48 | 49 | return fromBookingCreateApiResponse(body, data); 50 | } 51 | 52 | export async function createBookingPriceChangeViaApi( 53 | client: BookingApiClient, 54 | bookingIds: number[], 55 | priceChange: BookingPriceChange 56 | ): Promise { 57 | const response = await client.request.post(`${client.basePath}/price-change`, { 58 | data: toBookingPriceChangeApiPayload(priceChange, bookingIds), 59 | }); 60 | 61 | if (!response.ok()) { 62 | throw new Error( 63 | `Failed to create price change for bookings ${bookingIds} via API: ` + 64 | `${response.status()} ${await response.text()}` 65 | ); 66 | } 67 | } 68 | 69 | export async function createBookingAdjustmentViaApi( 70 | client: BookingApiClient, 71 | booking: Booking, 72 | adjustment: BookingAdjustmentData 73 | ): Promise { 74 | const bookingUpdatedAt = await getBookingUpdatedAtViaApi(client, booking.id); 75 | 76 | const response = await client.request.post(`${client.basePath}/${booking.id}/adjustment`, { 77 | data: toBookingAdjustmentApiPayload(booking, adjustment, bookingUpdatedAt), 78 | }); 79 | 80 | if (!response.ok()) { 81 | throw new Error( 82 | `Failed to create adjustment for booking ${booking.id} via API: ${response.status()} ${await response.text()}` 83 | ); 84 | } 85 | } 86 | 87 | async function getBookingUpdatedAtViaApi(client: BookingApiClient, id: number): Promise { 88 | const response = await client.request.get(`${client.basePath}/${id}`); 89 | 90 | if (!response.ok()) { > 91 | throw new Error(`Failed to fetch booking ${id} via API: ${response.status()} ${await response.text()}`); | ^ Error: Failed to fetch booking 982925893333518 via API: 404 { 92 | } 93 | 94 | const body = (await response.json()) as BookingShowApiResponse; 95 | const updatedAt = body.data.updated_at ?? body.data.updatedAt; 96 | 97 | if (updatedAt == null) { 98 | throw new Error(`Booking ${id} response contains neither updated_at nor updatedAt`); 99 | } 100 | 101 | return updatedAt; 102 | } 103 | 104 | export async function cancelBookingAtPeriodEndViaApi(client: BookingApiClient, id: number): Promise { 105 | const response = await client.request.put(`${client.basePath}/${id}/cancel`, { 106 | data: { type: 'end_of_period', cancel_on_date: null }, 107 | }); 108 | 109 | if (!response.ok()) { 110 | throw new Error( 111 | `Failed to cancel booking ${id} at period end via API: ${response.status()} ${await response.text()}` 112 | ); 113 | } 114 | } 115 | 116 | export async function cancelBookingNowViaApi(client: BookingApiClient, id: number): Promise { 117 | const body = { data: { type: 'now', cancel_on_date: null } }; 118 | 119 | const cancel = await client.request.put(`${client.basePath}/${id}/cancel`, body); 120 | 121 | if (cancel.ok() || cancel.status() === 404) { 122 | return; 123 | } 124 | 125 | const postpone = await client.request.put(`${client.basePath}/${id}/cancel-postpone`, body); 126 | 127 | if (postpone.ok() || postpone.status() === 404) { 128 | return; 129 | } 130 | 131 | throw new Error( 132 | `Failed to cancel booking ${id} via API: cancel ${cancel.status()}, ` + 133 | `postpone ${postpone.status()} ${await postpone.text()}` 134 | ); 135 | } 136 | ```