augmenttoolkit / server /src /services /airtrail /airtrailImport.ts
Leon4gr45's picture
Upload folder using huggingface_hub (part 5)
57a889c verified
Raw
History Blame Contribute Delete
5.17 kB
import type { AirtrailImportResult } from '@trek/shared';
import { db } from '../../db/database';
import { broadcast } from '../../websocket';
import { createReservation } from '../reservationService';
import { getAirtrailCredentials } from './airtrailService';
import { AirtrailRequestError, listFlights } from './airtrailClient';
import { canonicalHash, mapFlightToReservation } from './airtrailMapper';
interface ExistingFlightRow {
id: number;
reservation_time: string | null;
metadata: string | null;
from_code: string | null;
to_code: string | null;
}
function depDate(t: string | null): string | null {
return t && /^\d{4}-\d{2}-\d{2}/.test(t) ? t.slice(0, 10) : null;
}
/** A loose "same physical flight" key: flight number + date, else route + date. */
function softSignature(
date: string | null,
flightNumber: string | null,
fromCode: string | null,
toCode: string | null,
): string | null {
if (!date) return null;
if (flightNumber) return `fn:${flightNumber.toUpperCase()}@${date}`;
if (fromCode && toCode) return `rt:${fromCode.toUpperCase()}-${toCode.toUpperCase()}@${date}`;
return null;
}
/**
* Import the given AirTrail flights into a trip as reservations (type:'flight'),
* recording the AirTrail linkage for two-way sync and broadcasting each one live.
*
* Dedup: a flight already linked to this trip is skipped ('already-imported'); a
* flight that looks like one already in the trip — e.g. the same flight another
* member already imported from their own AirTrail — is skipped ('already-in-trip').
* The server re-fetches the flights by id with the caller's own key, so the client
* cannot inject arbitrary flight data.
*/
export async function importAirtrailFlights(
tripId: string | number,
userId: number,
flightIds: string[],
socketId: string | undefined,
): Promise<AirtrailImportResult> {
const creds = getAirtrailCredentials(userId);
if (!creds) throw new AirtrailRequestError('AirTrail is not connected', 400);
const wanted = new Set(flightIds.map(String));
const selected = (await listFlights(creds)).filter(f => wanted.has(String(f.id)));
const result: AirtrailImportResult = { imported: [], skipped: [] };
const linkedIds = new Set(
(db.prepare("SELECT external_id FROM reservations WHERE trip_id = ? AND external_source = 'airtrail'").all(tripId) as {
external_id: string | null;
}[])
.map(r => r.external_id)
.filter((v): v is string => !!v),
);
const existing = db
.prepare(
`SELECT r.id, r.reservation_time, r.metadata,
(SELECT code FROM reservation_endpoints WHERE reservation_id = r.id AND role = 'from' LIMIT 1) AS from_code,
(SELECT code FROM reservation_endpoints WHERE reservation_id = r.id AND role = 'to' LIMIT 1) AS to_code
FROM reservations r WHERE r.trip_id = ? AND r.type = 'flight'`,
)
.all(tripId) as ExistingFlightRow[];
const existingSigs = new Set<string>();
for (const row of existing) {
let fn: string | null = null;
try {
fn = row.metadata ? (JSON.parse(row.metadata).flight_number ?? null) : null;
} catch {
/* malformed metadata — ignore */
}
const sig = softSignature(depDate(row.reservation_time), fn, row.from_code, row.to_code);
if (sig) existingSigs.add(sig);
}
for (const flight of selected) {
const fid = String(flight.id);
if (linkedIds.has(fid)) {
result.skipped.push({ flightId: fid, reason: 'already-imported' });
continue;
}
const mapped = mapFlightToReservation(flight);
const sig = softSignature(
depDate(mapped.reservation_time),
(mapped.metadata.flight_number as string) ?? null,
mapped.endpoints.find(e => e.role === 'from')?.code ?? null,
mapped.endpoints.find(e => e.role === 'to')?.code ?? null,
);
if (sig && existingSigs.has(sig)) {
result.skipped.push({ flightId: fid, reason: 'already-in-trip', detail: mapped.title });
continue;
}
try {
const { reservation } = createReservation(tripId, mapped as any);
const now = new Date().toISOString();
db.prepare(
`UPDATE reservations SET external_source = 'airtrail', external_id = ?, external_owner_user_id = ?,
sync_enabled = 1, external_hash = ?, external_synced_at = ? WHERE id = ?`,
).run(fid, userId, canonicalHash(flight), now, reservation.id);
// Carry the linkage on the broadcast payload so members see the badge live.
reservation.external_source = 'airtrail';
reservation.external_id = fid;
reservation.external_owner_user_id = userId;
reservation.sync_enabled = 1;
reservation.external_synced_at = now;
broadcast(tripId, 'reservation:created', { reservation }, socketId);
if (sig) existingSigs.add(sig);
linkedIds.add(fid);
result.imported.push(fid);
} catch (err) {
console.error('[airtrail-import] failed to import flight', fid, err instanceof Error ? err.message : err);
result.skipped.push({ flightId: fid, reason: 'invalid', detail: err instanceof Error ? err.message : undefined });
}
}
return result;
}