fix(payments): reconcile with gateway before expiring unpaid holds

Replace the fixed 5-minute grace with a settlement check at expiry:
expire() calls the payment API's reconcile endpoint — paid intents are
kept and allocated via payment.succeeded, unverifiable results defer
expiry to the next tick, only verifiably unpaid holds expire.
This commit is contained in:
Marshal
2026-07-30 12:01:59 +00:00
parent 22de05fe8e
commit acb6f286d4
8 changed files with 130 additions and 42 deletions

View File

@@ -983,6 +983,17 @@ export class BillingService {
* never fire before the link exists. Throws when the invoice is not found or
* not in an open/payable status.
*/
/**
* Settlement check before expiring a payable order (reconcile-before-expire):
* live-queries the gateway for any settled intent on the source order. Kept
* on billing so the domain never talks to the payment service directly.
*/
reconcilePayable(
sourceId: string,
): Promise<{ paid: boolean; unverifiable: boolean }> {
return this.payment.reconcileShipment(sourceId);
}
async payInvoice(
invoiceId: string,
opts: {
@@ -1004,9 +1015,9 @@ export class BillingService {
// A booking's PREPAID invoice is only payable inside its pay window —
// `dueAt` mirrors booking.paymentDeadline (issuePayable at reserve time).
// Blocking INITIATION here is what makes the deadline real: the settle
// sweep's grace only shelters payments that started before this gate.
// Other invoice types keep dueAt as display-only.
// Blocking INITIATION here is what makes the deadline real: a payment
// STARTED before this gate but settling late is still honored by the
// expire-time gateway reconcile. Other invoice types keep dueAt display-only.
if (
invoice.source === Freight.InvoiceSource.Booking &&
invoice.type === "PREPAID" &&