# ТЗ: Дополнение расписания из FR24 flight-summary/full

**Дата:** 2026-04-21  
**Статус:** READY FOR DEV  
**Приоритет:** Высокий

---

## Контекст

FR24 `/api/flight-summary/full` уже используется в `fr24_worker.py` и возвращает
**все реальные рейсы** через московские аэропорты за сутки (до 20 000 записей).

Текущее поведение:
- `enrich_schedule()` — только **UPDATE** существующих строк в `fr24_ext.schedule`
- Рейсы из `flight_actual`, которых нет в Яндекс.Расписании, **теряются**

Проблема: Яндекс не покрывает 100% рейсов (чартеры, грузовые, технические, задержанные).
FR24 имеет полные данные. Нужно использовать `flight_actual` как дополнительный источник расписания.

---

## Что нужно сделать

### 1. Функция `supplement_schedule()` в `fr24_worker.py`

```python
def supplement_schedule(conn, target_date: date) -> int:
    """
    Insert into fr24_ext.schedule flights from flight_actual
    that have no matching schedule record.
    
    Source: fr24_ext.flight_actual
    Target: fr24_ext.schedule (source='fr24')
    
    Returns: number of rows inserted
    """
```

**Логика:**
1. Найти все рейсы в `flight_actual` за дату, у которых нет соответствия в `schedule`
2. Определить direction: если `origin_icao` в московских аэропортах → departure, иначе arrival
3. INSERT в `schedule` с `source='fr24'`, `status='actual'`

**SQL-скелет для поиска незаматченных:**
```sql
SELECT fa.*
FROM fr24_ext.flight_actual fa
WHERE fa.flight_date = %(date)s
  AND NOT EXISTS (
    SELECT 1 FROM fr24_ext.schedule s
    WHERE UPPER(REPLACE(s.flight_number,' ','')) = UPPER(REPLACE(fa.flight,' ',''))
      AND s.flight_date = fa.flight_date
  )
  AND fa.flight IS NOT NULL
  AND fa.flight != ''
```

**Поля для INSERT в schedule:**
```
flight_date     = target_date
airport_iata    = ICAO_TO_IATA[origin_icao или dest_icao] (московский аэропорт)
direction       = 'departure' если origin_icao московский, иначе 'arrival'
flight_number   = fa.flight (нормализовать: убрать лишние пробелы)
airline_iata    = первые 2 символа IATA (если можно определить)
origin_iata     = ICAO_TO_IATA.get(fa.origin_icao)
destination_iata = ICAO_TO_IATA.get(fa.dest_icao)
aircraft_type   = NULL (нет в flight_actual)
scheduled_at    = fa.datetime_takeoff (или datetime_landed для arrival)
actual_takeoff  = fa.datetime_takeoff
actual_landed   = fa.datetime_landed
status          = 'actual'
source          = 'fr24'
fr24_id         = fa.fr24_id
```

**ON CONFLICT:** `(flight_number, airport_iata, scheduled_at, direction)` — уже есть.

### 2. Также исправить баги из ревью

#### Баг A: ZBAD/PKX не в словаре ICAO_TO_IATA
Файл: `build_mart.py`
Добавить в словарь `ICAO_TO_IATA`:
```python
"ZBAD": "PKX",   # Beijing Daxing
"RKSI": "ICN",   # Seoul Incheon (уже есть, проверить)
```

#### Баг B: Soft match для FA/FR24 когда destination не совпадает
Файл: `build_mart.py`, функции `find_fa_track()` и `find_fr24_track()`

Текущая логика:
```python
if origin_iata and not destination_iata:  # только если dest == NULL
    # soft match by origin only
```

Нужно изменить на:
```python
# Если полный матч не нашёл → fallback на origin only
if origin_iata:
    for row in rows:
        orig_iata = ICAO_TO_IATA.get(row[2])
        if orig_iata == origin_iata:
            return (row[0], row[1])
```
(убрать условие `not destination_iata`)

### 3. Вызов `supplement_schedule()` в `run()` fr24_worker.py

После `enrich_schedule()`:
```python
# 4. Supplement schedule with flights from FR24 not in Yandex
try:
    supplemented = supplement_schedule(conn, target_date)
    conn.commit()
    stats["schedule_supplemented"] = supplemented
    log.info("FR24 worker: supplemented %d new schedule rows", supplemented)
except Exception as e:
    conn.rollback()
    log.error("FR24 worker: supplement_schedule failed: %s", e)
    stats["errors"] += 1
```

---

## Что НЕ нужно делать

- НЕ менять DDL/схему БД (колонки `fr24_id`, `actual_takeoff`, `actual_landed` в schedule уже есть или добавить как nullable)
- НЕ трогать RTL-SDR матч — он работает корректно
- НЕ запускать backfill — только текущая логика для новых дат

---

## Проверка результата

1. Запустить build для 19.04 или 20.04 (данные уже есть в `flight_actual`)
2. Проверить:
```sql
SELECT source, count(*) FROM fr24_ext.schedule 
WHERE flight_date = '2026-04-19' GROUP BY source;
-- Ожидаемо: yandex=~1570, fr24=N (новые)

SELECT count(*) FROM fr24_ext.schedule s
JOIN fr24_ext.flight_actual fa ON UPPER(REPLACE(fa.flight,' ','')) = UPPER(REPLACE(s.flight_number,' ',''))
WHERE s.flight_date = '2026-04-19' AND s.source = 'fr24';
```
3. Перезапустить mart build для даты — посмотреть увеличение `schedule_flights` и `with_track`

---

## Файлы для изменения

| Файл | Изменение |
|------|-----------|
| `ingest/tracks_fr24/fr24_worker.py` | Добавить `supplement_schedule()`, вызвать в `run()` |
| `ingest/mart/build_mart.py` | Добавить `ZBAD→PKX` в словарь, исправить soft match |

---

## Путь к проекту на VM

`/home/fr24/fr24/ingest/`

Деплой: `docker cp <file> fr24-tracks-fr24:/app/<file> && docker restart fr24-tracks-fr24`
