/** * Postgres migration set (D19). Numbered, applied in order by db-pg.ts's runner, * each inside its own transaction, recorded in schema_migrations. Inline strings * (not .sql files) so esbuild bundles them into dist/server.cjs with no file I/O. * * Translation choices vs the SQLite SCHEMA (deliberate, recorded in the D19 spec): * - every *_paise / money column is BIGINT (int4 overflows at ₹2.15 crore in paise); * - 0/1 flags stay INTEGER — repos say `active=1` and that SQL must be identical on * both engines (boolean would break it); * - JSON payloads stay TEXT — node-pg auto-parses json/jsonb columns into objects, * which would break the repos' JSON.parse at the edge. A jsonb ALTER is a later, * pg-only optimisation if we ever index into payloads; * - dates stay TEXT ISO (house convention, index-friendly, no timezone surprises); * - the reminder.rule_kind CHECK is born in its final widened form (SQLite reaches * it via the guarded table rebuild; Postgres starts correct). */ export interface PgMigration { id: string; sql: string } export const PG_MIGRATIONS: PgMigration[] = [ { id: '001-init', sql: ` CREATE TABLE IF NOT EXISTS staff_user ( id text PRIMARY KEY, email text NOT NULL UNIQUE, display_name text NOT NULL, role text NOT NULL CHECK (role IN ('owner','manager','staff')), phone text, title text, pw_salt text NOT NULL, pw_hash text NOT NULL, active integer NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS session ( token text PRIMARY KEY, staff_id text NOT NULL, expires_at text NOT NULL ); CREATE TABLE IF NOT EXISTS client ( id text PRIMARY KEY, code text NOT NULL UNIQUE, name text NOT NULL, gstin text, state_code text NOT NULL DEFAULT '32', address text NOT NULL DEFAULT '', contacts text NOT NULL DEFAULT '[]', status text NOT NULL DEFAULT 'active' CHECK (status IN ('lead','active','dormant','lost')), owner_id text, anydesk text, os text, district text, sector text, db_password_enc text, notes text NOT NULL DEFAULT '', source text NOT NULL DEFAULT 'hq', created_at text NOT NULL ); CREATE TABLE IF NOT EXISTS module ( id text PRIMARY KEY, code text NOT NULL UNIQUE, name text NOT NULL, sac text NOT NULL DEFAULT '998313', allowed_kinds text NOT NULL DEFAULT '["one_time","monthly","yearly","usage"]', multi_subscription integer NOT NULL DEFAULT 0, active integer NOT NULL DEFAULT 1, quote_content text NOT NULL DEFAULT '[]' ); CREATE TABLE IF NOT EXISTS module_price_book ( id text PRIMARY KEY, module_id text NOT NULL, edition text NOT NULL DEFAULT 'standard', kind text NOT NULL, price_paise bigint NOT NULL, effective_from text NOT NULL ); CREATE TABLE IF NOT EXISTS client_module ( id text PRIMARY KEY, client_id text NOT NULL, module_id text NOT NULL, status text NOT NULL DEFAULT 'quoted' CHECK (status IN ('quoted','ordered','installing','installed','trained','live','expired','cancelled')), kind text NOT NULL, edition text NOT NULL DEFAULT 'standard', installed_on text, completed_on text, trained_on text, next_renewal text, active integer NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS tax_class ( class_code text NOT NULL, rate_pct_bp integer NOT NULL, cess_pct_bp integer NOT NULL DEFAULT 0, effective_from text NOT NULL, effective_to text ); CREATE TABLE IF NOT EXISTS doc_series ( doc_type text NOT NULL, fy text NOT NULL, prefix text NOT NULL, next_seq integer NOT NULL, PRIMARY KEY (doc_type, fy) ); CREATE TABLE IF NOT EXISTS document ( id text PRIMARY KEY, doc_type text NOT NULL CHECK (doc_type IN ('QUOTATION','PROFORMA','INVOICE','RECEIPT','CREDIT_NOTE')), doc_no text, fy text NOT NULL, client_id text NOT NULL, doc_date text NOT NULL, due_date text, status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','sent','accepted','invoiced','part_paid','paid','lost','cancelled')), ref_doc_id text, taxable_paise bigint NOT NULL, cgst_paise bigint NOT NULL, sgst_paise bigint NOT NULL, igst_paise bigint NOT NULL, round_off_paise bigint NOT NULL, payable_paise bigint NOT NULL, payload text NOT NULL, source text NOT NULL DEFAULT 'hq', created_by text NOT NULL, created_at text NOT NULL, UNIQUE (doc_no) ); CREATE TABLE IF NOT EXISTS document_event ( id text PRIMARY KEY, document_id text NOT NULL, at_wall text NOT NULL, kind text NOT NULL, meta text NOT NULL DEFAULT '{}' ); CREATE TABLE IF NOT EXISTS document_share ( id text PRIMARY KEY, document_id text NOT NULL, token text NOT NULL UNIQUE, created_by text NOT NULL, created_at text NOT NULL, expires_at text, revoked integer NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS payment ( id text PRIMARY KEY, client_id text NOT NULL, received_on text NOT NULL, mode text NOT NULL CHECK (mode IN ('bank','upi','cheque','cash','other')), reference text NOT NULL DEFAULT '', amount_paise bigint NOT NULL, tds_paise bigint NOT NULL DEFAULT 0, created_by text NOT NULL, created_at text NOT NULL ); CREATE TABLE IF NOT EXISTS payment_allocation ( id text PRIMARY KEY, payment_id text NOT NULL, document_id text NOT NULL, amount_paise bigint NOT NULL ); CREATE TABLE IF NOT EXISTS email_account ( id text PRIMARY KEY, address text NOT NULL, refresh_token_enc text NOT NULL, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','dead')), updated_at text NOT NULL ); CREATE TABLE IF NOT EXISTS email_log ( id text PRIMARY KEY, document_id text, to_addr text NOT NULL, subject text NOT NULL, status text NOT NULL CHECK (status IN ('sent','failed')), gmail_message_id text, error text, at_wall text NOT NULL, bounced integer NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS setting ( key text PRIMARY KEY, value text NOT NULL ); CREATE TABLE IF NOT EXISTS audit_log ( id text PRIMARY KEY, at_wall text NOT NULL, user_id text NOT NULL, action text NOT NULL, entity text NOT NULL, entity_id text NOT NULL, before_json text, after_json text ); CREATE TABLE IF NOT EXISTS stg_client ( row_no integer PRIMARY KEY, code text, name text, gstin text, state_code text, address text, phone text, email text, status text, anydesk text, os text, district text, sector text, problems text NOT NULL DEFAULT '[]' ); CREATE TABLE IF NOT EXISTS stg_invoice ( row_no integer PRIMARY KEY, client_code text, doc_no text, doc_date text, taxable_paise bigint, tax_paise bigint, total_paise bigint, paid integer, problems text NOT NULL DEFAULT '[]' ); CREATE TABLE IF NOT EXISTS recurring_plan ( id text PRIMARY KEY, client_id text NOT NULL, client_module_id text, cadence text NOT NULL CHECK (cadence IN ('monthly','yearly')), amount_paise bigint, next_run text NOT NULL, policy text NOT NULL DEFAULT 'manual' CHECK (policy IN ('auto','manual')), active integer NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS amc_contract ( id text PRIMARY KEY, client_id text NOT NULL, coverage text NOT NULL DEFAULT '', period_from text NOT NULL, period_to text NOT NULL, amount_paise bigint NOT NULL, renewal_reminder_days integer NOT NULL DEFAULT 30, legacy_paid integer, invoice_doc_id text, active integer NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS interaction_type ( code text PRIMARY KEY, label text NOT NULL ); CREATE TABLE IF NOT EXISTS interaction ( id text PRIMARY KEY, client_id text NOT NULL, type_code text NOT NULL, on_date text NOT NULL, staff_id text NOT NULL, notes text NOT NULL DEFAULT '', outcome text CHECK (outcome IN ('positive','neutral','negative')), follow_up_on text, created_at text NOT NULL ); CREATE TABLE IF NOT EXISTS reminder ( id text PRIMARY KEY, rule_kind text NOT NULL CHECK (rule_kind IN ('invoice_overdue','renewal_due','amc_expiring','follow_up','recurring_generated','email_bounced','quote_followup')), subject_id text NOT NULL, due_period text NOT NULL, client_id text NOT NULL, doc_id text, status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','sent','failed','dismissed')), policy_applied text NOT NULL DEFAULT 'manual' CHECK (policy_applied IN ('auto','manual')), error text, created_at text NOT NULL, sent_at text, UNIQUE (rule_kind, subject_id, due_period) ); CREATE TABLE IF NOT EXISTS reminder_schedule ( id text PRIMARY KEY, rule_kind text NOT NULL, effective_from text NOT NULL, effective_to text, day_offsets text NOT NULL, subject text, body text ); CREATE TABLE IF NOT EXISTS aws_usage ( id text PRIMARY KEY, client_id text NOT NULL, month text NOT NULL, storage_gb double precision NOT NULL DEFAULT 0, transfer_gb double precision NOT NULL DEFAULT 0, cost_paise bigint NOT NULL DEFAULT 0, source text NOT NULL DEFAULT 'auto' CHECK (source IN ('auto','manual')), updated_at text NOT NULL, UNIQUE (client_id, month) ); `, }, { id: '002-apex-parity', sql: ` ALTER TABLE client_module ADD COLUMN IF NOT EXISTS provider text; ALTER TABLE client_module ADD COLUMN IF NOT EXISTS username text; ALTER TABLE client_module ADD COLUMN IF NOT EXISTS password_enc text; ALTER TABLE client_module ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT '[]'; ALTER TABLE client_module ADD COLUMN IF NOT EXISTS remark text; CREATE TABLE IF NOT EXISTS client_branch ( id text PRIMARY KEY, client_id text NOT NULL, name text NOT NULL, code text, active integer NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS ticket ( id text PRIMARY KEY, client_id text NOT NULL, branch_id text, module_code text, kind text NOT NULL DEFAULT '', description text NOT NULL DEFAULT '', status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','in_progress','waiting','closed','dropped')), assigned_to text, online_offline text, opened_on text NOT NULL, closed_on text, created_by text NOT NULL, created_at text NOT NULL, source text NOT NULL DEFAULT 'hq' ); `, }, { id: '003-module-field-schema', sql: ` ALTER TABLE module ADD COLUMN IF NOT EXISTS field_spec text NOT NULL DEFAULT '[]'; ALTER TABLE client_module ADD COLUMN IF NOT EXISTS field_values text NOT NULL DEFAULT '{}'; ALTER TABLE client_module ADD COLUMN IF NOT EXISTS secrets_enc text; `, }, { id: '004-usage-rate-band', sql: ` CREATE TABLE IF NOT EXISTS usage_rate_band ( id text PRIMARY KEY, module_id text NOT NULL, effective_from text NOT NULL, min_qty bigint NOT NULL, rate_paise bigint NOT NULL ); `, }, { id: '005-project-milestone', sql: ` CREATE TABLE IF NOT EXISTS project_milestone ( id text PRIMARY KEY, client_module_id text NOT NULL, key text NOT NULL, label text NOT NULL, done integer NOT NULL DEFAULT 0, done_on text, sort integer NOT NULL DEFAULT 0, UNIQUE (client_module_id, key) ); `, }, { id: '006-login-lockout', sql: ` ALTER TABLE staff_user ADD COLUMN IF NOT EXISTS failed_count integer NOT NULL DEFAULT 0; ALTER TABLE staff_user ADD COLUMN IF NOT EXISTS locked_until text; `, }, { // D24 (red-team): indexes for the queries that run over the real data. Same DDL as the // SQLite SCHEMA's INDEX_DDL (CREATE INDEX IF NOT EXISTS is valid on both engines). id: '007-indexes', sql: ` CREATE INDEX IF NOT EXISTS ix_document_client ON document (client_id); CREATE INDEX IF NOT EXISTS ix_document_type_status ON document (doc_type, status); CREATE INDEX IF NOT EXISTS ix_document_ref ON document (ref_doc_id); CREATE INDEX IF NOT EXISTS ix_document_created_by ON document (created_by); CREATE INDEX IF NOT EXISTS ix_document_event_doc ON document_event (document_id); CREATE INDEX IF NOT EXISTS ix_document_share_doc ON document_share (document_id); CREATE INDEX IF NOT EXISTS ix_payment_client ON payment (client_id); CREATE INDEX IF NOT EXISTS ix_alloc_doc ON payment_allocation (document_id); CREATE INDEX IF NOT EXISTS ix_alloc_payment ON payment_allocation (payment_id); CREATE INDEX IF NOT EXISTS ix_client_module_client ON client_module (client_id); CREATE INDEX IF NOT EXISTS ix_client_module_module ON client_module (module_id, active); CREATE INDEX IF NOT EXISTS ix_client_module_renewal ON client_module (next_renewal); CREATE INDEX IF NOT EXISTS ix_client_owner ON client (owner_id); CREATE INDEX IF NOT EXISTS ix_client_branch_client ON client_branch (client_id); CREATE INDEX IF NOT EXISTS ix_ticket_client ON ticket (client_id); CREATE INDEX IF NOT EXISTS ix_ticket_status ON ticket (status); CREATE INDEX IF NOT EXISTS ix_ticket_assigned ON ticket (assigned_to); CREATE INDEX IF NOT EXISTS ix_interaction_client ON interaction (client_id); CREATE INDEX IF NOT EXISTS ix_interaction_followup ON interaction (follow_up_on); CREATE INDEX IF NOT EXISTS ix_reminder_status ON reminder (status); CREATE INDEX IF NOT EXISTS ix_reminder_client ON reminder (client_id); CREATE INDEX IF NOT EXISTS ix_reminder_doc ON reminder (doc_id); CREATE INDEX IF NOT EXISTS ix_price_book_module ON module_price_book (module_id); CREATE INDEX IF NOT EXISTS ix_rate_band_module ON usage_rate_band (module_id, effective_from); CREATE INDEX IF NOT EXISTS ix_recurring_active_run ON recurring_plan (active, next_run); CREATE INDEX IF NOT EXISTS ix_amc_client ON amc_contract (client_id); CREATE INDEX IF NOT EXISTS ix_aws_client ON aws_usage (client_id); CREATE INDEX IF NOT EXISTS ix_session_staff ON session (staff_id); CREATE INDEX IF NOT EXISTS ix_audit_entity ON audit_log (entity, entity_id); CREATE INDEX IF NOT EXISTS ix_milestone_cm ON project_milestone (client_module_id); `, }, { id: '008-module-sort-order', sql: `ALTER TABLE module ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 100;`, }, { // D25: login by username. Backfill from email so existing accounts keep working. id: '009-staff-username', sql: ` ALTER TABLE staff_user ADD COLUMN IF NOT EXISTS username text; UPDATE staff_user SET username=email WHERE username IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS ix_staff_username ON staff_user (username); `, }, { // D28: SMS gateway balance-check telemetry (operational sync log, not audited). id: '010-sms-balance-check', sql: ` CREATE TABLE IF NOT EXISTS sms_balance_check ( client_module_id text PRIMARY KEY, status text NOT NULL, balance integer, message text, checked_at text NOT NULL ); `, }, ]