forked from sagnik/Project_Velocity
feat(crm): canonical crm and imported routes implementation
This commit is contained in:
708
backend/db/schema_crm_canonical.sql
Normal file
708
backend/db/schema_crm_canonical.sql
Normal file
@@ -0,0 +1,708 @@
|
||||
-- =============================================================================
|
||||
-- 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,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- 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.';
|
||||
Reference in New Issue
Block a user