From 70f4f7cef3f375201444679eff08a952ac155e1a Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 17:36:59 +0200 Subject: [PATCH] fix(planner): an imported booking with source files saves and opens its expense without file support Saving an imported booking in the desktop booking dialog when the dialog had no upload handler but the import carried source files rejected the save: the files were handed to the missing handler, the promise rejected with nothing to catch it, and the expense editor the user asked for never opened. The transport dialog already skipped the upload in that case, the booking dialog did not. The shared uploadBookingFiles helper now takes an optional handler and does nothing without one, so every form gets the same guard. Red test: FE-PLANNER-RESMODAL-106 in client/src/components/Planner/ReservationModal.test.tsx failed with "expected spy to be called with arguments" for onOpenExpense, plus the unhandled rejection "TypeError: upload is not a function". The helper case is covered by FE-PLANNER-BOOKINGFORM-008. No existing assertions changed. --- .../Planner/ReservationModal.test.tsx | 34 ++++++++++++++++++- .../Planner/bookingFormModel.test.ts | 6 +++- .../components/Planner/bookingFormModel.ts | 5 ++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/client/src/components/Planner/ReservationModal.test.tsx b/client/src/components/Planner/ReservationModal.test.tsx index b9d6a7922..a051bfb7c 100644 --- a/client/src/components/Planner/ReservationModal.test.tsx +++ b/client/src/components/Planner/ReservationModal.test.tsx @@ -1,4 +1,4 @@ -// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-105 +// FE-PLANNER-RESMODAL-001 to FE-PLANNER-RESMODAL-106 import { render, screen, waitFor, fireEvent, within, act } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; @@ -1409,6 +1409,38 @@ describe('ReservationModal', () => { expect(onSave.mock.calls[0][0].create_budget_entry).toEqual({ total_price: 50, category: 'accommodation' }); }); + it('FE-PLANNER-RESMODAL-106: an import with source files but no upload handler still saves and opens the expense editor', async () => { + // Without onFileUpload the pending source files cannot go anywhere; the save + // used to call the missing handler, reject, and never reach the expense step. + budgetEnabled(); + const onSave = vi.fn().mockResolvedValue({ id: 84 }); + const onOpenExpense = vi.fn(); + const prefill = hotelPrefill({ + type: 'restaurant', + metadata: {}, + _accommodation: undefined, + _sourceFiles: [new File(['pdf'], 'voucher.pdf', { type: 'application/pdf' })], + }); + render( + + ); + + await userEvent.click(screen.getByRole('button', { name: /Create expense/i })); + await waitFor(() => expect(onSave).toHaveBeenCalled()); + await waitFor(() => + expect(onOpenExpense).toHaveBeenCalledWith( + expect.objectContaining({ prefill: expect.objectContaining({ reservationId: 84 }) }) + ) + ); + }); + // ── File error paths ──────────────────────────────────────────────────────── it('FE-PLANNER-RESMODAL-077: a cancelled file dialog changes nothing', () => { diff --git a/client/src/components/Planner/bookingFormModel.test.ts b/client/src/components/Planner/bookingFormModel.test.ts index c6be97fc7..014d5c6b0 100644 --- a/client/src/components/Planner/bookingFormModel.test.ts +++ b/client/src/components/Planner/bookingFormModel.test.ts @@ -1,4 +1,4 @@ -// FE-PLANNER-BOOKINGFORM-001 to -007: the travellers, picked files, attached files +// FE-PLANNER-BOOKINGFORM-001 to -008: the travellers, picked files, attached files // and expense requests of the transport and booking forms, desktop and phone alike. import { typeToCostCategory } from '@trek/shared'; import { describe, expect, it, vi } from 'vitest'; @@ -60,6 +60,10 @@ describe('bookingFormModel', () => { expect(upload).not.toHaveBeenCalled(); }); + it('FE-PLANNER-BOOKINGFORM-008: without an upload handler the picked files are skipped and the save is not rejected', async () => { + await expect(uploadBookingFiles(undefined, 42, [new File(['a'], 'a.pdf')], 'Hotel stay')).resolves.toBeUndefined(); + }); + it('FE-PLANNER-BOOKINGFORM-005: an expense wish opens the editor for the saved booking, nothing else does', () => { const booking = { title: 'Hotel Adlon', type: 'hotel' }; const item = buildBudgetItem({ id: 7 }); diff --git a/client/src/components/Planner/bookingFormModel.ts b/client/src/components/Planner/bookingFormModel.ts index eab9b82b2..71a952afe 100644 --- a/client/src/components/Planner/bookingFormModel.ts +++ b/client/src/components/Planner/bookingFormModel.ts @@ -34,13 +34,16 @@ export function travelersChanged(reservation: Pick | n * the other, each described with the booking's title. Callers run it after both a * create and an edit: the form only holds files the user just picked, so nothing is * uploaded twice, and skipping the edit dropped them without a word (#2534). + * Without an upload handler (a dialog opened with no file support) there is + * nowhere to send them, so nothing is uploaded and the save carries on. */ export async function uploadBookingFiles( - upload: (fd: FormData) => Promise, + upload: ((fd: FormData) => Promise) | undefined, savedId: number, files: File[], description: string ): Promise { + if (!upload) return; for (const file of files) { const fd = new FormData(); fd.append('file', file);