feat: add expiration to invoice

This commit is contained in:
Nathnael
2026-07-01 06:19:07 +00:00
parent 0056dec924
commit 8ab27a6721
6 changed files with 105 additions and 0 deletions

View File

@@ -628,6 +628,58 @@ export class BillingService {
return this.markInvoiceAsRefunded(invoice.id, mg);
}
/**
* Expire a source's currently-open invoice (its pay window closed before
* settlement), then emit `${source}.invoice.expired`. Resolves the open invoice
* and transitions it to EXPIRED — a terminal, non-payable status (kept out of
* `OPEN_STATUSES`). No-op (returns null) when the source has no open invoice
* (already paid/cancelled/expired).
*
* Pass the caller's transaction `manager` (e.g. the booking pay-window expiry in
* the batch engine) to enlist in its DB transaction.
*/
async expirePayable(
source: Freight.InvoiceSource,
sourceId: string,
manager?: EntityManager,
): Promise<Invoice | null> {
const mg = manager ?? this.dataSource.manager;
const invoice = await mg.findOne(Invoice, {
where: { source, sourceId, status: In(OPEN_STATUSES) },
order: { issuedAt: "DESC" },
});
if (!invoice) return null;
return this.transition(
invoice.id,
Freight.InvoiceStatus.Expired,
"expired",
{},
mg,
);
}
/**
* Sync a source's open invoice `dueAt` to its real pay-window deadline. The
* booking invoice is generated before the pay window opens (at booking
* creation/approval), so its printed due date is refreshed when the batch engine
* sets `paymentDeadline`. No-op when the source has no open invoice.
*/
async syncPayableDueDate(
source: Freight.InvoiceSource,
sourceId: string,
dueAt: Date,
manager?: EntityManager,
): Promise<void> {
const mg = manager ?? this.dataSource.manager;
const invoice = await mg.findOne(Invoice, {
where: { source, sourceId, status: In(OPEN_STATUSES) },
order: { issuedAt: "DESC" },
});
if (!invoice) return;
await mg.update(Invoice, { id: invoice.id }, { dueAt });
}
// ── Payment initiation & settlement (the gateway boundary) ───────────────────
/**