diff --git a/app/app/Models/SupplierLeadDelivery.php b/app/app/Models/SupplierLeadDelivery.php new file mode 100644 index 00000000..5011de6c --- /dev/null +++ b/app/app/Models/SupplierLeadDelivery.php @@ -0,0 +1,27 @@ +pluck('column_name')->all(); + expect($cols)->toContain('supplier_lead_id') + ->toContain('tenant_id') + ->toContain('deal_id') + ->toContain('created_at'); + + $pk = collect(DB::select( + "SELECT a.attname FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = 'supplier_lead_deliveries'::regclass AND i.indisprimary" + ))->pluck('attname')->sort()->values()->all(); + expect($pk)->toBe(['supplier_lead_id', 'tenant_id']); + + $rls = DB::selectOne( + "SELECT relrowsecurity FROM pg_class WHERE relname = 'supplier_lead_deliveries'" + ); + expect($rls->relrowsecurity)->toBeTrue(); +}); diff --git a/db/CHANGELOG_schema.md b/db/CHANGELOG_schema.md index 2de61200..f7469a6d 100644 --- a/db/CHANGELOG_schema.md +++ b/db/CHANGELOG_schema.md @@ -1,8 +1,13 @@ # CHANGELOG schema.sql — Лидерра -**Назначение:** консолидированный журнал изменений `schema.sql`. Содержит двадцать девять записей в обратном хронологическом порядке (v8.32 → v8.31 → v8.30 → v8.29 → v8.28 → v8.27 → v8.26 → v8.25 → v8.24 → v8.23 → v8.22 → v8.21 → v8.20 → v8.19 → v8.18 → v8.17 → v8.16 → v8.15 → v8.14 → v8.13 → v8.12 → v8.11 → v8.10 → v8.9 → v8.8 → v8.7 → v8.6 → v8.5 → v8.4 → v8.3 → v8.2), как принято в keep-a-changelog. +**Назначение:** консолидированный журнал изменений `schema.sql`. Содержит тридцать записей в обратном хронологическом порядке (v8.33 → v8.32 → v8.31 → v8.30 → v8.29 → v8.28 → v8.27 → v8.26 → v8.25 → v8.24 → v8.23 → v8.22 → v8.21 → v8.20 → v8.19 → v8.18 → v8.17 → v8.16 → v8.15 → v8.14 → v8.13 → v8.12 → v8.11 → v8.10 → v8.9 → v8.8 → v8.7 → v8.6 → v8.5 → v8.4 → v8.3 → v8.2), как принято в keep-a-changelog. -**Файл схемы:** `schema.sql` (текущая версия — v8.32, консолидированная — разворачивает БД с нуля). +**Файл схемы:** `schema.sql` (текущая версия — v8.33, консолидированная — разворачивает БД с нуля). + +## v8.33 (2026-05-23) — Billing v2 Spec B: политика дублей (Phase 1) + +- **+таблица `supplier_lead_deliveries`** (PK `supplier_lead_id`+`tenant_id`, FK на `supplier_leads` ON DELETE CASCADE, `deal_id` без FK — `deals` партиционирована, RLS `tenant_isolation`). Замок «одна поставка одному клиенту = один оплаченный лид» для шеринг-пути (`RouteSupplierLeadJob`). INSERT-логика будет добавлена в следующем коммите. +- Метрики: +1 таблица, +1 RLS-политика. (Сверять с header `db/schema.sql`.) **История записей:** diff --git a/db/migrations/2026_05_23_200_supplier_lead_deliveries.sql b/db/migrations/2026_05_23_200_supplier_lead_deliveries.sql new file mode 100644 index 00000000..a9d20a0c --- /dev/null +++ b/db/migrations/2026_05_23_200_supplier_lead_deliveries.sql @@ -0,0 +1,18 @@ +-- ============================================================================= +-- supplier_lead_deliveries — замок «одна поставка одному клиенту = один раз» +-- (Billing v2 Spec B). Ключ по поставке (supplier_lead_id), НЕ по телефону — +-- разные поставки с одним телефоном остаются отдельными платными лидами. +-- Защищает шеринг-путь (RouteSupplierLeadJob) от наших собственных дублей +-- при гонках / перезапусках задачи / CSV-восстановлении. +-- ============================================================================= +CREATE TABLE supplier_lead_deliveries ( + supplier_lead_id BIGINT NOT NULL REFERENCES supplier_leads(id) ON DELETE CASCADE, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + deal_id BIGINT, -- созданная сделка; без FK (deals партиционирована) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (supplier_lead_id, tenant_id) +); + +ALTER TABLE supplier_lead_deliveries ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation ON supplier_lead_deliveries + USING (tenant_id = current_setting('app.current_tenant_id')::bigint); diff --git a/db/schema.sql b/db/schema.sql index de8f4dfe..5bfbc4b6 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,12 +1,12 @@ -- ============================================================================= -- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра») --- Версия: v8.32 (23.05.2026 — balance_transactions.type +'migration' для Billing v2 Spec A конвертации balance_leads → balance_rub) +-- Версия: v8.33 (23.05.2026 — Billing v2 Spec B: +supplier_lead_deliveries замок поставка↔клиент) -- Базовая версия: v8.31 (23.05.2026 — партиционирование 7 audit-таблиц помесячно (hole #2): auth_log / activity_log / tenant_operations_log / webhook_log / balance_transactions / pd_processing_log / saas_admin_audit_log; PK → (id, created_at|received_at); FK на webhook_log удалены (W1); retention defaults в system_settings) -- Базовая версия: v8.30 (23.05.2026 — scheduler_heartbeats: пульс планировщика, SaaS-level без RLS, 11 cron-задач, hole #6) -- Базовая версия: v8.29 (22.05.2026 — webhook_log: supplier audit columns) -- Базовая версия: v8.28 (22.05.2026 — tenant_operations_log: журнал тенант-уровневых операций вне сделок (проекты, API-ключи, webhook URL), append-only hash-chain, P2 operational journaling closure) -- Базовая версия: v8.27 (21.05.2026 — drop projects.archived_at: feature архива заменена настоящим удалением с защитой по сделкам (ProjectService::delete())) --- Метрики: 74 базовые таблицы (65 regular + 9 partitioned parents: deals + supplier_lead_costs + 7 audit) + 12 партиций / 125 индексов / 41 RLS-политика / 5 функций / 15 триггеров +-- Метрики: 75 базовые таблицы (66 regular + 9 partitioned parents: deals + supplier_lead_costs + 7 audit) + 12 партиций / 125 индексов / 42 RLS-политика / 5 функций / 15 триггеров -- Базовая версия: v8.25 (19.05.2026 — supplier_manual_sync_queue: SaaS-level Tier 3 очередь резерва канала миграции проектов) -- Базовая версия: v8.24 (18.05.2026 — supplier_leads.vid → nullable для CSV-recovered лидов (Путь 2)) -- Базовая версия: v8.20 (11.05.2026 — Plan 5 frontend projects UI: projects.archived_at TIMESTAMPTZ NULL для soft archive flow; tenants.limits JSONB NOT NULL DEFAULT '{}' для per-tenant project/user лимитов) @@ -2046,6 +2046,25 @@ CREATE INDEX supplier_leads_recovered_from_csv_partial -- -- REVOKE ALL ON supplier_leads FROM crm_app_user; +-- ============================================================================= +-- supplier_lead_deliveries — замок «одна поставка одному клиенту = один раз» +-- (Billing v2 Spec B). Ключ по поставке (supplier_lead_id), НЕ по телефону — +-- разные поставки с одним телефоном остаются отдельными платными лидами. +-- Защищает шеринг-путь (RouteSupplierLeadJob) от наших собственных дублей +-- при гонках / перезапусках задачи / CSV-восстановлении. +-- ============================================================================= +CREATE TABLE supplier_lead_deliveries ( + supplier_lead_id BIGINT NOT NULL REFERENCES supplier_leads(id) ON DELETE CASCADE, + tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + deal_id BIGINT, -- созданная сделка; без FK (deals партиционирована) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (supplier_lead_id, tenant_id) +); + +ALTER TABLE supplier_lead_deliveries ENABLE ROW LEVEL SECURITY; +CREATE POLICY tenant_isolation ON supplier_lead_deliveries + USING (tenant_id = current_setting('app.current_tenant_id')::bigint); + -- ============================================================================= -- 7. БИЛЛИНГ (SAAS-уровень)