feat: add invoice polling reconciliation
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
const config = loadConfig();
|
||||
const intervalMs = config.keyPool.balancePollSeconds * 1000;
|
||||
const renewalLeadTimeHours = 72;
|
||||
const invoiceReconciliationBatchSize = 100;
|
||||
const workerStore = createPrismaWorkerStore(prisma, {
|
||||
cooldownMinutes: config.keyPool.cooldownMinutes,
|
||||
failuresBeforeManualReview: config.keyPool.failuresBeforeManualReview,
|
||||
@@ -53,18 +54,6 @@ async function runTick(): Promise<void> {
|
||||
isTickRunning = true;
|
||||
|
||||
try {
|
||||
const expiredInvoices = await billingStore.expireElapsedPendingInvoices();
|
||||
|
||||
if (expiredInvoices.expiredCount > 0) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
service: "worker",
|
||||
event: "pending_invoices_expired",
|
||||
expiredCount: expiredInvoices.expiredCount,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const renewalNotifications = await billingStore.createUpcomingRenewalInvoices({
|
||||
paymentProvider: config.payment.provider,
|
||||
paymentProviderAdapter,
|
||||
@@ -101,6 +90,96 @@ async function runTick(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const pendingInvoices =
|
||||
await billingStore.listPendingInvoicesForReconciliation(invoiceReconciliationBatchSize);
|
||||
const reconciliationSummary = {
|
||||
polledCount: pendingInvoices.length,
|
||||
markedPaidCount: 0,
|
||||
markedExpiredCount: 0,
|
||||
markedCanceledCount: 0,
|
||||
alreadyTerminalCount: 0,
|
||||
ignoredCount: 0,
|
||||
failedCount: 0,
|
||||
};
|
||||
|
||||
for (const invoice of pendingInvoices) {
|
||||
try {
|
||||
const providerInvoice = await paymentProviderAdapter.getInvoiceStatus(invoice.providerInvoiceId);
|
||||
const result = await billingStore.reconcilePendingInvoice({
|
||||
invoiceId: invoice.id,
|
||||
providerStatus: providerInvoice.status,
|
||||
actor: {
|
||||
type: "system",
|
||||
ref: "invoice_reconciliation",
|
||||
},
|
||||
...(providerInvoice.paidAt ? { paidAt: providerInvoice.paidAt } : {}),
|
||||
});
|
||||
|
||||
switch (result.outcome) {
|
||||
case "marked_paid":
|
||||
reconciliationSummary.markedPaidCount += 1;
|
||||
break;
|
||||
case "marked_expired":
|
||||
reconciliationSummary.markedExpiredCount += 1;
|
||||
break;
|
||||
case "marked_canceled":
|
||||
reconciliationSummary.markedCanceledCount += 1;
|
||||
break;
|
||||
case "already_paid":
|
||||
case "already_expired":
|
||||
case "already_canceled":
|
||||
reconciliationSummary.alreadyTerminalCount += 1;
|
||||
break;
|
||||
case "ignored_terminal_state":
|
||||
reconciliationSummary.ignoredCount += 1;
|
||||
break;
|
||||
case "noop_pending":
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
reconciliationSummary.failedCount += 1;
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
service: "worker",
|
||||
event: "invoice_reconciliation_failed",
|
||||
invoiceId: invoice.id,
|
||||
providerInvoiceId: invoice.providerInvoiceId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
reconciliationSummary.polledCount > 0 ||
|
||||
reconciliationSummary.failedCount > 0 ||
|
||||
reconciliationSummary.markedPaidCount > 0 ||
|
||||
reconciliationSummary.markedExpiredCount > 0 ||
|
||||
reconciliationSummary.markedCanceledCount > 0 ||
|
||||
reconciliationSummary.alreadyTerminalCount > 0 ||
|
||||
reconciliationSummary.ignoredCount > 0
|
||||
) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
service: "worker",
|
||||
event: "pending_invoice_reconciliation",
|
||||
...reconciliationSummary,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const expiredInvoices = await billingStore.expireElapsedPendingInvoices();
|
||||
|
||||
if (expiredInvoices.expiredCount > 0) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
service: "worker",
|
||||
event: "pending_invoices_expired",
|
||||
expiredCount: expiredInvoices.expiredCount,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const recovery = await workerStore.recoverCooldownProviderKeys();
|
||||
|
||||
if (recovery.recoveredCount > 0) {
|
||||
|
||||
Reference in New Issue
Block a user