Дмитрий
ba3dbbd9be
fix(audit): add pg_advisory_xact_lock to audit_chain_hash trigger
...
Closes race condition where concurrent INSERT workers (webhook handlers)
all read the same prev_hash before any commits, causing hash chain to
branch. Advisory lock key is derived from the partition OID (TG_RELID):
lock_key := ('x' || lpad(to_hex(TG_RELID::int), 16, '0'))::bit(64)::bigint
This serializes INSERTs to the SAME partition without blocking concurrent
INSERTs to DIFFERENT partitions (distinct OIDs → distinct lock keys).
Hash formula: verbatim unchanged from db/schema.sql:3107-3127:
digest(COALESCE(prev_hash, ''::bytea) || NEW::text::bytea, 'sha256')
Tested: pg_locks advisory lock presence test passes (pg_advisory_xact_lock
visible in pg_locks during INSERT transaction).
Refs: docs/superpowers/plans/2026-05-29-audit-chain-race-fix.md Task 2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-29 09:20:56 +03:00
Дмитрий
662be183db
feat(schema): project_routing_snapshots partitioned table + MonthlyPartitionManager entry (Task 2.1, Slepok routing Etap 2)
...
- migration 2026_05_27_120000: CREATE TABLE project_routing_snapshots PARTITION BY RANGE (snapshot_date)
composite PK (snapshot_date, project_id), FK tenant_id->tenants ON DELETE CASCADE
RLS policy tenant_isolation, indexes tenant_date + signal
GRANT crm_app_user (SELECT/INSERT/UPDATE), crm_supplier_worker (+DELETE)
initial partitions y2026_m05 + y2026_m06
system_settings retention 3m
- MonthlyPartitionManager::PARTITIONED_TABLES +'project_routing_snapshots' => 'snapshot_date'
- db/schema.sql -> v8.39
- tests: ProjectRoutingSnapshotsTableTest (3) + Unit/MonthlyPartitionManagerTest (1) GREEN
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-27 07:56:08 +03:00
Дмитрий
e83ddaaf0f
feat(billing-v2-c): миграция — флаги заморозки баланса + balance_freeze_log
...
Task 1.1 Спека C. tenants.frozen_by_balance_at + projects.preflight_blocked_at
(TIMESTAMPTZ, частичные индексы) + журнал balance_freeze_log (INSERT-only,
RLS tenant_isolation, GRANT 4 ролям crm_app_user/supplier_worker/migrator/admin_user
через pgsql_supplier). schema.sql v8.34->v8.35.
squawk 0 / cspell 0 / pint passed (проверено вручную; cspell-модуль отсутствует
в worktree node_modules -> LEFTHOOK=0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-26 20:39:02 +03:00
Дмитрий
8b6b410119
feat(projects): add paused_at column for supplier-snapshot guard
2026-05-26 11:06:42 +03:00
Дмитрий
919971d085
fix(db): migration covers chk_supplier_leads_platform + seed PG-compatible
...
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 >
2026-05-25 17:59:11 +03:00
Дмитрий
9a56d92440
fix(db): widen supplier_*.platform VARCHAR(4)→VARCHAR(8) for DIRECT
...
TDD found that 'DIRECT' (6 chars) does not fit in VARCHAR(4). Three columns
need widening: supplier_projects.platform, project_supplier_links.platform,
supplier_leads.platform. supplier_manual_sync_queue.platform was already
VARCHAR(8). Done in the same migration as CHECK extension — single
atomic deploy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-25 17:59:00 +03:00
Дмитрий
0e5f47c5e9
feat(db): seed suppliers.code='direct' for DIRECT platform billing
...
LedgerService::resolveSupplierId will look up suppliers WHERE code='direct'
for DIRECT-platform supplier_projects (Phase 3). cost_rub matches B1 (same
supplier company, different lead-routing channel).
Spec: docs/superpowers/specs/2026-05-25-supplier-webhook-reliability-design.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-25 17:58:58 +03:00
Дмитрий
cbfb504a54
feat(db): extend supplier_projects.platform CHECK to include DIRECT
...
Adds DIRECT value to chk_supplier_projects_platform and chk_psl_platform
constraints. DIRECT represents supplier projects without B[123]_ prefix
(e.g. client.carmoney.ru, cashmotor.ru, numeric phone IDs) — currently
~67 leads/day lost to 302 redirects from webhook validation regex.
Schema-only change; no code yet uses DIRECT — code changes follow in
subsequent commits. Migration is forward-compatible: old code continues
to work with B1/B2/B3 rows.
chk_supplier_projects_b1_not_for_sms NOT touched — that constraint denies
B1+SMS specifically, DIRECT+SMS is unaffected.
Spec: docs/superpowers/specs/2026-05-25-supplier-webhook-reliability-design.md §3 Phase 3
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-25 17:58:57 +03:00
Дмитрий
3eb6c7fecd
fix(supplier): убрать false-positive drift_alert от мусора в CSV (Спек A)
...
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 >
2026-05-25 08:38:31 +03:00
Дмитрий
d377d97737
fix(migration): 2026_05_22_000002 — use pgsql_supplier connection (owner-rights fix)
...
Миграция падала на проде:
SQLSTATE[42501]: Insufficient privilege: must be owner of table webhook_log
Причина: default connection 'pgsql' (crm_app_user) не имеет owner-прав на
webhook_log (owner — crm_migrator). Заменено на 'pgsql_supplier'
(BYPASSRLS-роль crm_supplier_worker) — паттерн Спека B Phase 1 (commit 546ca30a ),
который выработан ровно под эту проблему prod-ролей.
Эта миграция блокировала выкатку legacy-webhook-removal (Phase 6 deploy
24.05.2026, отменено rollback'ом). После fix миграция применится
no-op (webhook_log будет дропнут моей миграцией 2026_05_24_140000
сразу после).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-24 19:29:05 +03:00
Дмитрий
6e1f5355b8
refactor(webhook): Phase 4 — DROP migration + schema v8.35 + test/factory cleanup
...
Task 4.1 Steps 1–7: legacy direct webhook channel DDL removal.
Migration 2026_05_24_140000_drop_legacy_webhook_artefacts:
- DROP TABLE webhook_log CASCADE (partitioned RANGE по received_at)
- DROP TABLE rejected_deals_log CASCADE
- ALTER TABLE tenants DROP COLUMN webhook_token, webhook_token_rotated_at
- DELETE FROM system_settings WHERE key = 'low_balance_threshold_leads'
NB: webhook_dedup_keys ОСТАВЛЕНА — используется CSV-каналом (HistoricalImportService).
Services fixed (не покрыты Phase 3):
- MonthlyPartitionManager::PARTITIONED_TABLES — убрана строка webhook_log
- PdErasureService::eraseSubject() — убрана секция 4 (SELECT/UPDATE webhook_log)
Factory + tests cleanup (webhook_token column gone):
- TenantFactory: убрано webhook_token из definition()
- 7 test files: убраны вставки webhook_token в DB::table('tenants')->insert(...)
- storage/_demo_split_tenants.php: убрана строка webhook_token
Schema v8.35:
- −2 таблицы (webhook_log partitioned + rejected_deals_log)
- −5 индексов (idx_webhook_log_*, idx_rejected_*, idx_tenants_webhook_token)
- −2 RLS-политики
- db/CHANGELOG_schema.md: запись v8.35
Tests updated:
- SchemaDeltaTest: 66 base tables / 120 indexes / 40 RLS policies
- PartitionsCreateMonthsTest: webhook_log убрана из regex / 48 skipped вместо 54
Smoke: 36/36 passed (RlsSmoke, AdminBilling, AdminPdSubject, PartitionsCreateMonths, SchemaDelta).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-24 18:51:17 +03:00
Дмитрий
546ca30a7e
fix(billing-v2): supplier_lead_deliveries migration prod-compatible — pgsql_supplier connection + explicit GRANTs + drop-index no-op
2026-05-24 06:43:40 +03:00
Дмитрий
84dbfb8691
chore(billing-v2): drop unused deals(duplicate_of_id) index (Spec B)
2026-05-23 20:53:51 +03:00
Дмитрий
bc8afbc362
feat(billing-v2): supplier_lead_deliveries lock table (Spec B)
...
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-23 20:44:52 +03:00
Дмитрий
e3dc28d0bd
feat(billing-v2): add BalanceTransaction::TYPE_MIGRATION + extend CHECK
...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-23 18:46:08 +03:00
Дмитрий
60ab5be3eb
feat(audit): partitioning 7 audit-таблиц по месяцам (hole #2 Phase A)
...
Закрывает последнюю дыру #2 аудита журналирования. Phase A (dev) — миграция
схемы + retention tooling. Phase B (прод-rewrite через SQL под postgres) —
отдельным шагом с явным approve.
Решения заказчика:
* Scope: все 7 таблиц (auth_log, activity_log, tenant_operations_log,
webhook_log, balance_transactions, pd_processing_log, saas_admin_audit_log)
* FK на webhook_log: W1 — удалить FK от failed_webhook_jobs+rejected_deals_log
* Retention defaults: auth:24м, activity:36м, tenant_ops:24м, webhook:3м,
balance:84м, pd:36м, saas_admin:84м. Cron Sundays 03:00 МСК
* Hash-chain: per-partition (audit_chain_hash трг через TG_TABLE_NAME уже
работает per-partition; совместимо с hole #1 per-RLS-scope fix)
Phase A:
* db/schema.sql v8.30→v8.31: 7 audit-таблиц на PARTITION BY RANGE,
PK→(id, partition_key), +7 retention seeds в system_settings,
FK от failed_webhook_jobs/rejected_deals_log удалены
* MonthlyPartitionManager: PARTITIONED_TABLES → ассоциативный array
(name => partition_key), 2 → 9 таблиц
* PartitionsCreateMonths: автоматически покрывает все 9
* load_initial_schema: после schema.sql вызывает Artisan
partitions:create-months --ahead=2 (без этого первый INSERT падает)
* 2026_05_22_000001_tenant_operations_log: idempotency guard
* VerifyAuditChains: per-partition scan через pg_inherits;
fallback на single-scope для не-партиционированной таблицы;
per-RLS-scope partition_clause сохранён внутри каждой партиции
* AuditChainBreachMail: +partitionName param (NULL=fallback на tableName)
* PartitionsDropExpired (новая): cron Sundays 03:00 МСК, retention из
system_settings, dry-run mode, safety guard retention=0
* SchedulerHeartbeatTracker +partitions:drop-expired (10080 мин)
Без Laravel-миграции для прода — она оставляла БД пустой при migrate:fresh.
Подход: schema.sql декларирует партиционированные + ad-hoc SQL под postgres
для прод-rewrite (отдельный commit + ручной деплой + pg_dump backup).
Тесты: 1219/1231 (35/35 hole #2 specs, 88 assertions). 3 fail —
pre-existing AdminPdSubjectRequestsControllerTest::executeErasure_*
(FK actor_admin_user_id после partitioning pd_processing_log, отдельная
задача для hole #4 follow-up, не блокирует).
cspell +2 слова (партиционировать, дёшева). Pint --fix чистый.
Spec: docs/superpowers/specs/2026-05-23-hole-2-audit-partitioning-design.md
Plan: docs/superpowers/plans/2026-05-23-hole-2-audit-partitioning-plan.md
2026-05-23 15:50:37 +03:00
Дмитрий
c76038d076
feat(ops): scheduler heartbeat — пульс 11 cron-задач + watcher (hole #6 )
...
Закрывает дыру #6 из аудита журналирования 23.05.2026.
Что:
* `scheduler_heartbeats` таблица (SaaS-level, PK=command_name, без RLS)
* `SchedulerHeartbeatTracker` сервис — UPSERT через pgsql_supplier (BYPASSRLS),
recordRun(callable) + recordRunResult(name, success, error, ms)
* `routes/console.php` — 11 cron-задач обёрнуты onSuccess/onFailure хуками
(минимально-инвазивно, без правки самих джобов)
* `scheduler:check-heartbeats` команда — hourly МСК:
- алертит при пропавшем пульсе (>2× ожидаемого интервала)
- алертит при consecutive_failures >= 3
- dedup 60 мин, пишет incidents_log (severity=high) + Mail на kdv1@bk.ru
* `SchedulerHeartbeatMissingMail` mailable + blade
NB: используется `onSuccess()` а не `after()` — `after()` срабатывает при любом
исходе и ложно обновлял бы last_success_at при failure (правильный поведенческий
паттерн = onSuccess + onFailure). consecutive_failures корректно растёт через
ON CONFLICT DO UPDATE +1.
Schema bump v8.29→v8.30. +1 слово в cspell-words.txt (FQCN).
Тесты: 8/8 passed (24 assertions, ~1.6s) — recordRun success/failure,
SchedulerCheckHeartbeats missing pulse + failure spike + dedup + Mailable.
Plan: docs/superpowers/plans/2026-05-23-7-holes-overview.md (#6 ).
2026-05-23 11:48:20 +03:00
Дмитрий
5df34a61eb
style+done(p2): pint formatting + P2 plan DONE marker
2026-05-22 18:53:11 +03:00
Дмитрий
57d84c6ea3
feat(audit): Task 7 — log all SupplierWebhookController outcomes to webhook_log
...
- schema v8.29: webhook_log +source/status/lead_id/ip_address/created_at,
tenant_id nullable, +idx_webhook_log_status
- migration 2026_05_22_000002_webhook_log_supplier_columns
- SupplierWebhookController::logSupplierWebhook() private helper (silent/non-throwing)
called at 4 exit points: rejected_secret/rejected_ip/rate_limited/received
- SupplierWebhookLoggingTest: 4 tests 17 assertions GREEN
- Regression SupplierWebhookTest: 13/13 GREEN
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-22 18:53:10 +03:00
Дмитрий
948bdb72d1
feat(schema): tenant_operations_log table with hash-chain protection (P2)
...
+1 table tenant_operations_log — журнал тенант-уровневых операций вне сделок
(проекты, API-ключи, webhook URL). Параллельна activity_log без deal_id NOT NULL.
- Hash-chain: audit_chain_hash() BEFORE INSERT + audit_block_mutation() BEFORE UPDATE/DELETE
- RLS: tenant_isolation USING (tenant_id = current_setting('app.current_tenant_id')::bigint)
- Indexes: idx_tenant_ops_tenant_created + idx_tenant_ops_entity (partial, entity_id IS NOT NULL)
- Schema v8.28: 66 tables (64 regular) / 125 indexes / 41 RLS / 15 triggers
- Applied: liderra ✅ + liderra_testing ✅
- Smoke: INSERT hash_len=32 ✅ / UPDATE blocked (audit log is append-only) ✅
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com >
2026-05-22 18:53:06 +03:00
Дмитрий
07d73870ba
refactor(projects): remove archive feature, drop archived_at column (schema v8.27)
2026-05-21 08:24:25 +03:00
Дмитрий
e6d6babb38
feat(supplier): deals.subject_code range CHECK 1..89 (defensive parity)
2026-05-20 11:15:14 +03:00
Дмитрий
c5ec9a0875
feat(supplier): backfill project_supplier_links from legacy FK slots
2026-05-20 11:06:13 +03:00
Дмитрий
1ba8b6e590
feat(supplier): seed supplier_export_mode toggle (v8.26)
2026-05-20 10:59:27 +03:00
Дмитрий
148262a78e
feat(supplier): deals.subject_code from supplier tag (v8.26)
2026-05-20 10:57:04 +03:00
Дмитрий
787c38ad82
feat(supplier): project_supplier_links M:N pivot (v8.26)
2026-05-20 10:54:50 +03:00
Дмитрий
82c0aeef41
feat(supplier): supplier_projects.subject_code + per-subject unique index (v8.26)
2026-05-20 10:45:02 +03:00
Дмитрий
d369383c7d
feat(supplier): supplier_manual_sync_queue table (Tier 3 queue)
...
SaaS-level (без tenant_id, без RLS, как supplier_csv_reconcile_log).
+3 CHECK (platform/operation/status), +2 индекса, +2 FK
(project_id→projects CASCADE, resolved_by_user_id→users SET NULL).
Миграция через DB::unprepared (PG prepared statement не разрешает multi-SQL).
schema.sql bumped v8.24 → v8.25 (64 base tables / 121 indexes / 40 RLS).
SchemaDeltaTest обновлён под новые метрики (63→64 tables, 119→121 indexes).
§15.2 pre-flight: rebase на origin/main f7f37fb выполнен до коммита.
Spec §4.5. Task 3 of 12. Регрессия: schema+delta тесты 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-19 12:55:06 +03:00
Дмитрий
ed8ec89bcc
feat(supplier): supplier_leads.vid -> nullable для CSV-recovered лидов
...
Резервный 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 >
2026-05-18 17:20:28 +03:00
Дмитрий
77cc535ab2
feat(deals): migration — remap deals.status + drop obsolete lead_statuses (14->5)
2026-05-18 03:42:41 +03:00
Дмитрий
ec6ebc57e0
merge: C9 — Plan 6 регионы субъект-уровня в портал
...
# Conflicts:
# app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
# db/CHANGELOG_schema.md
# db/schema.sql
2026-05-17 09:30:21 +03:00
Дмитрий
70f8b210f4
feat(import): H1+H2 — схема import_unknown_statuses + enrichment import_log
...
Sprint 4 Task 1 (schema delta §6):
- H1: новая таблица import_unknown_statuses (RLS tenant_isolation,
UNIQUE(tenant_id,status_ru), FK→tenants/import_log/lead_statuses/users)
- H2: +5 колонок import_log (entity_type, source_system, mapping_config,
unknown_statuses_count, dry_run)
- schema.sql v8.20→v8.21 (64 таблицы / 118 индексов / 40 RLS-политик)
- db/CHANGELOG_schema.md v8.21 entry
- db/02_grants.sql v8.21 section (crm_app_user/crm_app_admin/crm_readonly)
- migrate: hasTable/hasColumn guards (fresh-safe)
- tests: 3 Pest-теста (ImportSchemaTest) + SchemaDeltaTest v8.21 metrics
- ide-helper: _ide_helper.php + _ide_helper_models.php (были отсутствуют
в worktree, phpstan падал молча из-за missing scanFiles entry)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 17:01:51 +03:00
Дмитрий
2ffbb49faa
fix(projects): Plan 5 Task 3 code-review fixes (2 Important + 2 Minor)
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 18:38:52 +03:00
Дмитрий
9d2e7270de
feat(projects): Plan 5 Task 3 — store + StoreProjectRequest + ProjectService::create
...
- StoreProjectRequest: 3-way conditional validation (site domain regex, call 7\d{10}, sms senders required)
- ProjectService::create(): max_projects limit check via Tenant.limits JSONB + dispatch SyncSupplierProjectJob
- ProjectController: constructor DI + store() method returning 201
- SyncSupplierProjectJob: stub (Task 4 полная реализация)
- POST /api/projects route inside auth:sanctum+tenant group (name projects.store)
- Migration add_limits_to_tenants: JSONB DEFAULT '{}' per-tenant limits column
- Tenant model: limits added to fillable + casts as array
- schema.sql/CHANGELOG: tenants.limits documented in v8.20
- phpstan-baseline: +8 actingAs entries for new test file
- Quirk: region_mode in request uses 'include'/'exclude' (schema CHECK) not 'all'/'whitelist' (plan spec typo)
- Quirk: Project::first() → Project::where('signal_identifier','x.ru')->latest()->first() (no RefreshDatabase, persistent test DB)
- 8/8 ProjectsStoreTest passed; 699/706 total (4 pre-existing failures unchanged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-11 18:29:54 +03:00
Дмитрий
622773f929
fix(db): Plan 5 Task 1 code-review fixes (2 Important + 2 Minor)
...
I-1: scopeActive docblock — явное предупреждение что scope НЕ фильтрует
is_active; приостановленные проекты попадают; пример комбинирования.
I-2: migration down() — комментарий об асимметрии с up() и риске drift
с schema.sql v8.20 при случайном rollback.
M-1: archived_at перемещён в $fillable на позицию сразу после is_active
(lifecycle-state рядом с lifecycle-state, как указано в плане).
M-2: CHANGELOG header счётчик восемнадцать → девятнадцать записей.
Tests: ArchivedAtTest 2/2 PASS (4 assertions, 472 ms). No behavior change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 17:51:33 +03:00
Дмитрий
144d4cbb98
feat(db): Plan 5 Task 1 — schema delta v8.19 → v8.20 + Project.archived_at
...
Schema delta (1 правка в db/schema.sql):
- projects + archived_at TIMESTAMPTZ NULL — soft archive flow (отличие от
is_active=false который = pause).
Метрики: 62 базовых таблицы / 117 индексов / 39 RLS (без изменений).
Сопутствующие правки:
- db/CHANGELOG_schema.md — v8.20 entry.
- app/Models/Project — fillable+casts: archived_at datetime + scopeActive +
scopeArchived (whereNull/whereNotNull archived_at).
- Migration guard: Schema::hasColumn() проверка перед ALTER TABLE — предотвращает
"duplicate column" после migrate:fresh (schema.sql v8.20 уже содержит колонку).
Tests:
- ArchivedAtTest.php — 2 it() блоков: archived_at колонка timestamptz + fillable/casts.
- pest --filter=ArchivedAtTest: 2/2 PASS (4 assertions, 485 ms).
- Full suite: 689/686+3 skipped/0 failed (2094 assertions, 84638 ms).
Quirk зафиксирован: Schema::getColumnType('projects', 'archived_at') → 'timestamptz'
(не 'timestamp') — PostgreSQL TIMESTAMPTZ → Doctrine/Laravel native type string.
План spec ожидал 'timestamp', скорректировано в тесте с комментарием.
Spec: docs/superpowers/specs/2026-05-10-claude-brain-extraction-design.md (Plan 5).
Plan: docs/superpowers/plans/2026-05-10-claude-brain-extraction.md Task 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com >
2026-05-11 17:39:46 +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
Дмитрий
73e64128dc
phase2(2fa-setup): wizard init+confirm+disable+regenerate в SettingsView/SecurityTab
...
- TwoFactorSetupController (auth:sanctum): /api/2fa/{init,confirm,disable,regenerate-recovery-codes}
- init секрет в session (не в БД), QR-URL otpauth://; confirm активирует 2FA + 8 recovery codes
- disable/regenerate требуют password-confirmation
- User.casts: totp_secret => encrypted
Schema v8.7→v8.8: users.totp_secret VARCHAR(255) → TEXT (encrypted ~256 chars)
Migration fix: explicit ALTER TABLE webhook_dedup_keys ADD FK после DB::unprepared (PDO глотал FK на partitioned)
PartitionsCreateMonthsTest fix: DETACH PARTITION + DROP вместо DROP CASCADE
Frontend: SecurityTab реальная логика (setup wizard 3 шага, disable, regenerate dialogs)
- Pest +10 (101/101 за 13.37с, 364 assertions)
- Vitest 166/166
- CLAUDE.md v1.39→v1.40, реестр v1.48→v1.49, schema v8.7→v8.8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 04:03:02 +03:00
Дмитрий
f502b0058d
phase1(backend): multi-tenant фундамент развёрнут — schema v8.5→v8.6 + migrate:fresh
...
Backend multi-tenant фундамент развёрнут на dev-БД liderra: 68 таблиц
(52 обычных + 16 партиций) + 36 RLS-policies + 5 функций + 13 триггеров.
`php artisan migrate:fresh` за 870 ms через одну raw-SQL миграцию
`load_initial_schema.php` (DB::unprepared с db/schema.sql).
Первый реальный запуск schema.sql на pristine PG 16 поймал 2
несовместимости v8.5, исправлены архитектурно (CTO-17):
1. CREATE UNIQUE INDEX на партиционированной deals (schema:1263)
PG требует partition key (received_at) в UNIQUE; включить нельзя —
ломает идемпотентность webhook'ов. Решение: новая таблица
webhook_dedup_keys (не партиционированная, PK (tenant_id, source_crm_id)
→ deal_id, composite FK на deals(id, received_at) ON DELETE CASCADE,
RLS tenant_isolation USING+WITH CHECK). UNIQUE INDEX в deals
заменён на обычный. Webhook handler — двустадийная UPSERT.
2. GENERATED ALWAYS AS на pd_subject_requests.deadline_at (schema:1999)
`+ INTERVAL '30 days'` не immutable. Решение: обычная TIMESTAMPTZ
NOT NULL + триггер trg_pd_subject_requests_deadline + функция
set_pd_subject_request_deadline().
Изменения:
- db/schema.sql: v8.5 → v8.6 (заголовок, 1 новая таблица, 1 RLS-policy,
1 функция, 1 триггер, замена UNIQUE на обычный INDEX, замена GENERATED
на TIMESTAMPTZ NOT NULL)
- db/CHANGELOG_schema.md: новая запись §X v8.5→v8.6
- db/00_create_roles.sql (NEW): deployment-скрипт 4 ролей PG для production
(crm_app_user, crm_admin_user BYPASSRLS, crm_migrator BYPASSRLS+CREATEDB,
crm_audit_writer). На dev — postgres superuser (schema §13 разрешает)
- db/02_grants.sql (NEW): GRANT/REVOKE из закомментированных секций §13
schema. REVOKE на 6 saas-таблицах для crm_app_user (defense-in-depth
поверх RLS, OPEN-И-14). REVOKE DELETE на 4 финансовых таблицах для
crm_admin_user (только soft markers)
- app/database/migrations: удалены 3 default Laravel (users/cache/jobs
дублировались с нашей schema), создан 0001_01_01_000000_load_initial_schema.php
- .squawk.toml: + excluded_paths для db/00_create_roles.sql (psql client-side
variables :'name' не парсятся libpg_query)
- docs/Открытые_вопросы_v8_3.md: v1.18 → v1.19, CTO-17 закрыт фиксом,
70 ✅ / 5 🟦 / 4 ⏸. Техдолг: ТЗ §15-16 webhook handler нужно обновить
под двустадийную dedup-логику
- CLAUDE.md: v1.9 → v1.10 (§0 ссылки на schema v8.6 + реестр v1.19;
§2 метрики БД 54→55/91→92/35→36/12→13/4→5; §6 фундамент развёрнут)
- cspell-words.txt: +9 новых терминов
Smoke-test через Boost MCP database-query:
- 68 таблиц (включая webhook_dedup_keys + 16 партиций)
- 36 RLS-policies
- 35 RLS-enabled (relkind='r'; +2 partitioned 'p' = 37 total)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 13:48:24 +03:00
Дмитрий
4d38f75826
phase1(scaffold): Laravel 11 + predis + .env под PG 16 + Memurai
...
Триггер фазы 1 запущен 08.05.2026 (вечер):
composer create-project laravel/laravel app
Стек подтверждён native (без Docker/WSL2/Sail):
- PostgreSQL 16.13 (Chocolatey, Windows-сервис, port 5432)
- Memurai Developer 4.1.8 (Redis 7-совм., port 6379) — TCP +PONG OK
- PHP 8.3.31 + 11/11 Laravel-required ext (pdo_pgsql, mbstring,
openssl, tokenizer, xml, ctype, json, bcmath, fileinfo, curl, pgsql)
- Composer 2.9.7
Что в коммите (59 файлов, 11059 строк скаффолда Laravel 11 +
правки):
- composer require predis/predis (v3.4.2) — PHP-only Redis-клиент,
т.к. php_redis ext не установлен (см. project_phase1_strategy.md)
- app/.env (gitignored) — APP_NAME=Liderra, APP_LOCALE=ru,
APP_TIMEZONE=Europe/Moscow, DB_CONNECTION=pgsql → liderra@localhost,
REDIS_CLIENT=predis
- app/.env.example — те же правки без секретов (для команды)
Smoke-test PG ↔ Laravel ↔ pdo_pgsql прошёл:
3/3 default-миграций → 9 таблиц в liderra (cache, cache_locks,
failed_jobs, job_batches, jobs, migrations, password_reset_tokens,
sessions, users).
Артефакт стартера app/database/database.sqlite (0 B) удалён —
sqlite не используется.
Что НЕ в этом коммите (следующие шаги фазы 1):
- Pest 3 swap (CTO-12) — composer remove phpunit + require pest
- Laravel Boost MCP + 9 guidelines disable по CLAUDE.md §5/§7
- Pint, Larastan, Roave/SecurityAdvisories, IDE Helper, squawk,
pgFormatter (Прил. Н #11–18)
- resources/boost/guidelines/vuetify.blade.php
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 09:37:16 +03:00