All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m44s
630 lines
18 KiB
TypeScript
630 lines
18 KiB
TypeScript
import { db } from './db/index.js';
|
|
import { randomUUID } from 'crypto';
|
|
import type { PlanStatus } from './plans.js';
|
|
|
|
// --- Geo utilities ---
|
|
|
|
function haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
|
const R = 6371;
|
|
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
|
const a =
|
|
Math.sin(dLat / 2) ** 2 +
|
|
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) ** 2;
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
}
|
|
|
|
/** Convert a "YYYY-MM-DDTHH:MM" local datetime in an IANA timezone to a UTC ms timestamp. */
|
|
function tzLocalToUtcMs(localDt: string, tz: string): number {
|
|
const [datePart, timePart] = localDt.split('T');
|
|
const [y, mo, d] = datePart.split('-').map(Number);
|
|
const [h, mi] = timePart.split(':').map(Number);
|
|
// Treat the local time as UTC first, then correct for the timezone offset.
|
|
const candidate = Date.UTC(y, mo - 1, d, h, mi);
|
|
const fmt = new Intl.DateTimeFormat('en-GB', {
|
|
timeZone: tz,
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false
|
|
});
|
|
const parts = fmt.formatToParts(new Date(candidate));
|
|
const get = (type: string) => Number(parts.find((p) => p.type === type)?.value ?? '0');
|
|
const shownH = get('hour');
|
|
const shownMi = get('minute');
|
|
// Offset = difference between what we intended and what the TZ shows at that UTC moment
|
|
const offsetMins = (h - shownH) * 60 + (mi - shownMi);
|
|
return candidate + offsetMins * 60_000;
|
|
}
|
|
|
|
function durationMinutes(
|
|
depDatetime: string,
|
|
depTz: string,
|
|
arrDatetime: string,
|
|
arrTz: string
|
|
): number | null {
|
|
try {
|
|
const depMs = tzLocalToUtcMs(depDatetime, depTz);
|
|
const arrMs = tzLocalToUtcMs(arrDatetime, arrTz);
|
|
const mins = Math.round((arrMs - depMs) / 60_000);
|
|
return mins > 0 ? mins : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Look up the IANA timezone for an airport by its DB id. */
|
|
function airportTimezone(airportId: number | null | undefined): string | null {
|
|
if (!airportId) return null;
|
|
return (
|
|
db.get<{ timezone: string | null }>('SELECT timezone FROM airports WHERE id = ?', [airportId])
|
|
?.timezone ?? null
|
|
);
|
|
}
|
|
|
|
export interface Airport {
|
|
id: number;
|
|
iata_code: string | null;
|
|
icao_code: string | null;
|
|
name: string;
|
|
city: string | null;
|
|
country: string;
|
|
country_code: string;
|
|
latitude: number | null;
|
|
longitude: number | null;
|
|
timezone: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Airline {
|
|
id: number;
|
|
iata_code: string | null;
|
|
icao_code: string | null;
|
|
name: string;
|
|
country: string | null;
|
|
country_code: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface FlightBooking {
|
|
id: string;
|
|
plan_id: string;
|
|
confirmation_number: string | null;
|
|
price: number | null;
|
|
currency: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface FlightSegment {
|
|
id: string;
|
|
flight_booking_id: string;
|
|
departure_date: string;
|
|
airline_id: number | null;
|
|
airline_iata: string | null;
|
|
airline_icao: string | null;
|
|
airline_name: string | null;
|
|
flight_number: string | null;
|
|
position: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface FlightRoute {
|
|
id: string;
|
|
flight_segment_id: string;
|
|
departure_airport_id: number | null;
|
|
departure_airport_code: string | null;
|
|
departure_terminal: string | null;
|
|
departure_gate: string | null;
|
|
departure_datetime: string | null;
|
|
departure_timezone: string | null;
|
|
arrival_airport_id: number | null;
|
|
arrival_airport_code: string | null;
|
|
arrival_terminal: string | null;
|
|
arrival_gate: string | null;
|
|
arrival_date: string | null;
|
|
arrival_datetime: string | null;
|
|
arrival_timezone: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface CreateFlightInput {
|
|
tripId: string;
|
|
userId: string;
|
|
parentId?: string;
|
|
confirmationNumber?: string;
|
|
price?: number;
|
|
currency?: string;
|
|
status?: PlanStatus;
|
|
segments: Array<{
|
|
departureDate: string;
|
|
airlineId?: number;
|
|
airlineIata?: string;
|
|
airlineIcao?: string;
|
|
airlineName?: string;
|
|
flightNumber?: string;
|
|
route?: {
|
|
departureAirportId?: number;
|
|
departureAirportCode?: string;
|
|
departureTerminal?: string;
|
|
departureGate?: string;
|
|
departureDatetime?: string;
|
|
departureTimezone?: string;
|
|
arrivalAirportId?: number;
|
|
arrivalAirportCode?: string;
|
|
arrivalTerminal?: string;
|
|
arrivalGate?: string;
|
|
arrivalDate?: string;
|
|
arrivalDatetime?: string;
|
|
arrivalTimezone?: string;
|
|
};
|
|
}>;
|
|
passengerIds?: string[];
|
|
}
|
|
|
|
export function searchAirports(query: string): Airport[] {
|
|
if (!query.trim()) return [];
|
|
const pattern = `%${query.trim()}%`;
|
|
return db.all<Airport>(
|
|
`SELECT * FROM airports
|
|
WHERE name LIKE ? COLLATE NOCASE
|
|
OR iata_code LIKE ? COLLATE NOCASE
|
|
OR icao_code LIKE ? COLLATE NOCASE
|
|
OR city LIKE ? COLLATE NOCASE
|
|
ORDER BY
|
|
CASE WHEN iata_code LIKE ? COLLATE NOCASE THEN 0 ELSE 1 END,
|
|
CASE WHEN name LIKE ? COLLATE NOCASE THEN 0 ELSE 1 END
|
|
LIMIT 20`,
|
|
[pattern, pattern, pattern, pattern, `${query.trim()}%`, `${query.trim()}%`]
|
|
);
|
|
}
|
|
|
|
export function searchAirlines(query: string): Airline[] {
|
|
if (!query.trim()) return [];
|
|
const pattern = `%${query.trim()}%`;
|
|
return db.all<Airline>(
|
|
`SELECT * FROM airlines
|
|
WHERE name LIKE ? COLLATE NOCASE
|
|
OR iata_code LIKE ? COLLATE NOCASE
|
|
OR icao_code LIKE ? COLLATE NOCASE
|
|
ORDER BY
|
|
CASE WHEN iata_code LIKE ? COLLATE NOCASE THEN 0 ELSE 1 END,
|
|
CASE WHEN name LIKE ? COLLATE NOCASE THEN 0 ELSE 1 END
|
|
LIMIT 20`,
|
|
[pattern, pattern, pattern, `${query.trim()}%`, `${query.trim()}%`]
|
|
);
|
|
}
|
|
|
|
export function getAirportById(id: number): Airport | undefined {
|
|
return db.get<Airport>('SELECT * FROM airports WHERE id = ?', [id]);
|
|
}
|
|
|
|
export function getAirportByCode(code: string): Airport | undefined {
|
|
return db.get<Airport>('SELECT * FROM airports WHERE iata_code = ? OR icao_code = ?', [
|
|
code.toUpperCase(),
|
|
code.toUpperCase()
|
|
]);
|
|
}
|
|
|
|
export function getAirlineById(id: number): Airline | undefined {
|
|
return db.get<Airline>('SELECT * FROM airlines WHERE id = ?', [id]);
|
|
}
|
|
|
|
export function getAirlineByCode(code: string): Airline | undefined {
|
|
return db.get<Airline>('SELECT * FROM airlines WHERE iata_code = ? OR icao_code = ?', [
|
|
code.toUpperCase(),
|
|
code.toUpperCase()
|
|
]);
|
|
}
|
|
|
|
export function createFlight(input: CreateFlightInput): FlightBooking {
|
|
// First, create a plan entry for the flight
|
|
const planId = randomUUID();
|
|
const maxPos = db.get<{ pos: number }>(
|
|
`SELECT COALESCE(MAX(position), -1) + 1 as pos FROM plans WHERE trip_id = ? AND user_id = ?`,
|
|
[input.tripId, input.userId]
|
|
);
|
|
const position = maxPos?.pos ?? 0;
|
|
|
|
// Generate a title from the first segment
|
|
const firstSegment = input.segments[0];
|
|
const title = firstSegment
|
|
? (() => {
|
|
const airline = firstSegment.airlineName || firstSegment.airlineIata || 'Flight';
|
|
const flightNumber = firstSegment.flightNumber?.trim();
|
|
const dep = firstSegment.route?.departureAirportCode?.trim();
|
|
const arr = firstSegment.route?.arrivalAirportCode?.trim();
|
|
if (flightNumber) return `${airline} ${flightNumber}`;
|
|
if (dep || arr) return `Flight: ${dep || 'TBD'} -> ${arr || 'TBD'}`;
|
|
return 'Flight';
|
|
})()
|
|
: 'Flight';
|
|
|
|
db.run(
|
|
`INSERT INTO plans (id, trip_id, user_id, type, status, title, parent_id, position)
|
|
VALUES (?, ?, ?, 'transport', ?, ?, ?, ?)`,
|
|
[
|
|
planId,
|
|
input.tripId,
|
|
input.userId,
|
|
input.status ?? 'idea',
|
|
title,
|
|
input.parentId ?? null,
|
|
position
|
|
]
|
|
);
|
|
|
|
// Create the flight booking
|
|
const bookingId = randomUUID();
|
|
db.run(
|
|
`INSERT INTO flight_bookings (id, plan_id, confirmation_number, price, currency)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
[
|
|
bookingId,
|
|
planId,
|
|
input.confirmationNumber ?? null,
|
|
input.price ?? null,
|
|
input.currency ?? 'USD'
|
|
]
|
|
);
|
|
|
|
// Create flight segments
|
|
let segmentPosition = 0;
|
|
for (const segment of input.segments) {
|
|
const segmentId = randomUUID();
|
|
|
|
// Airline data comes fully resolved from the caller — no re-query needed
|
|
const airlineIata = segment.airlineIata;
|
|
const airlineIcao = segment.airlineIcao;
|
|
const airlineName = segment.airlineName;
|
|
|
|
db.run(
|
|
`INSERT INTO flight_segments (id, flight_booking_id, departure_date, airline_id, airline_iata, airline_icao, airline_name, flight_number, position)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
segmentId,
|
|
bookingId,
|
|
segment.departureDate,
|
|
segment.airlineId ?? null,
|
|
airlineIata ?? null,
|
|
airlineIcao ?? null,
|
|
airlineName ?? null,
|
|
segment.flightNumber?.trim() || '',
|
|
segmentPosition++
|
|
]
|
|
);
|
|
|
|
// Create route if provided
|
|
if (segment.route) {
|
|
const routeId = randomUUID();
|
|
db.run(
|
|
`INSERT INTO flight_routes (
|
|
id, flight_segment_id,
|
|
departure_airport_id, departure_airport_code, departure_terminal, departure_gate,
|
|
departure_datetime, departure_timezone,
|
|
arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate,
|
|
arrival_date, arrival_datetime, arrival_timezone
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
routeId,
|
|
segmentId,
|
|
segment.route.departureAirportId ?? null,
|
|
segment.route.departureAirportCode ?? null,
|
|
segment.route.departureTerminal ?? null,
|
|
segment.route.departureGate ?? null,
|
|
segment.route.departureDatetime ?? null,
|
|
segment.route.departureTimezone ??
|
|
airportTimezone(segment.route.departureAirportId) ??
|
|
null,
|
|
segment.route.arrivalAirportId ?? null,
|
|
segment.route.arrivalAirportCode ?? null,
|
|
segment.route.arrivalTerminal ?? null,
|
|
segment.route.arrivalGate ?? null,
|
|
segment.route.arrivalDate ?? null,
|
|
segment.route.arrivalDatetime ?? null,
|
|
segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Link passengers if provided
|
|
if (input.passengerIds && input.passengerIds.length > 0) {
|
|
for (const personId of input.passengerIds) {
|
|
db.run(
|
|
`INSERT OR IGNORE INTO flight_booking_passengers (flight_booking_id, person_id)
|
|
VALUES (?, ?)`,
|
|
[bookingId, personId]
|
|
);
|
|
}
|
|
}
|
|
|
|
return db.get<FlightBooking>('SELECT * FROM flight_bookings WHERE id = ?', [bookingId])!;
|
|
}
|
|
|
|
function enrichRoute(
|
|
route: FlightRoute | undefined
|
|
): (FlightRoute & { distanceKm: number | null; durationMins: number | null }) | null {
|
|
if (!route) return null;
|
|
|
|
let distanceKm: number | null = null;
|
|
let durationMins: number | null = null;
|
|
|
|
if (route.departure_airport_id && route.arrival_airport_id) {
|
|
const dep = db.get<{ latitude: number | null; longitude: number | null }>(
|
|
'SELECT latitude, longitude FROM airports WHERE id = ?',
|
|
[route.departure_airport_id]
|
|
);
|
|
const arr = db.get<{ latitude: number | null; longitude: number | null }>(
|
|
'SELECT latitude, longitude FROM airports WHERE id = ?',
|
|
[route.arrival_airport_id]
|
|
);
|
|
if (dep?.latitude && dep?.longitude && arr?.latitude && arr?.longitude) {
|
|
distanceKm = Math.round(
|
|
haversineKm(dep.latitude, dep.longitude, arr.latitude, arr.longitude)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
route.departure_datetime &&
|
|
route.arrival_datetime &&
|
|
route.departure_timezone &&
|
|
route.arrival_timezone
|
|
) {
|
|
durationMins = durationMinutes(
|
|
route.departure_datetime,
|
|
route.departure_timezone,
|
|
route.arrival_datetime,
|
|
route.arrival_timezone
|
|
);
|
|
}
|
|
|
|
return { ...route, distanceKm, durationMins };
|
|
}
|
|
|
|
export function getFlightBookingByPlanId(
|
|
planId: string,
|
|
userId: string
|
|
):
|
|
| (FlightBooking & {
|
|
segments: Array<
|
|
FlightSegment & {
|
|
route: (FlightRoute & { distanceKm: number | null; durationMins: number | null }) | null;
|
|
}
|
|
>;
|
|
passengerIds: string[];
|
|
})
|
|
| undefined {
|
|
const booking = db.get<FlightBooking>(
|
|
`SELECT fb.* FROM flight_bookings fb
|
|
JOIN plans p ON p.id = fb.plan_id
|
|
WHERE fb.plan_id = ? AND p.user_id = ?`,
|
|
[planId, userId]
|
|
);
|
|
|
|
if (!booking) return undefined;
|
|
|
|
const segments = db.all<FlightSegment>(
|
|
`SELECT * FROM flight_segments
|
|
WHERE flight_booking_id = ?
|
|
ORDER BY position ASC`,
|
|
[booking.id]
|
|
);
|
|
|
|
const segmentsWithRoutes = segments.map((segment) => {
|
|
const route = db.get<FlightRoute>('SELECT * FROM flight_routes WHERE flight_segment_id = ?', [
|
|
segment.id
|
|
]);
|
|
return { ...segment, route: enrichRoute(route) };
|
|
});
|
|
|
|
const passengerIds = db
|
|
.all<{
|
|
person_id: string;
|
|
}>('SELECT person_id FROM flight_booking_passengers WHERE flight_booking_id = ?', [booking.id])
|
|
.map((r) => r.person_id);
|
|
|
|
return { ...booking, segments: segmentsWithRoutes, passengerIds };
|
|
}
|
|
|
|
export function getFlightBookingsForTrip(
|
|
tripId: string,
|
|
userId: string
|
|
): Array<
|
|
FlightBooking & {
|
|
segments: Array<
|
|
FlightSegment & {
|
|
route: (FlightRoute & { distanceKm: number | null; durationMins: number | null }) | null;
|
|
}
|
|
>;
|
|
passengerIds: string[];
|
|
}
|
|
> {
|
|
const bookings = db.all<FlightBooking>(
|
|
`SELECT fb.* FROM flight_bookings fb
|
|
JOIN plans p ON p.id = fb.plan_id
|
|
WHERE p.trip_id = ? AND p.user_id = ? AND p.type = 'transport'
|
|
ORDER BY p.position ASC, fb.created_at ASC`,
|
|
[tripId, userId]
|
|
);
|
|
|
|
return bookings.map((booking) => {
|
|
const segments = db.all<FlightSegment>(
|
|
`SELECT * FROM flight_segments
|
|
WHERE flight_booking_id = ?
|
|
ORDER BY position ASC`,
|
|
[booking.id]
|
|
);
|
|
|
|
const segmentsWithRoutes = segments.map((segment) => {
|
|
const route = db.get<FlightRoute>('SELECT * FROM flight_routes WHERE flight_segment_id = ?', [
|
|
segment.id
|
|
]);
|
|
return { ...segment, route: enrichRoute(route) };
|
|
});
|
|
|
|
const passengerIds = db
|
|
.all<{
|
|
person_id: string;
|
|
}>('SELECT person_id FROM flight_booking_passengers WHERE flight_booking_id = ?', [
|
|
booking.id
|
|
])
|
|
.map((r) => r.person_id);
|
|
|
|
return { ...booking, segments: segmentsWithRoutes, passengerIds };
|
|
});
|
|
}
|
|
|
|
export interface UpdateFlightInput {
|
|
bookingId: string;
|
|
userId: string;
|
|
confirmationNumber?: string;
|
|
price?: number;
|
|
currency?: string;
|
|
status?: PlanStatus;
|
|
segments: Array<{
|
|
departureDate: string;
|
|
airlineId?: number;
|
|
airlineIata?: string;
|
|
airlineIcao?: string;
|
|
airlineName?: string;
|
|
flightNumber?: string;
|
|
route?: {
|
|
departureAirportId?: number;
|
|
departureAirportCode?: string;
|
|
departureTerminal?: string;
|
|
departureGate?: string;
|
|
departureDatetime?: string;
|
|
departureTimezone?: string;
|
|
arrivalAirportId?: number;
|
|
arrivalAirportCode?: string;
|
|
arrivalTerminal?: string;
|
|
arrivalGate?: string;
|
|
arrivalDate?: string;
|
|
arrivalDatetime?: string;
|
|
arrivalTimezone?: string;
|
|
};
|
|
}>;
|
|
passengerIds?: string[];
|
|
}
|
|
|
|
export function updateFlight(input: UpdateFlightInput): void {
|
|
// Verify booking belongs to user via plans table
|
|
const booking = db.get<FlightBooking & { plan_id: string }>(
|
|
`SELECT fb.* FROM flight_bookings fb
|
|
JOIN plans p ON p.id = fb.plan_id
|
|
WHERE fb.id = ? AND p.user_id = ?`,
|
|
[input.bookingId, input.userId]
|
|
);
|
|
if (!booking) throw new Error('Flight booking not found or not authorized');
|
|
|
|
// Derive new title from first segment
|
|
const firstSegment = input.segments[0];
|
|
const title = firstSegment
|
|
? (() => {
|
|
const airline = firstSegment.airlineName || firstSegment.airlineIata || 'Flight';
|
|
const flightNumber = firstSegment.flightNumber?.trim();
|
|
const dep = firstSegment.route?.departureAirportCode?.trim();
|
|
const arr = firstSegment.route?.arrivalAirportCode?.trim();
|
|
if (flightNumber) return `${airline} ${flightNumber}`;
|
|
if (dep || arr) return `Flight: ${dep || 'TBD'} -> ${arr || 'TBD'}`;
|
|
return 'Flight';
|
|
})()
|
|
: 'Flight';
|
|
|
|
// Update the plan row (status + title)
|
|
db.run(`UPDATE plans SET status = ?, title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [
|
|
input.status ?? 'idea',
|
|
title,
|
|
booking.plan_id
|
|
]);
|
|
|
|
// Update the booking row
|
|
db.run(
|
|
`UPDATE flight_bookings SET confirmation_number = ?, price = ?, currency = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
|
[
|
|
input.confirmationNumber ?? null,
|
|
input.price ?? null,
|
|
input.currency ?? 'USD',
|
|
input.bookingId
|
|
]
|
|
);
|
|
|
|
// Replace segments and routes wholesale
|
|
const existingSegments = db.all<{ id: string }>(
|
|
'SELECT id FROM flight_segments WHERE flight_booking_id = ?',
|
|
[input.bookingId]
|
|
);
|
|
for (const seg of existingSegments) {
|
|
db.run('DELETE FROM flight_routes WHERE flight_segment_id = ?', [seg.id]);
|
|
}
|
|
db.run('DELETE FROM flight_segments WHERE flight_booking_id = ?', [input.bookingId]);
|
|
|
|
let segmentPosition = 0;
|
|
for (const segment of input.segments) {
|
|
const segmentId = randomUUID();
|
|
db.run(
|
|
`INSERT INTO flight_segments (id, flight_booking_id, departure_date, airline_id, airline_iata, airline_icao, airline_name, flight_number, position)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
segmentId,
|
|
input.bookingId,
|
|
segment.departureDate,
|
|
segment.airlineId ?? null,
|
|
segment.airlineIata ?? null,
|
|
segment.airlineIcao ?? null,
|
|
segment.airlineName ?? null,
|
|
segment.flightNumber?.trim() || '',
|
|
segmentPosition++
|
|
]
|
|
);
|
|
|
|
if (segment.route) {
|
|
const routeId = randomUUID();
|
|
db.run(
|
|
`INSERT INTO flight_routes (
|
|
id, flight_segment_id,
|
|
departure_airport_id, departure_airport_code, departure_terminal, departure_gate,
|
|
departure_datetime, departure_timezone,
|
|
arrival_airport_id, arrival_airport_code, arrival_terminal, arrival_gate,
|
|
arrival_date, arrival_datetime, arrival_timezone
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
routeId,
|
|
segmentId,
|
|
segment.route.departureAirportId ?? null,
|
|
segment.route.departureAirportCode ?? null,
|
|
segment.route.departureTerminal ?? null,
|
|
segment.route.departureGate ?? null,
|
|
segment.route.departureDatetime ?? null,
|
|
segment.route.departureTimezone ??
|
|
airportTimezone(segment.route.departureAirportId) ??
|
|
null,
|
|
segment.route.arrivalAirportId ?? null,
|
|
segment.route.arrivalAirportCode ?? null,
|
|
segment.route.arrivalTerminal ?? null,
|
|
segment.route.arrivalGate ?? null,
|
|
segment.route.arrivalDate ?? null,
|
|
segment.route.arrivalDatetime ?? null,
|
|
segment.route.arrivalTimezone ?? airportTimezone(segment.route.arrivalAirportId) ?? null
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Replace passengers
|
|
db.run('DELETE FROM flight_booking_passengers WHERE flight_booking_id = ?', [input.bookingId]);
|
|
if (input.passengerIds && input.passengerIds.length > 0) {
|
|
for (const personId of input.passengerIds) {
|
|
db.run(
|
|
`INSERT OR IGNORE INTO flight_booking_passengers (flight_booking_id, person_id) VALUES (?, ?)`,
|
|
[input.bookingId, personId]
|
|
);
|
|
}
|
|
}
|
|
}
|