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.
108 lines
5.5 KiB
SQL
108 lines
5.5 KiB
SQL
-- 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.';
|