Commit Graph

237 Commits

Author SHA1 Message Date
Дмитрий dadfdcaa7e feat(commands): Plan 4 Task 5 — ResetMonthlyCountersCommand + Schedule monthlyOn(1, 00:00) МСК
Месячный cron-сброс tenants.delivered_in_month + projects.delivered_in_month
1-го числа каждого месяца в 00:00 МСК. Идёт через pgsql_supplier BYPASSRLS
connection (паттерн ResetDeliveredTodayCommand). Идемпотентный
(WHERE delivered_in_month <> 0 → повторный запуск 0 affected rows).

4 теста: reset multi-tenant + idempotency + Schedule registration +
BYPASSRLS without SET LOCAL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:28:13 +03:00
Дмитрий e401491947 feat(supplier): Plan 4 Task 4 — integrate LedgerService в RouteSupplierLeadJob + Task 3 carry-overs
Task 4 — integration:
- handle() / createDealCopyForProject() — +5-й параметр LedgerService.
- Заменён старый balance_leads-- + BalanceTransaction блок на
  \$ledger->chargeForDelivery(\$tenant, \$deal, \$lead) с try/catch для
  InsufficientBalanceException (Log::warning + rethrow; auto-pause flow
  в Task 6).
- LeadRouter::matchEligibleProjects — расширен фильтр tenant balance с
  (balance_leads > 0) на (balance_leads > 0 OR balance_rub > 0), чтобы
  rub-only tenant дошёл до LedgerService (single arbiter for dual-balance).
- 4 E2E теста в tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php:
  prepaid charge + BalanceTransaction (carry-over M-2), rub charge + BT,
  supplier_lead_costs gap-fix (2 deal-копии), retry idempotency.

Plan 4 Task 3 carry-overs (минорные правки по code-review d2030f9):
- I-2: PHPDoc на LedgerService::chargeForDelivery — @throws + @precondition
  (caller wraps в DB::transaction с lockForUpdate Tenant).
- I-4: trim() на raw_payload['project'] в resolveSupplierId (defense
  against whitespace).

Прочие правки:
- tests/Feature/Jobs/RouteSupplierLeadJobTest.php — +PricingTierSeeder
  в beforeEach + +5-й LedgerService параметр в runRouteJob().
- tests/Feature/Integration/SupplierLeadFlowTest.php — +PricingTierSeeder
  в beforeEach (test использует full webhook→job pipeline).
- tests/Feature/Services/LeadRouterTest.php — rename теста про balance_leads
  → \"zero in BOTH balance_leads AND balance_rub\" + новый тест
  \"rub-only tenant ДОЛЖЕН пройти\".
- phpstan-baseline.neon — +5 entries для TestCall::seed() + Tenant/LeadCharge
  property.notFound в новых файлах (IDE helper @mixin re-generation —
  отдельная задача).

Метрики: Pint clean, PHPStan 0 errors, Pest 646/643+3 skipped/0 failed
(21.1s parallel). Plan 4 Task 4 закрыт.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:06:38 +03:00
Дмитрий d2030f9121 feat(billing): Plan 4 Task 3 — LedgerService::chargeForDelivery (dual-balance + lead_charges/supplier_lead_costs INSERT) 2026-05-11 09:42:29 +03:00
Дмитрий 1e0c0ab90a fix(billing): Plan 4 Task 2 code-review fixes (2 Important + 1 Minor)
- PricingTierResolver::resolveForCount — InvalidArgumentException на
  $leadOrdinal < 1 (closes I-1: defensive contract validation).
- PricingTierRepository::activeAt — explicit @var Collection<int,
  PricingTier> annotation для type narrowing (closes I-2; firstOrFail
  отвергнут — Stan ругается на Eloquent\Model return-type).
- PricingTierResolverTest — +1 unit test (8/8 PASS): throws на 0/-1.
- PricingTierRepositoryTest — +1 integration test (5/5 PASS): excludes
  inactive tiers (closes M-2 coverage gap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:17:13 +03:00
Дмитрий e07d025efd feat(billing): Plan 4 Task 2 — PricingTierResolver + Repository (pure resolver + DB-обёртка)
- PricingTierResolver: pure-function, ищет tier для N-го лида (1-based).
  100→tier1, 101→tier2, 6000→tier6 (cumul.sum 1-6), 6001+→tier7 (NULL=unlimited).
  RuntimeException на пустой коллекции.
- PricingTierRepository::activeAt(Carbon): DB-обёртка, MAX(effective_from) <= $at
  per tier_no (учёт «новая сетка перекрывает старую»), is_active=true.
- 7 unit-тестов (in-memory, без БД) + 4 integration-теста (DatabaseTransactions
  с baseline-cleanup для seed-7-tiers из PricingTierSeeder Task 1).
- phpstan-baseline.neon: +3 entry (Pest TestCall::\$resolver/\$tiers/\$repo) —
  следуем project-convention (см. SupplierResolverTest идентичные baseline-entries).

Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §3.1
Plan: docs/superpowers/plans/2026-05-11-plan4-billing-csv-admin.md Task 2

Tests: 633 passed / 3 skipped / 0 failed (+11 new); pint clean; stan 0 errors above baseline.
2026-05-11 09:05:22 +03:00
Дмитрий 1e3c157603 fix(test): Plan 4 Task 1 regression — PricingTierTest::scopeActive baseline-aware
Task 1 (a907fea) добавил $this->call(PricingTierSeeder::class) в DatabaseSeeder,
который автоматически seed'ит 7 ступеней при migrate:fresh --seed на dev/testing.
Существующий PricingTierTest::scopeActive ожидал empty pricing_tiers до factory,
поэтому ->count() == 1 → 8 fail в isolation (pest tests/Feature/Models/PricingTierTest.php).

Fix: scopeActive теперь считает baseline = PricingTier::active()->count() ДО factory create,
ожидает baseline + 1 после factory (1 активный + past, 1 inactive, 1 active + future).

PricingTierTest::current() — не затронут: keyBy('tier_no') корректно перезаписывает
seed-rows (effective_from='1970-01-01') свежими factory-rows (effective_from=now()->subDay()).

Verified:
- pest tests/Feature/Models/PricingTierTest.php — 4/4 PASS, 12 assertions
- pest --parallel — 619 passed / 3 skipped / 0 failed / 1923 assertions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:59:06 +03:00
Дмитрий e5ee9dce0d fix(db): Plan 4 Task 1 code-review fixes (4 Important issues)
- chk_lead_charges_prepaid_zero_price moved inline в CREATE TABLE lead_charges
  (consistent с ~30 другими CHECK constraint'ами в schema.sql).
- LeadCharge.casts() — убран no-op 'charge_source' => 'string' (Eloquent
  возвращает VARCHAR как string без cast'а; consistent с SupplierLead.platform).
- SchemaDeltaTest — добавлен uses(DatabaseTransactions::class) для tests 1+2
  (rollback после теста, project convention LeadChargeTest/PricingTierTest).
- SchemaDeltaTest test #5 — замена destructive migrate:fresh на static parse
  count(CREATE TABLE) / count(CREATE INDEX) / count(CREATE POLICY) в schema.sql.
  Устраняет cross-test coupling в sequential pest run; параллельно убирает
  LARAVEL_PARALLEL_TESTING skip — теперь все 5 тестов выполняются в parallel.

Метрики из static parse: 62 base tables / 117 indexes / 39 RLS policies
(совпадают с schema v8.19, spec §2.4).

All 5 SchemaDeltaTest assertions still pass. No new schema changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:51:18 +03:00
Дмитрий a907fea031 feat(db): Plan 4 Task 1 — schema delta v8.18 → v8.19 + models/seeder
Schema delta (4 правки в db/schema.sql):
- tenants + delivered_in_month INT NOT NULL DEFAULT 0 CHECK (>=0) — month
  counter для PricingTierResolver O(1) lookup на горячем пути.
- lead_charges + charge_source VARCHAR(8) DEFAULT 'rub' CHECK IN ('prepaid','rub')
  + ALTER ADD CONSTRAINT chk_lead_charges_prepaid_zero_price (prepaid → price=0).
- supplier_leads + recovered_from_csv_at TIMESTAMPTZ + partial index
  WHERE recovered_from_csv_at IS NOT NULL.
- Новая таблица supplier_csv_reconcile_log (SaaS-level, без RLS) + 2 индекса
  (started_at DESC, partial status WHERE status IN ('drift_alert','failed')).

Метрики: 61 → 62 базовых таблиц / 114 → 117 индексов / 39 RLS (без изменений).

Сопутствующие правки:
- db/CHANGELOG_schema.md — v8.19 entry (18 записей).
- db/02_grants.sql — GRANT SELECT,INSERT,UPDATE on supplier_csv_reconcile_log
  + GRANT USAGE,SELECT on sequence для crm_supplier_worker.
- app/Models/Tenant — fillable+casts: delivered_in_month integer.
- app/Models/LeadCharge — fillable+casts: charge_source string.
- app/Models/SupplierLead — fillable+casts: recovered_from_csv_at datetime.
- LeadChargeFactory — defaults charge_source='rub' + prepaid() state.
- PricingTierSeeder (новый) — 7 ступеней дефолтного тарифа (placeholder,
  Plan 4 Открытый вопрос #1: 100/200/400/800/1500/3000/∞ leads at
  50000/45000/40000/35000/30000/27000/25000 копеек).
- DatabaseSeeder — call PricingTierSeeder; убран broken Laravel scaffold
  User::factory(['name' => ...]) (наша схема first_name/last_name).

Tests (Plan 4 surface):
- SchemaDeltaTest.php — 5 it() блоков: delivered_in_month CHECK, charge_source
  CHECK на prepaid+zero-price, recovered_from_csv_at колонка, reconcile_log
  таблица+status CHECK, migrate:fresh idempotency (skip in parallel).
- pest --filter=SchemaDeltaTest: 5/5 PASS (9 assertions, 2076 ms).
- pest --filter='Tenant|LeadCharge|SupplierLead|Plan4': 111/111 PASS.

CI gates:
- composer pint: passed.
- composer stan: passed (0 errors above baseline — @phpstan-ignore-next-line
  на $this->markTestSkipped в Pest closure rebound context).

Verify:
- migrate:fresh --seed на DB_DATABASE=liderra_testing: 0 errors, 754 ms.
- PricingTier::count() = 7.

Концерны (НЕ блокируют Task 1):
- pest --parallel: 617/622 PASS + 4 skipped + 1 flaky FAIL — flaky test
  колеблется между ProjectExtensionsTest::supplierB1_B2_B3_relations
  (SupplierProjectFactory race: closure пикает signal_type=sms до override
  platform=B1 → CHECK chk_supplier_projects_b1_not_for_sms violation)
  и NewLeadNotificationTest::webhook_дубль_Биз_19 (известный microsecond
  precision quirk в anti-spam exclusion, memory feedback_environment.md).
  Оба теста существуют на main (HEAD 0802f7c = plan4-billing branch HEAD
  до этого commit'а), Plan 4 их не трогает. Фиксы — вне Task 1 scope.

Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §2.
Plan: docs/superpowers/plans/2026-05-11-plan4-billing-csv-admin-plan.md Task 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:38:38 +03:00
Дмитрий 0802f7cf4c docs(plans): Plan 4 (Billing + CSV Reconcile + Admin) — 12 Tasks TDD decomposition
writing-plans skill output для Plan 4 spec (commit 901cf98). 12 атомарных
Task'ов в 3 фазах:

Фаза I — Billing core (Tasks 1-4):
  1. Schema delta v8.18 → v8.19 (+1 таблица supplier_csv_reconcile_log,
     +3 колонки, +3 индекса, +2 CHECK) + Eloquent fillable/casts +
     PricingTierSeeder с 7 дефолтными ступенями.
  2. PricingTierResolver (pure resolver) + PricingTierRepository (DB-обёртка
     для активной сетки) с 7 + 4 тестами.
  3. LedgerService::chargeForDelivery — dual-balance (prepaid-first + bcmath
     rub) + lead_charges/balance_transactions/supplier_lead_costs INSERT с
     6 integration тестами.
  4. Integration LedgerService в RouteSupplierLeadJob с 4 E2E sharing-flow
     тестами + retry-idempotency.

Фаза II — Operations (Tasks 5-8):
  5. ResetMonthlyCountersCommand + Schedule monthlyOn(1, '00:00') Europe/Moscow
     через pgsql_supplier BYPASSRLS, 4 теста.
  6. Auto-pause flow + ZeroBalancePausedMail + 1/час/tenant rate-limit через
     Redis SETNX, 5 sharing-isolation тестов.
  7. SupplierCsvParser (streaming generator) + SupplierPortalClient::downloadLeadsCsv
     с 5 + 3 unit/Http::fake тестами.
  8. CsvReconcileJob (hourly + 25h окно + drift > 5% email) + CsvDriftAlertMail
     + Schedule entry с 6 integration тестами.

Фаза III — UI (Tasks 9-12):
  9. AdminPricingTiersController + AdminPricingTiersView (7-tier CRUD + audit)
     + 4 Histoire variants + 5 Vitest тестов.
  10. AdminSuppliersController + AdminSupplierPricesView (B1/B2/B3 cost editor)
      + 2 Histoire + 3 Vitest.
  11. TenantChargesController + ChargesTab в BillingView + CSV export через
      StreamedResponse + 3 Histoire + 4 Vitest.
  12. Verification gate (14-step CV) + 7 новых Биз-вопросов в реестр +
      CLAUDE.md/memory bumps + FF-merge.

Каждый Task имеет TDD-цикл (failing test → run FAIL → impl → run PASS →
pint + stan + parallel pest → commit) с exact commands и полным кодом
(нет placeholders). Self-review против spec — все 7 разделов покрыты.

+1 термин в cspell-words.txt (bcmath) для прохождения lefthook cspell stage.

Inherits from: Plan 1+2+2.5+2.6+3 (HEAD origin/main = 901cf98 spec).
Parent spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:10:51 +03:00
Дмитрий 901cf98281 docs(specs): Plan 4 (Billing + CSV Reconcile + Admin) implementation design
Brainstorming output для Plan 4: активация ступенчатого биллинга
(pricing_tiers/lead_charges никем не читались/писались), резервный CSV-канал
приёма лидов из портала поставщика, Admin UI (pricing-tiers editor +
supplier-prices editor + tenant ChargesTab).

9 разделов: контекст + 8 бизнес-инвариантов + 9 out-of-scope; schema delta
v8.18 → v8.19 (+1 таблица supplier_csv_reconcile_log, +3 колонки
tenants.delivered_in_month/lead_charges.charge_source/supplier_leads.recovered_from_csv_at,
+3 индекса, +2 CHECK); billing flow в RouteSupplierLeadJob (3 новых сервиса
PricingTierResolver/LedgerService/InsufficientBalanceException + dual-balance
prepaid-first логика + bcmath денежная арифметика); monthly reset
(ResetMonthlyCountersCommand + Schedule monthlyOn(1,'00:00') Europe/Moscow);
auto-pause flow (project.is_active=false + email с rate-limit 1/час/tenant);
CSV reconcile (расширение SupplierPortalClient + SupplierCsvParser +
CsvReconcileJob hourly + drift > 5% → email); Admin UI (2 SaaS-admin
view + 1 tab в существующем BillingView); тестовая стратегия (+71 теста)
и 10 AC; ограничения (6 неверифицированных).

Закрывает TODO «Биллинг per Plan 4» в RouteSupplierLeadJob.php:48.
Inherits from Plans 1+2+2.5+2.6+3 (HEAD origin/main = 926fee9).

Self-review applied 5 fixes inline: AC ссылки §3.5 → §7.1; auto-pause UPDATE
через pgsql_supplier BYPASSRLS connection; supplier_id source для
supplier_lead_costs INSERT; bccomp(bcmul()) вместо PHP float compare;
GRANT-policy в db/02_grants.sql, не schema.sql.

7 открытых вопросов с дефолтами в §7.6 для последующего ввода в реестр.

+5 терминов в cspell-words.txt (декрементится / Инкрементится / Подписочный
/ bcdiv / TRUNC) для прохождения lefthook cspell stage.

Парный план (writing-plans output) будет в docs/superpowers/plans/ отдельным
коммитом после согласования spec'а.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 07:56:22 +03:00
Дмитрий 926fee9435 chore(lychee): exclude Plan 3 spec+plan files (project-root-relative paths)
Plan 3 spec и plan markdown files (commits 1a265b5 + 989256b) используют
project-root-relative ссылки на проектные файлы (app/, db/, lefthook.yml).
GitHub UI рендерит их корректно; lychee local resolution через relative
fallback к markdown file directory ломается с 33 'file not found' errors.

Альтернативы:
- Переписать ~30 ссылок на ../../../ prefixes — overhead высокий
- Fix lychee --base/--root-dir не работает на multi-file batch с разной
  глубиной vlozenii (docs/superpowers/specs/ vs docs/)

Это hygiene fix перед push merged main; pre-push lychee теперь pass.
Links валидируются вручную при review parent spec/plan.

Cause: pre-push lychee на git push origin main после merge supplier-sync-plan3.
2026-05-11 07:02:20 +03:00
Дмитрий 734b0ab5db merge: Plan 3 (Supplier Sync) — Tasks 3-9 + final review C-1+I-2 fixes
10 commits на branch supplier-sync-plan3:
- 6d6181b Task 3 — switch supplier-flow на pgsql_supplier (BYPASSRLS)
- 8c70255 Task 3 fix — 4 Important + 3 Minor
- 8fc9d3e Task 4 — SupplierPortalClient HTTP-обёртка
- a8a23cb Task 4 fix — 4 Important defense-in-depth
- f298984 Task 5 — RefreshSupplierSessionJob + PlaywrightBridge
- dedaae5 Task 6 — SyncSupplierProjectsJob + SupplierQuotaAllocator
- c685985 Task 7 — CleanupInactiveSupplierProjectsJob Phase A→B→C
- ecb6314 Task 8 — RetryFailedSupplierJobsCommand + 5 Schedule entries
- 8a611eb Task 9 — E2E mock-server skeleton (Linux CI pending)
- c52d42d Final review C-1+I-2 fixes (test load + lock TTL)

Closed backlog (Plan 2.6 #iv direction):
- BLOCKER #6: failed_webhook_jobs RLS NULL tenant under BYPASSRLS
- WARN #2: LeadRouter под crm_supplier_worker видит все tenants
- WARN #3: ResetDeliveredTodayCommand аналогично

Spec §7 acceptance: 9/10  (#4 session refresh smoke deferred — Task 1 BLOCKED).

Метрики merge'а:
- Pest parallel: 617/614+3 skipped/0 failed (1910 assertions)
- PHPStan 0 errors, Pint clean, gitleaks 0 leaks
- 0 schema changes (paritет с design §2.3)
- 6 supplier Schedule entries registered (artisan schedule:list)

Defer-to-post-merge (per final reviewer):
- Tasks 1+2 BLOCKED на credentials поставщика
- I-1 onOneServer (multi-server scaling gated на Б-1)
- I-3 save_orphan recovery (rare DB write fail после HTTP success)
- I-4 region_mode='exclude' regions_reverse passthrough
- I-5 #4 acceptance criterion deferred

Design: docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md (1a265b5)
Plan: docs/superpowers/plans/2026-05-11-supplier-sync-plan3.md (989256b)
2026-05-11 06:55:29 +03:00
Дмитрий c52d42d447 fix(supplier): Plan 3 final review C-1 + I-2 — TestCase autowire + lock TTL
C-1: PlaywrightBridgeTest + RefreshSupplierSessionJobTest swap line order
- uses(TestCase::class) was BEFORE use Tests\TestCase → Pest resolved to
  non-existent \TestCase → file-load fatal → 6 tests NEVER ran since 71c999b.
- Fix: import BEFORE uses() call (pattern matches 3 working files в same dir).
- Misdiagnosis correction: ранее "pao stream_filter" symptoms attributed to
  laravel/pao env quirk были фактически C-1. Full suite parallel:
  617 tests / 614 passed / 3 skipped / 0 failed.

I-2: RefreshSupplierSessionJob lock TTL bump 30s → 90s
- PlaywrightBridge::TIMEOUT_SECONDS=75s. Lock TTL 30s → auto-expiry while
  real chromium boot+login still в процессе → concurrent refresh race.
- Fix: lock(name, 90)->block(95, ...) — exceeds PlaywrightBridge timeout.

Baseline regenerated после C-1 fix (stale entries removed, fresh Mockery
type errors captured).

Verification:
- tests/Unit/Supplier/: 30/30 pass (was 22/30 — 6 не запускались, 2 skip)
- Full parallel suite: 617/614+3 skipped/0 failed
- PHPStan: 0 errors. Pint: clean.

Final code-review (subagent verdict pre-C-1): "Ready to merge, with C-1 fix".
После этого commit'а — verdict satisfied.

Issues defer-to-post-merge per reviewer (follow-up tracking в memory):
- I-1 onOneServer (multi-server scaling; gated на Б-1 production)
- I-3 save_orphan recovery path (rare DB-write fail during HTTP success)
- I-4 region_mode='exclude' regions_reverse passthrough
- I-5 #4 acceptance criterion deferred until Task 1 closes
2026-05-11 06:51:22 +03:00
Дмитрий 8a611eb054 feat(supplier): Plan 3 Task 9 — E2E mock-server skeleton (Linux CI completion pending)
Создан skeleton tests/Browser/SupplierIntegrationE2ETest.php с inline-комментариями
содержащими full mock-server impl (react/http + react/socket).

Skip pattern same as Browser/SmokeTest.php (Sprint 2 Phase A): ->skip() с
указанием на Linux CI requirement (ext-sockets + pest-plugin-browser).

Что покрыто другими тестами (не дублируем — see file header):
- Per-supplier_project routing (Plan 2 Task 6)
- SyncSupplierProjectsJob create/update flow (Plan 3 Task 6, 8 tests)
- Cleanup Phase A→B→C ordering (Plan 3 Task 7, 6 tests)
- Session refresh + retry (Plan 3 Task 5, 7 tests)
- PortalClient cookie/CSRF + retry (Plan 3 Task 4, 9 tests)

Linux CI completion (отдельная sprint после Б-1):
- composer require react/http react/socket --dev
- composer require pestphp/pest-plugin-browser --dev (если не установлен)
- Uncomment test body (lines 41-77 в файле)
- Run: php artisan test --testsuite=Browser

+1 test (skipped on Windows + Linux until completion).
2026-05-11 06:46:13 +03:00
Дмитрий ecb6314e3b feat(supplier): Plan 3 Task 8 — RetryFailedSupplierJobsCommand + 5 Schedule entries
Components:
- supplier:retry-failed Console command (hourly cron):
  Re-dispatch RouteSupplierLeadJob для failed_webhook_jobs eligible
  (retried_at IS NULL OR < NOW()-1h; max-age guard via failed_at).

5 Schedule entries в routes/console.php:
- RefreshSupplierSessionJob hourly + dailyAt('20:15') МСК
- SyncSupplierProjectsJob dailyAt('20:30') МСК
- CleanupInactiveSupplierProjectsJob dailyAt('02:00') МСК
- supplier:retry-failed hourly

NB: ->onOneServer() НЕ применяется — нет cache_locks таблицы (см.
project_state фаза 1). Все операции идемпотентны.

+9 tests (subagent built per actual failed_webhook_jobs schema —
retried_at/retry_count columns). PHPStan baseline +21 Pest TestCall
+ property access entries (Mockery+Pest compat pattern).

Schedule verified via `artisan schedule:list`: 5 supplier entries listed
alongside existing projects:reset-delivered-today.
2026-05-11 06:46:13 +03:00
Дмитрий c6859859a3 feat(supplier): Plan 3 Task 7 — CleanupInactiveSupplierProjectsJob (Phase A→B→C)
Daily 02:00 МСК cron, 3 фазы со строгим порядком:
- Phase A: re-activate supplier_projects где появился active liderra
  (СНАЧАЛА — safety: Phase C не удалит недавно вернувшихся)
- Phase B: mark inactive_since=NOW() для newly orphaned
- Phase C: для inactive_since < NOW() - 180d → rt-project-delete + local delete
  + 404 от поставщика → trust 'already deleted' + локальный delete

180-day TTL paritet со spec §3.3. Audit в supplier_sync_log на каждый delete.

SupplierProject не имеет SoftDeletes → используется hard delete; audit-trail
durability через JSONB request_payload snapshot (FK ON DELETE SET NULL зануляет
supplier_project_id, но строка лога остаётся).

+6 тестов (Phase A reactivation / Phase B mark / Phase C delete + audit /
critical ordering safety / 404 trust / < 180d boundary). 19/19 Feature/Supplier PASS.
2026-05-11 06:46:13 +03:00
Дмитрий dedaae5aaa feat(supplier): Plan 3 Task 6 — SyncSupplierProjectsJob + SupplierQuotaAllocator
Компоненты:
- SupplierQuotaAllocator: pure function distribution-логики
  - site/call: B1=ceil(t/3), B2=ceil(r/2), B3=remainder
  - sms-with-keyword: B2+B3 only (B1=0, spec §2.2 — B1 не поддерживает СМС)
  - Workdays/regions union, weekday-фильтрация по Europe/Moscow
  - Возвращает null когда нет projects на targetWeekday
- SyncSupplierProjectsJob: 20:30 МСК cron
  - SupplierProject::on('pgsql_supplier') — cross-tenant видимость
  - whereNull('inactive_since') — sync только активные
  - Адаптер Project → stdClass: daily_limit_target → daily_limit,
    delivery_days_mask bits → workdays, region_mask bits → regions
    (mask=255 catch-all → regions=[])
  - per-supplier_project failure-isolation (continue на one bad)
  - mass-fail abort: 50 consecutive transient → SupplierCriticalAlertMail
    + Sentry + break
  - sticky auth → email('sticky_auth') + Sentry + throw
  - time budget cutoff 20:55 МСК (5-мин safety margin до 21:00)
  - supplier_sync_log per action (action='create'/'update', http_status,
    error_message)
- SupplierCriticalAlertMail: ShouldQueue Mailable + text template
  - Unisender Go SMTP relay через config('services.supplier.alert_email')

NOTE про connection: следуем Task 3 learning — не используем public \$connection
(это queue connection, не DB). Queries через Model::on('pgsql_supplier').

NOTE про DB::transaction: НЕ оборачиваем syncOne, т.к. HTTP-call к supplier
выходит за границы транзакции (атомарности всё равно нет). Два DB-write
последовательно; ошибка между ними recoverable через retry на следующем cron-tick
(supplier_external_id уже записан, скип через SupplierProjectDto::equals()).

+18 тестов (10 allocator + 8 sync job).

phpstan-baseline.neon: +7 entries для PHPStan template-covariance issue в
SupplierQuotaAllocatorTest — \`Collection<int, object{...literal}&stdClass>\` не
suptype \`Collection<int, stdClass>\` per PHPStan invariance rule. Production
code clean (0 baseline entries).
2026-05-11 06:46:13 +03:00
Дмитрий f298984055 feat(supplier): Plan 3 Task 5 — RefreshSupplierSessionJob + PlaywrightBridge
Компоненты:
- app/playwright/{package.json, refresh-session.js} — изолированный Node.js
  + Playwright chromium subprocess для headless логина
- PlaywrightProcessHandle interface + SymfonyPlaywrightProcessHandle (prod) +
  StubPlaywrightProcessHandle (test) для DI без extending Symfony Process
- ProcessFactory + SymfonyProcessFactory
- PlaywrightBridge: PHP-обёртка, timeout 75s, JSON contract, exit code
  → SupplierAuthException
- RefreshSupplierSessionJob: stub → real (tries=3, backoff [2m/10m/30m],
  Cache::lock concurrent guard, Redis TTL 6h)
- supplier:session:refresh Console command
- AppServiceProvider binds ProcessFactory → SymfonyProcessFactory

+7 tests (4 PlaywrightBridge + 2 Job + 1 Command).

NOTE: DOM-селекторы placeholder — финализация после Task 1 discovery.
NOTE: app/playwright/node_modules в .gitignore.

Quirks resolved:
- Mockery::mock(Process::class) + laravel/pao = stream_filter_remove fatal.
  Решение: handle interface, pure-PHP test stub без extends Process.
- PHPStan Mockery union types — baseline entries (known Mockery+PHPStan compat).

KNOWN LIMITATION: на этой Windows машине pao stream filter conflict при
serial run SupplierPortalClient+RefreshSupplierSessionJob combo.
Tests pass individually + парами. Production Linux CI не affected.
2026-05-11 06:46:13 +03:00
Дмитрий a8a23cb269 fix(supplier): Plan 3 Task 4 code-review fixes (4 Important defense-in-depth)
Закрывает 4 Important issues из code-review Task 4 (a2c5374):
- #1 SupplierPortalClient: parse_url host validation → SupplierClientException
  вместо silent cookie skip + false-positive SupplierAuthException
- #2 dispatch_sync(RefreshSupplierSessionJob) обёрнут try/catch (request retry +
  loadSession) → raw exceptions translated в SupplierAuthException для
  consistency с error taxonomy перед Task 5 real Playwright impl
- #3 RefreshSupplierSessionJob stub handle() теперь throws LogicException
  с понятным сообщением (вместо silent no-op → confusing 'cache still empty'
  error). После Task 5 — LogicException заменяется real Playwright code.
  Снят final-модификатор класса (test override через container bind + Laravel
  dispatchSync serialization не работает с anonymous classes).
- #5 SupplierProjectDto::equals → canonical order для workdays/regions
  через sort в constructor (defense vs PG jsonb non-deterministic order).
  Без этого Task 6 SyncJob false-positive обнаруживал бы diff где его нет
  → unnecessary updateProject HTTP calls.

+3 tests в SupplierPortalClientTest (malformed url, 2 retry-translation paths)
+2 tests в новом SupplierProjectDtoTest (order-independent equals + non-equal)
+1 stub-класс ThrowingRefreshSupplierSessionJob (anonymous classes несовместимы
  с SerializesModels trait в dispatchSync).

Pest: 38/38 supplier-suite, 574/574 full suite (576 total, 2 skipped, +5 new
tests vs Task 4 baseline). PHPStan 0 errors. Pint clean.
2026-05-11 06:46:13 +03:00
Дмитрий 8fc9d3ec8a feat(supplier): Plan 3 Task 4 — SupplierPortalClient HTTP-обёртка над rt-*
Компоненты:
- SupplierProjectDto (readonly DTO, fromModel + equals)
- SupplierException иерархия (Auth/Transient/Client + abstract base)
  - SupplierAuthException: 401/403 sticky после refresh-retry
  - SupplierTransientException: 5xx/network/timeout (retryable)
  - SupplierClientException: 4xx 400/404/422 (наша ошибка payload)
- SupplierPortalClient: list/save/update/delete projects через Http facade
  + Redis cache cookie/CSRF (key 'supplier:session', TTL 6h)
  + auto-retry на 401/403 через dispatch_sync(RefreshSupplierSessionJob)
  + classification HTTP errors → 3 exception types
- RefreshSupplierSessionJob stub (handle() пустой; full impl в Task 5)
- config/services.supplier (login/password/portal_url/alert_email)

+9 тестов через Http::fake() в Unit/Supplier/SupplierPortalClientTest.php:
- cookie/CSRF attach
- 401 retry flow (single retry, sticky 401 → SupplierAuthException)
- 5xx → SupplierTransientException
- 4xx → SupplierClientException
- network error → SupplierTransientException
- save/update/delete payload shapes + responses

NOTE: toPayload() shape — placeholder; точные поля адаптируются
после Task 1 discovery + Task 2 spec §4.4 (отдельный fixup commit
перед Task 6 при расхождении наблюдаемого формата с предполагаемым).
2026-05-11 06:46:13 +03:00
Дмитрий 896565087d chore: declare brain v1.0 installed
Marker file for the new Claude Brain repository at c:/моя/проекты/claude-brain/.
Brain artifacts (CLAUDE.md / Pravila / Tooling / Plugin_stack_rules / hooks /
settings / plugin manifest / MCP templates) are now versioned independently
and synced into this project via:

  cd c:/моя/проекты/claude-brain
  ./scripts/install.sh --target=<this-repo> --version=brain-vX.Y

Future edits to brain artifacts: edit in claude-brain repo, then re-run install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 06:45:32 +03:00
Дмитрий 8c70255d2b fix(supplier): Plan 3 Task 3 code-review fixes (4 Important + 3 Minor)
Закрывает 4 Important issues из code-review Task 3 (6d6181b):
- config/database.php: inline 11-key duplication заменён на single-source
  pattern через локальную переменную $pgsqlConnection (config() внутри
  config-файла не работает — Repository ещё не bootstrap'нут); 'pgsql' и
  'pgsql_supplier' теперь оба ссылаются на $pgsqlConnection; PDO options
  block с string-key _role_purpose удалён (PDO ждёт integer ATTR_* keys)
- tests/Concerns/SharesSupplierPdo.php (новый): trait для cross-connection
  PDO visibility в DatabaseTransactions; setUp override из TestCase.php
  удалён (был global на 562 теста, forced eager PDO connect);
  trait применён к 5 supplier-flow тестам: SupplierConnectionTest,
  LeadRouterTest, RouteSupplierLeadJobTest, ResetDeliveredTodayCommandTest,
  SupplierLeadFlowTest (все нуждаются в cross-connection видимости)
- phpstan-baseline.neon: entry для Pest TestCall->artisan() в
  SupplierConnectionTest заменён на inline @phpstan-ignore-next-line
  — local + self-documenting; добавлен baseline-entry для
  SharesSupplierPdo trait.unused (PHPStan не видит Pest uses() как trait usage)

Plus 3 Minor:
- typos 'dafault'/'corretly' (удалились с setUp override из TestCase.php)
- RouteSupplierLeadJob.php PHPDoc: \$connection → DB_CONNECTION консистентность

Pest: 562 tests, 560 passed + 2 skipped (без regression). PHPStan: 0 errors. Pint: clean.
2026-05-11 01:26:24 +03:00
Дмитрий 6d6181b8cc feat(supplier): Plan 3 Task 3 — switch supplier-flow на pgsql_supplier (BYPASSRLS)
Закрывает 3 backlog-айтема Plan 2.6 одной правкой:
- BLOCKER #6: failed_webhook_jobs INSERT с tenant_id=NULL теперь проходит
  (BYPASSRLS обходит RLS-политику отвергавшую NULL под обычной ролью)
- WARN #2: LeadRouter::matchEligibleProjects видит projects всех tenant'ов
  через Project::on('pgsql_supplier') без SET LOCAL app.current_tenant_id
- WARN #3: ResetDeliveredTodayCommand обновляет projects всех tenant'ов
  через DB::connection('pgsql_supplier')

Архитектура: crm_supplier_worker BYPASSRLS-роль (создана Plan 2.6 #iv 7899071)
+ новый pgsql_supplier connection в config/database.php. WHERE(tenant_id=)
фильтры сохраняются как defense-in-depth.

Уточнение по Job's $connection: оригинальный план предполагал public $connection
= 'pgsql_supplier' на RouteSupplierLeadJob, но в Laravel Job's $connection
управляет очередью (sync/database/redis), не БД. Заменено на константу
RouteSupplierLeadJob::DB_CONNECTION + явный DB::connection(self::DB_CONNECTION)
в failed() callback'е. Это:
1) не ломает queue resolution (без этой правки тесты падают
   'pgsql_supplier queue connection has not been configured')
2) явно документирует intent — failed_webhook_jobs INSERT идёт через BYPASSRLS
3) handle()'s tenant-scoped транзакции остаются на default pgsql + SET LOCAL,
   где RLS нужна для defense-in-depth.

Также добавлено в tests/TestCase.php разделение PDO между pgsql и
pgsql_supplier connection'ами через setPdo/setReadPdo — иначе DatabaseTransactions
не откатывал бы supplier-side данные (две PDO-сессии = две независимые транзакции,
supplier не видит uncommitted INSERTs default-side).

Brainstorm decision: вариант C из 3 опций (A=schema bump, B=отдельная таблица,
C=BYPASSRLS-role). См. docs/superpowers/specs/2026-05-11-plan3-supplier-sync-design.md §1.

+4 теста в Feature/Supplier/SupplierConnectionTest.php (DB_CONNECTION constant +
BLOCKER#6 + WARN#2 + WARN#3). 0 schema changes.

Pest: 562/560 + 2 skipped (baseline 558/556 + 4 new = 562/560, ok). PHPStan: 0 errors
(добавлен 1 baseline entry для известного Pest+PHPStan limitation на artisan()).
Pint: clean.
2026-05-11 01:00:47 +03:00
Дмитрий 989256b034 docs(plans): Plan 3 (Supplier Sync) implementation plan — 9 Tasks TDD decomposition
Полный TDD-план для реализации parent design 2026-05-11-plan3-supplier-sync-design.md
(commit 1a265b5). 9 Tasks в 2 фазах:

Фаза I — Discovery (Tasks 1-2):
- Task 1: 5 HTTP-фиксаций через mcp__playwright__browser_* (с явным локальным
  снятием Pravila §6 на discovery-сессию + credentials в app/.env)
- Task 2: spec v1.0 → v1.1 (§4.4 observed AJAX formats)

Фаза II — Implementation (Tasks 3-9):
- Task 3: switch supplier-flow на pgsql_supplier BYPASSRLS (закрывает
  BLOCKER #6 + WARN #2/#3 одной правкой) + 4 regression tests
- Task 4: SupplierPortalClient + 3 Exceptions + DTO + 9 unit tests через Http::fake
- Task 5: PlaywrightBridge (Node subprocess) + RefreshSupplierSessionJob +
  Redis 6h TTL + 5 unit tests
- Task 6: SupplierQuotaAllocator (pure function) + SyncSupplierProjectsJob +
  SupplierCriticalAlertMail + 16 tests (9 allocator + 7 sync)
- Task 7: CleanupInactiveSupplierProjectsJob Phase A→B→C ordering safety +
  6 integration tests (включая критический ordering test)
- Task 8: RetryFailedSupplierJobsCommand + 5 Schedule entries +
  4 integration tests
- Task 9: E2E mock-server test (react/http + react/socket, Linux CI only) +
  lefthook pest-supplier-fast pre-commit job

Total ~50-60 новых тестов. 0 schema changes. Метрики приёма: Pest >=608 PASS,
Larastan 0 errors above baseline, gitleaks 0 leaks.

Self-review встроен: spec coverage 10/10, 0 placeholder violations, type
consistency verified across 9 Tasks (DTO/Exceptions/Client methods/connection
name/cache keys/role refs).

+HAR в cspell-words.txt (HTTP Archive format — для Task 1 discovery терминологии).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:41:04 +03:00
Дмитрий b53ee28cf2 docs: brain bootstrap discovery results (Phase 0 of brain extraction)
Captures pre-bootstrap verification: Claude CLI commands availability,
prerequisites (jq/python/git/gitleaks), jq deep-merge behavior, secret
rotation status (DEFERRED), ~/.claude/backups/ leak scope.

Required reading before Phase 1 (brain repo skeleton creation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:39:36 +03:00
Дмитрий 1a265b5a38 docs(specs): Plan 3 (Supplier Sync) implementation design
Brainstorming output: 9-Tasks decomposition в 2 фазы (Discovery + Implementation),
0 schema changes, parent spec 2026-05-10-supplier-integration-design.md.

Архитектурные решения (delegated decision-making у заказчика):
- BLOCKER #6: вариант C (переключение supplier-flow на crm_supplier_worker
  BYPASSRLS-роль из Plan 2.6 #iv 7899071) — закрывает BLOCKER #6 + WARN #2 + WARN #3
  одной правкой, 0 schema bump
- Headless browser: real Playwright через Node.js subprocess (страховка от
  Cloudflare/reCAPTCHA/JS-login/2FA на стороне поставщика в будущем)
- Discovery method: вариант А (Playwright MCP + credentials в .env, явное
  локальное снятие Pravila §6 на одну сессию)

Tasks:
1. Discovery через Playwright MCP (5 HTTP-фиксаций)
2. Spec update v1.0 → v1.1 (§4.4 observed AJAX formats)
3. Switch supplier-flow на pgsql_supplier connection (закрывает BLOCKER #6 + WARN #2/#3)
4. SupplierPortalClient (HTTP-обёртка над rt-*, Redis cache cookie/CSRF)
5. RefreshSupplierSessionJob + PlaywrightBridge (Node subprocess, 6h Redis TTL)
6. SyncSupplierProjectsJob + SupplierQuotaAllocator (20:30 МСК cron, B1/B2/B3 distribution)
7. CleanupInactiveSupplierProjectsJob (Phase A re-activate → B mark → C delete 180d)
8. RetryFailedSupplierJobsCommand (hourly cron + лимит 10 attempts/24h)
9. E2E integration test (Linux CI only, skip на Windows)

Exception hierarchy (3 классa): Auth/Transient/Client + abstract base.
Алерт-каналы: Sentry + email через Unisender Go SMTP relay (без Telegram).
Метрики приёма: Pest >=608/610, Larastan 0 errors, gitleaks 0 leaks.

+JSESSIONID в cspell-words.txt (рядом с PHPSESSID) для будущих spec'ов
session-cookie семантики.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:29:12 +03:00
Дмитрий 93314604f2 docs(plans): Sprint 5 «Pre-prod tooling» plan (Semgrep + Dependabot + Trivy prep)
План для активации фазы 3 tooling, не требующего YC-инфраструктуры:
  - Semgrep SAST + Semgrep MCP (npm run sast)
  - GitHub Dependabot (.github/dependabot.yml — npm × 2 + composer)
  - Trivy workflow prep (создан, отключён до Sprint 7 YC Docker pipeline)

Артефакт планировщика, готов к executing-plans. 548 строк.

cspell-words.txt: +1 термин (choco — Chocolatey shortname).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:34:30 +03:00
Дмитрий 64343bd189 feat(viz): hooks-skills-plugins-map.html — карта работы Claude Code
Vintage-blueprint визуализация всей системы плагинов/скилов/хуков для проекта
Лидерра. Артефакт параллельной Claude-сессии (mode «экономия 0%», 10.05.2026).

Включает реализации обоих spec'ов из этой сессии:
  - connections-graph: §X interactive force-directed network через D3.js v7
    (~50 узлов, 52 ребра, drag/click/hover/category filters)
  - section-VII-X: §VII Skills regroup на 5 plugin-based групп +
    §X UI локализация на русский + sidebar иерархия + edge-click handler

Tech: vanilla JS + D3.js v7 (CDN) + SVG + Google Fonts (Fraunces/Plus Jakarta
Sans/JetBrains Mono) + CSS variables. Single-file artifact, без сборки.

Размер: 126 KB (~3000 строк HTML+CSS+JS inline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:33:52 +03:00
Дмитрий 33fdce8664 docs(viz): §VII regroup + §X Russian/hierarchy spec + plan
Артефакты параллельной Claude-сессии (mode «экономия 0%», 10.05.2026 ночь финал).

Spec (366 строк): две связанные правки в hooks-skills-plugins-map.html:
1. §VII Skills — перегруппировка с 6 функциональных категорий на 5 plugin-based
   групп (4 плагина: superpowers/claude-md/frontend-design/upm + 1 standalone),
   28 skills без потерь.
2. §X interactive map — локализация UI-strings на русский (filter chips, legend,
   sidebar badges, tooltips); sidebar при клике на узел показывает иерархию:
   «За что отвечает» / «Кто руководит» / «Кем руководит» / «Связи»; edge-click
   handler с переходами к источнику/цели.

Plan (1012 строк): пошаговая реализация для executing-plans с TDD-структурой.
Точечные правки одного файла (HTML text + JS refactor + CSS additions).

cspell-words.txt: +6 терминов (lede/tgt/Скил/Sel/overhead/overhead'ный).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:33:22 +03:00
Дмитрий 5e38ff6d7e docs(viz): connections-graph spec + plan (D3 force-directed карта связей)
Артефакты параллельной Claude-сессии (mode «экономия 0%», 10.05.2026 ночь).

Spec (385 строк): расширение hooks-skills-plugins-map.html новой §X
«Связи — interactive map». Force-directed network через D3.js v7 (CDN),
~50 узлов (плагины + скилы + хук-скрипты + hook events + state-файл +
permissions + Pravila §12 + CLAUDE.md), 52 ребра. Vintage-blueprint
aesthetic. Drag/click/hover/category filters/reset.

Plan (1246 строк): пошаговая реализация для executing-plans с TDD-структурой.

cspell-words.txt: +3 термина (диспатчу/скилы/ребёр).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:31:44 +03:00
Дмитрий e6b89e676f docs(plans): Plan 2.6 supplier cleanup — закрытый plan-файл (Task 5 closure)
4 atomic commits в стэке (Plan 2.6 #i..#iv):
  - e71a02e supplier:check-webhook-secret deploy validator
  - f78a855 IP allowlist production fail-closed
  - 451a294 timestamp validation ±24h partition guard
  - 7899071 crm_supplier_worker BYPASSRLS-роль для queue worker

Pest 558/556 (+9 от Plan 2.5 baseline 549/547).
Larastan + Pint + squawk clean.
Memory updated (project_supplier_integration.md, project_state.md,
feedback_environment.md quirk #57).

Plan 3 backlog: BLOCKER #6 (RLS на failed_webhook_jobs INSERT NULL tenant)
+ 5 minor WARN + 5 NIT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:21:54 +03:00
Дмитрий 7899071f4e feat(db): crm_supplier_worker BYPASSRLS-роль для queue worker (Plan 2.6 #iv)
Закрывает CV.11 audit WARN minor #2 + #3 (LeadRouter + ResetDeliveredTodayCommand
под crm_app_user → RLS-policy tenant_isolation отвергает cross-tenant SELECT/UPDATE).

Архитектурное решение (Plan 2.6 brainstorm 10.05.2026 поздняя ночь, вариант C из 3
опций): новая PG-роль crm_supplier_worker с BYPASSRLS — privilege-boundary by design.
Queue worker = backend system process для cross-tenant операций (sharing-webhook
routing, global crons); web worker остаётся под crm_app_user (RLS-enforce).

WHERE(tenant_id=) фильтры в коде сохраняются как defense-in-depth.

Deploy:
  - Роль создаётся через db/00_create_roles.sql при первом deploy
    (psql -v crm_supplier_worker_password='<from-secrets>' ...).
  - GRANT'ы в db/02_grants.sql секция 5.
  - Queue worker .env: DB_USERNAME=crm_supplier_worker (отдельно от web .env).

Inline-warnings обновлены в LeadRouter.php + ResetDeliveredTodayCommand.php
(ссылка на crm_supplier_worker BYPASSRLS на prod, db/00_create_roles.sql).

00_create_roles.sql header bump v1.0 → v1.1 (4 → 5 ролей).

Без TDD-теста на роль (integration-тест требует CREATE ROLE в test DB +
смены connection — overhead не оправдан); smoke-grep verify пройден.

Pest 558/556 (+9 от Plan 2.5 baseline 549/547), Larastan + Pint + squawk green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:17:09 +03:00
Дмитрий 451a2944f7 fix(http): timestamp validation ±24h для partition guard (Plan 2.6 #iii)
Закрывает CV.11 audit WARN minor #5 (Carbon::createFromTimestamp(time) без
range guard → INSERT CRASH "no partition of relation deals found for row"
для timestamp вне текущего месячного окна deals_2026_MM).

Изменение: SupplierWebhookController::receive — добавлено min/max constraint
на 'time' = [now-24h, now+24h] unix-timestamp. Timestamp вне окна → 422
ValidationException.

±24h: покрывает retry-задержки поставщика (network-сбой) + clock-drift серверов;
шире окно (±48h+) = риск partition-промаха на стыке месяцев (нужен Plan 5
partition cron).

TDD: +3 теста (-2 days → 422; +2 days → 422; -6h → 202).

Regression-fix: existing test 'inserts supplier_lead row' использовал hardcoded
'time' => 1703781939 (Dec 28 2023) — теперь out-of-window. Заменено на time().

phpstan-baseline: postJson() count: 8 → 11 (+3 от Task 3 тестов).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:11:26 +03:00
Дмитрий f78a85595c fix(http): IP allowlist fail-closed в production env (Plan 2.6 #ii)
Закрывает CV.11 audit WARN #5 (пустой supplier_ip_allowlist '[]' = fail-open
на production — любой IP пропускается).

Изменение: SupplierWebhookController::verifyIpAllowlist — пустой allowlist
возвращает true только если env != production. На production пустой allowlist
блокирует (404). На dev/testing fail-open сохраняется (для localhost development).

TDD: +2 теста (production env empty → 404; testing env empty → 202).
Inline-warning header обновлён.

phpstan-baseline: count: 6 → 8 (postJson() Pest TestCall PhpDoc-quirk).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:08:16 +03:00
Дмитрий e71a02e498 feat(commands): supplier:check-webhook-secret deploy validator (Plan 2.6 #i)
Закрывает CV.11 audit WARN #4 (placeholder secret '__SET_ON_DEPLOY__' = silent
404 на production через verifySecret в SupplierWebhookController).

Console command для deploy-script: SELECT system_settings.supplier_webhook_secret
→ exit 1 если placeholder OR len < 32 OR row отсутствует. Иначе exit 0.

Использование: deploy-script вызывает `php artisan supplier:check-webhook-secret`
перед запуском приложения; non-zero exit прерывает deploy fail-fast.

TDD: 4 теста (placeholder rejected / short rejected / missing rejected / valid accepted).
phpstan-baseline +1 entry: Pest TestCall::artisan() PhpDoc-quirk (как
ResetDeliveredTodayCommandTest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:04:32 +03:00
Дмитрий fe778858ce docs(plans): close Plan 2/5 supplier webhook routing — status header
Все Tasks 1–9 + CV.1–CV.14 + Task 11 выполнены в main (fb55bfd..c1ae195, 16 commits).
Plan 2.5 hotfix (1ba1df8 fix #3 idempotency + c1ae195 fix #2 concurrency) закрыл
2 из 3 BLOCKER findings CV.11 audit.

В Plan 3 backlog:
- BLOCKER #6 (RLS на failed_webhook_jobs INSERT NULL tenant) — первая задача.
- WARN #4 (placeholder secret '__SET_ON_DEPLOY__') + WARN #5 (пустой IP allowlist)
  — operational deploy gates.
- 8 minor WARN + 5 NIT — Plan 3 NOTES.

Pest 549/547 (+2 от Plan 2 baseline 547/545), Larastan + Pint clean.

Чекбоксы Tasks/CV/Task11 ниже намеренно не tick'нуты — детали статуса
в memory/project_supplier_integration.md и memory/project_state.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:31:59 +03:00
Дмитрий c1ae195141 fix(jobs): RouteSupplierLeadJob — lockForUpdate Project + recheck delivered_today (Plan 2.5 #2)
Закрывает CV.11 audit BLOCKER #2 (Plan 2/5 closure).

Проблема: matchEligibleProjects делал SELECT delivered_today < limit БЕЗ lockForUpdate.
Между snapshot'ом и createDealCopyForProject (который инкрементит счётчик) — окно
для concurrent webhook'а:
  worker A видит delivered_today=9, limit=10 → OK; createDealCopyForProject → 10.
  worker B параллельно видит то же 9 → OK; createDealCopyForProject → 11. OVERCOMMIT.
Лимит daily_limit_target нарушен, баланс tenant'а списан дважды.

Fix: внутри createDealCopyForProject (после lockForUpdate Tenant) — lockForUpdate(Project)
+ recheck delivered_today >= COALESCE(effective_daily_limit_today, daily_limit_target).
Если уже at-limit под блокировкой → return false без charge / counter / deal-row +
Log::info('supplier_lead.project_at_limit_skipped') для observability.

TDD: новый тест 'rejects deal copy if delivered_today >= limit at lock time' симулирует
race через Mockery — мокнутый LeadRouter возвращает project уже at-limit
(delivered_today=1, daily_limit_target=1), как будто matchEligibleProjects делал SELECT
когда delivered_today=0. Assert: deal НЕ создаётся, counter не растёт, balance не списан.

Pest: 549/547 passed (+2 от baseline 547), 1745 assertions, 19s parallel.
Larastan + Pint: passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:26:12 +03:00
Дмитрий 1ba1df8df1 fix(jobs): RouteSupplierLeadJob — guard на processed_at для idempotency retry (Plan 2.5 #3)
Закрывает CV.11 audit BLOCKER #3 (Plan 2/5 closure).

Проблема: $tries=3 на retry-сценарий (DB hiccup, queue worker restart) — handle()
запускался повторно без guard'а на $lead->processed_at. Второй проход создавал
ВТОРОЙ Deal в БД с тем же vid (DuplicateDetector помечал его дублем без charge,
но deal-row оставался). Также $lead->update(['deals_created_count' => $createdCount])
переписывал счётчик: первый run = 1, второй run = 0 (все дубли) → искажение метрики.

Fix: в начале handle() после findOrFail — if ($lead->processed_at !== null) return;
+ Log::info с processed_at и deals_created_count для диагностики.

TDD: новый тест 'idempotent on retry — second handle() returns early, no ghost
duplicate deals' (RouteSupplierLeadJobTest:271). Проверяет 2 последовательных
вызова runRouteJob — assertion на Deal::count, balance_leads, delivered_today,
deals_created_count все остаются на 1st-run значениях.

Pest: 548/546 passed (+1 тест от baseline 547), 1740 assertions, 17s parallel.
Larastan + Pint: passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:20:34 +03:00
Дмитрий 67cb2cc946 docs(superpowers): economy-mode hook bypass closure spec + plan
6-component architecture (permissions block + 5 hooks) closing 8 critical/high
bypass paths: settings disable (H1), hook script edit (H2), prompt injection
(H4), state file delete (H6), subagent inheritance (H7), PostCompact loss
(H8), retry exhaustion (H9), verifier endpoint failure (H10), tool output
spoofing (H12), no-claim bypass (H13).

End-of-prompt parsing for "экономия N%". Shared state file in $TEMP.
Sonnet 4.6 verifier on Stop with decision:"block" + max 3 retry → escalate.

Spec: 964 lines, 12 sections.
Plan: 7 stages with TDD per task.
Runtime cost: ~\$7-14/month.

Stage 0 ratchet verified: auto-mode classifier blocks subagent Write on hook
scripts AND Bash heredoc bypass on settings.json.

Also adds 4 cspell vocabulary terms (парсингом/промпт/Mojibake/sed) used
in the new spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:39:23 +03:00
Дмитрий d5aa972730 docs(plan2): inline production warnings (RLS+NULL tenant + empty IP allowlist)
CV.11 final code-review WARNING #1 + #3:
- RouteSupplierLeadJob::failed() — explicit note про elevated-role требование
  (failed_webhook_jobs RLS-policy блокирует INSERT с tenant_id=NULL без BYPASSRLS)
- SupplierWebhookController header — предупреждение про пустой allowlist в prod

Остальные 3 WARNING + 8 NIT отложены в Plan 3 NOTES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:08:11 +03:00
Дмитрий 9daa94d917 feat(commands): projects:reset-delivered-today (00:00 МСК cron)
Spec §6.1: ежедневный сброс projects.delivered_today=0 после midnight МСК.
delivered_in_month НЕ трогаем (это месячный счётчик, Plan 4 cron).

Реализация: Artisan-команда `projects:reset-delivered-today` (idempotent
UPDATE без транзакции/локов — отрабатывает за <1 сек), Schedule в
routes/console.php с dailyAt('00:00')->timezone('Europe/Moscow').

NB: `withoutOverlapping()` пропущен — требует таблицу cache_locks, которой
нет в schema.sql (Laravel-default-миграции удалены в фазе 1). Идемпотентность
UPDATE делает overlap-защиту избыточной.

Tests: 2/2 pass, phpstan 0 errors (1 baseline для $this->artisan, как у
прочих Pest-тестов с artisan-helper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:58:47 +03:00
Дмитрий b6b5b0bc1f test(integration): supplier webhook → N deals end-to-end (sharing-model)
Полный сценарий: 1 webhook на B1_vashinvestor.ru → 3 deal-копии у 3 активных
tenant'ов + счётчики + balance. Paused tenant пропущен. Orphan supplier_project
создаёт stub, processed_at установлен, deals_created_count=0.

Запуск через Laravel sync queue (default test env) — без Bus::fake().

Spec §5-§6 e2e validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:43:44 +03:00
Дмитрий 280cfcd6cf feat(routes): register POST /api/webhook/supplier/{secret}
Spec §5.1 supplier-webhook endpoint. SupplierWebhookController tests
переходят с 405 на 8/8 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:39:43 +03:00
Дмитрий e41c8f5aef feat(http): SupplierWebhookController — platform-wide /api/webhook/supplier/{secret}
Defense-in-depth: secret (≥32 chars system_setting) + IP allowlist (CIDR).
Несовпадение → 404. UNIQUE vid → 200 OK на дубль (idempotency).

Тесты пока FAIL (route регистрируется в Task 7 — пишем "красные" тесты заранее
для TDD-цикла).

Spec §5.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:38:18 +03:00
Дмитрий 605c457c49 fix(jobs): RouteSupplierLeadJob — per-Project failure isolation + 2 tests
Code-review Important: один сбой Project не должен абортить routing для
остальных tenant'ов (sharing-model). + try/catch + Log::warning +
RuntimeException только если ВСЕ projects упали.

+ 2 новых теста: mixed routing (1 dup из 3 + 2 clean) и partial failure
(soft-delete tenant в середине loop'а).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:30:06 +03:00
Дмитрий c519044319 feat(jobs): RouteSupplierLeadJob — sharing-model deal-copies + charge per tenant
Распределяет supplier_lead по eligible Liderra-проектам через LeadRouter.
Для каждого: транзакция с SET LOCAL app.current_tenant_id, lockForUpdate,
DuplicateDetector check, balance_leads--, delivered_today/month++,
BalanceTransaction, ActivityLog, NotificationService::notifyNewLead.

Spec §5-§6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:05:21 +03:00
Дмитрий 9540090bba fix(services): LeadRouter — Europe/Moscow timezone for workday-mask check
Code-review Important: Carbon::now() resolved against process TZ (UTC quirk
из memory). Reset cron в 00:00 МСК — mismatch вызвал бы off-by-one на
рубеже полуночи. Тест синхронизирован (now('Europe/Moscow')) — иначе
mismatch test/service near midnight. + комментарий про unreachable default
match arm (защищён на DB-уровне через chk_supplier_projects_platform).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:56:15 +03:00
Дмитрий 31929c9dd2 feat(services): LeadRouter — eligible Liderra projects matcher
Plan 2/5 Task 4 — sharing-model routing (spec §6): для входящего лида
возвращает Collection<Project> учитывая platform FK + active + workdays +
region (PhonePrefixService::phoneMatchesRegions) + delivered_today <
COALESCE(effective_daily_limit_today, daily_limit_target) +
tenant.balance_leads > 0. Сортировка created_at ASC, id ASC (детерминированно).

Параллельно расширил Project model fillable/casts на delivered_today
(колонка добавлена в schema v8.18 Plan 2 Task 1, но Project::class не
обновлён — без этого тесты Mass-Assignment'а ломались).

Покрытие: 9 it-blocks (sharing across tenants, paused, workdays, daily quota,
fallback to daily_limit_target, region filter, balance_leads zero, FK routing
по platform, deterministic sort). DatabaseTransactions context + set_config
(session-scoped) для очистки app.current_tenant_id — sharing-flow работает
поверх N tenant'ов, RLS bypass через postgres BYPASSRLS на dev.

PHPStan: 0 errors. Pint: clean. Pest: 9/9 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:50:28 +03:00
Дмитрий 5f08459615 feat(services): PhonePrefixService — phone → federal district bit (MVP)
Маппинг мобильных + 30+ ABC-кодов городов на 8 ФО RF (synchronized
с projects.region_mask битами). Мобильные → all-districts (255).
Полный Минсвязи справочник отложен на Plan 5+.

Spec: §6 step 2 routing geo-filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:36:43 +03:00
Дмитрий aa37f4cbed feat(models): SupplierLead model + factory (raw-payload incoming webhooks)
SaaS-level модель для supplier_leads (Plan 2/5 Task 2).
belongsTo(SupplierProject) + array cast на raw_payload + datetime *_at.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:24:45 +03:00