82 lines
2.3 KiB
JavaScript
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]
|
|
);
|
|
}
|