diff --git a/cspell-words.txt b/cspell-words.txt index e2d45a6e..2c09da4c 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -897,6 +897,7 @@ symfony encrypter PHPSESSID JSESSIONID +HAR vashinvestor # v1.78 — Economy hook bypass closure spec/plan terms diff --git a/docs/superpowers/plans/2026-05-11-supplier-sync-plan3.md b/docs/superpowers/plans/2026-05-11-supplier-sync-plan3.md new file mode 100644 index 00000000..15cbf1a9 --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-supplier-sync-plan3.md @@ -0,0 +1,3188 @@ +# Plan 3 (Supplier Sync) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Закрыть BLOCKER #6 (`failed_webhook_jobs` RLS NULL tenant) + WARN #2/#3 (Plan 2.6 backlog про LeadRouter/ResetCmd под `crm_app_user`) и реализовать направление Лидерра → Поставщик: Playwright session manager, 20:30 МСК cron `SyncSupplierProjectsJob`, 180d cleanup, автоматический retry failed jobs. + +**Architecture:** 9 Tasks в 2 фазах. Фаза I — Discovery (Tasks 1–2): через `mcp__playwright__browser_*` MCP записываем 5 HTTP-фиксаций реальных AJAX-команд поставщика crm.bp-gr.ru; обновляем parent spec §4.4. Фаза II — Implementation (Tasks 3–9): переключение supplier-flow на `pgsql_supplier` connection с `crm_supplier_worker` BYPASSRLS-ролью (закрывает BLOCKER #6 + WARN #2/#3 одной правкой), `SupplierPortalClient` HTTP-обёртка, `RefreshSupplierSessionJob` + Node.js Playwright subprocess, `SyncSupplierProjectsJob` + чистый allocator, `CleanupInactiveSupplierProjectsJob` с порядком Phase A→B→C, `RetryFailedSupplierJobsCommand`, E2E integration test (Linux CI only). 0 schema changes. + +**Tech Stack:** PHP 8.3, Laravel 13.7, Pest 4, PostgreSQL 16 (`pgsql_supplier` connection с BYPASSRLS, role `crm_supplier_worker` уже создана Plan 2.6 #iv `7899071`), Redis 7 (Memurai на dev), Node.js + Playwright `chromium` (headless), Symfony Process (PHP-subprocess bridge), Mockery, Larastan, Unisender Go (email алерты). + +**Parent spec:** [2026-05-11-plan3-supplier-sync-design.md](../specs/2026-05-11-plan3-supplier-sync-design.md) (commit `1a265b5`). +**Inherits from:** Plan 1 Foundation `001d781` + Plan 2 Webhook+Routing `d5aa972` + Plan 2.5 hotfix `c1ae195`+`1ba1df8` + Plan 2.6 cleanup `7899071`. + +--- + +## Карта файлов + +| Файл | Действие | Назначение | Task | +|---|---|---|---| +| `app/tests/Fixtures/SupplierPortal/login.json` | Create | Discovery HAR login → cookie+CSRF response | 1 | +| `app/tests/Fixtures/SupplierPortal/projects-load.json` | Create | Discovery rt-projects-load запрос+ответ | 1 | +| `app/tests/Fixtures/SupplierPortal/project-save.json` | Create | Discovery rt-project-save запрос+ответ | 1 | +| `app/tests/Fixtures/SupplierPortal/project-update.json` | Create | Discovery rt-project-update запрос+ответ | 1 | +| `app/tests/Fixtures/SupplierPortal/project-delete.json` | Create | Discovery rt-project-delete запрос+ответ | 1 | +| [docs/superpowers/specs/2026-05-10-supplier-integration-design.md](../specs/2026-05-10-supplier-integration-design.md) | Modify | Добавить §4.4 «AJAX endpoints — observed formats» + bump v1.0 → v1.1 | 2 | +| [app/config/database.php](../../app/config/database.php) | Modify | +ключ `pgsql_supplier` (BYPASSRLS connection) | 3 | +| [app/.env.example](../../app/.env.example) | Modify | +`DB_SUPPLIER_USERNAME`/`DB_SUPPLIER_PASSWORD`/`SUPPLIER_LOGIN`/`SUPPLIER_PASSWORD`/`SUPPLIER_PORTAL_URL`/`SUPPLIER_ALERT_EMAIL` | 3 | +| [app/app/Jobs/RouteSupplierLeadJob.php](../../app/app/Jobs/RouteSupplierLeadJob.php) | Modify | `protected $connection = 'pgsql_supplier'` + удалить inline-warnings 258-263 | 3 | +| [app/app/Services/LeadRouter.php](../../app/app/Services/LeadRouter.php) | Modify | `Project::on('pgsql_supplier')` + удалить inline-warnings 25-29 | 3 | +| [app/app/Console/Commands/ResetDeliveredTodayCommand.php](../../app/app/Console/Commands/ResetDeliveredTodayCommand.php) | Modify | `Project::on('pgsql_supplier')` + удалить inline-warnings 16-18 | 3 | +| `app/tests/Feature/Supplier/SupplierConnectionTest.php` | Create | Regression tests BLOCKER #6 + WARN #2 + WARN #3 | 3 | +| `app/app/Services/Supplier/Dto/SupplierProjectDto.php` | Create | Read-only DTO (platform/signal_type/unique_key/limit/workdays/regions/status) | 4 | +| `app/app/Exceptions/Supplier/SupplierException.php` | Create | Abstract base exception | 4 | +| `app/app/Exceptions/Supplier/SupplierAuthException.php` | Create | 401/403 sticky после refresh-retry | 4 | +| `app/app/Exceptions/Supplier/SupplierTransientException.php` | Create | 5xx/network/timeout retryable | 4 | +| `app/app/Exceptions/Supplier/SupplierClientException.php` | Create | 4xx 400/404/422 non-retryable | 4 | +| `app/app/Services/Supplier/SupplierPortalClient.php` | Create | HTTP-обёртка над rt-* + Redis cookie/CSRF + retry на 401 | 4 | +| `app/tests/Unit/Supplier/SupplierPortalClientTest.php` | Create | Unit tests через Http::fake | 4 | +| `app/playwright/package.json` | Create | Изолированная npm dep `playwright` | 5 | +| `app/playwright/refresh-session.js` | Create | Node subprocess: chromium → login → cookies+CSRF → stdout JSON | 5 | +| `app/app/Services/Supplier/PlaywrightBridge.php` | Create | Symfony Process обёртка над node subprocess | 5 | +| `app/app/Jobs/Supplier/RefreshSupplierSessionJob.php` | Create | Spawn PlaywrightBridge + Redis lock + 6h TTL cache | 5 | +| `app/app/Console/Commands/SupplierSessionRefreshCommand.php` | Create | `supplier:session:refresh` (ручной запуск) | 5 | +| `app/tests/Unit/Supplier/PlaywrightBridgeTest.php` | Create | Unit с mocked Symfony\Process | 5 | +| `app/tests/Unit/Supplier/RefreshSupplierSessionJobTest.php` | Create | Unit с mocked PlaywrightBridge | 5 | +| `app/app/Services/Supplier/SupplierQuotaAllocator.php` | Create | Pure function: B1/B2/B3 distribution + workdays/regions union | 6 | +| `app/app/Jobs/Supplier/SyncSupplierProjectsJob.php` | Create | 20:30 МСК cron + per-supplier_project isolation + mass-fail abort | 6 | +| `app/app/Mail/SupplierCriticalAlertMail.php` | Create | Mailable для critical alerts (email via Unisender) | 6 | +| `app/config/services.php` | Modify | Добавить `supplier.alert_email` ключ | 6 | +| `app/tests/Unit/Supplier/SupplierQuotaAllocatorTest.php` | Create | 9 unit-тестов на distribution-математику | 6 | +| `app/tests/Feature/Supplier/SyncSupplierProjectsJobTest.php` | Create | Integration tests с Http::fake | 6 | +| `app/app/Jobs/Supplier/CleanupInactiveSupplierProjectsJob.php` | Create | Phase A re-activate → B mark → C delete 180d | 7 | +| `app/tests/Feature/Supplier/CleanupInactiveSupplierProjectsJobTest.php` | Create | Integration + критический ordering test | 7 | +| `app/app/Console/Commands/RetryFailedSupplierJobsCommand.php` | Create | `supplier:retry-failed` hourly + лимит 10/24h | 8 | +| `app/tests/Feature/Supplier/RetryFailedSupplierJobsCommandTest.php` | Create | Integration tests | 8 | +| `app/routes/console.php` | Modify | +5 Schedule entries | 8 | +| `app/tests/Browser/SupplierIntegrationE2ETest.php` | Create | Linux CI only mock-server → full flow | 9 | +| [lefthook.yml](../../lefthook.yml) | Modify | +2 jobs (playwright-fixtures-syntax + pest-supplier-fast) | 9 | + +--- + +## Task 1: Discovery через Playwright MCP — записать 5 HTTP-фиксаций реальных AJAX-команд поставщика + +**Files:** + +- Create: `app/tests/Fixtures/SupplierPortal/login.json` +- Create: `app/tests/Fixtures/SupplierPortal/projects-load.json` +- Create: `app/tests/Fixtures/SupplierPortal/project-save.json` +- Create: `app/tests/Fixtures/SupplierPortal/project-update.json` +- Create: `app/tests/Fixtures/SupplierPortal/project-delete.json` + +**Prerequisites (BLOCKER для старта):** + +- Заказчик передал `SUPPLIER_LOGIN` + `SUPPLIER_PASSWORD` через защищённый канал (НЕ plaintext в чат). +- Заказчик дал явное локальное снятие Pravila §6 на эту discovery-сессию. + +**Не TDD** — это manual probe; артефакт — 5 JSON-fixtures для использования в Task 4 `Http::fake()`. + +- [ ] **Step 1: Получить credentials и сохранить в `app/.env`** + +```bash +# Заказчик передаёт credentials. Записать в app/.env (не коммитить!): +# SUPPLIER_LOGIN= +# SUPPLIER_PASSWORD= +# SUPPLIER_PORTAL_URL=https://crm.bp-gr.ru +``` + +Verify: `grep -c "SUPPLIER_LOGIN" app/.env` должен вернуть 1. + +- [ ] **Step 2: Создать директорию для фикстур** + +```bash +mkdir -p app/tests/Fixtures/SupplierPortal +``` + +Verify: `ls -la app/tests/Fixtures/SupplierPortal` существует, пуст. + +- [ ] **Step 3: Через Playwright MCP залогиниться, записать login fixture** + +Использовать MCP-tools: + +1. `mcp__playwright__browser_navigate` → `https://crm.bp-gr.ru/admin/user/login` (или другой login URL — узнаем при навигации) +2. `mcp__playwright__browser_snapshot` — увидеть DOM, найти input-names для login/password +3. `mcp__playwright__browser_fill_form` → заполнить login + password +4. `mcp__playwright__browser_network_requests` — записать ВСЕ requests за login flow +5. `mcp__playwright__browser_click` на submit +6. Снова `mcp__playwright__browser_network_requests` — увидеть login POST с cookie+CSRF response + +Сохранить в `app/tests/Fixtures/SupplierPortal/login.json`: + +```json +{ + "step": "login", + "request": { + "method": "POST", + "url": "", + "headers": { + "" + }, + "body": "" + }, + "response": { + "status": 200, + "headers": { + "set-cookie": [ + "" + ] + }, + "body": "" + }, + "extracted": { + "session_cookie_name": "", + "csrf_token_selector": "