-- Monitored bank accounts (accounts we own and watch for incoming transfers) CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, bank TEXT NOT NULL, -- 'bci', 'santander', 'bancochile', etc. account_number TEXT NOT NULL, owner_name TEXT NOT NULL, owner_rut TEXT NOT NULL, -- normalized: 12345678-9 check_digit TEXT, -- BCI requires separate check digit api_key TEXT, -- BCI subscription key webhook_secret TEXT, -- key we embed in BCI subscription; BCI echoes it back in webhooks bci_subscription_id TEXT, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (bank, account_number) ); -- Expected payments registered by downstream services CREATE TABLE IF NOT EXISTS payment_intents ( id TEXT PRIMARY KEY, source_app TEXT NOT NULL, -- 'muralla', 'rover', etc. account_id TEXT NOT NULL REFERENCES accounts(id), nominal_amount INTEGER NOT NULL, -- what the customer "should" pay exact_amount INTEGER NOT NULL, -- jittered: always <= nominal jitter INTEGER NOT NULL DEFAULT 0, sender_rut TEXT, -- expected sender RUT (match hint, optional) reference_code TEXT NOT NULL UNIQUE, -- 6-char code; customer puts in transfer description callback_url TEXT NOT NULL, callback_secret TEXT, -- HMAC-SHA256 key for signing confirmations status TEXT NOT NULL DEFAULT 'pending', -- pending | matched | expired | cancelled metadata JSONB, -- passed through to callback payload created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ NOT NULL, matched_at TIMESTAMPTZ, inbound_id TEXT ); -- Unique constraint: no two live intents can share the same exact_amount on the same account CREATE UNIQUE INDEX IF NOT EXISTS payment_intents_slot ON payment_intents (exact_amount, account_id) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS payment_intents_rut ON payment_intents (sender_rut) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS payment_intents_status ON payment_intents (status, expires_at); -- Inbound transfers detected from any source CREATE TABLE IF NOT EXISTS inbound_payments ( id TEXT PRIMARY KEY, source TEXT NOT NULL, -- 'bci_webhook' | 'bank_parser' | 'email' account_id TEXT REFERENCES accounts(id), amount INTEGER NOT NULL, -- whole pesos (CLP) sender_rut TEXT, description TEXT, -- transfer description / gloss received_at TIMESTAMPTZ NOT NULL, -- when the bank says the transfer happened detected_at TIMESTAMPTZ NOT NULL DEFAULT now(), status TEXT NOT NULL DEFAULT 'unaccounted', -- unaccounted | matched | ignored intent_id TEXT, bank_ref TEXT, -- bank's own transaction ID for deduplication raw JSONB NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS inbound_bank_ref ON inbound_payments (bank_ref) WHERE bank_ref IS NOT NULL; CREATE INDEX IF NOT EXISTS inbound_status ON inbound_payments (status); CREATE INDEX IF NOT EXISTS inbound_detected ON inbound_payments (detected_at DESC); -- Match audit log CREATE TABLE IF NOT EXISTS match_log ( id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, inbound_id TEXT NOT NULL, confidence TEXT NOT NULL, -- 'reference_code' | 'exact_amount_rut' | 'amount_only' callback_status INTEGER, callback_attempts INTEGER DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );