← back to Ventura Corridor
db/migrations/010_response_tracking.sql
61 lines
-- Migration 010 — pitch response & outcome tracking
-- Once Steve walks the corridor with /walk-route.html or DMs via /linkedin.html,
-- replies/wins/losses need a structured DB home. Free-form `notes` already exists,
-- but the pipeline needs queryable response text + reply channel + win amount
-- + structured loss reasons + scheduled follow-up.
ALTER TABLE pitches
ADD COLUMN IF NOT EXISTS outreach_channel TEXT,
ADD COLUMN IF NOT EXISTS dw_proximity TEXT,
ADD COLUMN IF NOT EXISTS contact_name TEXT,
ADD COLUMN IF NOT EXISTS email TEXT,
ADD COLUMN IF NOT EXISTS phone TEXT,
ADD COLUMN IF NOT EXISTS linkedin TEXT,
ADD COLUMN IF NOT EXISTS reply_text TEXT,
ADD COLUMN IF NOT EXISTS reply_channel TEXT,
ADD COLUMN IF NOT EXISTS won_value_usd NUMERIC(10,2),
ADD COLUMN IF NOT EXISTS lost_reason TEXT,
ADD COLUMN IF NOT EXISTS next_followup_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS followup_notes TEXT;
CREATE INDEX IF NOT EXISTS idx_pitches_replied_at ON pitches (replied_at) WHERE replied_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_pitches_followup_at ON pitches (next_followup_at) WHERE next_followup_at IS NOT NULL AND closed_at IS NULL;
-- View: response funnel by channel
CREATE OR REPLACE VIEW v_response_funnel AS
SELECT
COALESCE(outreach_channel, 'unspecified') AS channel,
count(*) FILTER (WHERE sent_at IS NOT NULL) AS sent,
count(*) FILTER (WHERE replied_at IS NOT NULL) AS replied,
count(*) FILTER (WHERE status = 'won') AS won,
count(*) FILTER (WHERE status = 'lost') AS lost,
COALESCE(SUM(won_value_usd) FILTER (WHERE status = 'won'), 0) AS won_value_usd,
ROUND(
100.0 * count(*) FILTER (WHERE replied_at IS NOT NULL)
/ NULLIF(count(*) FILTER (WHERE sent_at IS NOT NULL), 0),
1
) AS reply_rate_pct
FROM pitches
WHERE sent_at IS NOT NULL
GROUP BY outreach_channel
ORDER BY sent DESC;
-- View: today's follow-ups (and any overdue)
CREATE OR REPLACE VIEW v_followups_due AS
SELECT
p.id, p.business_id, b.name, b.address, b.city, p.pitch_type, p.status,
p.outreach_channel, p.next_followup_at, p.followup_notes, p.replied_at,
p.contact_name, p.email, p.phone, p.linkedin,
CASE
WHEN p.next_followup_at::date < CURRENT_DATE THEN 'overdue'
WHEN p.next_followup_at::date = CURRENT_DATE THEN 'today'
ELSE 'upcoming'
END AS due_bucket,
(CURRENT_DATE - p.next_followup_at::date) AS days_overdue
FROM pitches p
JOIN businesses b ON b.id = p.business_id
WHERE p.next_followup_at IS NOT NULL
AND p.closed_at IS NULL
AND p.status NOT IN ('won','lost','skip')
ORDER BY p.next_followup_at ASC NULLS LAST;