# PRD: Reverse Scan — Country → Specific Airport(s) **Status:** Ready for implementation **Date:** 2026-03-01 **Target:** Web App (api_server.py + scan_processor.py + database + frontend) --- ## 1. Problem All scans today are fixed-origin, variable-destination: > *"From BDS — which German airports can I fly to directly?"* The opposite question is equally common but unsupported: > *"I want to fly to BRI or BDS — which German airport should I depart from?"* This is the natural perspective of a traveller based in Germany looking for the cheapest access point to Puglia, not a traveller already in Puglia wondering where to go. --- ## 2. Goal Let users create a scan where: - **Origin** = all airports in a country (e.g. DE) - **Destination** = one or more specific airports (e.g. BRI, BDS) The results table for a reverse scan shows one row per **origin airport × destination** pair, so the user can directly compare which German airport is cheapest for each destination. --- ## 3. User Stories - **As a user**, I want to search DE → BRI,BDS so I can see which German airport has the cheapest direct flight to Puglia. - **As a user**, I want to schedule a reverse scan to run weekly. - **As a user**, I want to mix both directions in my scan history. - **As a user**, I want the destination country in forward scans to be a dropdown, not free text. --- ## 4. Scope ### In scope - `scan_mode` field on scans and scheduled scans: `"forward"` (default) or `"reverse"` - Reverse scan: origin = ISO country (dropdown), destinations = specific airport(s) - Forward scan: destination country changed from text input to dropdown - New `GET /api/v1/countries` endpoint (powers both dropdowns) - `routes` table: add `origin_airport` column (nullable; populated for reverse scans) - `flights` table: add `origin_airport` column (nullable; populated for reverse scans) - ScanDetails routes table: show Origin column for reverse scans - Scheduled scans: support `scan_mode` ### Out of scope - Multi-country origin (e.g. DE + IT → BRI) - Round-trip reverse scans --- ## 5. Data Model Changes ### 5.1 `scans` table ```sql ALTER TABLE scans ADD COLUMN scan_mode TEXT NOT NULL DEFAULT 'forward' CHECK (scan_mode IN ('forward', 'reverse')); ``` Column semantics by mode: | Column | Forward (existing) | Reverse (new) | |--------|--------------------|---------------| | `origin` | Single IATA (e.g. `BDS`) | ISO country code (e.g. `DE`) | | `country` | ISO destination country OR comma-separated destination IATAs | Comma-separated destination IATAs (e.g. `BRI,BDS`) | | `scan_mode` | `forward` | `reverse` | All existing rows default to `forward`. No data migration needed. ### 5.2 `routes` table ```sql ALTER TABLE routes ADD COLUMN origin_airport TEXT; ``` - **Forward scans**: `origin_airport` = NULL (origin is always the scan-level `origin` IATA, already known) - **Reverse scans**: `origin_airport` = the specific origin IATA for this route (e.g. `BER`, `FRA`) The unique constraint on routes changes from `(scan_id, destination)` to `(scan_id, COALESCE(origin_airport, ''), destination)`. **Reverse scan routes example** for `DE → BRI, BDS`: | origin_airport | destination | flight_count | min_price | |----------------|-------------|--------------|-----------| | BER | BRI | 14 | €15 | | FMM | BRI | 8 | €17 | | DUS | BRI | 6 | €22 | | BER | BDS | 3 | €28 | | MUC | BDS | 2 | €34 | ### 5.3 `flights` table ```sql ALTER TABLE flights ADD COLUMN origin_airport TEXT; ``` - **Forward scans**: `origin_airport` = NULL (origin is the scan-level `origin` IATA) - **Reverse scans**: `origin_airport` = the specific IATA the flight departs from ### 5.4 `scheduled_scans` table ```sql ALTER TABLE scheduled_scans ADD COLUMN scan_mode TEXT NOT NULL DEFAULT 'forward' CHECK (scan_mode IN ('forward', 'reverse')); ``` Same column semantics as `scans`. When the scheduler fires a reverse scheduled scan, it creates a reverse scan child. --- ## 6. New API Endpoint ### `GET /api/v1/countries` Returns the list of countries that have at least one airport in the database. Powers both the forward-mode destination country dropdown and the reverse-mode origin country dropdown. **Response:** ```json { "data": [ { "code": "DE", "name": "Germany", "airport_count": 23 }, { "code": "IT", "name": "Italy", "airport_count": 41 }, ... ] } ``` Sorted alphabetically by name. Derived from `data/airports_by_country.json` at startup (same source as airport resolution). Rate limit: 100/min. Country names are resolved from a static ISO → name mapping in `api_server.py` (same `COUNTRY_NAME_TO_ISO` dict already in `airports.py`, inverted). --- ## 7. Backend Changes ### 7.1 `ScanRequest` model Add `scan_mode: Literal['forward', 'reverse'] = 'forward'`. Root validator enforces: **Forward mode** (unchanged): - `origin`: required, 3-char IATA - `country` OR `destinations`: required **Reverse mode**: - `origin`: required, 2-char ISO country code - `destinations`: required, 1–10 IATAs - `country` must be absent or null ### 7.2 `scan_processor.py` — `process_scan()` **Forward** (unchanged): ``` fixed origin IATA × [airports resolved from destination country / destination IATAs] ``` **Reverse** (new): ``` [airports resolved from origin country] × fixed destination IATA(s) ``` Steps for reverse: 1. Read `scan.origin` as ISO country code → resolve airport list via `airports.py` 2. Read `scan.country` as comma-separated destination IATAs 3. Build pairs: `[(orig, dest) for orig in origin_airports for dest in destination_iatas]` 4. For each pair: call `search_multiple_routes(orig, dest)` as usual 5. Save route with `origin_airport=orig`, `destination=dest` 6. Save each flight with `origin_airport=orig` `total_routes` = `len(origin_airports) × len(destination_iatas)`. ### 7.3 `GET /scans/{id}/routes` response Add `origin_airport: str | null` to the `Route` response model. Frontend uses this to show/hide the Origin column. ### 7.4 `GET /scans/{id}/flights` response Add `origin_airport: str | null` to the `Flight` response model. ### 7.5 Scheduled scans `ScheduleRequest` and `Schedule` models gain `scan_mode`. The scheduler's `_fire_due_scans()` function passes `scan_mode` when inserting child scans into the `scans` table. --- ## 8. Frontend Changes ### 8.1 New `countriesApi` client (`api.ts`) ```typescript export interface Country { code: string; name: string; airport_count: number; } export const countriesApi = { list: () => api.get<{ data: Country[] }>('/countries'), }; ``` ### 8.2 New `CountrySelect` component A standard `