fix(airtrail): stop syncing a booking once it grows extra stops

A linked flight that becomes multi-leg locally no longer matches the single
AirTrail flight it was imported from: pushing would rewrite that flight to
span the whole route, and the next pull would flatten the layover chain
back to a plain from/to. Both sync directions now detach the link instead —
the same state a joined import starts in, surfaced by the existing "Not
synced" badge.

TransportModal also carries metadata.airtrail_ids through re-saves (like it
already does for transit itineraries and day positions), so editing a
joined booking cannot cost it its import dedupe and get its legs re-offered
in the picker.
This commit is contained in:
Maurice
2026-07-13 16:17:15 +02:00
parent 545295d430
commit cfebf57505
4 changed files with 155 additions and 7 deletions
@@ -375,6 +375,31 @@ describe('TransportModal', () => {
expect(payload.endpoints.map((e: { role: string }) => e.role)).toEqual(['from', 'to']);
});
it('FE-PLANNER-TRANSMODAL-029: re-saving a joined AirTrail flight keeps metadata.airtrail_ids (#1535)', async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const res = buildReservation({ title: 'BRU → HEL → JFK', type: 'flight' }) as any;
res.metadata = {
airline: 'Finnair',
flight_number: 'AY1502',
airtrail_ids: ['101', '102'],
legs: [
{ from: 'BRU', to: 'HEL', flight_number: 'AY1502', dep_time: '08:00', arr_time: '12:30' },
{ from: 'HEL', to: 'JFK', flight_number: 'AY15', dep_time: '14:00', arr_time: '15:00' },
],
};
res.endpoints = [
{ role: 'from', sequence: 0, name: 'Brussels', code: 'BRU', lat: 50.9, lng: 4.48, timezone: 'Europe/Brussels', local_date: '2025-06-01', local_time: '08:00' },
{ role: 'stop', sequence: 1, name: 'Helsinki-Vantaa', code: 'HEL', lat: 60.32, lng: 24.96, timezone: 'Europe/Helsinki', local_date: '2025-06-01', local_time: '14:00' },
{ role: 'to', sequence: 2, name: 'JFK', code: 'JFK', lat: 40.64, lng: -73.78, timezone: 'America/New_York', local_date: '2025-06-01', local_time: '15:00' },
];
render(<TransportModal {...defaultProps} reservation={res} onSave={onSave} />);
// A routine edit (retitle + save) must not cost the booking its AirTrail
// linkage — the import picker relies on it to not re-offer the legs.
await userEvent.click(screen.getByRole('button', { name: /^Update$/i }));
await waitFor(() => expect(onSave).toHaveBeenCalled());
expect(onSave.mock.calls[0][0].metadata?.airtrail_ids).toEqual(['101', '102']);
});
// ── Manual / Automated creation switch (#1065) ─────────────────────────────
it('FE-PLANNER-TRANSMODAL-022: creating shows the Manual/Automated switch; Automated opens the transit search', async () => {
@@ -423,6 +423,10 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
near(prevFrom.lat, fromPick.location.lat) && near(prevFrom.lng, fromPick.location.lng) &&
near(prevTo.lat, toPick.location.lat) && near(prevTo.lng, toPick.location.lng))
if (keepTransit) metadata.transit = prevMeta.transit
// A joined AirTrail import (#1535) records every source flight id in
// metadata.airtrail_ids so the picker doesn't offer those legs again —
// an edit in this form must not drop that linkage.
if (Array.isArray(prevMeta.airtrail_ids)) metadata.airtrail_ids = prevMeta.airtrail_ids
const startDate = startDay?.date ?? null
const endDate = (endDay ?? startDay)?.date ?? null