-- backend/db/schema.sql - Velocity PostgreSQL schema -- Omnichannel Intelligence Graph for The Sentinel. -- -- Run via: -- psql -U velocity_user -d velocity_db -f schema.sql -- -- Or via Alembic (preferred - see backend/alembic/). -- Enable UUID generation CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ──────────────────────────────────────────────────────────────────────────── -- ENUM TYPES -- ──────────────────────────────────────────────────────────────────────────── DO $$ BEGIN CREATE TYPE role_enum AS ENUM ( 'ADMIN', 'SALES_DIRECTOR', 'SENIOR_BROKER', 'JUNIOR_BROKER' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE source_enum AS ENUM ('whatsapp', 'website', 'walkin'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE lead_status_enum AS ENUM ('new', 'engaged', 'qualified', 'hot', 'closed'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE qualification_enum AS ENUM ('whale', 'potential', 'tire_kicker'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE log_event_enum AS ENUM ( 'CALL_LOGGED', 'ASSET_VIEWED', 'SENTIMENT_SPIKE', 'QD_UPDATED', 'LEAD_TAGGED', 'WS_ASSET_OPENED' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ──────────────────────────────────────────────────────────────────────────── -- TABLE: users_and_roles (Manual RBAC backbone) -- ──────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS users_and_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role role_enum NOT NULL DEFAULT 'JUNIOR_BROKER', tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity', full_name TEXT, avatar_url TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, last_login TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Index for login lookups CREATE INDEX IF NOT EXISTS idx_users_email ON users_and_roles (email); CREATE INDEX IF NOT EXISTS idx_users_tenant_active ON users_and_roles (tenant_id, is_active); -- ──────────────────────────────────────────────────────────────────────────── -- TABLE: leads_intelligence (CRM core with QD scoring) -- ──────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS leads_intelligence ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, phone TEXT, email TEXT, source source_enum NOT NULL, status lead_status_enum NOT NULL DEFAULT 'new', qualification qualification_enum, budget TEXT, interest TEXT, -- Quantum Dynamics Score: 1-100, updated in real-time by NemoClaw quantum_dynamics_score INTEGER CHECK (quantum_dynamics_score BETWEEN 1 AND 100), -- Polymorphic CRM intelligence tags e.g. ['HNI', 'NRI', 'Hot Lead'] tags TEXT[] NOT NULL DEFAULT '{}', assigned_to UUID REFERENCES users_and_roles(id) ON DELETE SET NULL, last_message TEXT, last_active TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Auto-update updated_at on every modification CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_leads_updated_at ON leads_intelligence; CREATE TRIGGER trg_leads_updated_at BEFORE UPDATE ON leads_intelligence FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE INDEX IF NOT EXISTS idx_leads_status ON leads_intelligence (status); CREATE INDEX IF NOT EXISTS idx_leads_assigned ON leads_intelligence (assigned_to); -- GIN index for efficient tag array queries (e.g. WHERE 'HNI' = ANY(tags)) CREATE INDEX IF NOT EXISTS idx_leads_tags ON leads_intelligence USING GIN (tags); -- ──────────────────────────────────────────────────────────────────────────── -- TABLE: velocity_vault_assets (File Tracking Engine) -- ──────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS velocity_vault_assets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), asset_name TEXT NOT NULL, asset_type TEXT NOT NULL, -- 'pdf', 'image', 'video' storage_path TEXT NOT NULL, -- relative to /opt/dlami/nvme/assets/ -- Unique cryptographic string for every share instance tracking_hash VARCHAR(64) UNIQUE NOT NULL, lead_id UUID REFERENCES leads_intelligence(id) ON DELETE CASCADE, created_by UUID REFERENCES users_and_roles(id), -- Array of open timestamps; one entry appended per distinct open event opened_at TIMESTAMPTZ[] NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_vault_hash ON velocity_vault_assets (tracking_hash); CREATE INDEX IF NOT EXISTS idx_vault_lead ON velocity_vault_assets (lead_id); -- ──────────────────────────────────────────────────────────────────────────── -- TABLE: omnichannel_logs (Polymorphic event ingestion + sentimental history) -- ──────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS omnichannel_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_type log_event_enum NOT NULL, lead_id UUID REFERENCES leads_intelligence(id) ON DELETE CASCADE, -- JSONB payload — schema varies by event_type: -- SENTIMENT_SPIKE: {blend_shapes, qd_before, qd_after} -- WS_ASSET_OPENED: {ip, user_agent, tracking_hash} -- QD_UPDATED: {qd_score, reasoning, confidence} -- LEAD_TAGGED: {tags_added, tags_removed} payload JSONB NOT NULL DEFAULT '{}', -- For MediaPipe-correlated entries: exact ms offset in the stimulus video video_timestamp_ms BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Composite index supporting time-series sentiment history queries CREATE INDEX IF NOT EXISTS idx_logs_lead_type_time ON omnichannel_logs (lead_id, event_type, created_at DESC); -- Partial index for fast SENTIMENT_SPIKE lookups CREATE INDEX IF NOT EXISTS idx_logs_sentiment_spikes ON omnichannel_logs (lead_id, created_at DESC) WHERE event_type = 'SENTIMENT_SPIKE'; -- ──────────────────────────────────────────────────────────────────────────── -- TABLE: consent_log (GDPR biometric consent tracking) -- ──────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS consent_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_id UUID REFERENCES leads_intelligence(id) ON DELETE CASCADE, consented_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ip_address TEXT, user_agent TEXT, -- 'granted' or 'revoked' action TEXT NOT NULL DEFAULT 'granted' ); CREATE INDEX IF NOT EXISTS idx_consent_lead ON consent_log (lead_id, consented_at DESC);