Commit Graph

506 Commits

Author SHA1 Message Date
Дмитрий 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
Дмитрий f5c7c29301 test(schema): add UNIQUE-vid behavioral test + intent-clear placeholder assert
- Add it-block для UNIQUE INDEX idx_supplier_leads_vid_unique:
  две INSERT с одинаковым vid → вторая бросает QueryException
  (прямой behavioral тест webhook-идемпотентности).
- Replace tautological strlen($secret->value) >= 16 на toBe('__SET_ON_DEPLOY__')
  — было проверкой литерала, который мы сами и записали; теперь intent-clear
  assertion того, что seed кладёт placeholder. Реальная strength-валидация
  secret'а — дело deploy-time validator'а, вне scope Plan 2.
- Add uses(DatabaseTransactions::class) — приводит файл к проектному
  pattern (см. WebhookReceiveTest, TenantModelsTest, SetTenantContextTest).
  Без него новый INSERT с vid=999000111 коллидил бы при re-run, т.к.
  Pest.php применяет RefreshDatabase глобально не делает (закомментирован).

Code-review fixes for Plan 2 Task 1.
2026-05-10 18:16:19 +03:00
Дмитрий fb55bfdd1f feat(db): supplier_leads + projects.delivered_today + 2 system_settings (v8.18)
Plan 2/5 Task 1 — слой данных для supplier-webhook flow.

- supplier_leads (SaaS-level, без RLS) — raw payload incoming webhook'ов
- projects.delivered_today — дневной счётчик для проверки daily quota
- system_settings: supplier_webhook_secret + supplier_ip_allowlist

Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §5-§6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:07:31 +03:00
Дмитрий 00aaa9ea89 docs(plan): Plan 2/5 — Supplier Webhook + Sharing Routing
11 задач + 14-пунктовый Comprehensive Verification Gate.
Spec §5-§6: platform-wide webhook + N deal-копий через LeadRouter.
Legacy /api/webhook/{token} остаётся параллельным каналом.

+ allowlist generic pattern 7\d{3}1234567 для PhonePrefixService docs/tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:55:13 +03:00
Дмитрий 001d7819bf fix(supplier): close code-review BLOCKER+WARN — FK + CHECK + resolver guard
Code-review subagent (CV.12 в Plan 1) нашёл 1 BLOCKER + 2 actionable WARNINGs:

1. **BLOCKER** — projects.supplier_b{1,2,3}_project_id были голыми BIGINT без
   REFERENCES, вопреки явному комментарию «FK добавятся в Task 2». Task 2
   создал supplier_projects, но FK на projects не вернул. Можно было записать
   произвольный BIGINT в эти колонки.
   Fix: ALTER TABLE projects ADD CONSTRAINT … FOREIGN KEY … ON DELETE SET NULL
   для всех трёх + 3 partial index (WHERE NOT NULL) для FK lookup.

2. **WARNING** (Project-level B1+SMS guard) — CHECK существовал только на
   supplier_projects; Project::create(['signal_type'=>'sms','supplier_b1_project_id'=>…])
   проходил вопреки spec §2.2 «B1 не поддерживает СМС».
   Fix: ADD CONSTRAINT chk_projects_b1_not_for_sms
   CHECK (signal_type <> 'sms' OR supplier_b1_project_id IS NULL).

3. **WARNING** (resolver collision) — SupplierProjectResolver::resolveOrStub
   firstOrCreate на (platform, unique_key) без signal_type → при коллизии
   unique_key возвращал чужую запись с другим signal_type без ошибки.
   Fix: после firstOrCreate проверяется match signal_type, иначе DomainException.
   +1 тест на collision.

Schema bumped v8.16 → v8.17. Метрики: 60 таблиц / 111 индексов (+3) / 39 RLS.
Pest: 500/498 passed (+1 collision test). Larastan 0 errors. Pint clean.

Spec: §2.1, §2.2
Plan: Task 2 (закрытие code-review CV.12)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:32:02 +03:00
Дмитрий 44a1b21421 feat(rules): add Signal validators (Domain, Phone, SmsSender) with comprehensive datasets
- app/Rules/SignalIdentifier/DomainIdentifier.php — regex
  ^[a-z0-9-]+(\.[a-z0-9-]+)+$ (нижний регистр, без протокола/пути).
- app/Rules/SignalIdentifier/PhoneIdentifier.php — regex ^7\d{10}$
  (11 цифр, начинается с 7).
- app/Rules/SignalIdentifier/SmsSenderRule.php — 1-30 символов
  [A-Za-z0-9_-]; отвергает 11-значные номера (поставщик блокирует
  физический телефон в роли отправителя — alert "Важно!" в форме
  создания SMS-проекта).
- tests/Feature/Rules/SignalValidatorsTest.php — 24 теста с datasets:
  • Domain: 4 valid + 6 invalid (case, no-TLD, spaces, protocol, path, double-dot)
  • Phone: 3 valid + 6 invalid (8-prefix, length, plus, spaces, letters)
  • SmsSender: alpha+numeric short, 11-digit blocked, length>30 blocked,
    empty (с required), special chars blocked

Quirk: Laravel skips non-implicit rules для пустых строк. Тест empty
использует связку 'required' + правило (как в реальном FormRequest).

Pest: 499 / 497 passed / 2 skipped (473 + 24 новых = 497).
Larastan: 0 errors. Pint passed.

Spec: §3.1
Plan: Task 12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:05:19 +03:00
Дмитрий 59c57f9ec0 feat(services): add SupplierProjectResolver (resolveOrStub with B1+SMS guard)
- app/Services/SupplierProjects/SupplierProjectResolver.php — резолвер
  по ключу (platform, signal_type, unique_key). Возвращает existing supplier_project
  или создаёт pending stub (физическая sync произойдёт в SyncSupplierProjectsJob, Plan 3).
- Защита 1: InvalidArgumentException на платформу не из {B1,B2,B3}.
- Защита 2: InvalidArgumentException на signal_type не из {site,call,sms}.
- Защита 3: DomainException на B1+SMS combo (chk_supplier_projects_b1_not_for_sms).
- tests/Feature/Services/SupplierProjectResolverTest.php — 6 тестов:
  resolve existing, create stub, idempotency (no duplicates), B1+SMS guard,
  invalid platform, invalid signal_type.

Pest: 475 / 473 passed / 2 skipped (467 + 6 новых = 473).
Larastan: 0 errors. Pint passed.

Spec: §2.2, §4.1
Plan: Task 11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:02:25 +03:00
Дмитрий 99afcbc25c feat(models): extend Project with signal_type, sms_senders, supplier_b1/b2/b3 relations + scopes
- app/Models/Project.php — добавлены fillable+casts для supplier integration:
  signal_type, signal_identifier, sms_senders (jsonb array), sms_keyword,
  delivered_in_month, supplier_b{1,2,3}_project_id.
  + supplierB1/B2/B3() BelongsTo relations на SupplierProject (sharing-model).
  + scopeActiveOnDay($iso) — bitmask проверка по delivery_days_mask
    (bit 0 = Mon, bit 6 = Sun; ISO=1 → 1<<0 = 1; ISO=7 → 1<<6 = 64).
  + scopeForSignal($type, $identifier) — фильтр по сигналу (для роутинга в Plan 2).
- database/factories/ProjectFactory.php — defaults null/0 для новых полей
  (CHECK constraints не нарушаются: signal_type IS NULL → остальные опциональны).
  + state-методы asSiteSignal($domain), asCallSignal($phone), asSmsSignal($senders, $keyword).
- tests/Feature/Models/ProjectExtensionsTest.php — 6 тестов: signal_type fillable,
  sms_senders array cast + sms_keyword, SMS без keyword, supplierB1/B2/B3 relations,
  scopeActiveOnDay (bitmask Mon/Sat), scopeForSignal (3 сигнала + edge-case).

Pest: 469 / 467 passed / 2 skipped (461 + 6 новых = 467, с retry на transient
PG connection issues — на параллельных тестах с testing_rls_user GRANT тяжёл).
Larastan: 0 errors. Pint passed.

Spec: §2.1
Plan: Task 10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:59:53 +03:00
Дмитрий 084a952bfa feat(models): add SupplierSyncLog model + factory (audit trail для AJAX-sync)
- app/Models/SupplierSyncLog.php — fillable + casts (jsonb arrays + datetime)
  + supplierProject() BelongsTo relation (nullable, ON DELETE SET NULL —
    лог переживает удаление supplier-проекта для audit-trail).
  $timestamps = false (только created_at, без updated_at — append-only)
- database/factories/SupplierSyncLogFactory.php — реалистичные действия из enum
- tests/Feature/Models/SupplierSyncLogTest.php — 4 теста: factory,
  supplier_project relation, jsonb array casts, nullable FK lifecycle

Pest: 463 / 461 passed / 2 skipped (457 + 4 новых = 461).
Larastan: 0 errors. Pint passed.

Spec: §4.3
Plan: Task 9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:53:55 +03:00
Дмитрий 31f813315d feat(models): add LeadCharge ledger model + factory + relations to Tenant/Deal
- app/Models/LeadCharge.php — fillable + casts (datetime + integer)
  + tenant() BelongsTo relation
  + deal() BelongsTo relation (по deal_id, без deal_received_at — composite PK
    на партиционированной обеспечивается БД-уровнем через FK)
  + accessor priceRubles (kopecks → float)
- database/factories/LeadChargeFactory.php — НЕ создаёт реальный Deal автоматически
  (composite FK requires (deal_id, deal_received_at) пары); тесты с FK-целостностью
  явно создают Deal::factory() и передают пару в state()
- tests/Feature/Models/LeadChargeTest.php — 4 теста: factory, tenant relation,
  deal relation, priceRubles accessor. testing_rls_user setup в beforeEach
  для проверки RLS context из не-superuser контекста.

Quirk: SET LOCAL app.current_tenant_id НЕ принимает параметрическое связывание PG —
используем string interpolation с {$tenant->id} как в RlsSmokeTest pattern.

ide-helper:models -W -M -N синхронизировал docblocks (WebhookDedupKey).

Pest: 459 / 457 passed / 2 skipped (453 + 4 новых = 457).
Larastan: 0 errors. Pint passed.

Spec: §7.4
Plan: Task 8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:52:10 +03:00
Дмитрий 71e38ee0a9 feat(models): add PricingTier model with kopecks→rubles accessor + current() snapshot
- app/Models/PricingTier.php — fillable + casts (date, boolean, integer)
  + accessor priceRubles (kopecks → float rubles)
  + scopeActive (is_active=true AND effective_from <= today)
  + static current() — keyed by tier_no Collection<int, PricingTier>
- database/factories/PricingTierFactory.php — реалистичные ступени (300/700/1000/.../null)
- tests/Feature/Models/PricingTierTest.php — 4 теста: factory, accessor,
  scopeActive, current() snapshot всех 7 ступеней

ide-helper:models -W -M -N перегенерил docblocks (WebhookDedupKey synced
после schema v8.16).

Pest: 455 / 453 passed / 2 skipped (449 + 4 новых = 453).
Larastan: 0 errors. Pint auto-fix.

Spec: §7.2
Plan: Task 7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:48:00 +03:00
Дмитрий 62aa55f033 feat(models): add SupplierProject Eloquent model + factory + tests
- app/Models/SupplierProject.php — fillable + casts (jsonb arrays + datetime)
  + scopes: active() (inactive_since IS NULL), staleSince(N days),
  forSignal(signal_type, unique_key)
- database/factories/SupplierProjectFactory.php — корректно учитывает
  chk_supplier_projects_b1_not_for_sms (B1 не порождает SMS-проекты)
- tests/Feature/Models/SupplierProjectTest.php — 6 тестов: factory,
  array casts (workdays + regions), scopeActive, scopeStaleSince,
  scopeForSignal (3 платформы на один домен — UNIQUE (platform,unique_key))

ide-helper:models -W -M -N перегенерил docblocks для 4 существующих моделей
(SaasAdminAuditLog, SystemSetting, UserRecoveryCode, ImpersonationToken) —
синхронизировал @property после schema v8.16.

Pest: 451 / 449 passed / 2 skipped (было 443+6 новых от Task 6 = 449).
Larastan: 0 errors. Pint: passed.

Spec: §2.2
Plan: Task 6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:59:39 +03:00
Дмитрий 757f963929 fix(db): consolidate Plan 1 Tasks 1-5 into schema.sql (project convention)
Project convention использует schema.sql как single source of truth + один
load_initial_schema migration вместо incremental migrations. Мои 5 incremental
миграций конфликтовали с migrate:fresh: load_initial_schema применял
обновлённый schema.sql v8.16, а потом 000001-000005 пытались добавить уже
существующие колонки/таблицы (`signal_type already exists`).

Изменения:

- Удалены 5 incremental миграций 2026_05_10_00000{1..5}_*.
  Все DDL уже в schema.sql (v8.11 → v8.16, 4 commits 2ebe000/9b99d81/
  b08e1ed/7f694f7/9cf380f).
- В schema.sql lead_charges FK на partitioned deals(id, received_at)
  вынесен в самый конец файла (после section 5 с deals), DEFERRABLE
  INITIALLY DEFERRED. Иначе DB::unprepared() выдаёт "deals не существует"
  на load.
- Тесты в tests/Feature/Integration/ остаются — они проверяют
  структурные свойства (column existence, constraint name, RLS via
  pg_class) через information_schema, не зависят от того как именно
  schema создалась.

Verification:
- migrate:fresh OK на обеих БД (liderra + liderra_testing)
- Pest: 445 tests / 443 passed / 2 skipped / 0 failed
  (было 421 baseline + 24 новых для Tasks 1-5 = 443; +2 skipped browser tests)
- Larastan: 0 errors
- Pint: passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:50:39 +03:00
Дмитрий 9cf380f170 feat(db): create supplier_sync_log audit table (SaaS-level, append-only)
Append-only journal AJAX-синхронизаций с поставщиком crm.bp-gr.ru.
Используется для retry, отладки rt-project-* и алертов менеджеру.

- 9 columns: id, supplier_project_id (nullable FK SET NULL),
  action, request_payload (jsonb), response_body (jsonb),
  http_status, error_message, duration_ms, created_at
- 1 CHECK chk_supplier_sync_log_action
  (create/update/delete/disable/session_refresh)
- 3 индекса: supplier_project_id, action, created_at
- REVOKE ALL FROM crm_app_user (DO $$ conditional)
- No RLS (SaaS-level)

Spec: §4.3
Plan: Task 5
Test: 4/4 passed (table, action enum, FK, no RLS).

Schema v8.15 → v8.16. Метрики: 60 таблиц (+1) / 108 индексов (+3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:42:46 +03:00
Дмитрий 7f694f78e0 feat(db): create lead_charges ledger (tenant-scoped RLS, FK to partitioned deals)
Append-only ledger списаний за каждый доставленный лид. Tenant-scoped
с RLS tenant_isolation (ENABLE + FORCE + USING/WITH CHECK).

- 8 columns: id, tenant_id, deal_id, deal_received_at, tier_no,
  price_per_lead_kopecks, charged_at, created_at
- Composite FK lead_charges_deals_fk(deal_id, deal_received_at) →
  deals(id, received_at) DEFERRABLE INITIALLY DEFERRED
  (deals партиционирована — DEFERRABLE для атомарного deal+charge)
- 2 индекса: (tenant_id, charged_at), (deal_id, deal_received_at)
- RLS на (tenant_id = current_setting('app.current_tenant_id')::bigint)
- GRANT SELECT, INSERT для crm_app_user (без UPDATE/DELETE — append-only)

Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §7.4
Plan: docs/superpowers/plans/2026-05-10-supplier-foundation-plan.md Task 4

Test: 3/3 passed (table exists, composite FK to deals, RLS enforces
tenant isolation via testing_rls_user role).

Schema v8.14 → v8.15. Метрики: 59 таблиц (+1) / 105 индексов (+2) /
39 RLS (+1) / функции/триггеры без изменений.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:38:17 +03:00
Дмитрий b08e1edb33 feat(db): create pricing_tiers table (7-step volume billing, kopecks integer) 2026-05-10 12:54:02 +03:00
Дмитрий 9b99d81deb feat(db): create supplier_projects table (SaaS-level aggregate, B1/B2/B3 platforms)
Plan 1/5 Task 2 — SaaS-level агрегатная сущность для проектов у поставщиков.
Несколько Лидерра-tenant'ов могут шарить один supplier_project (sharing-model
spec §2.3): для site/call — по domain/phone; для sms — по (sender, keyword)
на B2 или (sender) на B3. RLS НЕ применяется (таблица не tenant-scoped),
defense-in-depth через REVOKE ALL FROM crm_app_user.

Колонки: platform, signal_type, unique_key (TEXT), supplier_external_id,
current_limit, current_workdays/regions (jsonb), sync_status (pending/ok/failed),
last_synced_at, inactive_since (TTL 180 дней), timestamps.

CHECK constraints (chk_supplier_projects_*):
- platform IN (B1, B2, B3)
- signal_type IN (site, call, sms)
- sync_status IN (pending, ok, failed)
- NOT (platform=B1 AND signal_type=sms) — B1 не поддерживает СМС

Indexes: UNIQUE(platform, unique_key); btree на sync_status, inactive_since.

Тесты: 6/6 (table+columns / unique / platform CHECK / sync_status CHECK / no RLS / no privileges).
Schema: v8.12 → v8.13. Метрики: 56→57 таблиц / 98→101 индексов; RLS/функции/триггеры без изменений.
2026-05-10 12:47:25 +03:00
Дмитрий 2ebe000271 feat(db): extend projects for supplier integration (signal_type, identifier, sms_senders, sms_keyword, delivered_in_month, b1/b2/b3 FK placeholders)
Plan 1/5 Task 1. Adds 8 columns + 3 CHECK constraints + 1 composite index
to projects table for supplier integration foundation. Schema bumped
v8.11 to v8.12. Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §2.1.

7/7 Pest tests pass; rollback verified.
2026-05-10 12:40:02 +03:00
Дмитрий 7ab7cf51cb docs(plans): supplier integration plan 1/5 — Foundation (schema + models + validators)
12 tasks + 3 verification gates covering:
- 5 migrations: extend projects, supplier_projects, pricing_tiers, lead_charges, supplier_sync_log
- 4 new Eloquent models + factories + Pest unit tests
- Project model extension with signal_type/sms_*/supplier_b1/b2/b3 relations
- 3 signal validators (Domain, Phone, SmsSender) with edge-case datasets
- SupplierProjectResolver service with B1+SMS guard
- Comprehensive verification gate: Larastan + squawk + pgFormatter + cspell + markdownlint + cycle-check + code-review subagent

Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §2, §7

cspell-words: +vashinvestor
.gitleaks.toml allowlist: +test phones for validator datasets

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:10:04 +03:00
Дмитрий 3f463f48ea docs(specs): close all 7 open questions in supplier integration design
Q1 SMS validation: B2 = (sender, keyword), B3 = sender only; B1 not available
Q2 Supplier pricing: фиксированная цена за лид
Q3 Liderra billing: 7-tier volume pricing, monthly reset (config in admin)
Q4 Geo filter: MVP, union at supplier + filter on our side
Q5 Webhook auth: defense-in-depth (IP allowlist + secret token in URL)
Q6 Cookie/CSRF refresh: hourly headless Playwright cron + reactive on 401/403
Q7 supplier_project TTL: immediate disable, delete after 180 days no activity

cspell-words.txt: +геофильтр, логинится, encrypter, PHPSESSID

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:39:29 +03:00
Дмитрий ac09603181 docs(specs): supplier integration design (Лидерра ↔ crm.bp-gr.ru)
Captures brainstorming output for two-way integration:
- Sharing model: один supplier-лид → много deal-копий клиентам Лидерры
- 3 supplier-проекта на источник (B1/B2/B3), shared между клиентами
- Dynamic limit adjustment через 20:30 МСК cron + AJAX rt-project-update
- Quota distribution Total/3 с приоритетом B1→B2 на остатке
- Instant FIFO routing по дате создания проекта клиента
- Резервирование обоих направлений: pending-changes queue + CSV reconciliation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:02:19 +03:00
Дмитрий 528fe16166 docs(plans): Sprint 6 Phase A — Reports backend implementation plan
6 tasks: 3 providers (managers/sources/billing) + registry extension +
S3 disk abstraction + file download endpoint. TDD with Pest 4.
Corrects schema column names vs spec (manager_id, type, balance_rub_after)
and uses May 2026 test dates (partitions start 2026-05-01).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:38:43 +03:00
Дмитрий 2a6f0289db docs(spec): Sprint 6 Post-MVP backend design (Reports + Notifications + AdminTenantDetail) 2026-05-10 09:13:45 +03:00
Дмитрий e5848bddec feat(tooling): Trivy CI workflow prep — disabled until YC Docker (#26) 2026-05-10 08:45:17 +03:00
Дмитрий 7123123b48 chore(claude): allow npm run sast in settings.json (#25) 2026-05-10 08:43:52 +03:00
Дмитрий 40125ba5a5 feat(tooling): Semgrep MCP server in .mcp.json (#25) 2026-05-10 08:42:25 +03:00
Дмитрий 53fb1ec27e feat(ci): Semgrep SAST workflow — push/PR to main (#25) 2026-05-10 08:40:52 +03:00
Дмитрий 5983cc17e3 feat(tooling): Semgrep config + npm run sast script (#25) 2026-05-10 08:39:34 +03:00
Дмитрий 3112584ab8 chore(deps): add Dependabot config — npm×2 + composer weekly (#27) 2026-05-10 08:36:36 +03:00
Дмитрий 3c13cc5d64 docs(rules): третий аудит нормативки — закрытие 13 находок (CLAUDE.md v1.86 + PSR_v1 v1.7 + Tooling v1.15)
P0 (4) — арифметические конфликты в CLAUDE.md, прошли мимо второго аудита:
- §3 header «Карта 28 инструментов» → «33» (header застрял с pre-FD эпохи)
- §3.4 header «(+5, итого 28)» → «итого 29» (после #30 в фазу 2)
- §3.3 footer «из 30 номеров минус #1 = 29» → «33 номеров: 29 phase-active + 3 off-phase + 1 historic»
- §6 «Активно: 19 инструментов из 29» (vs раскладка 9+8+7=24) → «24» в обоих местах

P1 (5) — sync stale `+`-refs после второго аудита:
- PSR_v1 шапка: «CLAUDE.md v1.84+/Pravila v1.9+» → «v1.86+/v1.10+»
- Tooling шапка: «Pravila v1.9+/PSR_v1 v1.5+/CLAUDE.md v1.84+» → «v1.10+/v1.7+/v1.86+»
- CLAUDE.md §5 п.5 «PSR_v1 v1.5+» → «v1.7+»

P2 (2) — внутренние несогласованности:
- PSR_v1 line 4/856 «slot уровня 2.5» → «уровня 2b» (описка внутри changelog'а v1.6)
- CLAUDE.md §3.3 #33 «вне Pravila §13» → «вне UI-пула §13» (Pravila §13.2 v1.10 включает claude-md-management как infrastructure subsection)

Sync-правки в connected files:
- README.md + README_АРХИВ_v8_5.md «карта 28 инструментов» → «33»
- Tooling §11.5/§12 «не входят в 28 инструментов» → «33 формализованные позиции»

Через `/claude-md-management:claude-md-improver` с cross-impact-анализом перед каждой группой правок. Pravila v1.10 без изменений (cross-refs к нему уже актуальны).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 06:32:17 +03:00
Дмитрий 07ed3f1be6 docs(rules): второй аудит нормативки — закрытие 15 находок (CLAUDE.md v1.85 + Pravila v1.10 + PSR_v1 v1.6 + Tooling v1.14)
Закрыто 15 находок второго аудита правил использования плагинов и скилов
(4 P0 реальных противоречий + 7 P1 stale-refs + 4 P2 структурных).
Через `/claude-md-management:claude-md-improver`.

P0 — реальные противоречия:
- Tooling §10.3 шаг 2: «3 skills» → «14, все запреты сняты Pravila §11»
  (закрыта внутри-документная контрадикция с §4.1 того же файла)
- CLAUDE.md §6: арифметика «33» исправлена (+1 заменённый PG MCP
  исторически; раньше 19/29 + 3 = 32, не 33)
- Pravila §13.2: «v1.4 (15 правил)» → «v1.6 (16 правил R0–R15)»
- Tooling §13: добавлены v1.13 + v1.14 entries (раньше история
  обрывалась на v1.12, хотя шапка описывала v1.13)

P1 — массовый stale-refs дрейф v1.4→v1.6 + v1.12→v1.14:
- CLAUDE.md §0 cross-refs (Pravila/PSR_v1/Tooling rows), §3.3 #31, #32, §5 п.12
- Pravila §11.5 («10 правил»→«16»), §13.9, §13.10
- Tooling §4.4 («10 правил»→«16»), §4.5, §4.6, §4.7

P2 — структурные:
- Tooling Прил. Н добавлен explicit-слотом уровня 2b в priority chain
  (CLAUDE.md §1, PSR_v1 R0.1 таблица, Tooling §7) — раньше формальная
  дыра «PSR_v1 R0.1 говорит stack ниже Tooling, но Tooling нет ни
  в одной priority chain»
- PSR_v1 R0.4.A свёрнут до cross-ref на Pravila §12.3 SoT — устранён
  риск дрейфа формулировок (раньше параллелил список разной формулировкой)
- Pravila §0 +note про §11 локальное override-исключение над §2.2/§4.5/§8.4
  (раньше §11 формально стоял ниже §9 в цепочке вопреки фактическому
  override §2.2/§4.5/§8.4 при skill-инвокации)
- PSR_v1 R0.6 hard-стопы пронумерованы 1–11 для надёжности cross-refs
  «пункт 9/10/11» (раньше буллет-list, ссылки молча ломались)

Все 4 файла нормативной документации внутренне непротиворечивы.
История версий синхронизирована.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 05:56:43 +03:00
Дмитрий ffe34e1b17 docs(registry): Sprint 4 «Audit tail» closure record (v1.76→v1.77)
Sprint 4 закрыл 3 оставшихся audit O-* пункта (5 коммитов f77c91d..b912724):
- O-perf-04: keyset pagination в DealController::index + 3 Pest-теста
- O-refactor-04 хвост: 8 Vue-компонентов >300 строк разделены (AppLayout
  R0.6 hard-стоп снят явным запросом заказчика)
- O-refactor-06: rollup-plugin-visualizer + knip + cleanup ~165 строк
  dead-code в composables/

Acceptance закрыт за исключением 2 known DealsView/DealDetailDrawer
(intentional Sprint 3 defineExpose-контракт).

Регрессия: Pest 421 / Vitest 416 / Larastan 0 / vue-tsc 0 / ESLint 0 / build OK.

Sprint 0 push выполнен (4f36bd3..8c6374d, 21 коммит). PAT Workflows R&W
подтверждена через API-диагностику.

CLAUDE.md/Pravila/Tooling/Plugin_stack_rules — параллельно обновляются
другой сессией (PSR_v1 v1.4 формализация UPM/21st/R15 motion в локальных
коммитах 833e3e6 + cf1aabb), не трогаю в этом коммите.

cspell-words: +oxc (oxc-parser Windows quirk knip), +консты (Russian slang).

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