# Спецификация 02: FitFetcher — цикл забора и дедупликация Источники: Ф-8.1, module_reqs разд. 2 (два рубежа дедупа), 🛡3 (сбой одного файла не останавливает пакет). ## 1. Сигнатура ```csharp public FitFetcher(IConnectionString cs, GarminClient client, string downloadDir, string athleteName); public (int Downloaded, int Skipped, int Failed) FetchNew( int limit = 20, CancellationToken ct = default); ``` ## 2. Алгоритм 1. **Атлет**: `athleteName` → `src.athlete` (первый по имени); не найден → `FetchException` (CLI переведёт в код 1; 🛡1 — факты без атлета не пишутся). 2. `client.Login()` — исключения пробрасываются (без частичного результата). 3. `ListActivities(limit)` → фильтр `activityType == "strength_training"`. 4. Для каждой активности, по порядку списка: - `downloadDir\{activityId}.fit` существует → `Skipped++` (рубеж 1 — без сети), далее; - скачать → записать во временный файл → `FitParser.Parse`: сбой → файл остаётся как `{activityId}.fit`? **нет** — битый download переносится в `{activityId}.fit.bad` (не мешает кэшу), `Failed++`, далее (🛡3); - `WorkoutImporter.Import(athleteId, facts)` — дедуп по `fit_file_id` внутри (возврат существующего id, записи нет); если это дубль (возврат id существующей строки, файл не новый) → скачанный файл **удаляется** (рубеж 2, 🛡2), `Skipped++`; - иначе файл переименовывается в `{activityId}.fit` (кэш), `Downloaded++`. 5. Возврат (Downloaded, Skipped, Failed). ## 3. Ошибки и коды - `FetchException` (атлет/конфигурация) → CLI код 1; - `GarminAuthException` → CLI код 2 + stdout-подсказка «используйте папку импорта (--mode import)» (fallback О-1-А); - сбои отдельных файлов → код 2 при `Failed > 0`. ## 4. Инварианты 🛡 - записи в БД — только через `WorkoutImporter` (конвейер m06); - изоляция: факты пишутся только `athleteId` атлета-владельца учётки; - повторный запуск не качает дубли и не создаёт дублей в БД.