Found via TDD that supplier_leads has its own platform CHECK constraint
(chk_supplier_leads_platform) and that the seed migration was missing
NOT NULL columns (accepts_types, channel). Migration now:
- widens supplier_projects/project_supplier_links/supplier_leads.platform
VARCHAR(4) → VARCHAR(8) (DIRECT is 6 chars)
- extends three CHECK constraints to include 'DIRECT'
Seed migration uses raw SQL INSERT to properly serialize PG ARRAY type
for accepts_types column. channel='sites' (valid per suppliers_channel_check).
db/schema.sql synced — 3 platform columns and 3 CHECK constraints updated.
CHANGELOG_schema.md entry pending Task 9.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CsvReconcileJob каждый час стабильно ставил drift_alert ~40-50% (10 запусков
подряд на проде → admin-блок «Здоровье резервного канала» показывал «down»),
потому что поставщик crm.bp-gr.ru кладёт телефон/URL в поле «project» CSV.
Парсер extractPlatform() корректно их скипал, но строки оставались и в
count(missing), и в total_csv_rows формулы drift'а → стабильный false-positive.
Фикс (вариант A из брейнсторма с заказчиком):
- schema v8.36: +supplier_csv_reconcile_log.unparseable_count INTEGER NOT NULL DEFAULT 0
- CsvReconcileJob: считает $unparseableCount отдельно, новая формула
drift = max(0, missing − unparseable) / max(1, total − unparseable)
- Миграция (pgsql_supplier, Спек B pattern, IF NOT EXISTS — idempotent)
- TDD: +2 теста (100matched+10junk → ok; mixed 95+5junk+3real → drift по реальным).
Существующие 7 кейсов GREEN без изменений (unparseable=0 → формула идентична).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Резервный CSV-канал (Путь 2): отчёт поставщика «Запрос номеров» не
содержит vid -> CSV-recovered лиды имеют vid=NULL. UNIQUE-индекс
idx_supplier_leads_vid_unique сохранён (PostgreSQL NULL != NULL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-review Task 1: явный per-table GRANT-блок для import_unknown_statuses
использовал несуществующие роли (crm_app_admin / crm_readonly). Реальные роли —
crm_app_user / crm_admin_user / crm_migrator / crm_audit_writer /
crm_supplier_worker (db/00_create_roles.sql). Блок удалён целиком из
db/02_grants.sql и db/schema.sql: import_unknown_statuses — обычная
tenant-scoped таблица, покрыта umbrella GRANT ... ON ALL TABLES +
ALTER DEFAULT PRIVILEGES (как import_log), явный per-table grant не нужен.
ImportSchemaTest: UNIQUE-тест усилен — проверяет состав колонок
(status_ru, tenant_id), а не только наличие constraint'а типа 'u'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds INT[] column + GIN index to support 89-code regions (Plan 6).
region_mask/region_mode kept for backward-compat (DEPRECATED, removal in Plan 6.5).
Empty array semantically equivalent to legacy region_mask=255 (all of Russia).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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/функции/триггеры без изменений.
Webhook PoC раскрыл архитектурный пробел в schema v8.6: §5.5-спецификация
делает INSERT в webhook_dedup_keys ДО INSERT в deals (атомарный захват
ключа), но FK был immediate. Решение в две стадии:
1. schema.sql v8.6 → v8.7 — DEFERRABLE INITIALLY DEFERRED на FK
(deal_id, deal_received_at) → deals. ON DELETE CASCADE остаётся
immediate. В bare-транзакции production worker'а решает проблему.
2. Pivot Job на pg_advisory_xact_lock — Pest-тесты с DatabaseTransactions
trait всё равно падали: PG проверяет deferred FK на RELEASE SAVEPOINT,
не на outer COMMIT. Воспроизведено standalone PHP-скриптом, это
PG-семантика subtransactions. Advisory lock работает identically
в любой вложенности транзакций. DEFERRABLE FK сохранён в schema
как defense-in-depth для batch-импортов без savepoint.
Backend стек:
- app/app/Models/Deal.php — composite PK через override
setKeysForSaveQuery (PG требует id+received_at для partition pruning)
- app/app/Models/WebhookDedupKey.php — мини-модель для тестов и debug
- app/database/factories/DealFactory.php — fake данные с received_at
в текущей партиции
- app/app/Jobs/ProcessWebhookJob.php — advisory-lock-based upsert
по §5.5 v8.7. PoC scope: dedup + balance check + project findOrCreate.
TODO для следующих ветвей: BalanceTransaction, SupplierLeadCost,
ActivityLog, RejectedDealsLog, DuplicateDetector (Биз-19).
- app/tests/Feature/DealModelTest.php — 6 тестов composite PK + связи
- app/tests/Feature/ProcessWebhookJobTest.php — 6 тестов: новая сделка,
дубль vid, balance=0, изоляция тенантов, findOrCreate проекта,
ON DELETE CASCADE.
Pest 31/31 за 2.7 сек. Pint + Larastan чисто (phpstan-baseline регенерирован,
scanFiles _ide_helper_models.php добавлен в phpstan.neon).
Документы:
- db/CHANGELOG_schema.md §W (две стадии решения)
- narrative §2.4/§5.5/§6.5/§11 синхронизированы под advisory lock
- Реестр Открытые_вопросы v1.20 → v1.21
- CLAUDE.md v1.11 → v1.12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Получен handoff-пакет liderra_v8_handoff/ от дизайнера Платона
(kpd9363@gmail.com) от 07.05.2026 — v8 Forest. Заказчик 08.05 решил
применить только в части дизайна, имени, логотипа. Функционал, состав
страниц и правила (CTO-11, click-wrap, SSO break-glass, 14 статусов
воронки) — без изменений (источник — ТЗ v8.5/schema v8.5).
Что сделано:
- Массовая замена Лидпоток→Лидерра (с учётом падежей: Лидерры/Лидерре)
в 33 файлах (449 вхождений) — все .md/.sql/.json/.toml/.yml/.txt/.html,
кроме исторических упоминаний внутри liderra_v8_handoff/
- Удалён docs/brandbook.md v1.1 — заменён на BRANDBOOK_v2.md из handoff
- Скопированы 13 концептов liderra_v8_handoff/concepts/v8_*.html в
web/v8/. Удалены старые web/01-login.html, 02-dashboard.html,
03-deals.html, index.html (палитра v1.1 deprecated)
- CLAUDE.md v1.0→v1.1: §0 (BRANDBOOK_v2 + DEVELOPER_HANDOFF в источниках),
§2 (палитра Forest, Inter+JBM, Lucide), §5 п.6 (anti-pattern Inter
снят — в Forest Inter наш основной шрифт), §6 (13 концептов в web/v8/)
- Реестр Открытые_вопросы_v8_3.md v1.12→v1.13: добавлена запись о
ребрендинге + 4 точечных расхождений handoff vs ТЗ (статусы воронки,
click-wrap чекбоксы, SSO fallback, axe violations)
- package.json/package-lock.json: name lidpotok→liderra
4 расхождения handoff vs ТЗ (НЕ применены, источник истины — ТЗ/schema):
1. 14 «обобщённых» статусов в BRANDBOOK_v2 §3.6 ≠ 14 slug'ов в
schema.sql:2076 (совпадает 2 из 14: «Переговоры», «Оплачено»).
Источник — schema/ТЗ §6.4 (реселлерская модель из аудита crm.bp-gr.ru,
6 системных + 8 настраиваемых статусов).
2. 3-й click-wrap в v8_login.html («маркетинг-опционально») ≠ ТЗ §1.5/§4.1
(«согласие на ПДн», обязательное, OPEN-Ж-3).
3. SSO в v8_admin.html («локальный 2FA fallback») ≠ ТЗ OPEN-И-13
(break-glass super_admin, локальный 2FA выключен).
4. Заявление «axe-core 4.10.2 — 0 violations» в README handoff — локально
Pa11y 9.1.1 + axe нашёл 81 violation на 10/13 HTML (преимущественно
color-contrast на декоративных separator'ах с --ink-disabled).
Чисто: settings/errors/palette_options.
Что НЕ включено в коммит:
- лендинг/TZ_landing_v1_0.md — untracked, не моя работа в этой сессии
- .tmp/ — gitignored
Что осталось (для следующих сессий):
- Возможное переименование GitHub-репо CoralMinister/lidpotok → liderra
(отдельное решение заказчика)
- Опционально: обратная связь Платону по 4 расхождениям handoff vs ТЗ
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>