From 1da5af94ab3c7f569ca8509baae07c5776dde681 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 29 Jun 2026 07:32:42 +0000 Subject: [PATCH] feat: scoped the payables by type --- .../modules/billing/billing.service.spec.ts | 2 ++ .../src/modules/billing/billing.service.ts | 35 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts b/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts index 62b3cebaa..767aabec8 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts @@ -160,6 +160,7 @@ describe("BillingService.settlePayable", () => { const settled = await service.settlePayable( Freight.InvoiceSource.Booking, "booking-1", + Freight.InvoiceType.Prepaid, "pay-1", mg as never, ); @@ -184,6 +185,7 @@ describe("BillingService.settlePayable", () => { const settled = await service.settlePayable( Freight.InvoiceSource.Booking, "booking-1", + Freight.InvoiceType.Prepaid, "pay-1", mg as never, ); diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.ts b/apps/edr-freight-api/src/modules/billing/billing.service.ts index ff41eb4e6..f79548266 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts @@ -306,37 +306,44 @@ export class BillingService { * none. This is the billing document of record for "what is owed" — callers * (e.g. `payment.service.initiatePayment`) should charge `invoice.totalAmount` * against it rather than recomputing from the source's own total, so - * discounts/penalties/adjustments carried on the invoice are honored. Returns - * the most recent open (unpaid, non-cancelled) invoice. + * discounts/penalties/adjustments carried on the invoice are honored. + * + * `type` selects which invoice when a source carries several (e.g. a booking's + * up-front vs final charge). Returns the most recent open (unpaid, + * non-cancelled) invoice of that type. */ findPayable( source: Freight.InvoiceSource, sourceId: string, + type: string, ): Promise { return this.dataSource.getRepository(Invoice).findOne({ - where: { source, sourceId, status: In(OPEN_STATUSES) }, + where: { source, sourceId, type, status: In(OPEN_STATUSES) }, order: { issuedAt: "DESC" }, }); } /** - * Settle a source's open invoice as paid and link the gateway payment, then - * emit `${source}.invoice.paid`. Resolves the open invoice via {@link findPayable} - * then delegates to {@link markInvoiceAsPaid}. Full-payment only — no partial - * settlement. No-op (returns null) when the source has no open invoice. + * Settle a source's open invoice of `type` as paid and link the gateway + * payment, then emit `${source}.invoice.paid`. Resolves the open invoice then + * delegates to {@link markInvoiceAsPaid}. Full-payment only — no partial + * settlement. No-op (returns null) when the source has no open invoice of that + * type. * - * Pass the caller's transaction `manager` (e.g. from + * `type` selects which invoice when a source carries several (e.g. a booking's + * up-front vs final charge). Pass the caller's transaction `manager` (e.g. from * `payment.service.finalizePaymentSuccess`) to enlist in its DB transaction. */ async settlePayable( source: Freight.InvoiceSource, sourceId: string, + type: string, paymentId: string | null, manager?: EntityManager, ): Promise { const mg = manager ?? this.dataSource.manager; const invoice = await mg.findOne(Invoice, { - where: { source, sourceId, status: In(OPEN_STATUSES) }, + where: { source, sourceId, type, status: In(OPEN_STATUSES) }, order: { issuedAt: "DESC" }, }); if (!invoice) return null; @@ -345,9 +352,10 @@ export class BillingService { } /** - * Refund a source's paid invoice, then emit `${source}.invoice.refunded`. - * Resolves the paid invoice then delegates to {@link markInvoiceAsRefunded}. - * No-op (returns null) when the source has no paid invoice. + * Refund a source's paid invoice of `type`, then emit + * `${source}.invoice.refunded`. Resolves the paid invoice then delegates to + * {@link markInvoiceAsRefunded}. No-op (returns null) when the source has no + * paid invoice of that type. * * Pass the caller's transaction `manager` (e.g. from `payment.service.refund`) * to enlist in its DB transaction. @@ -355,11 +363,12 @@ export class BillingService { async refundPayable( source: Freight.InvoiceSource, sourceId: string, + type: string, manager?: EntityManager, ): Promise { const mg = manager ?? this.dataSource.manager; const invoice = await mg.findOne(Invoice, { - where: { source, sourceId, status: Freight.InvoiceStatus.Paid }, + where: { source, sourceId, type, status: Freight.InvoiceStatus.Paid }, order: { issuedAt: "DESC" }, }); if (!invoice) return null;