2f55632792
Оба job'а инжектят SupplierProjectChannel (DI → FailoverProjectChannel) вместо прямого SupplierPortalClient. Catch TierEscalatedException + WindowDeferredException — эскалация/перенос пропускают элемент, не валят job. SyncSupplierProjectJob (singular): handle переписан — find-or-create local supplier_projects row, portal-create через channel. ОТКЛОНЕНИЕ ОТ plan Step 8.1: план писал channel-результат (portal external_id) прямо в projects.supplier_b*_ project_id, но эта колонка — FK на supplier_projects.id (local), не portal id. Сохранена семантика ensureSupplierProject — job создаёт local row с supplier_external_id и пишет в FK local id. ensureSupplierProject удалён из SupplierPortalClient (был единственный consumer — этот job). SyncSupplierProjectsJob (plural): handle/syncOne принимают channel; create → createProjectForLiderra, update → updateProjectForLiderra (context-project из liderraProjects->first() для project_id в очереди яруса 3). Tests: singular переписан под SupplierProjectChannel mock (6 tests, incl. idempotency reuse); plural — handle(AjaxProjectChannel) для non-failover ветки (Http::fake-контракт сохранён). Larastan отложен на T12 (worktree quirk — гонится в основной копии). Регрессия Pest 966/963/0 / 3 skipped. Spec §5. Task 8 of 12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>