/** * db.js — PostgreSQL connection pool + schema initialisation for Curio HMS. * Tables: tenants, users, pending_registrations */ const { Pool } = require('pg'); const bcrypt = require('bcryptjs'); const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgres://postgres:curio_secret@curio-db:5432/curio', ssl: false, }); // ── Schema ────────────────────────────────────────────────────────────────── const SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS tenants ( id TEXT PRIMARY KEY, name TEXT NOT NULL, logo TEXT DEFAULT 'C', primary_color TEXT DEFAULT '#0F172A', secondary_color TEXT DEFAULT '#38BDF8', consultation_fee INTEGER DEFAULT 500, currency TEXT DEFAULT '₹', plan TEXT DEFAULT 'basic', status TEXT DEFAULT 'active', whatsapp_number TEXT, specialty TEXT, city TEXT, working_hours JSONB DEFAULT '[{"start":"09:00","end":"13:00"},{"start":"17:00","end":"22:00"}]', days_off JSONB DEFAULT '[0]', holidays JSONB DEFAULT '[]', services_catalog JSONB DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, name TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ('SUPER_ADMIN','OWNER','DOCTOR','RECEPTIONIST','PHARMACIST','LAB_TECH','PATIENT')), profile_media JSONB DEFAULT '{}', q_and_a JSONB DEFAULT '[]', blocked_dates JSONB DEFAULT '[]', status TEXT DEFAULT 'active', created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS pending_registrations ( id SERIAL PRIMARY KEY, owner_name TEXT NOT NULL, clinic_name TEXT NOT NULL, specialty TEXT, city TEXT, whatsapp_number TEXT NOT NULL, email TEXT NOT NULL, password_hash TEXT NOT NULL, plan TEXT DEFAULT 'basic', status TEXT DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS transactions ( id SERIAL PRIMARY KEY, order_id TEXT NOT NULL UNIQUE, payment_id TEXT, amount INTEGER NOT NULL, currency TEXT DEFAULT 'INR', status TEXT DEFAULT 'created', tenant_id TEXT REFERENCES tenants(id), user_id INTEGER REFERENCES users(id), created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE IF NOT EXISTS agent_configs ( id SERIAL PRIMARY KEY, tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE, agent_type TEXT NOT NULL CHECK (agent_type IN ('BILLING', 'TRIAGE', 'FOLLOWUP', 'INVENTORY', 'CDSS')), is_active BOOLEAN DEFAULT false, config JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT now(), UNIQUE(tenant_id, agent_type) ); CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_pending_status ON pending_registrations(status); CREATE INDEX IF NOT EXISTS idx_transactions_order ON transactions(order_id); CREATE INDEX IF NOT EXISTS idx_agent_configs_tenant ON agent_configs(tenant_id); -- Migrations for existing DBs ALTER TABLE tenants ADD COLUMN IF NOT EXISTS services_catalog JSONB DEFAULT '[]'; ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_media JSONB DEFAULT '{}'; ALTER TABLE users ADD COLUMN IF NOT EXISTS q_and_a JSONB DEFAULT '[]'; ALTER TABLE users ADD COLUMN IF NOT EXISTS blocked_dates JSONB DEFAULT '[]'; ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_check; ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('SUPER_ADMIN','OWNER','DOCTOR','RECEPTIONIST','PHARMACIST','LAB_TECH','PATIENT')); `; // ── Seed super admin ───────────────────────────────────────────────────────── async function seedSuperAdmin() { const email = process.env.SUPER_ADMIN_EMAIL || 'admin@curio.app'; const password = process.env.SUPER_ADMIN_PASSWORD || 'superadmin'; const existing = await pool.query('SELECT id FROM users WHERE email = $1', [email]); if (existing.rows.length > 0) return; const hash = await bcrypt.hash(password, 10); await pool.query( `INSERT INTO users (tenant_id, email, password_hash, name, role) VALUES (NULL, $1, $2, 'Super Admin', 'SUPER_ADMIN')`, [email, hash] ); console.log(`[DB] Super admin seeded: ${email}`); } // ── Seed legacy in-memory tenants into DB ───────────────────────────────────── async function seedLegacyTenants() { const legacy = [ { id: 'default', name: 'Curio Clinic', logo: 'C', primary_color: '#0F172A', secondary_color: '#38BDF8', consultation_fee: 500, plan: 'basic', working_hours: JSON.stringify([{ start: '09:00', end: '13:00' }, { start: '17:00', end: '22:00' }]), days_off: JSON.stringify([0]), }, { id: 'sharma-clinic', name: "Dr. Sharma's Cardiology", logo: 'S', primary_color: '#1E3A8A', secondary_color: '#60A5FA', consultation_fee: 800, plan: 'pro', working_hours: JSON.stringify([{ start: '10:00', end: '14:00' }, { start: '18:00', end: '21:00' }]), days_off: JSON.stringify([0, 6]), }, { id: 'apollo-hospitals', name: 'Apollo Multispecialty', logo: 'A', primary_color: '#065F46', secondary_color: '#34D399', consultation_fee: 1200, plan: 'enterprise', working_hours: JSON.stringify([{ start: '09:00', end: '22:00' }]), days_off: JSON.stringify([]), }, ]; for (const t of legacy) { await pool.query( `INSERT INTO tenants (id, name, logo, primary_color, secondary_color, consultation_fee, plan, working_hours, days_off) VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9::jsonb) ON CONFLICT (id) DO NOTHING`, [t.id, t.name, t.logo, t.primary_color, t.secondary_color, t.consultation_fee, t.plan, t.working_hours, t.days_off] ); } console.log('[DB] Legacy tenants seeded.'); } // ── Init ────────────────────────────────────────────────────────────────────── async function initSchema() { try { await pool.query(SCHEMA_SQL); console.log('[DB] Schema ready.'); await seedSuperAdmin(); await seedLegacyTenants(); } catch (err) { console.error('[DB] Schema init failed:', err.message); // Don't crash — let server start even if DB is temporarily unavailable } } // ── Exports ─────────────────────────────────────────────────────────────────── function query(sql, params) { return pool.query(sql, params); } module.exports = { query, pool, initSchema };