fix(schema): синк дрейфа lead-region в schema.sql + guard миграции 05_31 (v8.46)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-19 12:55:04 +03:00
parent ef28196082
commit 1cb00e149c
3 changed files with 148 additions and 17 deletions
+105 -2
View File
@@ -1,6 +1,7 @@
-- =============================================================================
-- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра»)
-- Версия: v8.45 (19.06.2026 — G3: DROP таблицы reminders (фича «Напоминания» снята с продукта). Удалены: таблица reminders, 4 индекса (idx_reminders_due/deal/tenant_user_active/tenant_active), RLS-политика tenant_isolation, COMMENT'ы; ключ "reminder" убран из DEFAULT users.notification_preferences. Счётчики: таблиц 73→72 (regular 65→64) / индексов 121→117 / RLS-политик 40→39. Миграция 2026_06_19_130000_drop_reminders_table)
-- Версия: v8.46 (19.06.2026 — Sync дрейфа lead-region: слой миграции 2026_05_31_100000 (live на проде с 31.05) добавлен в тело schema.sql, отсутствовал ранее (v8.40 держал DDL только в дельта-миграции). Добавлены таблицы phone_ranges_imports, phone_ranges (+idx_phone_ranges_lookup, GRANT) — regular; lead_region_resolution_log — partitioned-parent (PARTITION BY received_at, +idx_lrrl_lead_id/idx_lrrl_source, +2 стартовые партиции m05/m06, GRANT). +4 колонки supplier_leads (resolved_subject_code/region_source/dadata_qc/phone_operator), +2 колонки deals (phone_operator/region_substituted). Счётчики: таблиц 72→75 (regular 64→66, partitioned parents 8→9) / партиций 12→14 / индексов 117→120. Миграция 2026_05_31_100000 (теперь guard-нута под migrate:fresh))
-- Базовая версия: v8.45 (19.06.2026 — G3: DROP таблицы reminders (фича «Напоминания» снята с продукта). Удалены: таблица reminders, 4 индекса (idx_reminders_due/deal/tenant_user_active/tenant_active), RLS-политика tenant_isolation, COMMENT'ы; ключ "reminder" убран из DEFAULT users.notification_preferences. Счётчики: таблиц 73→72 (regular 65→64) / индексов 121→117 / RLS-политик 40→39. Миграция 2026_06_19_130000_drop_reminders_table)
-- Базовая версия: v8.44 (19.06.2026 — G2-B: users.notification_preferences DEFAULT new_lead.email false→true (почтовый дайджест новых сделок по умолчанию; существующие пользователи дотягиваются UPDATE false→true, только живые). Структурно без изменений — правка дефолтного значения; счётчики таблиц/индексов/RLS/функций/триггеров без изменений. Миграция 2026_06_19_120000, спека docs/superpowers/specs/2026-06-19-g2b-new-lead-email-default-on-design.md)
-- Базовая версия: v8.43 (18.06.2026 — G1/SP2 реквизиты клиента: tenant_requisites (1:1 tenants), RLS tenant isolation, лёгкие поля (тип лица/контакт/телефон) гейтят первый проект; тяжёлые (ИНН/КПП/ОГРН/юр.адрес/банк) nullable. Миграция 2026_06_18_140000, спека docs/superpowers/specs/2026-06-18-g1-sp2-requisites-gate-spec-v1.md)
-- Базовая версия: v8.42 (18.06.2026 — G1/SP1 самозапись клиента: email_verifications +code_hash +failed_attempts (6-значный код подтверждения почты, bcrypt, TTL 15м, 5 попыток) + tenants.status расширен VARCHAR(20)→(30) (латентный дефект: CHECK допускал 21-символьное 'pending_email_confirm', колонка была 20). Счётчики таблиц/индексов/RLS/функций/триггеров без изменений. Миграция 2026_06_18_120000, спека docs/superpowers/specs/2026-06-18-g1-sp1-self-registration-email-spec-v3.md)
@@ -17,7 +18,7 @@
-- Базовая версия: 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()))
-- Метрики: 72 базовые таблицы (64 regular + 8 partitioned parents: deals + supplier_lead_costs + 6 audit) + 12 партиций / 117 индексов / 39 RLS-политик / 5 функций / 15 триггеров
-- Метрики: 75 базовых таблиц (66 regular + 9 partitioned parents: deals + supplier_lead_costs + 6 audit + lead_region_resolution_log) + 14 партиций / 120 индексов / 39 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 лимитов)
@@ -1711,6 +1712,10 @@ CREATE TABLE deals (
-- enrichment-сервиса). Используется для filter в §10.3 + аналитики §12.
region_code VARCHAR(8),
subject_code SMALLINT, -- v8.26: субъект РФ 1..89 из тега поставщика (raw_payload[tag]); NULL = вся РФ/неизвестно
-- v8.40 (lead region resolution): оператор связи (UI-карточка) + флаг подмены
-- региона каскадом DaData→Россвязь→tag (миграция 2026_05_31_100000).
phone_operator TEXT,
region_substituted BOOLEAN NOT NULL DEFAULT FALSE,
city VARCHAR(100),
-- v8.5 (Биз-22): простая lead scoring модель без ML.
-- time_in_form_seconds — сколько секунд физлицо заполняло форму
@@ -1996,6 +2001,14 @@ CREATE TABLE supplier_leads (
recovered_from_csv_at TIMESTAMPTZ,
deals_created_count INTEGER,
error TEXT,
-- v8.40 (lead region resolution): денормализованный display + persistent
-- idempotency для retry резолва региона лида (миграция 2026_05_31_100000).
resolved_subject_code SMALLINT
CHECK (resolved_subject_code IS NULL OR resolved_subject_code BETWEEN 1 AND 89),
region_source TEXT
CHECK (region_source IN ('dadata','rossvyaz','tag','unknown')),
dadata_qc SMALLINT,
phone_operator TEXT,
CONSTRAINT chk_supplier_leads_platform
CHECK (platform IN ('B1','B2','B3','DIRECT')),
@@ -2018,6 +2031,96 @@ CREATE INDEX supplier_leads_recovered_from_csv_partial
--
-- REVOKE ALL ON supplier_leads FROM crm_app_user;
-- =============================================================================
-- Lead-region (телефон → регион) — реестр Россвязи + аудит резолва
-- v8.40 (31.05.2026), миграция 2026_05_31_100000. SaaS-level (без RLS):
-- phone_ranges/phone_ranges_imports — публичные данные; lead_region_resolution_log
-- — партиционированный аудит (паттерн activity_log). Порядок: imports → phone_ranges
-- (FK на imports) → lead_region_resolution_log.
-- =============================================================================
-- phone_ranges_imports (журнал импортов; на него FK из phone_ranges, создаётся первым)
CREATE TABLE phone_ranges_imports (
id BIGSERIAL PRIMARY KEY,
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
source_url TEXT NOT NULL,
rows_inserted INTEGER NOT NULL DEFAULT 0,
rows_updated INTEGER NOT NULL DEFAULT 0,
checksum_sha256 TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'in_progress'
CHECK (status IN ('in_progress','completed','failed','rolled_back')),
error TEXT,
completed_at TIMESTAMPTZ
);
COMMENT ON TABLE phone_ranges_imports IS
'Журнал импортов реестра Россвязи (idempotency по checksum_sha256, atomic-swap откат).';
-- phone_ranges (реестр диапазонов Россвязи; SaaS-level, без RLS — публичные данные)
CREATE TABLE phone_ranges (
id BIGSERIAL PRIMARY KEY,
def_code SMALLINT NOT NULL,
from_num BIGINT NOT NULL,
to_num BIGINT NOT NULL,
operator TEXT NOT NULL,
region TEXT NOT NULL,
region_normalized TEXT,
subject_code SMALLINT,
imported_at TIMESTAMPTZ NOT NULL,
import_id BIGINT NOT NULL REFERENCES phone_ranges_imports(id),
CONSTRAINT chk_phone_ranges_def_code CHECK (def_code BETWEEN 300 AND 999),
CONSTRAINT chk_phone_ranges_subject_code CHECK (subject_code IS NULL OR subject_code BETWEEN 1 AND 89),
CONSTRAINT chk_phone_ranges_range_valid CHECK (from_num <= to_num)
);
CREATE INDEX idx_phone_ranges_lookup ON phone_ranges (def_code, from_num, to_num);
COMMENT ON TABLE phone_ranges IS
'Реестр диапазонов нумерации Россвязи (rossvyaz.gov.ru). Локальный fallback для LeadRegionResolver. Обновляется ежемесячным cron-импортом.';
GRANT SELECT ON phone_ranges, phone_ranges_imports TO crm_app_user, crm_supplier_worker;
-- lead_region_resolution_log (SaaS-level, партиционирован по received_at, паттерн activity_log)
CREATE TABLE lead_region_resolution_log (
id BIGSERIAL,
supplier_lead_id BIGINT NOT NULL,
received_at TIMESTAMPTZ NOT NULL,
phone_masked TEXT NOT NULL,
subject_code_resolved SMALLINT,
subject_code_from_tag SMALLINT,
region_source TEXT NOT NULL
CHECK (region_source IN ('dadata','rossvyaz','tag','unknown')),
dadata_qc SMALLINT,
dadata_provider TEXT,
dadata_type TEXT,
dadata_response_masked JSONB,
rossvyaz_matched BOOLEAN NOT NULL DEFAULT FALSE,
actual_subject_code SMALLINT
CHECK (actual_subject_code IS NULL OR actual_subject_code BETWEEN 1 AND 89),
substituted_subject_code SMALLINT
CHECK (substituted_subject_code IS NULL OR substituted_subject_code BETWEEN 1 AND 89),
routing_step SMALLINT
CHECK (routing_step IS NULL OR routing_step BETWEEN 1 AND 3),
phone_operator TEXT,
cache_hit BOOLEAN NOT NULL DEFAULT FALSE,
duration_ms INTEGER,
resolved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, received_at)
) PARTITION BY RANGE (received_at);
CREATE INDEX idx_lrrl_lead_id ON lead_region_resolution_log (supplier_lead_id);
CREATE INDEX idx_lrrl_source ON lead_region_resolution_log (region_source, received_at);
COMMENT ON TABLE lead_region_resolution_log IS
'Аудит каждого резолва региона лида (источник, qc, оператор, шаг каскада). Партиции помесячно по received_at (MonthlyPartitionManager).';
GRANT SELECT, INSERT ON lead_region_resolution_log TO crm_supplier_worker;
GRANT SELECT ON lead_region_resolution_log TO crm_app_user;
-- Стартовые партиции (далее их подхватывает partitions:create-months / MonthlyPartitionManager).
CREATE TABLE lead_region_resolution_log_y2026_m05
PARTITION OF lead_region_resolution_log
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE lead_region_resolution_log_y2026_m06
PARTITION OF lead_region_resolution_log
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- =============================================================================
-- supplier_lead_deliveries — замок «одна поставка одному клиенту = один раз»
-- (Billing v2 Spec B). Ключ по поставке (supplier_lead_id), НЕ по телефону —