-- ──────────────────────────────────────────────────────────────────────────── -- Oracle Schema Extension v2 — Multi-Surface Platform and Oracle Expansion -- Date: 2026-04-18 -- Author: Velocity Platform Team -- Depends on: schema_oracle.sql (must be applied first) -- PostgreSQL 14+ required · UUID via pgcrypto already enabled -- ──────────────────────────────────────────────────────────────────────────── -- ─── 1. Oracle Template Taxonomy ───────────────────────────────────────────── CREATE TABLE IF NOT EXISTS oracle_template_chapters ( chapter_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS oracle_template_subchapters ( subchapter_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chapter_id UUID NOT NULL REFERENCES oracle_template_chapters(chapter_id) ON DELETE CASCADE, tenant_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, sort_order INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS oracle_template_seed_examples ( example_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_id UUID NOT NULL REFERENCES oracle_component_templates(template_id) ON DELETE CASCADE, chapter_id UUID REFERENCES oracle_template_chapters(chapter_id), subchapter_id UUID REFERENCES oracle_template_subchapters(subchapter_id), title TEXT NOT NULL, example_json JSONB NOT NULL, quality_notes TEXT, is_canonical BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Extend oracle_component_templates with chapter/subchapter linkage -- (additive columns — does not alter existing rows) ALTER TABLE oracle_component_templates ADD COLUMN IF NOT EXISTS chapter_id UUID REFERENCES oracle_template_chapters(chapter_id), ADD COLUMN IF NOT EXISTS subchapter_id UUID REFERENCES oracle_template_subchapters(subchapter_id), ADD COLUMN IF NOT EXISTS json_template JSONB, ADD COLUMN IF NOT EXISTS description TEXT; -- ─── 2. Kimi Synthetic Data Jobs ───────────────────────────────────────────── CREATE TABLE IF NOT EXISTS oracle_synthetic_generation_jobs ( job_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, template_id UUID NOT NULL REFERENCES oracle_component_templates(template_id), chapter_id UUID REFERENCES oracle_template_chapters(chapter_id), subchapter_id UUID REFERENCES oracle_template_subchapters(subchapter_id), model TEXT NOT NULL DEFAULT 'kimi', status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','completed','failed','cancelled')), requested_count INTEGER NOT NULL DEFAULT 10, accepted_count INTEGER NOT NULL DEFAULT 0, error_message TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 3. Inventory Pipeline ─────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS inventory_import_batches ( batch_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, source_type TEXT NOT NULL CHECK (source_type IN ('csv','json','api_push','manual')), submitted_by TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','validating','processing','completed','failed','partial')), total_rows INTEGER NOT NULL DEFAULT 0, accepted_rows INTEGER NOT NULL DEFAULT 0, rejected_rows INTEGER NOT NULL DEFAULT 0, error_summary JSONB NOT NULL DEFAULT '[]'::JSONB, source_file_ref TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS inventory_properties ( property_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, batch_id UUID REFERENCES inventory_import_batches(batch_id), source_id TEXT, -- external source identifier project_name TEXT NOT NULL, developer_name TEXT NOT NULL, location JSONB NOT NULL DEFAULT '{}'::JSONB, -- {city, district, lat, lng} property_type TEXT NOT NULL, -- apartment, villa, penthouse, plot, etc. price_bands JSONB NOT NULL DEFAULT '[]'::JSONB, -- [{minAED, maxAED, unitType}] unit_mix JSONB NOT NULL DEFAULT '[]'::JSONB, -- [{bedrooms, count, sizeSqft}] amenities TEXT[] NOT NULL DEFAULT '{}', status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','archived','draft','under_review')), validation_state JSONB NOT NULL DEFAULT '{}'::JSONB, ingested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS inventory_media_assets ( media_asset_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES inventory_properties(property_id) ON DELETE CASCADE, tenant_id TEXT NOT NULL, media_type TEXT NOT NULL CHECK (media_type IN ('image','video','floorplan','brochure','360','vr')), url TEXT NOT NULL, thumbnail_url TEXT, sort_order INTEGER NOT NULL DEFAULT 0, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, uploaded_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 4. Edge Communication Events ──────────────────────────────────────────── CREATE TABLE IF NOT EXISTS edge_communication_events ( event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, lead_id TEXT NOT NULL, channel TEXT NOT NULL CHECK (channel IN ('pstn','whatsapp_message','whatsapp_voice', 'whatsapp_video','email','facebook_message', 'instagram_message','in_app_voip','manual_note')), direction TEXT NOT NULL CHECK (direction IN ('inbound','outbound')), provider TEXT, -- twilio, vonage, meta, etc. capture_mode TEXT NOT NULL CHECK (capture_mode IN ('direct_api','provider_routed','operator_import','operator_note')), consent_state TEXT NOT NULL DEFAULT 'unknown' CHECK (consent_state IN ('unknown','granted','denied','not_required')), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), duration_seconds INTEGER, summary TEXT, raw_reference TEXT, -- provider message/call ID recording_ref TEXT, -- storage path or URL provider_metadata JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS edge_communication_memory_facts ( fact_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, lead_id TEXT NOT NULL, event_id UUID REFERENCES edge_communication_events(event_id), fact_type TEXT NOT NULL CHECK (fact_type IN ('promise','preference','follow_up_date', 'objection','interest_signal','budget','timeline', 'constraint','decision_maker_note','custom')), fact_text TEXT NOT NULL, effective_date DATE, confidence NUMERIC(4,3) NOT NULL DEFAULT 1.0 CHECK (confidence BETWEEN 0 AND 1), extracted_from TEXT NOT NULL CHECK (extracted_from IN ('transcript','message_thread','operator_note','import')), is_confirmed BOOLEAN NOT NULL DEFAULT FALSE, confirmed_by TEXT, confirmed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 5. Transcription Jobs and Segments ────────────────────────────────────── CREATE TABLE IF NOT EXISTS edge_transcription_jobs ( transcription_job_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, event_id UUID NOT NULL REFERENCES edge_communication_events(event_id) ON DELETE CASCADE, media_type TEXT NOT NULL CHECK (media_type IN ('audio','video')), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','queued','processing','completed','failed')), transcript_ref TEXT, -- storage path to diarized JSON provider TEXT NOT NULL DEFAULT 'nemoclaw', consent_state TEXT NOT NULL DEFAULT 'unknown' CHECK (consent_state IN ('unknown','granted','denied')), speaker_count INTEGER, word_count INTEGER, language TEXT NOT NULL DEFAULT 'en', error_message TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS edge_transcript_segments ( segment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), transcription_job_id UUID NOT NULL REFERENCES edge_transcription_jobs(transcription_job_id) ON DELETE CASCADE, event_id UUID NOT NULL REFERENCES edge_communication_events(event_id), speaker_label TEXT NOT NULL, -- SPEAKER_00, SPEAKER_01, etc. start_ms INTEGER NOT NULL, end_ms INTEGER NOT NULL, text TEXT NOT NULL, confidence NUMERIC(4,3) NOT NULL DEFAULT 1.0, is_agent_turn BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 6. User Calendar Events ───────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS user_calendar_events ( calendar_event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, owner_user_id TEXT NOT NULL, lead_id TEXT, source_event_id UUID REFERENCES edge_communication_events(event_id), title TEXT NOT NULL, description TEXT, start_at TIMESTAMPTZ NOT NULL, end_at TIMESTAMPTZ NOT NULL, all_day BOOLEAN NOT NULL DEFAULT FALSE, status TEXT NOT NULL DEFAULT 'confirmed' CHECK (status IN ('tentative','confirmed','done','cancelled')), reminder_minutes INTEGER[] NOT NULL DEFAULT '{15}'::INTEGER[], created_by TEXT NOT NULL CHECK (created_by IN ('user','nemoclaw_suggested','operator_import')), is_nemoclaw_confirmed BOOLEAN NOT NULL DEFAULT FALSE, location TEXT, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 7. Insight Recommendations ────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS insight_recommendations ( recommendation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, lead_id TEXT NOT NULL, source_event_id UUID REFERENCES edge_communication_events(event_id), recommendation_type TEXT NOT NULL CHECK (recommendation_type IN ('follow_up_call','send_message', 'schedule_meeting','update_crm', 'update_qd_score','send_property_info', 'escalate','custom')), summary TEXT NOT NULL, suggested_action TEXT NOT NULL, target_system TEXT NOT NULL CHECK (target_system IN ('crm','calendar','qd_score','whatsapp','email','operator')), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','accepted','dismissed','acted_upon')), confidence NUMERIC(4,3) NOT NULL DEFAULT 0.8, acted_by TEXT, acted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 8. Admin Action Events ─────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS admin_action_events ( action_event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, action_id TEXT NOT NULL UNIQUE, -- idempotency key from client action_type TEXT NOT NULL CHECK (action_type IN ( 'user_create','user_deactivate','user_role_change', 'tenant_config_update','inventory_batch_approve', 'inventory_batch_reject','template_publish','template_archive', 'synthetic_job_trigger','synthetic_job_cancel', 'system_health_check','queue_drain','debug_event_export', 'install_register','install_deregister' )), target_type TEXT NOT NULL, target_id TEXT NOT NULL, requested_by TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}'::JSONB, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','processing','completed','failed','rejected')), result_message TEXT, result_artifacts JSONB NOT NULL DEFAULT '[]'::JSONB, executed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── 9. Surface Sessions (cross-surface telemetry) ─────────────────────────── CREATE TABLE IF NOT EXISTS surface_sessions ( session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL, user_id TEXT NOT NULL, surface_type TEXT NOT NULL CHECK (surface_type IN ('webos','ipad','android_tablet', 'iphone_edge','android_phone_edge')), app_version TEXT NOT NULL, started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ended_at TIMESTAMPTZ, last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), screen_sequence TEXT[] NOT NULL DEFAULT '{}', metadata JSONB NOT NULL DEFAULT '{}'::JSONB ); -- ─── Indexes ────────────────────────────────────────────────────────────────── -- Template taxonomy CREATE INDEX IF NOT EXISTS idx_tmpl_chapters_tenant ON oracle_template_chapters(tenant_id, is_active); CREATE INDEX IF NOT EXISTS idx_tmpl_subchapters_chapter ON oracle_template_subchapters(chapter_id, is_active); CREATE INDEX IF NOT EXISTS idx_tmpl_seed_examples_template ON oracle_template_seed_examples(template_id); CREATE INDEX IF NOT EXISTS idx_tmpl_seed_examples_chapter ON oracle_template_seed_examples(chapter_id); -- Synthetic jobs CREATE INDEX IF NOT EXISTS idx_synthetic_jobs_tenant ON oracle_synthetic_generation_jobs(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_synthetic_jobs_template ON oracle_synthetic_generation_jobs(template_id); -- Inventory CREATE INDEX IF NOT EXISTS idx_inv_batches_tenant ON inventory_import_batches(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_inv_props_tenant ON inventory_properties(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_inv_props_batch ON inventory_properties(batch_id); CREATE INDEX IF NOT EXISTS idx_inv_media_property ON inventory_media_assets(property_id); -- Edge communication CREATE INDEX IF NOT EXISTS idx_edge_events_lead ON edge_communication_events(tenant_id, lead_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_edge_events_channel ON edge_communication_events(channel, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_edge_memory_lead ON edge_communication_memory_facts(tenant_id, lead_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_edge_memory_event ON edge_communication_memory_facts(event_id); -- Transcription CREATE INDEX IF NOT EXISTS idx_transcription_jobs_event ON edge_transcription_jobs(event_id); CREATE INDEX IF NOT EXISTS idx_transcription_jobs_status ON edge_transcription_jobs(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_transcript_segments_job ON edge_transcript_segments(transcription_job_id, start_ms); -- Calendar CREATE INDEX IF NOT EXISTS idx_calendar_events_owner ON user_calendar_events(tenant_id, owner_user_id, start_at); CREATE INDEX IF NOT EXISTS idx_calendar_events_lead ON user_calendar_events(lead_id, start_at); -- Insights CREATE INDEX IF NOT EXISTS idx_insights_lead ON insight_recommendations(tenant_id, lead_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_insights_status ON insight_recommendations(status, created_at DESC); -- Admin CREATE INDEX IF NOT EXISTS idx_admin_actions_tenant ON admin_action_events(tenant_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_admin_actions_type ON admin_action_events(action_type, status); -- Surface sessions CREATE INDEX IF NOT EXISTS idx_surface_sessions_user ON surface_sessions(tenant_id, user_id, started_at DESC); CREATE INDEX IF NOT EXISTS idx_surface_sessions_type ON surface_sessions(surface_type, started_at DESC);