import 'dotenv/config' import { getDb } from './client.js' const sql = getDb() const migrate = async () => { await sql` CREATE EXTENSION IF NOT EXISTS "pgcrypto" ` await sql` CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL UNIQUE, permissions JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS customers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), firebase_uid VARCHAR(255) UNIQUE, phone VARCHAR(20) UNIQUE, display_name VARCHAR(100) NOT NULL, is_anonymous BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS mitras ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), firebase_uid VARCHAR(255) UNIQUE, phone VARCHAR(20) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS control_center_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), firebase_uid VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, role_id UUID NOT NULL REFERENCES roles(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS app_config ( key VARCHAR(100) PRIMARY KEY, value JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` INSERT INTO app_config (key, value) VALUES ('anonymity', '{"enabled": false}') ON CONFLICT (key) DO NOTHING ` // --- Phase 2: Mitra Online Status & Pairing --- await sql` CREATE TABLE IF NOT EXISTS mitra_online_status ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), mitra_id UUID NOT NULL UNIQUE REFERENCES mitras(id), is_online BOOLEAN NOT NULL DEFAULT FALSE, last_online_at TIMESTAMPTZ, last_offline_at TIMESTAMPTZ, last_heartbeat_at TIMESTAMPTZ, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS mitra_online_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), mitra_id UUID NOT NULL REFERENCES mitras(id), status VARCHAR(10) NOT NULL, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_mitra_online_logs_mitra_id ON mitra_online_logs (mitra_id) ` await sql` CREATE TABLE IF NOT EXISTS chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customers(id), mitra_id UUID REFERENCES mitras(id), status VARCHAR(30) NOT NULL DEFAULT 'searching', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), paired_at TIMESTAMPTZ, ended_at TIMESTAMPTZ, ended_by VARCHAR(20) ) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_customer_id ON chat_sessions (customer_id) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_mitra_id ON chat_sessions (mitra_id) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_status ON chat_sessions (status) ` // Composite index for the per-mitra active-session count subquery used by the // 5s availability poll and the per-blast capacity filter. await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_mitra_status ON chat_sessions (mitra_id, status) ` await sql` CREATE TABLE IF NOT EXISTS chat_request_notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), mitra_id UUID NOT NULL REFERENCES mitras(id), notified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), response VARCHAR(20), responded_at TIMESTAMPTZ ) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_request_notifications_session_id ON chat_request_notifications (session_id) ` await sql` INSERT INTO app_config (key, value) VALUES ('max_customers_per_mitra', '{"value": 3}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3: Chat Engine --- // Add session duration/pricing columns to chat_sessions await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS duration_minutes INT, ADD COLUMN IF NOT EXISTS price INT DEFAULT 0, ADD COLUMN IF NOT EXISTS is_free_trial BOOLEAN NOT NULL DEFAULT FALSE, ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS extended_minutes INT NOT NULL DEFAULT 0 ` // Add FCM token columns await sql` ALTER TABLE customers ADD COLUMN IF NOT EXISTS fcm_token VARCHAR(255) ` await sql` ALTER TABLE mitras ADD COLUMN IF NOT EXISTS fcm_token VARCHAR(255) ` await sql` CREATE TABLE IF NOT EXISTS chat_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), sender_type VARCHAR(10) NOT NULL, sender_id UUID NOT NULL, type VARCHAR(20) NOT NULL DEFAULT 'text', content TEXT NOT NULL, metadata JSONB, status VARCHAR(20) NOT NULL DEFAULT 'sent', delivered_at TIMESTAMPTZ, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_messages_session_created ON chat_messages (session_id, created_at) ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_messages_session_status ON chat_messages (session_id, status) ` await sql` CREATE TABLE IF NOT EXISTS session_closures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), user_type VARCHAR(10) NOT NULL, user_id UUID NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE TABLE IF NOT EXISTS session_extensions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id), requested_duration_minutes INT NOT NULL, requested_price INT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), responded_at TIMESTAMPTZ ) ` await sql` CREATE TABLE IF NOT EXISTS customer_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customers(id), session_id UUID NOT NULL REFERENCES chat_sessions(id), type VARCHAR(20) NOT NULL, amount INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_customer_transactions_customer_id ON customer_transactions (customer_id) ` // Phase 3 config keys await sql` INSERT INTO app_config (key, value) VALUES ('free_trial_enabled', '{"value": true}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('free_trial_duration_minutes', '{"value": 5}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('extension_timeout_seconds', '{"value": 60}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('early_end_mitra_enabled', '{"value": false}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('early_end_customer_enabled', '{"value": false}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('price_tiers', ${sql.json({ tiers: [ { duration_minutes: 1, price: 5000, label: '1 Menit (Test)' }, { duration_minutes: 15, price: 30000, label: '15 Menit' }, { duration_minutes: 30, price: 60000, label: '30 Menit' }, { duration_minutes: 45, price: 100000, label: '45 Menit' }, { duration_minutes: 60, price: 150000, label: '60 Menit' }, { duration_minutes: 1440, price: 250000, label: '24 Jam' }, ]})}) ON CONFLICT (key) DO NOTHING ` // --- Phase 3.1: Mitra Ping Config --- await sql` INSERT INTO app_config (key, value) VALUES ('require_mitra_ping', '{"value": true}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('mitra_ping_interval_seconds', '{"value": 15}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3.2: Mitra Request Activity Log --- await sql` ALTER TABLE chat_request_notifications ADD COLUMN IF NOT EXISTS active_session_count INT NOT NULL DEFAULT 0 ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_request_notifications_mitra_notified ON chat_request_notifications (mitra_id, notified_at) ` // --- Phase 3.3: Session Topic Sensitivity --- await sql` ALTER TABLE chat_sessions ADD COLUMN IF NOT EXISTS topic_sensitivity VARCHAR(16) NOT NULL DEFAULT 'regular' ` await sql` CREATE INDEX IF NOT EXISTS idx_chat_sessions_topic_sensitivity ON chat_sessions (topic_sensitivity) ` await sql` CREATE TABLE IF NOT EXISTS session_sensitivity_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE, changed_by_mitra_id UUID NOT NULL REFERENCES mitras(id), from_value VARCHAR(16) NOT NULL, to_value VARCHAR(16) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` await sql` CREATE INDEX IF NOT EXISTS idx_session_sensitivity_log_session ON session_sensitivity_log (session_id) ` await sql` INSERT INTO app_config (key, value) VALUES ('sensitive_flip_confirmation_enabled', '{"value": true}') ON CONFLICT (key) DO NOTHING ` await sql` INSERT INTO app_config (key, value) VALUES ('sensitive_flag_one_way_latch', '{"value": false}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3.4: Self-Managed Auth --- // Customers: add social identity columns await sql` ALTER TABLE customers ADD COLUMN IF NOT EXISTS email VARCHAR(255), ADD COLUMN IF NOT EXISTS google_sub VARCHAR(255), ADD COLUMN IF NOT EXISTS apple_sub VARCHAR(255) ` await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_google_sub ON customers (google_sub) WHERE google_sub IS NOT NULL` await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_apple_sub ON customers (apple_sub) WHERE apple_sub IS NOT NULL` // display_name is set after sign-in via the set-display-name screen for // direct phone/Google/Apple sign-ups (no anonymous bootstrap). Allow null. await sql`ALTER TABLE customers ALTER COLUMN display_name DROP NOT NULL` // Control center users: password-based auth columns // firebase_uid stays for backward compat during migration; will be dropped in a later cleanup migration await sql`ALTER TABLE control_center_users ALTER COLUMN firebase_uid DROP NOT NULL` await sql` ALTER TABLE control_center_users ADD COLUMN IF NOT EXISTS password_hash VARCHAR(60) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS failed_login_count INT NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS lockout_until TIMESTAMPTZ ` // Auth sessions (refresh tokens + multi-device) await sql` CREATE TABLE IF NOT EXISTS auth_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_type VARCHAR(16) NOT NULL, user_id UUID NOT NULL, refresh_token_hash VARCHAR(60) NOT NULL, device_info JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ ) ` await sql`CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions (user_type, user_id)` await sql`CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions (expires_at) WHERE revoked_at IS NULL` // OTP requests (Fazpass reference + rate-limit tracking) await sql` CREATE TABLE IF NOT EXISTS otp_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), phone VARCHAR(20) NOT NULL, ip_address VARCHAR(45), user_type VARCHAR(16) NOT NULL, fazpass_reference VARCHAR(255), channel VARCHAR(16), attempts INT NOT NULL DEFAULT 0, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL ) ` await sql`CREATE INDEX IF NOT EXISTS idx_otp_requests_phone_created ON otp_requests (phone, created_at)` await sql`CREATE INDEX IF NOT EXISTS idx_otp_requests_ip_created ON otp_requests (ip_address, created_at)` // Auth-related app_config defaults await sql` INSERT INTO app_config (key, value) VALUES ('otp_max_per_phone_per_hour', '{"value": 3}'), ('otp_max_per_ip_per_hour', '{"value": 10}'), ('otp_resend_cooldown_seconds', '{"value": 60}'), ('otp_verify_max_attempts', '{"value": 5}'), ('cc_login_max_attempts', '{"value": 5}'), ('cc_login_lockout_minutes', '{"value": 15}') ON CONFLICT (key) DO NOTHING ` // --- Phase 3.7: Paid Pairing Flow + Returning-Chat + Extension Flip --- // payment_sessions: customer-initiated payment intents (mocked) that gate pairing await sql` CREATE TABLE IF NOT EXISTS payment_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customers(id), amount INTEGER NOT NULL DEFAULT 0, duration_minutes INTEGER NOT NULL, is_free_trial BOOLEAN NOT NULL DEFAULT false, is_extension BOOLEAN NOT NULL DEFAULT false, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','confirmed','consumed','failed_pairing','abandoned','expired')), targeted_mitra_id UUID REFERENCES mitras(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), confirmed_at TIMESTAMPTZ, consumed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL ) ` await sql` CREATE INDEX IF NOT EXISTS idx_payment_sessions_customer ON payment_sessions (customer_id) ` await sql` CREATE INDEX IF NOT EXISTS idx_payment_sessions_status_expires ON payment_sessions (status, expires_at) ` // pairing_failures: cause-tagged audit rows for confirmed payments that did not yield a chat await sql` CREATE TABLE IF NOT EXISTS pairing_failures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), payment_session_id UUID NOT NULL REFERENCES payment_sessions(id) ON DELETE CASCADE, customer_id UUID NOT NULL REFERENCES customers(id), targeted_mitra_id UUID REFERENCES mitras(id), cause_tag TEXT NOT NULL CHECK (cause_tag IN ( 'no_mitra_available', 'all_mitras_rejected', 'targeted_mitra_offline', 'targeted_mitra_rejected', 'targeted_mitra_timeout', 'payment_session_expired', 'customer_cancelled' )), amount INTEGER NOT NULL, operator_action TEXT CHECK (operator_action IS NULL OR operator_action IN ('refunded','credited','no_action')), actioned_by UUID REFERENCES control_center_users(id), actioned_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) ` // Phase 3.7 follow-up: extend the pairing_failures.cause_tag CHECK to include the two // extension-specific tags. Idempotent: drop the existing check (whatever its exact name) and // re-add the expanded list. Postgres auto-names CHECK constraints `