import { query } from './db.js'; import { newId, hmacSign } from './util.js'; const CALLBACK_TIMEOUT_MS = 10_000; const MAX_ATTEMPTS = 3; /** * Mark intent + inbound as matched in a transaction, then fire the callback. * Callback failures are logged but don't roll back the match — the payment * happened regardless; downstream can poll /intent/:id as fallback. */ export async function confirmAndDispatch(intent, inbound, confidence) { const now = new Date().toISOString(); await query( `UPDATE payment_intents SET status = 'matched', matched_at = $1, inbound_id = $2 WHERE id = $3`, [now, inbound.id, intent.id] ); await query( `UPDATE inbound_payments SET status = 'matched', intent_id = $1 WHERE id = $2`, [intent.id, inbound.id] ); await fireCallback(intent, inbound, confidence); } async function fireCallback(intent, inbound, confidence) { const payload = { event: 'payment.confirmed', intent_id: intent.id, source_app: intent.source_app, inbound_id: inbound.id, confidence, amount: inbound.amount, nominal_amount: intent.nominal_amount, exact_amount: intent.exact_amount, jitter: intent.jitter, sender_rut: inbound.sender_rut ?? null, received_at: inbound.received_at, reference_code: intent.reference_code, metadata: intent.metadata ?? null, }; const body = JSON.stringify(payload); const headers = { 'Content-Type': 'application/json' }; if (intent.callback_secret) { headers['X-Cashier-Signature'] = `sha256=${hmacSign(intent.callback_secret, body)}`; } let lastStatus = null; let attempts = 0; for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { attempts = attempt; try { const res = await fetch(intent.callback_url, { method: 'POST', headers, body, signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS), }); lastStatus = res.status; if (res.ok) break; } catch { lastStatus = 0; } if (attempt < MAX_ATTEMPTS) { await new Promise(r => setTimeout(r, attempt * 2000)); } } await query( `INSERT INTO match_log (id, intent_id, inbound_id, confidence, callback_status, callback_attempts) VALUES ($1, $2, $3, $4, $5, $6)`, [newId(), intent.id, inbound.id, confidence, lastStatus, attempts] ); }