← back to NationalPaperHangers
db/migrations/023_measure_jobs.sql
53 lines
-- Council Idea #5 — Measure→Installer Lead Handoff (SAFE subset, BUNDLE-A).
-- Captures a structured installer-lead from the /measure result page when a
-- homeowner explicitly opts in ("Get this quoted by a local installer"), and
-- routes it to the single NEAREST CLAIMED installer who services that ZIP.
--
-- Why a DEDICATED table (not a reuse of installer_interest):
-- installer_interest has UNIQUE(installer_id, email) — an email-signup row
-- semantically — which structurally breaks one-email-many-jobs (a homeowner
-- can measure several projects). A job-spec is a different record than a
-- "notify me when live" email capture, and we want sqft/rolls queryable.
--
-- Channel (a) in-app ONLY at this stage: a routed row IS the installer's inbox.
-- The installer dashboard reads measure_jobs WHERE routed_to = <me>. NOTHING is
-- emailed/sent from here — outbound email (channel b) + the prod deploy are
-- separate Steve-gated steps (drafted to pending-approval).
--
-- Routing is claimed-only by design: pushing a lead to an UNCLAIMED (scraped)
-- installer would be unsolicited B2B contact = CAN-SPAM exposure. route_status
-- 'no_installer' is the graceful path when no claimed installer is in range —
-- the homeowner is shown the standard /find browse list instead of a dead end.
CREATE TABLE IF NOT EXISTS measure_jobs (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
zip TEXT,
sqft NUMERIC,
rolls INTEGER,
wall_count INTEGER,
material TEXT, -- product/material context from the form, if any
product_sku TEXT, -- when launched from a DW/wallco PDP deep-link
product_name TEXT,
customer_name TEXT,
customer_email TEXT,
customer_phone TEXT,
notes TEXT,
ip_hash TEXT, -- sha256 prefix, same pattern as installer_interest (never raw IP)
routed_to BIGINT REFERENCES installers(id) ON DELETE SET NULL, -- NULL until routed
routed_at TIMESTAMPTZ,
route_status TEXT NOT NULL DEFAULT 'captured'
CHECK (route_status IN ('captured','routed','delivered','declined','no_installer')),
-- in-app read receipt: set when the routed installer first views the lead in
-- their dashboard (channel a "delivery"). Distinct from any future email send.
viewed_at TIMESTAMPTZ
);
-- The installer's "inbox" query: their routed, not-yet-actioned leads, newest first.
CREATE INDEX IF NOT EXISTS measure_jobs_routed_to_idx
ON measure_jobs (routed_to, created_at DESC) WHERE routed_to IS NOT NULL;
-- Ops/funnel slicing by status + recency.
CREATE INDEX IF NOT EXISTS measure_jobs_status_idx
ON measure_jobs (route_status, created_at DESC);