# Требования к модулю: m07-fit-fetch > Модуль — проект `Server.WatchBody.Fetch` и режим `--mode fetch` > единой точки входа CLI: неофициальный клиент Garmin Connect под > учётной записью владельца (Ф-8.1, решение опросника 4.3, ОВ-1-А), > скачивание .fit новых силовых тренировок в `Garmin.DownloadDir`, > дедупликация по `fit_file_id` (🛡 — повторный запуск не качает > дубли), разбор и запись фактов конвейером m06. Самодостаточная > копия требований (d-dev-6). ## 1. Назначение и границы **В границах модуля:** - класс `GarminClient` (HTTP-клиент Garmin Connect): SSO-логин (embed → csrf → signin → ticket → OAuth1 → Bearer) по контракту семьи библиотек `garminconnect` (tech-req разд. 5); список активностей; скачивание .fit по activityId. `HttpMessageHandler` инъецируется (тесты — фейковый handler с формальным SSO-флоу и настоящими .fit-байтами; против живого Garmin прогон не автоматизируется — прототип ОВ этапа 7); - класс `FitFetcher`: `FetchNew()` — логин → список → фильтр `strength_training` → скачивание `{activityId}.fit` в `Garmin.DownloadDir` (файл уже в папке → скачивания нет) → `FitParser` → `WorkoutImporter` (m06) → скачанный файл, давший дубль по `fit_file_id` в БД, удаляется. Возврат (downloaded, skipped, failed); - режим `--mode fetch` в `Program.cs` Web (единая точка входа, requirements 2.3): `Garmin.Login/Password` обязательны (иначе код 1), коды 0/1/2 — как у import; расписание — внешний планировщик ОС (ОВ-4, интервал 15 мин — в код не входит); - конфиг `Garmin.Login` / `Garmin.Password` / `Garmin.DownloadDir` (+ `Garmin.BaseUrl` — замена точек входа для тестов); - тесты (интеграционные против docker `watchbody-pg` + фейковый Garmin-handler). **Вне границ модуля:** парсинг/запись фактов (m06, конвейер общий), fallback-папка импорта (m06, работает независимо — деградация при блокировке логина), метрики (m04 пересчитает при следующем sync), несколько учётных записей атлетов (ОВ-10 — v2), расписание (ОС), MFA-логин (не поддерживается в v1 — сообщается ошибкой). ## 2. Дедупликация (🛡, Ф-8.1) Два рубежа: 1. **Быстрый (без сети)**: файл `{activityId}.fit` уже есть в `Garmin.DownloadDir` → activityId пропускается, повторного скачивания нет. 2. **По БД**: скачанный файл разбирается, `fit_file_id` (FileId сообщения) ищется в `src.workout` — есть → это дубль (повторный забор того же .fit): файл удаляется, запись не выполняется (`WorkoutImporter` m06 возвращает существующий id — запись идемпотентна и без удаления файла). ## 3. Инварианты (🛡 — нарушать запрещено) 1. **Изоляция атлета** — факты пишутся на атлета-владельца учётки (config → атлет с этим именем; не найден — ошибка fetch, файл остаётся в DownloadDir). 2. **Повторный запуск не качает дубли** — оба рубежа разд. 2. 3. **Сбой одного файла не останавливает пакет** (как import) — файл остаётся в DownloadDir, failed++, итоговый код 2. 4. **Тайны** — пароль Garmin в логи не пишется (З-2, З-6); куки/ токены не логируются. 5. **Детерминированность записи** — только конвейер m06, никаких собственных SQL-записей. ## 4. Критерии приёмки модуля 1. Фейковый Garmin: логин (формальный SSO), список из strength + cardio активностей — скачивается и импортируется только strength; `facts_json`/`avg_hr` в БД. 2. Повторный `FetchNew()` — 0 скачиваний (папка-кэш), БД без дублей. 3. .fit, уже импортированный ранее (тот же `fit_file_id` под другим activityId) — скачивается, файл удаляется, второй строки нет. 4. Неуспешный логин (фейковый 401/antics) — исключение с сообщением без пароля; CLI-код 2; fallback — подсказка про папку импорта. 5. Тесты интеграционные против docker `watchbody-pg` — зелёные; сборка решения без ошибок.