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>
This commit is contained in:
Дмитрий
2026-05-16 17:01:51 +03:00
parent dfb25d0d79
commit fada3f348c
6 changed files with 182 additions and 11 deletions
+43 -3
View File
@@ -1,7 +1,8 @@
-- =============================================================================
-- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра»)
-- Версия: 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 лимитов)
-- Метрики: 63 базовые таблицы (61 regular + 2 partitioned parents: deals + supplier_lead_costs) + 12 партиций / 117 индексов / 39 RLS-политик / 5 функций / 13 триггеров
-- Версия: v8.21 (16.05.2026 — Sprint 4: import_unknown_statuses + import_log enrichment (+5 колонок))
-- Метрики: 64 базовые таблицы (62 regular + 2 partitioned parents: deals + supplier_lead_costs) + 12 партиций / 118 индексов / 40 RLS-политик / 5 функций / 13 триггеров
-- Базовая версия: 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 лимитов)
-- Базовая версия: v8.19 (11.05.2026 — Plan 4 billing+csv+admin: tenants.delivered_in_month, lead_charges.charge_source + CHECK, supplier_leads.recovered_from_csv_at, supplier_csv_reconcile_log)
-- Базовая версия: v8.18 (10.05.2026 — Plan 2/5 Task 1: supplier_leads SaaS-level + projects.delivered_today + 2 system_settings rows для supplier-webhook + IP allowlist defense-in-depth)
-- Базовая версия: v8.17 (10.05.2026 — Plan 1/5 Task 2 fix: FK projects.supplier_b{1,2,3}_project_id → supplier_projects (ON DELETE SET NULL) + 3 partial index + CHECK chk_projects_b1_not_for_sms (defense-in-depth дублирует chk_supplier_projects_b1_not_for_sms на Project-уровне). Закрывает code-review BLOCKER#1 + WARNING#3 от 10.05.2026 поздний вечер)
@@ -1489,9 +1490,38 @@ CREATE TABLE import_log (
CHECK (status IN ('pending','processing','done','failed')),
error_message TEXT,
started_at TIMESTAMPTZ DEFAULT NOW(),
finished_at TIMESTAMPTZ
finished_at TIMESTAMPTZ,
-- Sprint 4 (H2): enrichment-колонки для исторической миграции лидов (раздел 6.4)
entity_type VARCHAR(20) NOT NULL DEFAULT 'leads'
CHECK (entity_type IN ('leads','projects')),
source_system VARCHAR(50) NOT NULL DEFAULT 'crm.bp-gr.ru',
mapping_config JSONB,
unknown_statuses_count INT NOT NULL DEFAULT 0,
dry_run BOOLEAN NOT NULL DEFAULT FALSE
);
-- -----------------------------------------------------------------------------
-- import_unknown_statuses — tenant-level маппинг неизвестных статусов CSV (раздел 6.4)
-- Sprint 4 (H1): русский статус из CSV, не найденный в STATUS_RU_TO_SLUG, пишется сюда.
-- Wizard (§6.6) проставляет mapped_to_slug; повторный импорт применяет маппинг.
-- -----------------------------------------------------------------------------
CREATE TABLE import_unknown_statuses (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
import_log_id BIGINT REFERENCES import_log(id) ON DELETE SET NULL,
status_ru VARCHAR(100) NOT NULL,
occurrences INT NOT NULL DEFAULT 0,
mapped_to_slug VARCHAR(50) REFERENCES lead_statuses(slug),
resolved_at TIMESTAMPTZ,
resolved_by BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
UNIQUE (tenant_id, status_ru)
);
CREATE INDEX idx_import_unknown_statuses_unresolved
ON import_unknown_statuses (tenant_id) WHERE mapped_to_slug IS NULL;
-- =============================================================================
-- 5. DEALS — партиционированная по received_at (раздел 7.3)
@@ -2703,6 +2733,7 @@ ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
ALTER TABLE comment_templates ENABLE ROW LEVEL SECURITY;
ALTER TABLE deal_tags ENABLE ROW LEVEL SECURITY;
ALTER TABLE import_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE import_unknown_statuses ENABLE ROW LEVEL SECURITY;
ALTER TABLE activity_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE reminders ENABLE ROW LEVEL SECURITY;
ALTER TABLE webhook_log ENABLE ROW LEVEL SECURITY;
@@ -2743,6 +2774,7 @@ CREATE POLICY tenant_isolation ON push_subscriptions USING (tenant_id = cur
CREATE POLICY tenant_isolation ON comment_templates USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
CREATE POLICY tenant_isolation ON deal_tags USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
CREATE POLICY tenant_isolation ON import_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
CREATE POLICY tenant_isolation ON import_unknown_statuses USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
CREATE POLICY tenant_isolation ON activity_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
CREATE POLICY tenant_isolation ON reminders USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
CREATE POLICY tenant_isolation ON webhook_log USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
@@ -2870,6 +2902,14 @@ CREATE POLICY tenant_isolation ON auth_log USING (
-- USAGE на sequences соответствующих таблиц;
-- запрещено: SELECT, UPDATE, DELETE, TRUNCATE.
-- v8.21 (Sprint 4): import_unknown_statuses — tenant-level маппинг неизвестных статусов CSV.
-- RLS tenant_isolation активна. Права аналогичны import_log (tenant-таблица).
-- Деплой через db/02_grants.sql (явная секция v8.21).
--
-- GRANT SELECT, INSERT, UPDATE, DELETE ON import_unknown_statuses TO crm_app_user, crm_app_admin;
-- GRANT SELECT ON import_unknown_statuses TO crm_readonly;
-- GRANT USAGE, SELECT ON SEQUENCE import_unknown_statuses_id_seq TO crm_app_user, crm_app_admin;
-- v8.5 (OPEN-И-14): defense-in-depth — REVOKE ALL на 6 saas-таблицах
-- от crm_app_user. К этим таблицам tenant-приложение доступа НЕ должно
-- иметь даже теоретически (RLS + REVOKE = 2 барьера).