kua-cashier/dispatcher.js

82 lines
2.3 KiB
JavaScript

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]
);
}