File size: 3,937 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import fs from 'node:fs';
import path from 'node:path';
import { db } from '../db/database';

export interface Airport {
  iata: string;
  icao: string | null;
  name: string;
  city: string;
  country: string;
  lat: number;
  lng: number;
  tz: string;
}

let cache: Airport[] | null = null;
let byIata: Map<string, Airport> | null = null;

function load(): Airport[] {
  if (cache) return cache;
  const file = path.join(__dirname, '..', '..', 'assets', 'airports.json');
  if (!fs.existsSync(file)) {
    console.warn('[airports] airports.json missing — run `node scripts/build-airports.mjs`');
    cache = [];
    byIata = new Map();
    return cache;
  }
  const raw = fs.readFileSync(file, 'utf8');
  cache = JSON.parse(raw) as Airport[];
  byIata = new Map(cache.map(a => [a.iata, a]));
  return cache;
}

export function findByIata(code: string): Airport | null {
  load();
  return byIata!.get(code.toUpperCase()) ?? null;
}

export function searchAirports(query: string, limit = 12): Airport[] {
  const all = load();
  const q = query.trim().toLowerCase();
  if (!q) return [];

  const upper = q.toUpperCase();
  if (q.length === 3) {
    const exact = byIata!.get(upper);
    if (exact) return [exact];
  }

  const matches: Array<{ a: Airport; score: number }> = [];
  for (const a of all) {
    let score = 0;
    if (a.iata === upper) score = 100;
    else if (a.icao === upper) score = 90;
    else if (a.iata.startsWith(upper)) score = 70;
    else if (a.city.toLowerCase().startsWith(q)) score = 60;
    else if (a.name.toLowerCase().startsWith(q)) score = 50;
    else if (a.city.toLowerCase().includes(q)) score = 30;
    else if (a.name.toLowerCase().includes(q)) score = 20;
    if (score > 0) matches.push({ a, score });
  }
  matches.sort((x, y) => y.score - x.score || x.a.iata.localeCompare(y.a.iata));
  return matches.slice(0, limit).map(m => m.a);
}

export function backfillFlightEndpoints(): void {
  const pending = db.prepare(`
    SELECT r.id, r.metadata, r.reservation_time, r.reservation_end_time
    FROM reservations r
    WHERE r.type = 'flight'
      AND NOT EXISTS (SELECT 1 FROM reservation_endpoints e WHERE e.reservation_id = r.id)
  `).all() as { id: number; metadata: string | null; reservation_time: string | null; reservation_end_time: string | null }[];

  if (pending.length === 0) return;

  load();
  const insert = db.prepare(`
    INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `);
  const markReview = db.prepare('UPDATE reservations SET needs_review = 1 WHERE id = ?');

  let filled = 0;
  let flagged = 0;
  for (const r of pending) {
    if (!r.metadata) { markReview.run(r.id); flagged++; continue; }
    let meta: any;
    try { meta = JSON.parse(r.metadata); } catch { markReview.run(r.id); flagged++; continue; }

    const dep = meta.departure_airport ? findByIata(String(meta.departure_airport).slice(0, 3)) : null;
    const arr = meta.arrival_airport ? findByIata(String(meta.arrival_airport).slice(0, 3)) : null;

    if (!dep || !arr) { markReview.run(r.id); flagged++; continue; }

    const split = (iso: string | null) => {
      if (!iso) return { date: null as string | null, time: null as string | null };
      const [date, time] = iso.split('T');
      return { date: date || null, time: time ? time.slice(0, 5) : null };
    };
    const depParts = split(r.reservation_time);
    const arrParts = split(r.reservation_end_time);

    insert.run(r.id, 'from', 0, dep.city ? `${dep.city} (${dep.iata})` : dep.name, dep.iata, dep.lat, dep.lng, dep.tz, depParts.time, depParts.date);
    insert.run(r.id, 'to', 1, arr.city ? `${arr.city} (${arr.iata})` : arr.name, arr.iata, arr.lat, arr.lng, arr.tz, arrParts.time, arrParts.date);
    filled++;
  }

  console.log(`[airports] Backfill: ${filled} filled, ${flagged} flagged for review`);
}