# Спецификация 02: WorkoutImporter — сопоставление и запись фактов Источники: Ф-8.4, tables.md (`src.workout`: `UNIQUE(athlete_id, fit_file_id)`, окно ± 15 минут), 🛡2 module_reqs. ## 1. Сигнатура ```csharp public Guid Import(int athleteId, FitFacts facts); // → workout id // + перегрузка для упрощённого факта TCX/GPX (см. §4) ``` ## 2. Алгоритм 1. **Дедуп**: `workouts.Select(athleteId, DateFrom/DateTo = день факта)` содержит строку с тем же `fit_file_id` → возврат её id, **записей нет** (🛡3; повторный импорт — пропуск). 2. **Сопоставление** (окно ± 15 минут от `facts.StartedAt`, Ф-8.4): - кандидат — тренировка телефона: `program_version_id`/`session_kind` любые, но **без** `fit_file_id` (уже связанная не перецепляется); ближайшая по `|started_at − факт|`; - несколько кандидатов в окне → ближайший; равенство (невозможно практически) — по возрастанию id (детерминированность 🛡). 3. **Сопоставилась** → `workouts.Update`: дописываются только `facts_json`, `avg_hr`, `hr_recovery_s`, `fit_file_id`; `started_at`/`status`/`session_kind`/подходы телефона не меняются (🛡2). Существующий `facts_json` перезаписывается (файл один — у тренировки не может быть двух .fit). 4. **Не сопоставилась** → новая строка «только .fit»: `Id = new Guid()` (сервер, Р2-Б), `StartedAt = facts.StartedAt`, `Status = "finished"`, `SessionKind = "normal"` + поля фактов. Подходы (`workout_set`) не создаются (модуль не знает соответствие упражнений — v1). ## 3. Возврат workout id связанной/созданной строки (для лога). ## 4. Упрощённый факт TCX/GPX `TcxGpxParser` возвращает тот же `FitFacts`, где `FactsJson` — схема `workout-facts/1` с `"schema": "workout-facts/1"` и заполненными `session`/`records` (пульсы), `laps` пустой, `sets` пустой, `fit_file_id` = имя файла, `device` = "". Запись — тот же алгоритм §2. ## 5. Инварианты 🛡 - все выборки по `athleteId` (🛡1); - сопоставление не удаляет и не переписывает телефонные данные; - идемпотентность по `fit_file_id`.