mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
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:
@@ -31,10 +31,10 @@ vi.mock('../../../src/services/airtrail/airtrailService', () => ({
|
||||
getAirtrailCredentials: vi.fn(),
|
||||
}));
|
||||
|
||||
import { pushReservationToAirtrail } from '../../../src/services/airtrail/airtrailSync';
|
||||
import { pushReservationToAirtrail, runAirtrailSyncForUser } from '../../../src/services/airtrail/airtrailSync';
|
||||
import { db } from '../../../src/db/database';
|
||||
import { getReservationWithJoins } from '../../../src/services/reservationService';
|
||||
import { getFlight, saveFlight } from '../../../src/services/airtrail/airtrailClient';
|
||||
import { getReservation, getReservationWithJoins, updateReservation } from '../../../src/services/reservationService';
|
||||
import { getFlight, listFlights, saveFlight } from '../../../src/services/airtrail/airtrailClient';
|
||||
import { isAirtrailWriteEnabled, getAirtrailCredentials } from '../../../src/services/airtrail/airtrailService';
|
||||
|
||||
const linkedRow = { id: 5, trip_id: 9, external_id: '42', external_owner_user_id: 7, sync_enabled: 1 };
|
||||
@@ -42,10 +42,12 @@ const runSpy = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Route db reads: global sync setting + the linked reservation row.
|
||||
// Route db reads: global sync setting, the linked reservation row and the
|
||||
// endpoint count the multi-leg guard checks (#1535) — two = plain from/to.
|
||||
(db.prepare as any).mockImplementation((sql: string) => ({
|
||||
get: () => {
|
||||
if (sql.includes('app_settings')) return { value: 'true' };
|
||||
if (sql.includes('FROM reservation_endpoints')) return { n: 2 };
|
||||
if (sql.includes('FROM reservations')) return { ...linkedRow };
|
||||
return undefined;
|
||||
},
|
||||
@@ -89,4 +91,86 @@ describe('pushReservationToAirtrail write gate (#1240)', () => {
|
||||
expect(payload.departureTerminal).toBe('7'); // spread preserved the unmanaged field
|
||||
expect(payload.from).toBe('JFK'); // TREK-managed field still applied as a code
|
||||
});
|
||||
|
||||
it('#1535 detaches instead of pushing when the reservation grew extra stops', async () => {
|
||||
(isAirtrailWriteEnabled as any).mockReturnValue(true);
|
||||
(db.prepare as any).mockImplementation((sql: string) => ({
|
||||
get: () => {
|
||||
if (sql.includes('app_settings')) return { value: 'true' };
|
||||
if (sql.includes('FROM reservation_endpoints')) return { n: 3 }; // from + stop + to
|
||||
if (sql.includes('FROM reservations')) return { ...linkedRow };
|
||||
return undefined;
|
||||
},
|
||||
run: (...args: any[]) => {
|
||||
runSpy(sql, args);
|
||||
return {};
|
||||
},
|
||||
all: () => [],
|
||||
}));
|
||||
await pushReservationToAirtrail(5, 9);
|
||||
// Pushing would rewrite the single AirTrail flight to span the whole route.
|
||||
expect(saveFlight).not.toHaveBeenCalled();
|
||||
expect(runSpy).toHaveBeenCalledWith(expect.stringContaining('sync_enabled = 0'), [5]);
|
||||
});
|
||||
|
||||
it('#1535 detaches on metadata.legs even when the endpoint count is not available', async () => {
|
||||
(isAirtrailWriteEnabled as any).mockReturnValue(true);
|
||||
(getReservationWithJoins as any).mockReturnValue({
|
||||
external_id: '42',
|
||||
reservation_time: '2021-09-01T19:00',
|
||||
metadata: JSON.stringify({ legs: [{ from: 'BRU', to: 'HEL' }, { from: 'HEL', to: 'JFK' }] }),
|
||||
endpoints: [],
|
||||
});
|
||||
await pushReservationToAirtrail(5, 9);
|
||||
expect(saveFlight).not.toHaveBeenCalled();
|
||||
expect(runSpy).toHaveBeenCalledWith(expect.stringContaining('sync_enabled = 0'), [5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inbound sync multi-leg guard (#1535)', () => {
|
||||
it('detaches instead of flattening when a linked reservation grew extra stops locally', async () => {
|
||||
// A remote change is pending (stored hash differs from canonicalHash's
|
||||
// 'hash'), but the local reservation has become multi-leg — applying the
|
||||
// single-flight shape would flatten the layover chain.
|
||||
(db.prepare as any).mockImplementation((sql: string) => ({
|
||||
get: () => {
|
||||
if (sql.includes('app_settings')) return { value: 'true' };
|
||||
if (sql.includes('FROM reservation_endpoints')) return { n: 3 };
|
||||
return undefined;
|
||||
},
|
||||
all: () => (sql.includes('sync_enabled = 1') ? [{ id: 5, trip_id: 9, external_id: '42', external_hash: 'stale' }] : []),
|
||||
run: (...args: any[]) => {
|
||||
runSpy(sql, args);
|
||||
return {};
|
||||
},
|
||||
}));
|
||||
(listFlights as any).mockResolvedValue([{ id: 42 }]);
|
||||
(getReservation as any).mockReturnValue({ id: 5, metadata: JSON.stringify({}) });
|
||||
|
||||
const { changed } = await runAirtrailSyncForUser(7);
|
||||
expect(updateReservation).not.toHaveBeenCalled();
|
||||
expect(runSpy).toHaveBeenCalledWith(expect.stringContaining('sync_enabled = 0'), [5]);
|
||||
expect(changed).toBe(1);
|
||||
});
|
||||
|
||||
it('still applies a remote change to a plain single-leg reservation', async () => {
|
||||
(db.prepare as any).mockImplementation((sql: string) => ({
|
||||
get: () => {
|
||||
if (sql.includes('app_settings')) return { value: 'true' };
|
||||
if (sql.includes('FROM reservation_endpoints')) return { n: 2 };
|
||||
return undefined;
|
||||
},
|
||||
all: () => (sql.includes('sync_enabled = 1') ? [{ id: 5, trip_id: 9, external_id: '42', external_hash: 'stale' }] : []),
|
||||
run: (...args: any[]) => {
|
||||
runSpy(sql, args);
|
||||
return {};
|
||||
},
|
||||
}));
|
||||
(listFlights as any).mockResolvedValue([{ id: 42 }]);
|
||||
(getReservation as any).mockReturnValue({ id: 5, metadata: JSON.stringify({}) });
|
||||
|
||||
await runAirtrailSyncForUser(7);
|
||||
expect(updateReservation).toHaveBeenCalledTimes(1);
|
||||
expect(runSpy).not.toHaveBeenCalledWith(expect.stringContaining('sync_enabled = 0'), expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user