docs(supplier): spec — project migration channel failover (3-tier resilience)
Резерв канала миграции проектов Лидерра → crm.bp-gr.ru: AJAX rt-project-* → авто-браузер «Мои проекты» → operator worklist. 4 секции согласованы заказчиком поэтапно 19.05.2026. Зеркало входящего дизайна (webhook + CSV-сверка): 2026-05-18-supplier-csv-reconcile-channel-design.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# Резервный канал миграции проектов Лидерра → поставщик — дизайн
|
||||
|
||||
**Дата:** 19.05.2026
|
||||
**Статус:** утверждён заказчиком (4 секции согласованы поэтапно)
|
||||
**Эпик:** интеграция с поставщиком crm.bp-gr.ru — резерв канала проектов
|
||||
**Связано:** [2026-05-18-supplier-csv-reconcile-channel-design.md](2026-05-18-supplier-csv-reconcile-channel-design.md) (зеркальная архитектура: тот резервирует ВХОДЯЩЕЕ направление — лиды, этот резервирует ИСХОДЯЩЕЕ — миграцию проектов)
|
||||
|
||||
## 1. Цель
|
||||
|
||||
`SyncSupplierProjectJob` (singular, on-edit) и `SyncSupplierProjectsJob` (plural, ночной крон) сегодня мигрируют проект Лидерра → поставщик crm.bp-gr.ru только через AJAX `rt-project-save/update/delete` ([SupplierPortalClient.php:103-121](../../../app/app/Services/Supplier/SupplierPortalClient.php#L103-L121)). Этот канал — единственный, и он:
|
||||
|
||||
- В коде помечен как **placeholder, на реальном портале не верифицирован** ([SupplierPortalClient.php:24-28](../../../app/app/Services/Supplier/SupplierPortalClient.php#L24-L28)).
|
||||
- Реверс-инжиниринг внутреннего AJAX «Мои проекты» — может сломаться при правках портала без предупреждения (это не публичный контракт).
|
||||
- Зависит от эмуляции сессии через `PlaywrightBridge` ([RefreshSupplierSessionJob.php:43](../../../app/app/Jobs/Supplier/RefreshSupplierSessionJob.php#L43)) — отдельная точка отказа (логин-форма, креды).
|
||||
|
||||
Цель: **полный резерв канала миграции проектов**. Не точечная защита от одного сбоя — полная избыточность, по образцу резерва входящих лидов (webhook + CSV-сверка).
|
||||
|
||||
## 2. Граница и пределы резерва
|
||||
|
||||
Разведка портала 19.05.2026 (Playwright read-only по согласию заказчика, снапшоты `rt-projects-snapshot.yml`, `rt-add-project-form.yml`, `user-api-page.yml`) установила:
|
||||
|
||||
- **У поставщика нет API управления проектами.** Раздел «API» меню — это интеграции *доставки* лидов (Битрикс / amoCRM / Google-таблица / Unisender + webhook). `/admin/user/api` — только настройка webhook'а («Апи ссылка», протокол, статус) + docs формата входящих лидов.
|
||||
- **Единственная дверь к проектам** — UI «Мои проекты» (`/admin/visit/rt`), за ним внутренний AJAX `rt-project-*`. Наш HTTP-канал бьёт именно туда.
|
||||
- **Внеканальной приёмной стороны не существует** — у поставщика нет менеджера/поддержки, принимающей запросы по почте/мессенджеру (подтверждено заказчиком).
|
||||
|
||||
**Следствие — потолок резерва.** Резервный канал может быть только *вторым механизмом в ту же дверь*. Он переживёт слом контракта `rt-project-*`, поломку нашей сессионной авторизации, частичные сбои портала. Полный даун хоста crm.bp-gr.ru роняет оба пути одновременно — это принятый предел, не недоработка дизайна.
|
||||
|
||||
## 3. Архитектура — три яруса
|
||||
|
||||
| Ярус | Механизм | Скорость | Когда работает |
|
||||
|---|---|---|---|
|
||||
| 1 | **AJAX** `rt-project-*` (primary) | быстро | штатно |
|
||||
| 2 | **Браузерная автоматизация** формы «Мои проекты» через `PlaywrightBridge` | медленно (Chromium boot + загрузки страниц) | контракт/auth яруса 1 сломан |
|
||||
| 3 | **Operator worklist** — оператор Лидерры вносит руками в crm.bp-gr.ru | сколько займёт у человека | оба автоматических яруса упали ИЛИ хост недоступен |
|
||||
|
||||
`FailoverProjectChannel` — декоратор-оркестратор: пробует ярус 1 → классифицирует исход → ярус 2 / прыжок на 3. Это зеркало входящего дизайна: webhook (push, машинный) + CSV-сверка (другой механизм, тот же портал).
|
||||
|
||||
## 4. Компоненты
|
||||
|
||||
### 4.1. `SupplierProjectChannel` — интерфейс
|
||||
|
||||
Новый интерфейс `App\Services\Supplier\Channel\SupplierProjectChannel`:
|
||||
|
||||
```php
|
||||
interface SupplierProjectChannel
|
||||
{
|
||||
public function createProject(SupplierProjectDto $dto): int; // external_id
|
||||
public function updateProject(int $externalId, SupplierProjectDto $dto): void;
|
||||
public function listProjects(): array; // для дедуп-сверки
|
||||
}
|
||||
```
|
||||
|
||||
Контракт нейтрален к транспорту. Реализации — три (см. ниже).
|
||||
|
||||
### 4.2. `AjaxProjectChannel` — ярус 1, тонкий адаптер
|
||||
|
||||
Имплементирует интерфейс через существующий `SupplierPortalClient` (его `saveProject`/`updateProject`/`listProjects` + `request()`/`loadSession()` остаются HTTP-плумбингом — [SupplierPortalClient.php:103-121](../../../app/app/Services/Supplier/SupplierPortalClient.php#L103-L121)). Тонкий wrapper, чтобы не ломать существующий код CSV-канала, который тоже использует `SupplierPortalClient`.
|
||||
|
||||
### 4.3. `FormProjectChannel` — ярус 2, браузерная автоматизация
|
||||
|
||||
Новый класс `App\Services\Supplier\Channel\FormProjectChannel`. Зависит от `PlaywrightBridge` ([RefreshSupplierSessionJob.php:43](../../../app/app/Jobs/Supplier/RefreshSupplierSessionJob.php#L43)) — тот уже водит headless Chromium для логина, инфра переиспользуется.
|
||||
|
||||
Новый Node-скрипт `app/playwright/manage-project.js` (рядом с `refresh-session.js`):
|
||||
|
||||
- Принимает аргументы `{operation, externalId|null, dto}`.
|
||||
- Логин (как `refresh-session.js`, селекторы Yii2 `#loginform-username`/`#loginform-password`), переход на `/admin/visit/rt`, ожидание загрузки таблицы.
|
||||
- Для `create`: клик «Добавить проект» → форма из 11 полей (снапшот разведки `rt-add-project-form.yml`) → заполнить (`textbox` Тег/Название, `checkbox` B1/B2/B3, `combobox` Источники сбора, `tree`/`switch` Регион include/exclude, `textarea` Список сайтов или вкладка «Файл» при необходимости, `spinbutton` Лимит в день, `checkbox` дни Пн-Вс) → «Сохранить» → дождаться появления новой строки в таблице → прочитать её `id` → вернуть.
|
||||
- Для `update`: найти проект в таблице по `externalId` → открыть форму редактирования → изменить нужные поля → «Сохранить».
|
||||
- Для `listProjects`: прочитать таблицу через snapshot/evaluate → массив `{id, platform, signal_type, unique_key, limit, workdays, regions, ...}`.
|
||||
|
||||
PHP-сторона — `FormProjectChannel` вызывает bridge с JSON-полезной нагрузкой и парсит ответ.
|
||||
|
||||
### 4.4. `FailoverProjectChannel` — декоратор
|
||||
|
||||
Новый класс `App\Services\Supplier\Channel\FailoverProjectChannel`. Конструктор: `AjaxProjectChannel`, `FormProjectChannel`, `ManualSyncQueueRepository`, `Mailer`.
|
||||
|
||||
`createProject(dto)`:
|
||||
|
||||
1. **Локальная проверка существования** (`SupplierProject::where(platform, signal_type, unique_key)`) — логика `ensureSupplierProject` ([SupplierPortalClient.php:54-91](../../../app/app/Services/Supplier/SupplierPortalClient.php#L54-L91)). Если есть — вернуть id.
|
||||
2. **Портальная сверка** через `listProjects()` (кэш на прогон job) по тому же ключу — если на портале уже есть (полу-успех яруса 1 в прошлом запуске, или другой Лидерра-проект на тот же канал), «усыновить»: создать локальную строку с external_id, вернуть id.
|
||||
3. Иначе — попытка яруса 1. Классификация исключения:
|
||||
- Успех → запись в `supplier_sync_log`, вернуть external_id.
|
||||
- `SupplierTransientException` (5xx, [SupplierPortalClient.php:291](../../../app/app/Services/Supplier/SupplierPortalClient.php#L291); сеть, [.php:263](../../../app/app/Services/Supplier/SupplierPortalClient.php#L263)) — `FailoverProjectChannel` сам ретраит транзиент (N попыток с backoff; точные значения пинуем в плане); после исчерпания → **хост недоступен** → прыжок на ярус 3 (ярус 2 по тому же хосту тоже не зайдёт), reason `portal_unreachable`. Существующий job-уровневый `$tries=3` ([SyncSupplierProjectJob.php:35](../../../app/app/Jobs/SyncSupplierProjectJob.php#L35)) — на catastrophic-сценарии вне канала (например, `FailoverProjectChannel` бросил неожиданное исключение).
|
||||
- `SupplierClientException` (4xx, [.php:299](../../../app/app/Services/Supplier/SupplierPortalClient.php#L299)) или неожиданная форма ответа (`saveProject` без `id` и т.п.) → **слом контракта** → ярус 2.
|
||||
- `SupplierAuthException` ([.php:270](../../../app/app/Services/Supplier/SupplierPortalClient.php#L270)) → ярус 2 (его свежий интерактивный логин может пройти, если протух только cookie).
|
||||
4. Ярус 2 — `FormProjectChannel.createProject(dto)`. На успех → запись в `supplier_projects` + `supplier_sync_log`, алёрт `failover_to_form` (инфо). На любой сбой (логин не прошёл, селекторы формы не найдены, save error-тост, таймаут) → запись в `supplier_manual_sync_queue` (status `pending`) + алёрт `manual_required`.
|
||||
|
||||
`updateProject(externalId, dto)`: аналогично без шагов 1–2 (проект уже привязан).
|
||||
|
||||
`listProjects()`: ярус 1; на client-exc → ярус 2 (browser-чтение таблицы).
|
||||
|
||||
### 4.5. Очередь яруса 3 — `supplier_manual_sync_queue`
|
||||
|
||||
Новая SaaS-level таблица (как `supplier_csv_reconcile_log` — без `tenant_id`, без RLS; доступ только SaaS-admin через admin-controller):
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | bigserial PK | |
|
||||
| `project_id` | bigint NOT NULL, FK `projects` | Лидерра-проект |
|
||||
| `platform` | varchar CHECK `B1`/`B2`/`B3` | |
|
||||
| `operation` | varchar CHECK `create`/`update` | |
|
||||
| `external_id` | varchar NULL | для update — `supplier_external_id` |
|
||||
| `payload_snapshot` | jsonb NOT NULL | снимок DTO (signal_type, unique_key, limit, workdays, regions, regions_reverse, ...) — оператор видит точные значения для ввода на портале |
|
||||
| `failure_reason` | varchar NOT NULL | `portal_unreachable` / `contract_break` / `auth_failure` / `form_selector_break` / `form_save_error` / ... |
|
||||
| `status` | varchar CHECK `pending`/`resolved`/`cancelled` | |
|
||||
| `resolved_by_user_id` | bigint NULL FK `users` | оператор, отметивший выполненным |
|
||||
| `created_at`, `resolved_at` | timestamp | |
|
||||
|
||||
Миграция + обновление `db/schema.sql` + запись в `db/CHANGELOG_schema.md` обязательны (правило §4.2 Pravila).
|
||||
|
||||
### 4.6. Admin UI — worklist (расширение существующего)
|
||||
|
||||
Расширяем существующий админ-экран «Интеграция с поставщиком» (`AdminSupplierIntegrationController` + `AdminSupplierIntegrationView.vue`, созданы для CSV-канала, [spec 2026-05-18 §4.4](2026-05-18-supplier-csv-reconcile-channel-design.md)). Точное расположение контроллера — `app/app/Http/Controllers/Api/Admin/` (по конвенции; **не верифицировал именно файл в этой сессии**, запиную при планировании).
|
||||
|
||||
Новые endpoint'ы:
|
||||
|
||||
- `GET /api/admin/supplier-integration/manual-queue` — список pending записей с полным `payload_snapshot` (чтобы оператор видел, что вносить).
|
||||
- `POST /api/admin/supplier-integration/manual-queue/{id}/resolve` — оператор отмечает done. Backend:
|
||||
1. Дёргает `listProjects()` (ярус 1; fallback на ярус 2 если ярус 1 не работает).
|
||||
2. Ищет проект по `(platform, signal_type, unique_key)` из снапшота.
|
||||
3. Если нашёл — создаёт/обновляет локальный `supplier_projects`, ставит FK на `projects.supplier_b{1,2,3}_project_id`, помечает строку очереди `resolved`.
|
||||
4. Если не нашёл — возвращает 409 «проект не найден на портале, проверьте, что вы действительно его создали» (оператор перепроверяет).
|
||||
|
||||
`AdminSupplierIntegrationView.vue` — +секция «Ручная очередь»:
|
||||
|
||||
- Таблица: project name, platform, operation, payload (читаемое представление), reason, created_at.
|
||||
- Кнопка «Отметить выполнено» (с подтверждением).
|
||||
- Счётчик pending в шапке экрана + колокольчик-уведомление при появлении новых записей.
|
||||
|
||||
### 4.7. Расписание
|
||||
|
||||
- `SyncSupplierProjectsJob` крон: 20:30 МСК → **18:00 МСК**.
|
||||
- `RefreshSupplierSessionJob` ежедневный триггер: 20:15 → **17:45** МСК (15 мин до sync, [RefreshSupplierSessionJob.php:24](../../../app/app/Jobs/Supplier/RefreshSupplierSessionJob.php#L24)). Hourly-trigger остаётся.
|
||||
- `TIME_BUDGET_CUTOFF = '20:55'` ([SyncSupplierProjectsJob.php:60](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php#L60)) — **остаётся** (страховка от правок после портального дедлайна 21:00; при 18:00-старте обычно недостижим, остаётся safety rail).
|
||||
|
||||
Рацио 18:00: эскалация на ярус 2 (медленный) или ярус 3 (ручной оператор) — в рабочее время, не поздно вечером. Запас ~3 часа до 21:00.
|
||||
|
||||
Точный файл scheduler-записи (`routes/console.php` либо `app/Console/Kernel.php` — Laravel 13) — **не верифицировал**, запиную при планировании.
|
||||
|
||||
## 5. Поток данных
|
||||
|
||||
**On project create/edit** ([ProjectService.php:212-234](../../../app/app/Services/Project/ProjectService.php#L212-L234)): `ProjectService::create/update` → dispatch `SyncSupplierProjectJob(projectId)` → для каждой нужной платформы ([SyncSupplierProjectJob.php:70-81](../../../app/app/Jobs/SyncSupplierProjectJob.php#L70-L81)) → `FailoverProjectChannel.createProject(dto)` → ярусы. FK `projects.supplier_b{1,2,3}_project_id` ставится только при успехе яруса 1 или 2. При прыжке на ярус 3 — FK пока null, операция в очереди; FK ставится при resolve.
|
||||
|
||||
**Ночной 18:00** ([SyncSupplierProjectsJob.php](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php)): итерация по supplier_projects → расчёт allocation через `SupplierQuotaAllocator` ([.php:134-141](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php#L134-L141)) → если изменилось ([.php:147-149](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php#L147-L149)) → ветвление: `external_id IS NULL` → `FailoverProjectChannel.createProject` ([.php:158](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php#L158)), иначе → `updateProject` ([.php:168](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php#L168)) — обе через ярусы. Окно 18:00–20:55.
|
||||
|
||||
**Закрытие яруса 3:** оператор внёс правки на портале → жмёт «Отметить выполнено» → backend reconcile через `listProjects()` → дописывает `supplier_projects` + FK, помечает строку очереди `resolved`.
|
||||
|
||||
## 6. Обработка ошибок и эскалация
|
||||
|
||||
Сжатая матрица (детали в §4.4):
|
||||
|
||||
| Ярус 1 исход | Куда |
|
||||
|---|---|
|
||||
| Успех | done |
|
||||
| Транзиент исчерпан (5xx/сеть) | сразу ярус 3 (`portal_unreachable`) — ярус 2 пропускаем |
|
||||
| 4xx или непарсимый ответ | ярус 2 |
|
||||
| Sticky 401/403 | ярус 2 |
|
||||
| Ярус 2 любой сбой | ярус 3 (`form_selector_break`/`auth_failure`/...) |
|
||||
|
||||
Алёрты: реюз `SupplierCriticalAlertMail` ([SyncSupplierProjectsJob.php:88](../../../app/app/Jobs/Supplier/SyncSupplierProjectsJob.php#L88)). Новые типы:
|
||||
|
||||
- `failover_to_form` — инфо: AJAX сломан, ярус 2 несёт нагрузку (команде сигнал: чинить primary).
|
||||
- `manual_required` — ярус 3: оператор обязан вмешаться.
|
||||
|
||||
Дополнительно — отражение в админ-экране (плашка + колокольчик).
|
||||
|
||||
`$tries` у job'ов — пересмотр при планировании; повтор обеспечивает крон, ретрай-механизм внутри job'а не нужен.
|
||||
|
||||
## 7. Защита от дублей (идемпотентность)
|
||||
|
||||
**Опасность:** ярус 1 `createProject` полу-успешен — портал создал проект, но наш парсинг ответа упал → мы думаем «не вышло» → ярус 2 повторяет create → **дубль на портале**.
|
||||
|
||||
**Гард** в `FailoverProjectChannel.createProject` (шаги 1–2 §4.4): локальная проверка + портальная сверка через `listProjects()` (кэш на прогон job) до любой попытки create. Так create идемпотентен сквозь все ярусы и через ручное закрытие яруса 3.
|
||||
|
||||
Update — идемпотентен по своей природе (повтор `updateProject(externalId, dto)` с тем же DTO даёт то же состояние).
|
||||
|
||||
## 8. Временное окно поставщика
|
||||
|
||||
Портал на `/admin/visit/rt` показывает alert: «правки до 21:00 МСК; в 22:00–00:00 создание/редактирование запрещено» (зафиксировано снапшотом `rt-projects-snapshot.yml`). Заказчик уточнил 19.05.2026: это **UX-предупреждение пользователю** (для управления его ожиданиями), не технический забор; функционально портал работает 24/7. Соответственно:
|
||||
|
||||
- Канал и ярусы **не гейтятся** по времени.
|
||||
- 18:00-крон — рационален «эскалация в рабочее время», не «успеть до 21:00».
|
||||
|
||||
**Защитная ветка** (на случай, если 22:00–00:00 всё-таки технический reject): `FailoverProjectChannel` классифицирует window-связанный отказ (специфичный 4xx/строка ошибки) как `WindowDeferred` — **не сбой, не эскалация**; операция переносится на следующий ретрай/тик. Если окно полностью мягкое (опыт заказчика подтвердит), ветка не сработает ни разу и стоит ноль.
|
||||
|
||||
**Не верифицировал:** технический ли это reject в 22:00–00:00 на `rt-project-save`. Час 22:00–00:00 живьём не протестировать в день разведки (09:37 МСК). Если из эксплуатации станет ясно, что отказ невозможен — защитную ветку удалить.
|
||||
|
||||
## 9. Тестирование
|
||||
|
||||
- **`FailoverProjectChannel` — приоритет №1**, мозг фичи. Чистое юнит-тестирование с тест-даблами ярусов 1 и 2 (поддельные каналы, бросающие сконфигурированные исключения). Покрыть матрицу эскалации (§4.4 / §6) целиком: успех — никаких эскалаций; transient-exhausted → прыжок на ярус 3 (ярус 2 НЕ зван); client-exc → ярус 2 → успех / провал → очередь; auth-exc → ярус 2; идемпотентность — портальный матч `listProjects` → create не зван, FK усыновлён; `WindowDeferred` → ни очереди, ни эскалации. Детерминированно, быстро.
|
||||
- **`AjaxProjectChannel`** — Pest feature через `Http::fake()` (паттерн `CsvReconcileJobTest`): create/update/list, 4xx → `SupplierClientException`, 5xx → transient, 401/403 → auth. *Ограничение:* `Http::fake` проверяет НАШ код против предполагаемого контракта, не реальный портал; контракт верифицируется отдельным live-шагом (Задача 1 плана, см. §11 ниже).
|
||||
- **`FormProjectChannel` (PHP)**: `PlaywrightBridge` мокается на PHP-границе (он DI-инжектируемый, [RefreshSupplierSessionJob.php:43](../../../app/app/Jobs/Supplier/RefreshSupplierSessionJob.php#L43)) — тест проверяет правильность вызова bridge + обработку возвращаемых значений.
|
||||
- **Node-скрипт `manage-project.js`**: прогон против сохранённого статического HTML-фикстура формы «Мои проекты» — снапшот разведки `rt-add-project-form.yml` используется как seed-фикстура. Покрывает логику заполнения полей без живого портала.
|
||||
- **Миграция `supplier_manual_sync_queue`**: Pest feature/db — миграция применяется, `db/schema.sql` + `CHANGELOG_schema.md` синхронны, db-smoke (CHECK-constraint'ы, FK на `projects`).
|
||||
- **Джобы** `SyncSupplierProjectJob`/`SyncSupplierProjectsJob` — существующие тесты обновляются под инъекцию `FailoverProjectChannel`. +Тест расписания (18:00 крон, 17:45 session-refresh).
|
||||
- **`AdminSupplierIntegrationController`** — Pest feature: getJson worklist, postJson resolve + reconcile-эффект.
|
||||
- **`AdminSupplierIntegrationView.vue`** — Vitest: новая секция worklist рендерит pending; кнопка «выполнено» дёргает endpoint; счётчик/колокольчик при появлении новой записи.
|
||||
- **End-to-end `FormProjectChannel` против боевого crm.bp-gr.ru** — только **ручной smoke с живой сессией** (как webhook live-test / discovery T3). CI реальный портал не трогает: нет кредов, внешняя зависимость, создавал бы настоящие проекты. Live-smoke — отдельная gated-задача плана (read-mostly + один контролируемый create+откат через `deleteProject`).
|
||||
|
||||
Larastan baseline возможно потребует bump (новые классы) — зелёный larastan — обязательное условие коммита.
|
||||
|
||||
## 10. Что НЕ входит (YAGNI)
|
||||
|
||||
- Бэк-портирование `rt-project-delete` через ярусы — текущий код delete не зовёт нигде вне расширения channel'а; вероятно понадобится, но за рамками этого spec'а.
|
||||
- Pull-листинг проектов с портала как фоновый sync (метод `listProjects` уже есть) — на сегодня не нужен; используется только для дедуп-сверки в моменте create.
|
||||
- Out-of-band канал (e-mail / FTP / менеджер) — у поставщика приёма вне портала нет (см. §2).
|
||||
- Замена AJAX на browser как primary — отвергнуто: один механизм не даёт «полного резерва».
|
||||
- Параллельный browser-канал «для верификации каждого AJAX-вызова» — слишком дорого; cross-проверка делается на этапе discovery (Задача 1 плана) однократно.
|
||||
|
||||
## 11. Журнал решений
|
||||
|
||||
| # | Решение | Обоснование |
|
||||
|---|---|---|
|
||||
| 1 | Три яруса A+C (AJAX → авто-браузер → ручной worklist) | Максимальная избыточность в пределах единственной двери поставщика; зеркало webhook↔CSV |
|
||||
| 2 | Ярус 1 (AJAX) остаётся primary | Быстро; ярус 2 медленный (Chromium boot + загрузки страниц) — нежелателен по умолчанию |
|
||||
| 3 | Transient-exhausted → прыжок прямо на ярус 3 (ярус 2 пропускаем) | Если хост недоступен, браузер по тому же хосту тоже не зайдёт — пустая трата времени и контекста |
|
||||
| 4 | Ярус 3 ручной с точным worklist (не алёрт-без-данных) | Без точного снимка payload оператор не знает, что вносить — ошибки и расхождения |
|
||||
| 5 | Крон 18:00 МСК (был 20:30) | Эскалация на ярус 2/3 в рабочее время, не поздно вечером (запас ~3 часа до 21:00) |
|
||||
| 6 | Временное окно — не гейт (заказчик 19.05.2026) | UX-предупреждение, а не технический забор; защитная ветка `WindowDeferred` на случай, если 22:00–00:00 окажется hard-reject |
|
||||
| 7 | Идемпотентность через портальный `listProjects()` до create | Защита от дубля при полу-успехе яруса 1 / повторе ярусом 2 |
|
||||
| 8 | Очередь яруса 3 — отдельная SaaS-таблица (не расширение `supplier_sync_log`) | `supplier_sync_log` — append-only log; ярус 3 — resolvable queue (pending/resolved), отдельная семантика |
|
||||
| 9 | Discovery яруса 1 (верификация `rt-project-*` на боевом портале) — первая задача плана | AJAX endpoints placeholder, не верифицированы ([SupplierPortalClient.php:24-28](../../../app/app/Services/Supplier/SupplierPortalClient.php#L24-L28)); fixup-commit при расхождении; `FormProjectChannel` (ярус 2) служит независимым оракулом сверки |
|
||||
| 10 | Расширение существующего `AdminSupplierIntegrationView` (не новый экран) | Эта же админ-секция уже покрывает CSV-сверку; «здоровье интеграции с поставщиком» — один экран, две сущности (CSV + manual queue) |
|
||||
Reference in New Issue
Block a user