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
@@ -24,7 +24,7 @@ return new class extends Migration
DB::unprepared(<<<'SQL'
-- 1. phone_ranges_imports (журнал импортов; на него FK из phone_ranges, создаём первым)
CREATE TABLE phone_ranges_imports (
CREATE TABLE IF NOT EXISTS phone_ranges_imports (
id BIGSERIAL PRIMARY KEY,
imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
source_url TEXT NOT NULL,
@@ -40,7 +40,7 @@ return new class extends Migration
'Журнал импортов реестра Россвязи (idempotency по checksum_sha256, atomic-swap откат).';
-- 2. phone_ranges (реестр диапазонов Россвязи; SaaS-level, без RLS публичные данные)
CREATE TABLE phone_ranges (
CREATE TABLE IF NOT EXISTS phone_ranges (
id BIGSERIAL PRIMARY KEY,
def_code SMALLINT NOT NULL,
from_num BIGINT NOT NULL,
@@ -55,14 +55,14 @@ return new class extends Migration
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);
CREATE INDEX IF NOT EXISTS 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;
-- 3. lead_region_resolution_log (SaaS-level, партиционирован по received_at, паттерн activity_log)
CREATE TABLE lead_region_resolution_log (
CREATE TABLE IF NOT EXISTS lead_region_resolution_log (
id BIGSERIAL,
supplier_lead_id BIGINT NOT NULL,
received_at TIMESTAMPTZ NOT NULL,
@@ -89,8 +89,8 @@ return new class extends Migration
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);
CREATE INDEX IF NOT EXISTS idx_lrrl_lead_id ON lead_region_resolution_log (supplier_lead_id);
CREATE INDEX IF NOT EXISTS idx_lrrl_source ON lead_region_resolution_log (region_source, received_at);
COMMENT ON TABLE lead_region_resolution_log IS
'Аудит каждого резолва региона лида (источник, qc, оператор, шаг каскада). Партиции помесячно по received_at (MonthlyPartitionManager).';
@@ -98,26 +98,26 @@ return new class extends Migration
GRANT SELECT ON lead_region_resolution_log TO crm_app_user;
-- Стартовые партиции (далее их подхватывает partitions:create-months после Task 1.2).
CREATE TABLE lead_region_resolution_log_y2026_m05
CREATE TABLE IF NOT EXISTS 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
CREATE TABLE IF NOT EXISTS lead_region_resolution_log_y2026_m06
PARTITION OF lead_region_resolution_log
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- 4. supplier_leads: +4 колонки (denormalized display + persistent idempotency для retry).
ALTER TABLE supplier_leads
ADD COLUMN resolved_subject_code SMALLINT
ADD COLUMN IF NOT EXISTS resolved_subject_code SMALLINT
CHECK (resolved_subject_code IS NULL OR resolved_subject_code BETWEEN 1 AND 89),
ADD COLUMN region_source TEXT
ADD COLUMN IF NOT EXISTS region_source TEXT
CHECK (region_source IN ('dadata','rossvyaz','tag','unknown')),
ADD COLUMN dadata_qc SMALLINT,
ADD COLUMN phone_operator TEXT;
ADD COLUMN IF NOT EXISTS dadata_qc SMALLINT,
ADD COLUMN IF NOT EXISTS phone_operator TEXT;
-- 5. deals: +2 колонки (UI-карточка + флаг подмены региона).
ALTER TABLE deals
ADD COLUMN phone_operator TEXT,
ADD COLUMN region_substituted BOOLEAN NOT NULL DEFAULT FALSE;
ADD COLUMN IF NOT EXISTS phone_operator TEXT,
ADD COLUMN IF NOT EXISTS region_substituted BOOLEAN NOT NULL DEFAULT FALSE;
SQL);
// Регистрация retention для lead_region_resolution_log (system_settings, 12 месяцев ≈ 365 дней).
+29 -1
View File
@@ -2,7 +2,35 @@
**Назначение:** консолидированный журнал изменений `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.41, консолидированная — разворачивает БД с нуля).
**Файл схемы:** `schema.sql` (текущая версия — v8.46, консолидированная — разворачивает БД с нуля).
## v8.46 (2026-06-19) — Sync дрейфа: слой lead-region в schema.sql
Добавлен слой lead-region, отсутствовавший в `schema.sql` (на проде живёт с 31.05.2026,
DDL держался только в дельта-миграции `2026_05_31_100000` — см. v8.40, где это сделано
осознанно «как v8.39»). Реконсиляция дрейфа: теперь объекты есть в теле канонической
схемы, а миграция guard-нута (`IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`) и под
`migrate:fresh` (schema.sql уже содержит объекты) становится no-op.
Источник: миграция `app/database/migrations/2026_05_31_100000_create_phone_ranges_and_resolution_log.php`.
**Добавлено в schema.sql:**
- **`phone_ranges_imports`** — журнал импортов реестра Россвязи (regular, без RLS).
- **`phone_ranges`** — реестр диапазонов нумерации Россвязи (regular, без RLS) +
индекс `idx_phone_ranges_lookup` + `GRANT SELECT` для `crm_app_user`/`crm_supplier_worker`.
- **`lead_region_resolution_log`** — партиционированный (`PARTITION BY RANGE (received_at)`,
composite PK `(id, received_at)`) аудит резолва региона лида + индексы `idx_lrrl_lead_id`,
`idx_lrrl_source` + 2 стартовые партиции `_y2026_m05` / `_y2026_m06` +
`GRANT SELECT, INSERT` для `crm_supplier_worker`, `GRANT SELECT` для `crm_app_user`.
- **`supplier_leads` +4 колонки:** `resolved_subject_code` (CHECK 1..89), `region_source`
(CHECK enum dadata/rossvyaz/tag/unknown), `dadata_qc`, `phone_operator`.
- **`deals` +2 колонки:** `phone_operator`, `region_substituted` (NOT NULL DEFAULT FALSE).
NB: обе отсутствовали в теле schema.sql (не только `phone_operator`).
**Счётчики:** таблиц `72 → 75` (regular `64 → 66`, partitioned parents `8 → 9`) /
партиций `12 → 14` / индексов `117 → 120`. RLS-политики / функции / триггеры — без
изменений (lead-region — SaaS-level, без RLS).
## v8.45 (2026-06-19) — G3: удалена таблица reminders
+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), НЕ по телефону —