-- ============================================================================= -- schema_crm_canonical.sql -- Project Velocity — Canonical CRM and Platform Schema -- ============================================================================= -- Covers: crm_*, intel_*, inventory_*, workflow_* canonical domains -- as specified in Doc 09: Database Schema and Root API Spec -- and Doc 07: Contracts and Schema Blueprint -- -- Run AFTER schema.sql and schema_addendum.sql -- psql -U velocity_user -d velocity_db -f schema_crm_canonical.sql -- -- Existing tables: users_and_roles, leads_intelligence, velocity_vault_assets, -- omnichannel_logs, consent_log, video_scene_maps, -- perception_sessions, cctv_events, leads, chat_logs -- These are treated as legacy feeders per the reconciliation matrix. -- ============================================================================= CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- ───────────────────────────────────────────────────────────────────────────── -- ENUM TYPES — Canonical Domain -- ───────────────────────────────────────────────────────────────────────────── DO $$ BEGIN CREATE TYPE crm_lead_status AS ENUM ( 'new', 'contacted', 'qualified', 'site_visit_scheduled', 'site_visited', 'negotiation', 'booking_initiated', 'booked', 'lost', 'dormant' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE crm_opportunity_stage AS ENUM ( 'prospect', 'qualified', 'proposal', 'site_visit', 'negotiation', 'booking', 'agreement', 'closed_won', 'closed_lost' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE crm_account_type AS ENUM ( 'individual', 'company', 'broker', 'developer', 'referral_partner', 'nri_family' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE crm_relationship_type AS ENUM ( 'spouse', 'parent', 'sibling', 'business_partner', 'broker_referral', 'co_buyer', 'family_member', 'advisor' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE intel_channel AS ENUM ( 'whatsapp', 'phone', 'email', 'site_visit', 'office_meeting', 'video_call', 'cctv', 'perception_session', 'system' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE intel_call_direction AS ENUM ('inbound', 'outbound'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE wf_status AS ENUM ( 'pending', 'review_required', 'approved', 'rejected', 'executed', 'failed', 'cancelled' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE import_lifecycle AS ENUM ( 'uploaded', 'parsed', 'mapped', 'proposed', 'approved', 'committed', 'failed' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ───────────────────────────────────────────────────────────────────────────── -- SECTION 1: CRM CORE DOMAIN (crm_*) -- ───────────────────────────────────────────────────────────────────────────── -- TABLE: crm_people -- Purpose: canonical person-level contact identity CREATE TABLE IF NOT EXISTS crm_people ( person_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), full_name TEXT NOT NULL, primary_email TEXT, primary_phone TEXT, secondary_phone TEXT, linkedin_url TEXT, city TEXT, nationality TEXT, buyer_type TEXT, -- high_intent, slow_burn_investor, nri, etc. persona_labels JSONB NOT NULL DEFAULT '[]'::jsonb, source_confidence FLOAT CHECK (source_confidence BETWEEN 0.0 AND 1.0), -- Legacy feeder references (migration linkage) legacy_lead_id TEXT, -- links to old leads.id legacy_li_id UUID, -- links to leads_intelligence.id -- Metadata metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_crm_people_email ON crm_people (primary_email); CREATE INDEX IF NOT EXISTS idx_crm_people_phone ON crm_people (primary_phone); CREATE INDEX IF NOT EXISTS idx_crm_people_name_trgm ON crm_people USING GIN (full_name gin_trgm_ops); CREATE INDEX IF NOT EXISTS idx_crm_people_buyer_type ON crm_people (buyer_type); -- TABLE: crm_accounts -- Purpose: company, employer, brokerage, or client organization CREATE TABLE IF NOT EXISTS crm_accounts ( account_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_name TEXT NOT NULL, parent_account_id UUID REFERENCES crm_accounts(account_id) ON DELETE SET NULL, account_type crm_account_type NOT NULL DEFAULT 'company', industry TEXT, location_ref TEXT, website TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_crm_accounts_name ON crm_accounts (account_name); CREATE INDEX IF NOT EXISTS idx_crm_accounts_type ON crm_accounts (account_type); -- TABLE: crm_households -- Purpose: family or co-buyer unit grouping CREATE TABLE IF NOT EXISTS crm_households ( household_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_name TEXT NOT NULL, primary_person_id UUID REFERENCES crm_people(person_id) ON DELETE SET NULL, notes TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- TABLE: crm_relationships -- Purpose: person-to-person relationship graph CREATE TABLE IF NOT EXISTS crm_relationships ( relationship_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_a_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, person_b_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, relationship_type crm_relationship_type NOT NULL, household_id UUID REFERENCES crm_households(household_id) ON DELETE SET NULL, notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (person_a_id, person_b_id, relationship_type) ); CREATE INDEX IF NOT EXISTS idx_crm_rel_a ON crm_relationships (person_a_id); CREATE INDEX IF NOT EXISTS idx_crm_rel_b ON crm_relationships (person_b_id); -- TABLE: crm_leads -- Purpose: funnel-stage commercial qualification layer CREATE TABLE IF NOT EXISTS crm_leads ( lead_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, account_id UUID REFERENCES crm_accounts(account_id) ON DELETE SET NULL, source_system TEXT DEFAULT 'velocity', status crm_lead_status NOT NULL DEFAULT 'new', budget_band TEXT, urgency TEXT, -- low, medium, high, critical financing_posture TEXT, -- cash, loan, nri_remittance, emi timeline_to_decision TEXT, objections JSONB NOT NULL DEFAULT '[]'::jsonb, motivations JSONB NOT NULL DEFAULT '[]'::jsonb, assigned_user_id UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, -- Legacy feeder legacy_lead_id TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_crm_leads_person ON crm_leads (person_id); CREATE INDEX IF NOT EXISTS idx_crm_leads_status ON crm_leads (status); CREATE INDEX IF NOT EXISTS idx_crm_leads_assigned ON crm_leads (assigned_user_id); CREATE INDEX IF NOT EXISTS idx_crm_leads_source ON crm_leads (source_system); -- TABLE: crm_opportunities -- Purpose: deal pipeline objects CREATE TABLE IF NOT EXISTS crm_opportunities ( opportunity_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_id UUID NOT NULL REFERENCES crm_leads(lead_id) ON DELETE CASCADE, project_id UUID, -- references inventory_projects unit_id UUID, -- references inventory_units stage crm_opportunity_stage NOT NULL DEFAULT 'prospect', value DECIMAL(15, 2), probability INTEGER CHECK (probability BETWEEN 0 AND 100), expected_close_date DATE, next_action TEXT, notes TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_crm_opp_lead ON crm_opportunities (lead_id); CREATE INDEX IF NOT EXISTS idx_crm_opp_stage ON crm_opportunities (stage); CREATE INDEX IF NOT EXISTS idx_crm_opp_project ON crm_opportunities (project_id); -- TABLE: crm_property_interests -- Purpose: project and unit interest linking per client CREATE TABLE IF NOT EXISTS crm_property_interests ( interest_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, lead_id UUID REFERENCES crm_leads(lead_id) ON DELETE SET NULL, project_id UUID, project_name TEXT NOT NULL, unit_preference TEXT, configuration TEXT, -- 2BHK, 3BHK, Penthouse, etc. budget_min DECIMAL(15, 2), budget_max DECIMAL(15, 2), priority INTEGER DEFAULT 1, -- 1 = primary, 2 = secondary notes TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ALTER TABLE crm_property_interests ADD COLUMN IF NOT EXISTS metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb; CREATE INDEX IF NOT EXISTS idx_crm_pi_person ON crm_property_interests (person_id); CREATE INDEX IF NOT EXISTS idx_crm_pi_project ON crm_property_interests (project_id); -- TABLE: crm_stage_history -- Purpose: canonical audit trail of lead stage transitions CREATE TABLE IF NOT EXISTS crm_stage_history ( history_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_id UUID NOT NULL REFERENCES crm_leads(lead_id) ON DELETE CASCADE, from_status TEXT, to_status TEXT NOT NULL, changed_by UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, changed_by_type TEXT DEFAULT 'human', -- human, ai, system notes TEXT, happened_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_crm_stage_lead ON crm_stage_history (lead_id, happened_at DESC); -- ───────────────────────────────────────────────────────────────────────────── -- SECTION 2: INTERACTION AND EVIDENCE GRAPH (intel_*) -- ───────────────────────────────────────────────────────────────────────────── -- TABLE: intel_interactions -- Purpose: umbrella interaction event record CREATE TABLE IF NOT EXISTS intel_interactions ( interaction_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, lead_id UUID REFERENCES crm_leads(lead_id) ON DELETE SET NULL, channel intel_channel NOT NULL, interaction_type TEXT NOT NULL, -- message, call, visit, email, note happened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), summary TEXT, source_ref TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_intel_int_person ON intel_interactions (person_id, happened_at DESC); CREATE INDEX IF NOT EXISTS idx_intel_int_lead ON intel_interactions (lead_id, happened_at DESC); CREATE INDEX IF NOT EXISTS idx_intel_int_channel ON intel_interactions (channel); -- TABLE: intel_messages -- Purpose: text-level message records (WhatsApp, chat) CREATE TABLE IF NOT EXISTS intel_messages ( message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), interaction_id UUID NOT NULL REFERENCES intel_interactions(interaction_id) ON DELETE CASCADE, thread_id UUID, sender_role TEXT NOT NULL, -- lead, broker, system, oracle sender_name TEXT, message_text TEXT NOT NULL, delivered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_msg_interaction ON intel_messages (interaction_id, delivered_at DESC); -- TABLE: intel_whatsapp_threads -- Purpose: WhatsApp thread-level summaries CREATE TABLE IF NOT EXISTS intel_whatsapp_threads ( thread_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, lead_id UUID REFERENCES crm_leads(lead_id) ON DELETE SET NULL, phone_number TEXT, thread_summary TEXT, message_count INTEGER DEFAULT 0, last_message_at TIMESTAMPTZ, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_intel_wa_person ON intel_whatsapp_threads (person_id); -- TABLE: intel_calls -- Purpose: voice call records CREATE TABLE IF NOT EXISTS intel_calls ( call_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), interaction_id UUID NOT NULL REFERENCES intel_interactions(interaction_id) ON DELETE CASCADE, call_direction intel_call_direction NOT NULL DEFAULT 'outbound', duration_seconds INTEGER, recording_ref TEXT, -- storage path or URL to recording transcript_ref TEXT, -- path to transcript JSON call_outcome TEXT, -- connected, no_answer, voicemail, dropped called_number TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_call_interaction ON intel_calls (interaction_id); -- TABLE: intel_transcripts -- Purpose: transcript and speaker segmentation storage CREATE TABLE IF NOT EXISTS intel_transcripts ( transcript_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), call_id UUID REFERENCES intel_calls(call_id) ON DELETE SET NULL, interaction_id UUID REFERENCES intel_interactions(interaction_id) ON DELETE SET NULL, language TEXT DEFAULT 'en', full_text TEXT, speaker_segments_json JSONB NOT NULL DEFAULT '[]'::jsonb, confidence FLOAT CHECK (confidence BETWEEN 0.0 AND 1.0), word_count INTEGER, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_intel_transcript_call ON intel_transcripts (call_id); CREATE INDEX IF NOT EXISTS idx_intel_transcript_interaction ON intel_transcripts (interaction_id); -- TABLE: intel_emails -- Purpose: email thread records CREATE TABLE IF NOT EXISTS intel_emails ( email_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), interaction_id UUID NOT NULL REFERENCES intel_interactions(interaction_id) ON DELETE CASCADE, from_address TEXT, to_addresses JSONB NOT NULL DEFAULT '[]'::jsonb, subject TEXT, body_text TEXT, has_attachments BOOLEAN DEFAULT FALSE, sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_email_interaction ON intel_emails (interaction_id); -- TABLE: intel_visits -- Purpose: site visit and meeting records CREATE TABLE IF NOT EXISTS intel_visits ( visit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, lead_id UUID REFERENCES crm_leads(lead_id) ON DELETE SET NULL, project_id UUID, project_name TEXT, unit_id UUID, visited_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), visit_notes TEXT, host_user_id UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, revisit_intent TEXT, -- very_likely, likely, uncertain, unlikely cctv_session_ref TEXT, perception_session_ref TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_visits_person ON intel_visits (person_id, visited_at DESC); CREATE INDEX IF NOT EXISTS idx_intel_visits_project ON intel_visits (project_id); -- TABLE: intel_reminders -- Purpose: reminders and follow-up task chains CREATE TABLE IF NOT EXISTS intel_reminders ( reminder_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, lead_id UUID REFERENCES crm_leads(lead_id) ON DELETE SET NULL, opportunity_id UUID REFERENCES crm_opportunities(opportunity_id) ON DELETE SET NULL, reminder_type TEXT NOT NULL, -- call_back, follow_up, site_visit, document, negotiation title TEXT NOT NULL, notes TEXT, due_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'pending', -- pending, done, snoozed, cancelled assigned_to UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, created_by_type TEXT DEFAULT 'human', -- human, ai, system priority TEXT DEFAULT 'normal', -- low, normal, high, urgent metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_intel_reminder_person ON intel_reminders (person_id, due_at); CREATE INDEX IF NOT EXISTS idx_intel_reminder_status ON intel_reminders (status, due_at); CREATE INDEX IF NOT EXISTS idx_intel_reminder_assigned ON intel_reminders (assigned_to, due_at); -- TABLE: intel_qd_scores -- Purpose: latest meaningful QD summary by client CREATE TABLE IF NOT EXISTS intel_qd_scores ( qd_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, score_type TEXT NOT NULL, -- intent_score, urgency_score, engagement_score current_value FLOAT NOT NULL CHECK (current_value BETWEEN 0.0 AND 1.0), computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), evidence_refs_json JSONB NOT NULL DEFAULT '[]'::jsonb, reasoning TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, UNIQUE (person_id, score_type) ); CREATE INDEX IF NOT EXISTS idx_intel_qd_person ON intel_qd_scores (person_id); -- TABLE: intel_qd_timeseries -- Purpose: time-series QD propagation and shifts CREATE TABLE IF NOT EXISTS intel_qd_timeseries ( timeseries_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID NOT NULL REFERENCES crm_people(person_id) ON DELETE CASCADE, score_type TEXT NOT NULL, signal_source TEXT, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), value FLOAT NOT NULL CHECK (value BETWEEN 0.0 AND 1.0), delta FLOAT, evidence_ref TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_qd_ts_person ON intel_qd_timeseries (person_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_intel_qd_ts_type ON intel_qd_timeseries (score_type, timestamp DESC); -- TABLE: intel_vehicle_events -- Purpose: number-plate and vehicle detection events CREATE TABLE IF NOT EXISTS intel_vehicle_events ( event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID REFERENCES crm_people(person_id) ON DELETE SET NULL, visit_id UUID REFERENCES intel_visits(visit_id) ON DELETE SET NULL, zone TEXT, license_plate_hash TEXT, -- hashed for privacy vehicle_class TEXT, -- luxury, standard, unknown wealth_indicator TEXT, -- HNI, standard, unknown cctv_ref TEXT, captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_vehicle_person ON intel_vehicle_events (person_id); -- TABLE: intel_perception_events -- Purpose: behavioral and dwell-time intelligence from perception sessions CREATE TABLE IF NOT EXISTS intel_perception_events ( perception_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID REFERENCES crm_people(person_id) ON DELETE SET NULL, visit_id UUID REFERENCES intel_visits(visit_id) ON DELETE SET NULL, session_ref TEXT, -- perception_sessions.id linkage event_type TEXT NOT NULL, -- room_dwell, engagement_spike, exit rooms_visited JSONB NOT NULL DEFAULT '[]'::jsonb, dwell_time_seconds INTEGER, engagement_score FLOAT CHECK (engagement_score BETWEEN 0.0 AND 1.0), camera_id TEXT, media_ref TEXT, happened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_perception_person ON intel_perception_events (person_id); -- TABLE: intel_cctv_links -- Purpose: CCTV evidence references linked to client/visit contexts CREATE TABLE IF NOT EXISTS intel_cctv_links ( link_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), person_id UUID REFERENCES crm_people(person_id) ON DELETE SET NULL, visit_id UUID REFERENCES intel_visits(visit_id) ON DELETE SET NULL, cctv_event_id UUID REFERENCES cctv_events(id) ON DELETE SET NULL, clip_ref TEXT, camera_zone TEXT, confidence FLOAT CHECK (confidence BETWEEN 0.0 AND 1.0), linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_intel_cctv_person ON intel_cctv_links (person_id); -- ───────────────────────────────────────────────────────────────────────────── -- SECTION 3: INVENTORY DOMAIN (inventory_*) -- ───────────────────────────────────────────────────────────────────────────── -- TABLE: inventory_projects -- Purpose: project-level inventory master CREATE TABLE IF NOT EXISTS inventory_projects ( project_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_name TEXT NOT NULL UNIQUE, developer_name TEXT NOT NULL, city TEXT NOT NULL DEFAULT 'Kolkata', micro_market TEXT, address TEXT, total_units INTEGER, rera_number TEXT, project_status TEXT DEFAULT 'active', -- active, sold_out, upcoming launch_date DATE, possession_date DATE, location_json JSONB NOT NULL DEFAULT '{}'::jsonb, amenities_json JSONB NOT NULL DEFAULT '[]'::jsonb, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_inv_projects_name ON inventory_projects (project_name); CREATE INDEX IF NOT EXISTS idx_inv_projects_market ON inventory_projects (micro_market); -- TABLE: inventory_units -- Purpose: unit-level availability and attributes CREATE TABLE IF NOT EXISTS inventory_units ( unit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id UUID NOT NULL REFERENCES inventory_projects(project_id) ON DELETE CASCADE, unit_label TEXT NOT NULL, configuration TEXT NOT NULL, -- 2BHK, 3BHK, Penthouse, etc. area_sqft DECIMAL(10, 2), price_current DECIMAL(15, 2), price_psf DECIMAL(10, 2), status TEXT NOT NULL DEFAULT 'available', -- available, reserved, sold, hold floor INTEGER, tower TEXT, facing TEXT, has_attached_amenities JSONB NOT NULL DEFAULT '[]'::jsonb, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (project_id, unit_label) ); CREATE INDEX IF NOT EXISTS idx_inv_units_project ON inventory_units (project_id); CREATE INDEX IF NOT EXISTS idx_inv_units_status ON inventory_units (status); CREATE INDEX IF NOT EXISTS idx_inv_units_config ON inventory_units (configuration); -- TABLE: inventory_import_jobs -- Purpose: track inventory CSV import operations CREATE TABLE IF NOT EXISTS inventory_import_jobs ( job_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id UUID REFERENCES inventory_projects(project_id) ON DELETE SET NULL, filename TEXT NOT NULL, row_count INTEGER, imported_by UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'pending', errors_json JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); -- ───────────────────────────────────────────────────────────────────────────── -- SECTION 4: AI WORKFLOW AND GOVERNANCE (workflow_*) -- ───────────────────────────────────────────────────────────────────────────── -- TABLE: workflow_actions -- Purpose: track proposed AI/human actions before approval CREATE TABLE IF NOT EXISTS workflow_actions ( action_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action_type TEXT NOT NULL, -- import_review, merge_proposal, writeback, enrichment target_domain TEXT NOT NULL, -- crm, intel, inventory target_entity_ref TEXT, proposal_payload JSONB NOT NULL DEFAULT '{}'::jsonb, reasoning_summary TEXT, evidence_refs JSONB NOT NULL DEFAULT '[]'::jsonb, confidence FLOAT CHECK (confidence BETWEEN 0.0 AND 1.0), status wf_status NOT NULL DEFAULT 'pending', approval_required BOOLEAN NOT NULL DEFAULT TRUE, created_by_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_wf_actions_status ON workflow_actions (status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_wf_actions_domain ON workflow_actions (target_domain); -- TABLE: workflow_approvals -- Purpose: explicit human review decisions CREATE TABLE IF NOT EXISTS workflow_approvals ( decision_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action_id UUID NOT NULL REFERENCES workflow_actions(action_id) ON DELETE CASCADE, reviewer_id UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, decision TEXT NOT NULL, -- approved, rejected, needs_more_info decision_notes TEXT, decided_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_wf_approvals_action ON workflow_approvals (action_id); -- TABLE: workflow_writebacks -- Purpose: track AI-suggested and approved canonical mutations CREATE TABLE IF NOT EXISTS workflow_writebacks ( writeback_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action_id UUID REFERENCES workflow_actions(action_id) ON DELETE SET NULL, approval_id UUID REFERENCES workflow_approvals(decision_id) ON DELETE SET NULL, target_domain TEXT NOT NULL, target_entity_ref TEXT NOT NULL, change_payload JSONB NOT NULL DEFAULT '{}'::jsonb, status wf_status NOT NULL DEFAULT 'pending', approved_by UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, executed_at TIMESTAMPTZ, error_detail TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_wf_wb_status ON workflow_writebacks (status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_wf_wb_domain ON workflow_writebacks (target_domain); -- TABLE: workflow_import_batches -- Purpose: CRM import batch lifecycle tracking (RawImportBatch contract) CREATE TABLE IF NOT EXISTS workflow_import_batches ( batch_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_system TEXT NOT NULL, -- csv_upload, salesforce, hubspot, manual uploaded_filename TEXT, mime_type TEXT DEFAULT 'text/csv', storage_ref TEXT, row_count INTEGER, mapped_count INTEGER DEFAULT 0, unresolved_count INTEGER DEFAULT 0, canonical_count INTEGER DEFAULT 0, uploaded_by UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, lifecycle import_lifecycle NOT NULL DEFAULT 'uploaded', mapping_manifest JSONB NOT NULL DEFAULT '{}'::jsonb, errors_json JSONB NOT NULL DEFAULT '[]'::jsonb, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_wf_import_lifecycle ON workflow_import_batches (lifecycle, created_at DESC); CREATE INDEX IF NOT EXISTS idx_wf_import_user ON workflow_import_batches (uploaded_by); -- TABLE: workflow_agent_runs -- Purpose: track NemoClaw and AI agent invocation logs CREATE TABLE IF NOT EXISTS workflow_agent_runs ( run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_name TEXT NOT NULL, -- nemoclaw, import_mapper, enrichment_engine trigger_type TEXT NOT NULL, -- import, enrichment, qd_update, writeback trigger_ref TEXT, input_payload JSONB NOT NULL DEFAULT '{}'::jsonb, output_payload JSONB NOT NULL DEFAULT '{}'::jsonb, status TEXT NOT NULL DEFAULT 'running', -- running, completed, failed duration_ms INTEGER, error_detail TEXT, started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_wf_agent_runs_agent ON workflow_agent_runs (agent_name, started_at DESC); CREATE INDEX IF NOT EXISTS idx_wf_agent_runs_status ON workflow_agent_runs (status); -- ───────────────────────────────────────────────────────────────────────────── -- TENANT HARDENING FOR SHARED CRM SURFACES -- ───────────────────────────────────────────────────────────────────────────── ALTER TABLE crm_people ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE crm_accounts ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE crm_leads ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE crm_property_interests ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE intel_interactions ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE intel_reminders ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE intel_qd_scores ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE intel_qd_timeseries ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE workflow_actions ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE workflow_approvals ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; ALTER TABLE workflow_import_batches ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity'; CREATE INDEX IF NOT EXISTS idx_crm_people_tenant_created ON crm_people (tenant_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_crm_leads_tenant_status ON crm_leads (tenant_id, status, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_crm_opportunities_tenant_stage ON crm_opportunities (tenant_id, stage, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_crm_property_interests_tenant_person ON crm_property_interests (tenant_id, person_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_intel_interactions_tenant_person ON intel_interactions (tenant_id, person_id, happened_at DESC); CREATE INDEX IF NOT EXISTS idx_intel_reminders_tenant_status ON intel_reminders (tenant_id, status, due_at); CREATE INDEX IF NOT EXISTS idx_intel_qd_scores_tenant_person ON intel_qd_scores (tenant_id, person_id, score_type); CREATE INDEX IF NOT EXISTS idx_intel_qd_timeseries_tenant_person ON intel_qd_timeseries (tenant_id, person_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_wf_actions_tenant_status ON workflow_actions (tenant_id, status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_wf_import_batches_tenant_lifecycle ON workflow_import_batches (tenant_id, lifecycle, created_at DESC); -- ───────────────────────────────────────────────────────────────────────────── -- TRIGGERS: auto-update updated_at -- ───────────────────────────────────────────────────────────────────────────── CREATE OR REPLACE FUNCTION set_canonical_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; DO $$ DECLARE t TEXT; BEGIN FOREACH t IN ARRAY ARRAY[ 'crm_people', 'crm_accounts', 'crm_leads', 'crm_opportunities', 'inventory_projects', 'inventory_units', 'workflow_actions', 'workflow_import_batches' ] LOOP EXECUTE format( 'DROP TRIGGER IF EXISTS trg_%s_updated_at ON %s; CREATE TRIGGER trg_%s_updated_at BEFORE UPDATE ON %s FOR EACH ROW EXECUTE FUNCTION set_canonical_updated_at();', t, t, t, t ); END LOOP; END $$; -- ───────────────────────────────────────────────────────────────────────────── -- INVENTORY SEED: 14 Canonical Kolkata Projects -- ───────────────────────────────────────────────────────────────────────────── INSERT INTO inventory_projects (project_id, project_name, developer_name, city, micro_market) VALUES (gen_random_uuid(), 'Eden Devprayag', 'Eden Group', 'Kolkata', 'Rajarhat'), (gen_random_uuid(), 'Sugam Prakriti', 'Sugam Homes', 'Kolkata', 'Barasat'), (gen_random_uuid(), 'Atri Aqua', 'Atri Developers', 'Kolkata', 'New Town'), (gen_random_uuid(), 'Atri Surya Toron', 'Atri Developers', 'Kolkata', 'Rajarhat'), (gen_random_uuid(), 'Siddha Suburbia Bungalow', 'Siddha Group', 'Kolkata', 'Madanpur'), (gen_random_uuid(), 'Merlin Avana', 'Merlin Group', 'Kolkata', 'Tangra'), (gen_random_uuid(), 'DTC Good Earth', 'DTC Projects', 'Kolkata', 'New Town'), (gen_random_uuid(), 'Siddha Serena', 'Siddha Group', 'Kolkata', 'New Town'), (gen_random_uuid(), 'Siddha Sky Waterfront', 'Siddha Group', 'Kolkata', 'Beliaghata'), (gen_random_uuid(), 'Godrej Blue', 'Godrej Properties', 'Kolkata', 'New Town'), (gen_random_uuid(), 'DTC Sojon', 'DTC Projects', 'Kolkata', 'Rajarhat'), (gen_random_uuid(), 'Shriram Grand City', 'Shriram Properties', 'Kolkata', 'Howrah'), (gen_random_uuid(), 'Godrej Elevate', 'Godrej Properties', 'Kolkata', 'Dum Dum'), (gen_random_uuid(), 'Ambuja Utpaala', 'Ambuja Neotia', 'Kolkata', 'Tollygunge') ON CONFLICT (project_name) DO NOTHING; -- ───────────────────────────────────────────────────────────────────────────── -- COMMENTS -- ───────────────────────────────────────────────────────────────────────────── COMMENT ON TABLE crm_people IS 'Canonical person-level contact identity. Primary join key across all CRM tables.'; COMMENT ON TABLE crm_leads IS 'Funnel-stage commercial qualification. One person may have multiple lead contexts.'; COMMENT ON TABLE crm_opportunities IS 'Deal pipeline objects linked to leads and inventory.'; COMMENT ON TABLE intel_interactions IS 'Umbrella interaction event. All channels (WhatsApp, call, email, visit) link here.'; COMMENT ON TABLE intel_transcripts IS 'Speaker-segmented call transcripts. speaker_segments_json is first-class data.'; COMMENT ON TABLE intel_qd_scores IS 'Latest QD summary by score_type per client. UNIQUE constraint enforces one row per type.'; COMMENT ON TABLE inventory_projects IS 'Master project catalog. 14 canonical Kolkata projects seeded.'; COMMENT ON TABLE workflow_import_batches IS 'RawImportBatch contract. Immutable after upload.'; COMMENT ON TABLE workflow_writebacks IS 'AI-proposed canonical mutations. Never auto-execute without approval.'; -- ----------------------------------------------------------------------------- -- Synthetic CRM v2 enrichment columns and Oracle read models -- ----------------------------------------------------------------------------- ALTER TABLE crm_people ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS engagement_score FLOAT, ADD COLUMN IF NOT EXISTS communication_preference TEXT, ADD COLUMN IF NOT EXISTS best_contact_time TEXT; ALTER TABLE crm_households ADD COLUMN IF NOT EXISTS size INTEGER, ADD COLUMN IF NOT EXISTS combined_budget_band TEXT, ADD COLUMN IF NOT EXISTS decision_maker_id UUID REFERENCES crm_people(person_id) ON DELETE SET NULL; ALTER TABLE crm_leads ADD COLUMN IF NOT EXISTS stage TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS broker_team TEXT, ADD COLUMN IF NOT EXISTS engagement_score FLOAT, ADD COLUMN IF NOT EXISTS last_activity_at TIMESTAMPTZ; ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS deal_velocity TEXT, ADD COLUMN IF NOT EXISTS risk_factors JSONB NOT NULL DEFAULT '[]'::jsonb; ALTER TABLE crm_property_interests ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS last_discussed_at TIMESTAMPTZ; ALTER TABLE crm_stage_history ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS transition_duration_days INTEGER; ALTER TABLE intel_interactions ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS broker_team TEXT, ADD COLUMN IF NOT EXISTS sentiment TEXT, ADD COLUMN IF NOT EXISTS sentiment_score FLOAT, ADD COLUMN IF NOT EXISTS intent_label TEXT, ADD COLUMN IF NOT EXISTS emotion_tags JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS client_engagement_level TEXT; ALTER TABLE intel_calls ADD COLUMN IF NOT EXISTS objection_tags JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS outcome_summary TEXT, ADD COLUMN IF NOT EXISTS follow_up_actions JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS call_quality_score FLOAT; ALTER TABLE intel_emails ADD COLUMN IF NOT EXISTS sentiment TEXT, ADD COLUMN IF NOT EXISTS intent_label TEXT, ADD COLUMN IF NOT EXISTS engagement_score FLOAT, ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS response_expected BOOLEAN; ALTER TABLE intel_reminders ADD COLUMN IF NOT EXISTS interaction_id UUID REFERENCES intel_interactions(interaction_id) ON DELETE SET NULL, ADD COLUMN IF NOT EXISTS context_snippet TEXT, ADD COLUMN IF NOT EXISTS completion_percentage INTEGER, ADD COLUMN IF NOT EXISTS overdue_days INTEGER, ADD COLUMN IF NOT EXISTS outcome_notes TEXT; ALTER TABLE intel_transcripts ADD COLUMN IF NOT EXISTS call_outcome TEXT, ADD COLUMN IF NOT EXISTS follow_up_required BOOLEAN, ADD COLUMN IF NOT EXISTS emotion_tags JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS call_summary TEXT; ALTER TABLE intel_visits ADD COLUMN IF NOT EXISTS outcome_type TEXT, ADD COLUMN IF NOT EXISTS visit_duration_minutes INTEGER, ADD COLUMN IF NOT EXISTS interest_signals JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS interest_score FLOAT, ADD COLUMN IF NOT EXISTS companion_type TEXT, ADD COLUMN IF NOT EXISTS companion_count INTEGER, ADD COLUMN IF NOT EXISTS objections_raised JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS follow_up_required BOOLEAN, ADD COLUMN IF NOT EXISTS next_steps TEXT, ADD COLUMN IF NOT EXISTS broker_notes TEXT, ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT; ALTER TABLE intel_whatsapp_threads ADD COLUMN IF NOT EXISTS broker_id TEXT, ADD COLUMN IF NOT EXISTS broker_name TEXT, ADD COLUMN IF NOT EXISTS topic_category TEXT, ADD COLUMN IF NOT EXISTS sentiment_direction TEXT, ADD COLUMN IF NOT EXISTS resolution_status TEXT; ALTER TABLE intel_qd_scores ADD COLUMN IF NOT EXISTS score_drivers JSONB NOT NULL DEFAULT '[]'::jsonb, ADD COLUMN IF NOT EXISTS trend_direction TEXT, ADD COLUMN IF NOT EXISTS explanation TEXT, ADD COLUMN IF NOT EXISTS confidence FLOAT; ALTER TABLE intel_qd_timeseries ADD COLUMN IF NOT EXISTS broker_id TEXT; CREATE TABLE IF NOT EXISTS intel_email_threads ( thread_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), subject TEXT, first_email_at TIMESTAMPTZ, last_email_at TIMESTAMPTZ, email_count INTEGER DEFAULT 0, participants JSONB NOT NULL DEFAULT '[]'::jsonb, status TEXT, broker_id TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE TABLE IF NOT EXISTS intel_call_objections ( objection_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), call_id UUID REFERENCES intel_calls(call_id) ON DELETE CASCADE, objection_type TEXT, category TEXT, severity TEXT, status TEXT, client_quote TEXT, agent_response TEXT, resolution_strategy TEXT, extracted_at TIMESTAMPTZ, confidence_score FLOAT, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE TABLE IF NOT EXISTS intel_extracted_facts ( fact_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), interaction_id UUID REFERENCES intel_interactions(interaction_id) ON DELETE SET NULL, person_id UUID REFERENCES crm_people(person_id) ON DELETE CASCADE, fact_type TEXT NOT NULL, fact_value TEXT, confidence FLOAT, extracted_from TEXT, source_context TEXT, extracted_at TIMESTAMPTZ, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE TABLE IF NOT EXISTS read_last_contacted ( person_id UUID PRIMARY KEY REFERENCES crm_people(person_id) ON DELETE CASCADE, last_contact_at TIMESTAMPTZ, last_channel TEXT, last_interaction_type TEXT, days_since_contact INTEGER, interactions_last_7d INTEGER, interactions_last_30d INTEGER, interactions_last_90d INTEGER, total_interactions INTEGER, current_stage TEXT, broker_id TEXT, broker_name TEXT, computed_at TIMESTAMPTZ, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE TABLE IF NOT EXISTS read_next_best_action ( person_id UUID PRIMARY KEY REFERENCES crm_people(person_id) ON DELETE CASCADE, recommended_action TEXT, priority TEXT, rationale TEXT, suggested_channel TEXT, due_within_days INTEGER, broker_id TEXT, broker_name TEXT, opportunity_context TEXT, computed_at TIMESTAMPTZ, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE INDEX IF NOT EXISTS idx_read_last_contacted_at ON read_last_contacted (last_contact_at DESC); CREATE INDEX IF NOT EXISTS idx_read_next_best_priority ON read_next_best_action (priority); CREATE INDEX IF NOT EXISTS idx_intel_facts_person ON intel_extracted_facts (person_id, extracted_at DESC); CREATE INDEX IF NOT EXISTS idx_intel_objections_call ON intel_call_objections (call_id);