← back to Ventura Corridor

db/migrations/002_enrichment.sql

76 lines

-- Phase v: enrichment for "who to contact and how" analysis.
-- Two new tables: business_enrichment (1:1 with business) and business_contacts
-- (1:many — each named human associated with the business).

BEGIN;

-- ─── business_enrichment ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS business_enrichment (
  business_id        BIGINT PRIMARY KEY REFERENCES businesses(id) ON DELETE CASCADE,
  enriched_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  -- Ownership / structure
  ownership_class    TEXT,                  -- 'corporate' | 'franchise' | 'independent' | 'nonprofit' | 'unknown'
  parent_company     TEXT,                  -- e.g., "CVS Health Corporation"
  parent_brand       TEXT,                  -- e.g., "CVS"
  corporate_domain   TEXT,                  -- e.g., "cvshealth.com" (often different from cvs.com)
  is_franchise       BOOLEAN DEFAULT FALSE,
  is_chain           BOOLEAN DEFAULT FALSE,
  -- Best contact route inferred from the data we have
  best_contact_path  TEXT,                  -- 'local-manager' | 'corporate-pr' | 'owner-direct' | 'franchisee' | 'unknown'
  best_contact_note  TEXT,
  -- Niche metadata
  cuisine            TEXT,                  -- restaurants only
  price_band         TEXT,                  -- '$' | '$$' | '$$$' | '$$$$'
  hours_json         JSONB,                 -- weekly hours if extracted
  -- Social
  linkedin_company   TEXT,                  -- LinkedIn company page URL
  facebook_url       TEXT,
  instagram_handle   TEXT,
  twitter_handle     TEXT,
  -- Reviews
  rating_google      NUMERIC(3,2),
  rating_yelp        NUMERIC(3,2),
  review_count_total INTEGER,
  -- Provenance + quality
  enrichment_sources TEXT[],                -- which agents touched this row
  confidence         NUMERIC(3,2),          -- 0..1 overall confidence
  raw_signals        JSONB                  -- full detail blob for debugging
);
CREATE INDEX IF NOT EXISTS idx_enrichment_class ON business_enrichment (ownership_class);
CREATE INDEX IF NOT EXISTS idx_enrichment_chain ON business_enrichment (is_chain) WHERE is_chain;

-- ─── business_contacts ───────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS business_contacts (
  id                 BIGSERIAL PRIMARY KEY,
  business_id        BIGINT NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
  -- Role within the business
  role               TEXT NOT NULL,         -- 'owner' | 'co-owner' | 'gm' | 'chef' | 'manager' | 'franchisee'
                                            --  | 'marketing' | 'corporate_local_rep' | 'pr' | 'media-relations'
  person_name        TEXT,
  person_title       TEXT,                  -- raw title pulled from source ("Owner", "Executive Chef", "GM")
  person_linkedin    TEXT,
  person_email       TEXT,                  -- only when sourced cleanly (Hunter, About page)
  person_phone       TEXT,
  -- Provenance
  source             TEXT NOT NULL,         -- 'linkedin' | 'website-about' | 'gmb' | 'yelp' | 'manual' | 'other'
  source_url         TEXT,
  source_snippet     TEXT,                  -- the bit of text we pulled the contact from
  confidence         NUMERIC(3,2),          -- 0..1
  first_seen_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  last_verified_at   TIMESTAMPTZ,
  notes              TEXT,
  -- Synthesised dedupe key (functional indexes can't be UNIQUE inline in PG;
  -- compute it here and use a unique index below).
  dedupe_key         TEXT GENERATED ALWAYS AS (COALESCE(person_linkedin, person_name, '')) STORED
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_contacts_business_source_dedupe
  ON business_contacts (business_id, source, dedupe_key);
CREATE INDEX IF NOT EXISTS idx_contacts_business ON business_contacts (business_id);
CREATE INDEX IF NOT EXISTS idx_contacts_role ON business_contacts (role);

-- ─── Migrations ledger ───────────────────────────────────────────────
INSERT INTO schema_migrations (filename) VALUES ('002_enrichment.sql')
  ON CONFLICT (filename) DO NOTHING;

COMMIT;