File size: 4,210 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Multi-leg (layover) flight support.
//
// A flight booking is ONE reservation whose route is an ordered chain of airports
// (e.g. FRA -> BER -> HND). The geometry + order are the source of truth in
// `reservation.endpoints` (role 'from' for the first airport, 'stop' for each
// intermediate one, 'to' for the last, ordered by `sequence`). The per-leg detail
// — airline, flight number, and each segment's own day/time — lives in
// `metadata.legs`. The top-level metadata (`departure_airport`/`arrival_airport`/
// `airline`/`flight_number`) and `day_id`/`end_day_id` mirror the FIRST and LAST
// leg so legacy readers keep working.
//
// A legacy single-leg flight (two endpoints, flat metadata, no `metadata.legs`)
// is normalised here into a one-leg chain, so every renderer can use one path.

import type { Reservation, ReservationEndpoint } from '../types'

export interface FlightLeg {
  from: string | null // IATA code (or null)
  to: string | null
  airline?: string
  flight_number?: string
  dep_day_id?: number | null
  dep_time?: string | null // 'HH:mm'
  arr_day_id?: number | null
  arr_time?: string | null
}

/** reservation.metadata may be a JSON string or an already-parsed object. */
export function parseReservationMetadata(r: Pick<Reservation, 'metadata'>): Record<string, any> {
  const m = r.metadata
  if (!m) return {}
  if (typeof m === 'string') {
    try {
      let parsed = JSON.parse(m || '{}')
      // Defensive: an earlier bug could double-encode metadata (a JSON string of a
      // JSON string) — unwrap it once more so saved flights heal on read.
      if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed) } catch { /* keep */ } }
      return (parsed && typeof parsed === 'object') ? parsed : {}
    } catch { return {} }
  }
  return m as Record<string, any>
}

/** Endpoints ordered by `sequence` (geometry + order source of truth). */
export function orderedEndpoints(r: Pick<Reservation, 'endpoints'>): ReservationEndpoint[] {
  return (r.endpoints || []).slice().sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
}

/**
 * Ordered legs of a flight. `metadata.legs` is preferred; otherwise a single leg
 * is derived from the endpoints (and finally the flat metadata) so that legacy
 * single-leg flights — and flights created before this feature — still work.
 */
export function getFlightLegs(r: Reservation): FlightLeg[] {
  const meta = parseReservationMetadata(r)
  if (Array.isArray(meta.legs) && meta.legs.length > 0) {
    return meta.legs.map((l: any): FlightLeg => ({
      from: l.from ?? null,
      to: l.to ?? null,
      airline: l.airline || undefined,
      flight_number: l.flight_number || undefined,
      dep_day_id: l.dep_day_id ?? null,
      dep_time: l.dep_time ?? null,
      arr_day_id: l.arr_day_id ?? null,
      arr_time: l.arr_time ?? null,
    }))
  }
  // Legacy fallback: one leg from the endpoints / flat metadata.
  const eps = orderedEndpoints(r)
  const first = eps[0]
  const last = eps[eps.length - 1]
  const fromCode = first?.code ?? meta.departure_airport ?? null
  const toCode = last?.code ?? meta.arrival_airport ?? null
  if (!fromCode && !toCode) return []
  return [{
    from: fromCode,
    to: toCode,
    airline: meta.airline || undefined,
    flight_number: meta.flight_number || undefined,
    dep_day_id: r.day_id ?? null,
    dep_time: first?.local_time ?? null,
    arr_day_id: r.end_day_id ?? r.day_id ?? null,
    arr_time: last?.local_time ?? null,
  }]
}

/** Number of flight segments. 1 for a simple from -> to booking. */
export function legCount(r: Reservation): number {
  return getFlightLegs(r).length
}

export function isMultiLegFlight(r: Reservation): boolean {
  return r.type === 'flight' && legCount(r) > 1
}

/**
 * Ordered route labels (IATA codes, or names when no code) for display, e.g.
 * ['FRA','BER','HND']. Uses endpoints; falls back to the flat metadata pair.
 */
export function routeStops(r: Reservation): string[] {
  const eps = orderedEndpoints(r)
  if (eps.length >= 2) return eps.map(e => e.code || e.name)
  const meta = parseReservationMetadata(r)
  return [meta.departure_airport, meta.arrival_airport].filter(Boolean) as string[]
}