Files
TREK/server/tests/unit/services/kitineraryMapper.test.ts
T
Maurice 016fdeec64 fix(booking-import): read a printed 12-hour clock instead of slicing it (#2094)
splitIso reached the time by taking characters 11..16, so "2026-06-11T01:11 PM"
became 01:11. The hour survived and the half of the day did not, which is why
AM and 24-hour times in the same document imported correctly. The arrival then
sorted before the departure and the router rolled it onto the next day, so a
three-hour hop was rendered as an overnight.

The client has done this properly since #1725; this is the server half, which
was never pulled across. normalizeLocalDateTime rewrites a value only when it
actually finds a meridiem, so a date-only value, a value carrying an offset and
anything the parser does not know come back byte for byte.

The mapper has only two exits for a date, toIsoString and splitIso, so this
covers all eight types and every emitted field at once. It also stops
sameConnection comparing a NaN timestamp, which silently merged unrelated legs
into one booking.
2026-08-28 14:25:08 +02:00

110 lines
4.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { mapReservations } from '../../../src/nest/booking-import/kitinerary-mapper';
const airport = (iata: string, lat: number, lng: number) => ({
iataCode: iata,
name: iata,
geo: { latitude: lat, longitude: lng },
});
const flight = (pnr: string, dep: any, arr: any, depTime: string, arrTime: string, flightNumber: string) => ({
'@type': 'FlightReservation',
reservationNumber: pnr,
reservationFor: {
departureAirport: dep,
arrivalAirport: arr,
departureTime: depTime,
arrivalTime: arrTime,
airline: { name: 'Lufthansa', iataCode: 'LH' },
flightNumber,
},
});
const FRA = airport('FRA', 50.04, 8.57);
const BER = airport('BER', 52.36, 13.50);
const HND = airport('HND', 35.55, 139.78);
describe('kitinerary mapper — multi-leg flight grouping', () => {
it('groups two connecting same-PNR legs into one multi-leg booking', () => {
const { items } = mapReservations([
flight('ABC123', FRA, BER, '2026-06-11T10:00:00', '2026-06-11T12:00:00', 'LH 100'),
flight('ABC123', BER, HND, '2026-06-11T14:30:00', '2026-06-11T23:30:00', 'LH 200'),
] as any, 'test.json');
expect(items).toHaveLength(1);
const booking = items[0];
expect(booking.type).toBe('flight');
expect(booking.endpoints).toHaveLength(3);
expect(booking.endpoints!.map(e => e.role)).toEqual(['from', 'stop', 'to']);
expect(booking.endpoints!.map(e => e.sequence)).toEqual([0, 1, 2]);
const meta = booking.metadata as any;
expect(meta.legs).toHaveLength(2);
expect(meta.legs[0]).toMatchObject({ from: 'FRA', to: 'BER', flight_number: 'LH 100' });
expect(meta.legs[1]).toMatchObject({ from: 'BER', to: 'HND', flight_number: 'LH 200' });
expect(meta.departure_airport).toBe('FRA');
expect(meta.arrival_airport).toBe('HND');
expect(booking.reservation_time).toContain('10:00');
expect(booking.reservation_end_time).toContain('23:30');
});
it('keeps a round trip (same PNR, multi-day gap) as two separate bookings', () => {
const { items } = mapReservations([
flight('RT999', FRA, HND, '2026-06-11T10:00:00', '2026-06-11T20:00:00', 'LH 700'),
flight('RT999', HND, FRA, '2026-06-20T10:00:00', '2026-06-20T18:00:00', 'LH 701'),
] as any, 'test.json');
expect(items).toHaveLength(2);
expect((items[0].metadata as any).legs).toBeUndefined();
expect((items[1].metadata as any).legs).toBeUndefined();
});
it('leaves a single flight unchanged (two endpoints, no legs array)', () => {
const { items } = mapReservations([
flight('S1', FRA, BER, '2026-06-11T10:00:00', '2026-06-11T12:00:00', 'LH 1'),
] as any, 'test.json');
expect(items).toHaveLength(1);
expect(items[0].endpoints).toHaveLength(2);
expect((items[0].metadata as any).legs).toBeUndefined();
});
});
describe('kitinerary mapper — printed 12-hour clocks (#2094)', () => {
it('reads a PM arrival as the afternoon instead of slicing the meridiem off', () => {
// Before the fix the endpoint carried '01:11', because splitIso sliced
// characters 11..16 out of '2026-06-11T01:11 PM'. The hour survived and the
// arrival landed twelve hours early.
const { items } = mapReservations([
flight('PM1', FRA, BER, '2026-06-11T09:51 am', '2026-06-11T01:11 pm', 'LH 300'),
] as any, 'meridiem.json');
expect(items).toHaveLength(1);
const [dep, arr] = items[0].endpoints!;
expect(dep.local_time).toBe('09:51');
expect(arr.local_time).toBe('13:11');
expect(items[0].reservation_time).toBe('2026-06-11T09:51:00');
expect(items[0].reservation_end_time).toBe('2026-06-11T13:11:00');
});
it('leaves a 24-hour document byte for byte where it was', () => {
const { items } = mapReservations([
flight('H24', FRA, BER, '2026-06-11T10:00:00', '2026-06-11T12:00:00', 'LH 400'),
] as any, 'plain.json');
expect(items[0].reservation_time).toBe('2026-06-11T10:00:00');
expect(items[0].endpoints![1].local_time).toBe('12:00');
});
it('keeps two legs apart that a NaN comparison used to merge', () => {
// sameConnection does `new Date(value).getTime()`, which is NaN for a
// printed meridiem, and both of its comparisons are false against NaN, so
// the guard fell through and merged unrelated legs into one booking.
const { items } = mapReservations([
flight('SAME', FRA, BER, '2026-06-11T09:00 am', '2026-06-11T10:00 am', 'LH 500'),
flight('SAME', HND, FRA, '2026-06-18T09:00 am', '2026-06-18T05:00 pm', 'LH 600'),
] as any, 'two-legs.json');
expect(items).toHaveLength(2);
});
});