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

@@ -31,6 +31,24 @@ export class PaymentClientService {
return this.call("POST", "/payments/initiate", request);
}
/**
* POST /payments/reconcile — settlement check for a domain order
* (reconcile-before-cancel). Live-queries every non-failed intent at the
* provider and registers any late capture found (flips it to SUCCEEDED and
* emits payment.succeeded). `unverifiable: true` = could not confirm
* "not paid" — the caller must NOT cancel/expire the order.
*/
async reconcileReference(
referenceType: PaymentReferenceType,
referenceId: string,
): Promise<{ paid: boolean; unverifiable: boolean }> {
return this.call("POST", "/payments/reconcile", {
service: PaymentService.FREIGHT,
referenceType,
referenceId,
});
}
/** GET /payments/intents?… — active intent by domain reference; null when none exists. */
async getIntentByReference(
referenceType: PaymentReferenceType,

View File

@@ -188,6 +188,30 @@ export class PaymentService {
* marked paid WITHOUT emitting — the caller (billing) settles inline after it
* has stored the intent id, avoiding a settle-before-correlation race.
*/
/**
* Reconcile-before-cancel: ask the payment service whether ANY intent for
* this shipment actually settled at the provider (bank/gateway). A late
* capture found there is registered as SUCCEEDED and emits payment.succeeded,
* which drives the normal paid flow. A network/provider error reports
* `unverifiable` — the caller must not expire the order on unknown.
*/
async reconcileShipment(
referenceId: string,
): Promise<{ paid: boolean; unverifiable: boolean }> {
try {
const result = await this.paymentClient.reconcileReference(
PaymentReferenceType.SHIPMENT,
referenceId,
);
return { paid: result.paid, unverifiable: result.unverifiable };
} catch (err) {
this.logger.warn(
`Reconcile for shipment ${referenceId} failed: ${(err as Error).message}`,
);
return { paid: false, unverifiable: true };
}
}
async initiate(input: InitiateIntentInput): Promise<InitiateIntentResult> {
try {