mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 12:30:58 +00:00
feat(billing): add PAYMENT_PROCESSING invoice status on payment success redirect (all except CBE bill)
This commit is contained in:
@@ -52,6 +52,8 @@ const DEFAULT_DUE_DAYS = 14;
|
||||
const OPEN_STATUSES: Freight.InvoiceStatus[] = [
|
||||
Freight.InvoiceStatus.Issued,
|
||||
Freight.InvoiceStatus.Pending,
|
||||
// Success-redirect ack; still unsettled, so it must stay payable/settleable.
|
||||
Freight.InvoiceStatus.PaymentProcessing,
|
||||
Freight.InvoiceStatus.PartiallyPaid,
|
||||
Freight.InvoiceStatus.Overdue,
|
||||
];
|
||||
@@ -924,6 +926,26 @@ export class BillingService {
|
||||
});
|
||||
if (!invoice) return null;
|
||||
|
||||
// Reconcile-before-expire, caller-proof: an invoice with a payment intent may
|
||||
// have settled at the gateway without the webhook landing yet. `paid` — leave
|
||||
// it open, the (re-emitted) payment.succeeded settles it. `unverifiable` —
|
||||
// never expire on unknown; the caller's next sweep retries. Invoices with no
|
||||
// intent (`paymentId` null) were never payable at a gateway and expire directly.
|
||||
if (invoice.paymentId) {
|
||||
const { paid, unverifiable } = await this.reconcilePayable(
|
||||
invoice.sourceId,
|
||||
);
|
||||
if (paid || unverifiable) {
|
||||
this.logger.warn(
|
||||
`expirePayable skipped for invoice ${invoice.invoiceNumber} (${invoice.id}) — ` +
|
||||
(paid
|
||||
? "gateway reconcile found a settled payment"
|
||||
: "settlement unverifiable at the gateway"),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.transition(
|
||||
invoice.id,
|
||||
Freight.InvoiceStatus.Expired,
|
||||
@@ -1028,6 +1050,40 @@ export class BillingService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Success-redirect ack (see PaymentService.acknowledgeSuccessRedirect): move
|
||||
* the invoice linked to a gateway intent to PAYMENT_PROCESSING. Only from
|
||||
* ISSUED/PENDING — never overwrites a settlement (PAID/PARTIALLY_PAID) and
|
||||
* is idempotent. Balance untouched: this is a display state, not a
|
||||
* settlement; settleByPaymentId still performs the real transition.
|
||||
*/
|
||||
async markInvoicePaymentProcessing(paymentId: string): Promise<void> {
|
||||
await this.dataSource.getRepository(Invoice).update(
|
||||
{
|
||||
paymentId,
|
||||
status: In([
|
||||
Freight.InvoiceStatus.Issued,
|
||||
Freight.InvoiceStatus.Pending,
|
||||
]),
|
||||
},
|
||||
{ status: Freight.InvoiceStatus.PaymentProcessing },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counterpart of {@link markInvoicePaymentProcessing} for a failed intent:
|
||||
* PAYMENT_PROCESSING → PENDING so the invoice reads payable again for a
|
||||
* retry. No-op from any other status.
|
||||
*/
|
||||
async revertInvoicePaymentProcessing(paymentId: string): Promise<void> {
|
||||
await this.dataSource
|
||||
.getRepository(Invoice)
|
||||
.update(
|
||||
{ paymentId, status: Freight.InvoiceStatus.PaymentProcessing },
|
||||
{ status: Freight.InvoiceStatus.Pending },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Payment initiation & settlement (the gateway boundary) ───────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user