FleetOS Emergent Contacts Migration Plan
π― Objective
π Phase 1: Database Migration
Step 1.1: Create Migration File
-- ============================================
-- FLEETOS EMERGENT CONTACTS MIGRATION
-- Implements BIBLE-compliant architecture:
-- - Companies own the work
-- - Contacts are emergent actors (not owners)
-- Safe to run: Uses IF NOT EXISTS
-- ============================================
-- ============================================
-- PART 1: CONTACTS TABLE (Emergent)
-- ============================================
CREATE TABLE IF NOT EXISTS contacts (
contact_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_id UUID NOT NULL REFERENCES companies(company_id) ON DELETE CASCADE,
contact_value TEXT UNIQUE NOT NULL, -- Canonical email or phone
contact_type TEXT CHECK (contact_type IN ('email', 'phone')),
display_name TEXT,
first_seen_at TIMESTAMP DEFAULT NOW(),
last_seen_at TIMESTAMP DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_contacts_company ON contacts(company_id);
CREATE INDEX IF NOT EXISTS idx_contacts_value ON contacts(contact_value);
CREATE INDEX IF NOT EXISTS idx_contacts_last_seen ON contacts(last_seen_at DESC);
COMMENT ON TABLE contacts IS 'Emergent contacts - created on first access, no signup required';
COMMENT ON COLUMN contacts.contact_value IS 'Canonical email (lowercase) or phone (E.164 format)';
COMMENT ON COLUMN contacts.first_seen_at IS 'When this contact first accessed FleetOS';
COMMENT ON COLUMN contacts.last_seen_at IS 'Most recent access (for "last active" routing)';
-- ============================================
-- PART 2: ADD COMPANY COLUMNS TO SERVICE_REQUESTS
-- ============================================
-- Add fleet_company_id (companies own the work)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name='service_requests' AND column_name='fleet_company_id'
) THEN
ALTER TABLE service_requests
ADD COLUMN fleet_company_id UUID REFERENCES companies(company_id);
RAISE NOTICE 'β
Added fleet_company_id column';
ELSE
RAISE NOTICE 'βΉοΈ fleet_company_id already exists';
END IF;
END $$;
-- Add supplier_company_id (companies own the work)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name='service_requests' AND column_name='supplier_company_id'
) THEN
ALTER TABLE service_requests
ADD COLUMN supplier_company_id UUID REFERENCES companies(company_id);
RAISE NOTICE 'β
Added supplier_company_id column';
ELSE
RAISE NOTICE 'βΉοΈ supplier_company_id already exists';
END IF;
END $$;
-- Add submitted_by_contact_id (emergent actor, not owner)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name='service_requests' AND column_name='submitted_by_contact_id'
) THEN
ALTER TABLE service_requests
ADD COLUMN submitted_by_contact_id UUID REFERENCES contacts(contact_id);
RAISE NOTICE 'β
Added submitted_by_contact_id column';
ELSE
RAISE NOTICE 'βΉοΈ submitted_by_contact_id already exists';
END IF;
END $$;
-- Add assigned_to_contact_id (emergent actor, not owner)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name='service_requests' AND column_name='assigned_to_contact_id'
) THEN
ALTER TABLE service_requests
ADD COLUMN assigned_to_contact_id UUID REFERENCES contacts(contact_id);
RAISE NOTICE 'β
Added assigned_to_contact_id column';
ELSE
RAISE NOTICE 'βΉοΈ assigned_to_contact_id already exists';
END IF;
END $$;
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_service_requests_fleet_company
ON service_requests(fleet_company_id);
CREATE INDEX IF NOT EXISTS idx_service_requests_supplier_company
ON service_requests(supplier_company_id);
CREATE INDEX IF NOT EXISTS idx_service_requests_submitted_by
ON service_requests(submitted_by_contact_id);
CREATE INDEX IF NOT EXISTS idx_service_requests_assigned_to
ON service_requests(assigned_to_contact_id);
-- ============================================
-- PART 3: EVENT LOG (Audit Trail)
-- ============================================
CREATE TABLE IF NOT EXISTS event_log (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type TEXT NOT NULL,
company_id UUID REFERENCES companies(company_id) ON DELETE CASCADE,
contact_id UUID REFERENCES contacts(contact_id) ON DELETE SET NULL,
target_request_id UUID REFERENCES service_requests(request_id) ON DELETE CASCADE,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_event_log_company ON event_log(company_id);
CREATE INDEX IF NOT EXISTS idx_event_log_contact ON event_log(contact_id);
CREATE INDEX IF NOT EXISTS idx_event_log_created ON event_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_event_log_type ON event_log(event_type);
COMMENT ON TABLE event_log IS 'Audit trail - tracks every access and action';
COMMENT ON COLUMN event_log.contact_id IS 'Which emergent contact acted';
-- ============================================
-- PART 4: HELPER FUNCTIONS
-- ============================================
-- Function: Get or create emergent contact
CREATE OR REPLACE FUNCTION get_or_create_contact(
p_contact_value TEXT,
p_company_id UUID,
p_contact_type TEXT,
p_display_name TEXT DEFAULT NULL
) RETURNS UUID AS $$
DECLARE
v_contact_id UUID;
BEGIN
-- Try to find existing contact
SELECT contact_id INTO v_contact_id
FROM contacts
WHERE contact_value = LOWER(TRIM(p_contact_value));
-- If not found, create emergent contact
IF v_contact_id IS NULL THEN
INSERT INTO contacts (
company_id,
contact_value,
contact_type,
display_name,
first_seen_at,
last_seen_at
) VALUES (
p_company_id,
LOWER(TRIM(p_contact_value)),
p_contact_type,
p_display_name,
NOW(),
NOW()
)
RETURNING contact_id INTO v_contact_id;
RAISE NOTICE 'β¨ Emergent contact created: %', p_contact_value;
ELSE
-- Update last_seen for existing contact
UPDATE contacts
SET last_seen_at = NOW()
WHERE contact_id = v_contact_id;
END IF;
RETURN v_contact_id;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION get_or_create_contact IS 'Get existing or create emergent contact on first access';
-- Function: Get last active contact for company
CREATE OR REPLACE FUNCTION get_last_active_contact(p_company_id UUID)
RETURNS TABLE(contact_id UUID, contact_value TEXT, last_seen_at TIMESTAMP) AS $$
BEGIN
RETURN QUERY
SELECT c.contact_id, c.contact_value, c.last_seen_at
FROM contacts c
WHERE c.company_id = p_company_id
ORDER BY c.last_seen_at DESC
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION get_last_active_contact IS 'Find last active contact for org-level fallback notifications';
-- ============================================
-- PART 5: UPDATE STATUS ENUM
-- ============================================
-- Drop existing constraint if exists
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.constraint_column_usage
WHERE constraint_name = 'valid_status'
) THEN
ALTER TABLE service_requests DROP CONSTRAINT valid_status;
RAISE NOTICE 'β
Dropped old status constraint';
END IF;
END $$;
-- Add new BIBLE-compliant status constraint
ALTER TABLE service_requests
ADD CONSTRAINT valid_status
CHECK (status IN ('new', 'acknowledged', 'in_progress', 'completed', 'declined'));
RAISE NOTICE 'β
Added BIBLE-compliant status constraint (new β acknowledged β in_progress β completed/declined)';
-- ============================================
-- MIGRATION COMPLETE
-- ============================================
DO $$
BEGIN
RAISE NOTICE '========================================';
RAISE NOTICE 'β
FLEETOS EMERGENT CONTACTS MIGRATION COMPLETE';
RAISE NOTICE '========================================';
RAISE NOTICE '';
RAISE NOTICE 'Created:';
RAISE NOTICE ' β
contacts table (emergent actors)';
RAISE NOTICE ' β
event_log table (audit trail)';
RAISE NOTICE '';
RAISE NOTICE 'Added to service_requests:';
RAISE NOTICE ' β
fleet_company_id (companies own work)';
RAISE NOTICE ' β
supplier_company_id (companies own work)';
RAISE NOTICE ' β
submitted_by_contact_id (emergent actor)';
RAISE NOTICE ' β
assigned_to_contact_id (emergent actor)';
RAISE NOTICE '';
RAISE NOTICE 'Helper functions:';
RAISE NOTICE ' β
get_or_create_contact() - emergent contact creation';
RAISE NOTICE ' β
get_last_active_contact() - for fallback routing';
RAISE NOTICE '';
RAISE NOTICE 'Status lifecycle: new β acknowledged β in_progress β completed/declined';
RAISE NOTICE '';
RAISE NOTICE 'β οΈ NEXT: Update application code to use new architecture';
RAISE NOTICE '========================================';
END $$;Step 1.2: Verify Migration
π Phase 2: Code Changes
Step 2.1: Create ContactService
Step 2.2: Create EventLogService
Step 2.3: Update MagicLinkService
Step 2.4: Update AuthService.sendDailyAccessLink()
Step 2.5: Update QueryService.createServiceRequest()
Step 2.6: Update Seed Functions
Step 2.7: Update Signup Handlers
Step 2.8: Update Dashboard Queries
π§ͺ Phase 3: Testing Checklist
Pre-Migration Tests
Migration Tests
Code Tests
Verification Queries
π Phase 4: Rollback Strategy
If Migration Fails
β
Success Criteria
π Timeline
Phase
Task
Time
π Execution Order
π Files Changed Summary
New Files (3):
Modified Files (5):
β
Ready to Execute
Last updated
Was this helpful?