-- 015_job_queue.sql -- Persistent job queue for portal automation with retry and deferral. -- -- Replaces the fire-and-forget in-memory job dispatch. -- A scheduler thread in job_server.py polls this table every 60s. -- -- Portal hours awareness: -- BC Corporate Online: Mon-Sat 06:00-22:00 PT, Sun 13:00-22:00 PT -- IRS EIN Assistant: Mon-Fri 07:00-22:00 ET -- All others: 24/7 (no deferral needed) -- -- Retry policy: exponential backoff, 5 attempts max, then escalate to admin. BEGIN; CREATE TABLE IF NOT EXISTS job_queue ( id SERIAL PRIMARY KEY, job_type TEXT NOT NULL, -- 'file_entity' | 'obtain_ein' | 'name_search' | 'process_crtc' | 'deliver' payload JSONB NOT NULL, -- order details, state_code, order_name, etc. status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'deferred', 'cancelled')), attempts INTEGER NOT NULL DEFAULT 0, max_attempts INTEGER NOT NULL DEFAULT 5, run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- next eligible run time started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, last_error TEXT, -- Portal context portal_tz TEXT, -- e.g. 'America/Vancouver' for BC jobs portal_hours JSONB, -- {mon:[6,22], tue:[6,22], ..., sun:[13,22]} -- Linkage order_id TEXT, -- denormalized for easy lookup order_type TEXT, -- 'canada_crtc' | 'formation' created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Scheduler polls this index every 60s for due, runnable jobs CREATE INDEX IF NOT EXISTS idx_job_queue_runnable ON job_queue (run_at, created_at) WHERE status IN ('pending', 'deferred'); CREATE INDEX IF NOT EXISTS idx_job_queue_order ON job_queue (order_id) WHERE status NOT IN ('completed', 'cancelled'); COMMIT;