gendesign/data/sql/52_schema_prinzip_crm.sql
lekss361 522ecca5c4 ship #13-16: multi-target Dockerfile, per-object drill-in, CRM pipeline,
442-sweep instructions
#16 Dockerfile: runner (lean, 1.6 GB) + runner-with-chromium (2.9 GB).
   Compose + deploy.yml pushes 2 GHCR images. Backend saves 1.3 GB.
#15 5 backend object endpoints + /analytics/objects/[id] page (gallery,
   POI table, sale chart) + sparkline-driven PrinzipObjectsTable.
#14 prinzip_leads/deals + 2 MVs. Import script with PII-hashing &
   --inspect-only mode. 3 funnel endpoints + Sankey/Monthly charts.
#13 README section with UI/CLI/SQL for 442-object sweep.
2026-04-27 19:32:37 +03:00

108 lines
5.5 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- PRINZIP CRM — заявки и сделки за 8 лет (102K leads / 9K deals / 39.9 млрд ₽).
-- Источник: SQLite-дамп analytics.prinzipevent.ru (49 МБ).
-- Импорт: data/sql/52_import_prinzip_crm.py --sqlite data/raw/prinzip_crm.sqlite.
-- См. план C:\Users\user\.claude\plans\crispy-waddling-swing.md (#14).
-- ── Заявки (~102K) ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS prinzip_leads (
lead_id BIGINT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
source TEXT, -- web/call/cian/avito/...
channel TEXT, -- ad campaign / utm
name TEXT,
phone_hash TEXT, -- SHA256(phone)[:16] для PII compliance
phone_last4 TEXT, -- последние 4 цифры для UI
email_hash TEXT, -- md5(email)
obj_interest BIGINT, -- soft FK → domrf_kn_objects.obj_id
rooms INT,
budget_min NUMERIC,
budget_max NUMERIC,
status TEXT, -- new/contacted/qualified/lost/converted
converted BOOLEAN DEFAULT FALSE,
deal_id BIGINT -- soft FK → prinzip_deals (если converted)
);
CREATE INDEX IF NOT EXISTS idx_leads_created ON prinzip_leads (created_at);
CREATE INDEX IF NOT EXISTS idx_leads_status ON prinzip_leads (status, created_at);
CREATE INDEX IF NOT EXISTS idx_leads_obj ON prinzip_leads (obj_interest);
CREATE INDEX IF NOT EXISTS idx_leads_source ON prinzip_leads (source);
-- ── Сделки (~9K, sum revenue 39.9 млрд ₽) ──────────────────────────────────
CREATE TABLE IF NOT EXISTS prinzip_deals (
deal_id BIGINT PRIMARY KEY,
lead_id BIGINT, -- soft FK → prinzip_leads
closed_at TIMESTAMPTZ NOT NULL,
obj_id BIGINT, -- soft FK → domrf_kn_objects
flat_ods_id TEXT, -- match с domrf_kn_flats.ods_id
rooms INT,
total_area NUMERIC,
deal_price NUMERIC, -- ₽
price_per_m2 NUMERIC,
payment_type TEXT, -- cash/mortgage/escrow/installment
mortgage_bank TEXT,
mortgage_rate NUMERIC,
deal_stage TEXT -- reservation/dku/contract/done
);
CREATE INDEX IF NOT EXISTS idx_deals_closed ON prinzip_deals (closed_at);
CREATE INDEX IF NOT EXISTS idx_deals_obj ON prinzip_deals (obj_id);
CREATE INDEX IF NOT EXISTS idx_deals_lead ON prinzip_deals (lead_id);
-- ── Воронка по месяцам — denormalized aggregate (для UI чартов) ────────────
-- REFRESH MATERIALIZED VIEW prinzip_funnel_monthly после каждого импорта.
DROP MATERIALIZED VIEW IF EXISTS prinzip_funnel_monthly;
CREATE MATERIALIZED VIEW prinzip_funnel_monthly AS
SELECT
DATE_TRUNC('month', created_at)::date AS month,
COALESCE(source, 'unknown') AS source,
COUNT(*) AS leads,
COUNT(*) FILTER (WHERE status IN ('qualified', 'contacted', 'converted')) AS engaged,
COUNT(*) FILTER (WHERE converted) AS converted,
ROUND(100.0 * COUNT(*) FILTER (WHERE converted) / NULLIF(COUNT(*), 0), 2) AS conv_pct
FROM prinzip_leads
GROUP BY 1, 2
ORDER BY 1 DESC, 2;
CREATE UNIQUE INDEX IF NOT EXISTS idx_funnel_monthly_pk
ON prinzip_funnel_monthly (month, source);
-- ── Воронка по ЖК (агрегат) ────────────────────────────────────────────────
DROP MATERIALIZED VIEW IF EXISTS prinzip_funnel_by_object;
CREATE MATERIALIZED VIEW prinzip_funnel_by_object AS
WITH leads_by_obj AS (
SELECT obj_interest AS obj_id,
COUNT(*) AS leads_count
FROM prinzip_leads
WHERE obj_interest IS NOT NULL
GROUP BY obj_interest
),
deals_by_obj AS (
SELECT obj_id,
COUNT(*) AS deals_count,
SUM(deal_price) AS total_revenue,
AVG(deal_price) AS avg_deal_price
FROM prinzip_deals
WHERE obj_id IS NOT NULL
GROUP BY obj_id
)
SELECT
COALESCE(l.obj_id, d.obj_id) AS obj_id,
o.comm_name,
COALESCE(l.leads_count, 0) AS leads_count,
COALESCE(d.deals_count, 0) AS deals_count,
ROUND(100.0 * d.deals_count / NULLIF(l.leads_count, 0), 2) AS conv_pct,
d.total_revenue,
d.avg_deal_price
FROM leads_by_obj l
FULL OUTER JOIN deals_by_obj d ON d.obj_id = l.obj_id
LEFT JOIN domrf_kn_objects o
ON o.obj_id = COALESCE(l.obj_id, d.obj_id)
AND o.snapshot_date = (SELECT MAX(snapshot_date) FROM domrf_kn_objects);
CREATE UNIQUE INDEX IF NOT EXISTS idx_funnel_by_obj_pk
ON prinzip_funnel_by_object (obj_id);
COMMENT ON TABLE prinzip_leads IS
'PRINZIP CRM заявки. PII (phone/email) хешированы при импорте.';
COMMENT ON TABLE prinzip_deals IS
'PRINZIP CRM сделки. Sum deal_price ≈ 39.9 млрд ₽ за 8 лет.';
COMMENT ON MATERIALIZED VIEW prinzip_funnel_monthly IS
'Воронка leads → engaged → converted по месяцам и source. REFRESH после import.';
COMMENT ON MATERIALIZED VIEW prinzip_funnel_by_object IS
'Conversion per ЖК: leads / deals / revenue / avg deal_price.';