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>
This commit is contained in:
+60
-6
@@ -1,12 +1,13 @@
|
||||
-- =============================================================================
|
||||
-- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра»)
|
||||
-- Версия: v8.34 (23.05.2026 — Billing v2 Spec B: −индекс deals(duplicate_of_id) — телефонный дедуп удалён)
|
||||
-- Версия: v8.35 (24.05.2026 — Billing v2 Spec C: +balance_freeze_log / +tenants.frozen_by_balance_at / +projects.preflight_blocked_at)
|
||||
-- Базовая версия: v8.34 (23.05.2026 — Billing v2 Spec B: −индекс deals(duplicate_of_id) — телефонный дедуп удалён)
|
||||
-- Базовая версия: 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()))
|
||||
-- Метрики: 75 базовые таблицы (66 regular + 9 partitioned parents: deals + supplier_lead_costs + 7 audit) + 12 партиций / 125 индексов / 42 RLS-политика / 5 функций / 15 триггеров
|
||||
-- Метрики: 76 базовые таблицы (67 regular + 9 partitioned parents: deals + supplier_lead_costs + 7 audit) + 12 партиций / 127 индексов / 43 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 лимитов)
|
||||
@@ -674,12 +675,17 @@ CREATE TABLE tenants (
|
||||
-- Метаданные
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ,
|
||||
deleted_at TIMESTAMPTZ -- soft delete (раздел 4.5)
|
||||
deleted_at TIMESTAMPTZ, -- soft delete (раздел 4.5)
|
||||
-- v8.35 (Billing v2 Spec C): флаг заморозки по балансу.
|
||||
-- Не NULL = тенант заморожен (баланс < стоимость дня лидов). PreflightBalanceService.
|
||||
frozen_by_balance_at TIMESTAMPTZ NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tenants_subdomain ON tenants(subdomain) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_tenants_webhook_token ON tenants(webhook_token) WHERE deleted_at IS NULL AND status = 'active';
|
||||
CREATE INDEX idx_tenants_inactive ON tenants(last_activity_at) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_tenants_subdomain ON tenants(subdomain) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_tenants_webhook_token ON tenants(webhook_token) WHERE deleted_at IS NULL AND status = 'active';
|
||||
CREATE INDEX idx_tenants_inactive ON tenants(last_activity_at) WHERE deleted_at IS NULL;
|
||||
-- v8.35: частичный индекс для заморозки (sparse — большинство тенантов не заморожены)
|
||||
CREATE INDEX tenants_frozen_by_balance_idx ON tenants (frozen_by_balance_at) WHERE frozen_by_balance_at IS NOT NULL;
|
||||
|
||||
-- Forward FK на tenants для SaaS-админских таблиц, объявленных выше
|
||||
-- (saas_admin_sessions.impersonating_tenant_id — Ю-1; impersonation_tokens.tenant_id).
|
||||
@@ -845,6 +851,9 @@ CREATE TABLE projects (
|
||||
CHECK (ttfr_target_minutes BETWEEN 1 AND 1440),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ,
|
||||
-- v8.35 (Billing v2 Spec C): флаг точечной блокировки проекта по preflight.
|
||||
-- Не NULL = проект заблокирован (не хватает баланса на ближайшие N лидов).
|
||||
preflight_blocked_at TIMESTAMPTZ NULL,
|
||||
UNIQUE (tenant_id, name),
|
||||
CONSTRAINT chk_projects_daily_limit_positive
|
||||
CHECK (daily_limit_target > 0),
|
||||
@@ -876,6 +885,8 @@ CREATE INDEX idx_projects_tenant_signal
|
||||
ON projects(tenant_id, signal_type, signal_identifier);
|
||||
-- v8.20 (Plan 6): GIN-индекс для outbound regions queries.
|
||||
CREATE INDEX idx_projects_regions ON projects USING GIN (regions);
|
||||
-- v8.35: частичный индекс для preflight-блокировки (sparse — большинство проектов не заблокированы)
|
||||
CREATE INDEX projects_preflight_blocked_idx ON projects (preflight_blocked_at) WHERE preflight_blocked_at IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN projects.daily_limit_target IS
|
||||
'Целевой дневной лимит лидов, заданный клиентом. Фактический лимит на '
|
||||
@@ -3348,6 +3359,49 @@ ALTER TABLE lead_charges
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- balance_freeze_log — журнал заморозок/разморозок тенантов по балансу
|
||||
-- (Billing v2 Spec C, v8.35). INSERT-only: записи не обновляются.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS balance_freeze_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
event_type VARCHAR(30) NOT NULL,
|
||||
triggered_by VARCHAR(30) NOT NULL,
|
||||
balance_rub_at_event DECIMAL(12,2) NOT NULL,
|
||||
required_leads INTEGER NOT NULL,
|
||||
capacity_leads INTEGER NOT NULL,
|
||||
total_daily_limit INTEGER NOT NULL,
|
||||
details JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE balance_freeze_log ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON balance_freeze_log
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint);
|
||||
|
||||
CREATE INDEX balance_freeze_log_tenant_idx ON balance_freeze_log (tenant_id, created_at DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_app_user') THEN
|
||||
GRANT SELECT, INSERT ON balance_freeze_log TO crm_app_user;
|
||||
GRANT USAGE, SELECT ON SEQUENCE balance_freeze_log_id_seq TO crm_app_user;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_supplier_worker') THEN
|
||||
GRANT SELECT, INSERT ON balance_freeze_log TO crm_supplier_worker;
|
||||
GRANT USAGE, SELECT ON SEQUENCE balance_freeze_log_id_seq TO crm_supplier_worker;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_migrator') THEN
|
||||
GRANT SELECT, INSERT ON balance_freeze_log TO crm_migrator;
|
||||
GRANT USAGE, SELECT ON SEQUENCE balance_freeze_log_id_seq TO crm_migrator;
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_app_admin') THEN
|
||||
GRANT SELECT, INSERT ON balance_freeze_log TO crm_app_admin;
|
||||
GRANT USAGE, SELECT ON SEQUENCE balance_freeze_log_id_seq TO crm_app_admin;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =============================================================================
|
||||
-- КОНЕЦ schema.sql
|
||||
-- =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user