From 1c012ce1e2d7e39c42921a6315af8617b035fdf6 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Thu, 2 Jul 2026 11:47:35 +0300 Subject: [PATCH] refactor(payment): centralize invoice processing with detailed tracking and configurable logging --- .../src/config/database.config.ts | 4 +- .../src/modules/billing/billing.module.ts | 3 +- .../modules/billing/billing.service.spec.ts | 83 ++-------- .../src/modules/billing/billing.service.ts | 149 +++++++++--------- .../payment.controller.ts} | 31 ++-- .../bookings/booking-payment.service.ts | 43 ----- .../bookings/booking-transition.service.ts | 1 + .../src/modules/bookings/bookings.module.ts | 6 +- .../src/modules/bookings/pay.controller.ts | 27 ---- .../src/modules/payment/payments.dto.ts | 4 +- .../train-scheduling/booking-batch.service.ts | 4 +- 11 files changed, 107 insertions(+), 248 deletions(-) rename apps/edr-freight-api/src/modules/{bookings/booking-payment.controller.ts => billing/payment.controller.ts} (83%) delete mode 100644 apps/edr-freight-api/src/modules/bookings/booking-payment.service.ts delete mode 100644 apps/edr-freight-api/src/modules/bookings/pay.controller.ts diff --git a/apps/edr-freight-api/src/config/database.config.ts b/apps/edr-freight-api/src/config/database.config.ts index 0e7375b19..ad171f488 100644 --- a/apps/edr-freight-api/src/config/database.config.ts +++ b/apps/edr-freight-api/src/config/database.config.ts @@ -118,6 +118,8 @@ export default registerAs("database", (): TypeOrmModuleOptions => { migrationsRun: true, // Schema changes via migrations only (synchronize breaks ITMLS backfill on existing rows). synchronize: false, - logging: process.env.NODE_ENV === "development", + logging: process.env.DB_LOG + ? process.env.DB_LOG === "true" + : process.env.NODE_ENV === "development", }; }); diff --git a/apps/edr-freight-api/src/modules/billing/billing.module.ts b/apps/edr-freight-api/src/modules/billing/billing.module.ts index dc78cd6e9..771156fd3 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.module.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { BillingController } from "./billing.controller"; import { PortalBillingController } from "./portal-billing.controller"; +import { PaymentController } from "./payment.controller"; import { BillingService } from "./billing.service"; import { DocumentsModule } from "./documents/documents.module"; import { Invoice } from "./entities/invoice.entity"; @@ -19,7 +20,7 @@ import { CompaniesModule } from "../companies/companies.module"; CompaniesModule, DocumentsModule, ], - controllers: [BillingController, PortalBillingController], + controllers: [BillingController, PortalBillingController, PaymentController], providers: [BillingService, InvoiceRepository, InvoiceLineRepository], exports: [BillingService], }) 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 61597264b..84040ebdd 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 @@ -116,12 +116,14 @@ describe("BillingService.generateInvoice", () => { }); describe("BillingService.markInvoiceAsPaid", () => { - it("marks the invoice PAID, links the payment, and emits ${source}.invoice.paid", async () => { + it("marks the invoice PAID, stamps amounts/paidAt, links the payment, and emits ${source}.invoice.paid", async () => { const open = { id: "inv-1", status: Freight.InvoiceStatus.Pending, source: "booking", sourceId: "booking-1", + totalAmount: 1500, + paidAt: null, }; const mg = { findOne: jest.fn().mockResolvedValue(open), @@ -143,7 +145,13 @@ describe("BillingService.markInvoiceAsPaid", () => { expect(mg.update).toHaveBeenCalledWith( expect.anything(), { id: "inv-1" }, - { status: Freight.InvoiceStatus.Paid, paymentId: "pay-1" }, + { + status: Freight.InvoiceStatus.Paid, + paymentId: "pay-1", + paidAt: expect.any(Date), + paidAmount: 1500, + balanceAmount: 0, + }, ); expect(events.emit).toHaveBeenCalledWith( "booking.invoice.paid", @@ -265,74 +273,3 @@ describe("BillingService.recordPayment", () => { expect(mg.update).not.toHaveBeenCalled(); }); }); - -describe("BillingService.settlePayable", () => { - it("settles the source's open invoice PAID and emits ${source}.invoice.paid", async () => { - const open = { - id: "inv-1", - status: Freight.InvoiceStatus.Pending, - source: Freight.InvoiceSource.Booking, - sourceId: "booking-1", - }; - const mg = { - findOne: jest.fn().mockResolvedValue(open), - update: jest.fn().mockResolvedValue(undefined), - }; - const events = makeEvents(); - const service = new BillingService( - { manager: mg } as never, - {} as never, - {} as never, - events as never, - {} as never, // payment - {} as never, // companies - {} as never, // invoiceDocuments - ); - - const settled = await service.settlePayable( - Freight.InvoiceSource.Booking, - "booking-1", - "pay-1", - mg as never, - ); - - expect(settled?.status).toBe(Freight.InvoiceStatus.Paid); - expect(mg.update).toHaveBeenCalledWith( - expect.anything(), - { id: "inv-1" }, - { status: Freight.InvoiceStatus.Paid, paymentId: "pay-1" }, - ); - expect(events.emit).toHaveBeenCalledWith( - "booking.invoice.paid", - expect.anything(), - ); - }); - - it("is a no-op (returns null) when the source has no open invoice", async () => { - const mg = { - findOne: jest.fn().mockResolvedValue(null), - update: jest.fn().mockResolvedValue(undefined), - }; - const events = makeEvents(); - const service = new BillingService( - { manager: mg } as never, - {} as never, - {} as never, - events as never, - {} as never, // payment - {} as never, // companies - {} as never, // invoiceDocuments - ); - - const settled = await service.settlePayable( - Freight.InvoiceSource.Booking, - "booking-1", - "pay-1", - mg as never, - ); - - expect(settled).toBeNull(); - expect(mg.update).not.toHaveBeenCalled(); - expect(events.emit).not.toHaveBeenCalled(); - }); -}); 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 443f511ee..0eabcbde7 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts @@ -49,7 +49,6 @@ const DEFAULT_DUE_DAYS = 14; /** Statuses an invoice can still be settled (paid/refunded/cancelled) from. */ const OPEN_STATUSES: Freight.InvoiceStatus[] = [ - Freight.InvoiceStatus.Draft, Freight.InvoiceStatus.Issued, Freight.InvoiceStatus.Pending, Freight.InvoiceStatus.PartiallyPaid, @@ -285,20 +284,15 @@ export class BillingService { /** * Initiate gateway payment for one of the customer's own invoices. Verifies - * ownership, then charges whichever open invoice the source currently has - * (see {@link payInvoice}). + * ownership, then charges the invoice directly by ID (see {@link payInvoice}). */ async payInvoiceForUser( id: string, userId: string, opts: PayInvoiceOptions = {}, ): Promise { - const invoice = await this.findByIdForUser(id, userId); - return this.payInvoice( - invoice.source as Freight.InvoiceSource, - invoice.sourceId, - opts, - ); + await this.findByIdForUser(id, userId); + return this.payInvoice(id, opts); } /** Sealed invoice PDF for one of the customer's own invoices (ownership-checked). */ @@ -422,23 +416,39 @@ export class BillingService { // ── State transitions ──────────────────────────────────────────────────────── /** - * Mark an invoice paid and link the gateway payment, then emit - * `${source}.invoice.paid`. Full-payment only — no partial settlement. - * No-op when the invoice is already paid. Pass `manager` to enlist in a - * caller's transaction. + * Mark an invoice paid, stamp the paid timestamp, sync paid/balance amounts, + * link the gateway payment, then emit `${source}.invoice.paid`. Full-payment + * only — no partial settlement. No-op when the invoice is already paid. + * Pass `manager` to enlist in a caller's transaction. */ async markInvoiceAsPaid( invoiceId: string, paymentId: string | null = null, manager?: EntityManager, ): Promise { - return this.transition( - invoiceId, - Freight.InvoiceStatus.Paid, - "paid", - { paymentId: paymentId ?? undefined }, - manager, - ); + const mg = manager ?? this.dataSource.manager; + const invoice = await mg.findOne(Invoice, { where: { id: invoiceId } }); + if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`); + if (invoice.status === Freight.InvoiceStatus.Paid) return invoice; + + await mg.update(Invoice, { id: invoiceId }, { + status: Freight.InvoiceStatus.Paid, + paymentId, + paidAt: invoice.paidAt ?? new Date(), + paidAmount: invoice.totalAmount, + balanceAmount: 0, + } as never); + + const updated = { + ...invoice, + status: Freight.InvoiceStatus.Paid, + paymentId, + paidAt: invoice.paidAt ?? new Date(), + paidAmount: invoice.totalAmount, + balanceAmount: 0, + } as Invoice; + this.emitInvoiceEvent("paid", updated); + return updated; } /** @@ -628,56 +638,24 @@ export class BillingService { } /** - * Settle a source's currently-open invoice 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. - * - * Type-blind by design: settles whichever invoice is due; any per-type reaction - * belongs in the `${source}.invoice.paid` handler, which reads `invoice.type`. - * Pass the caller's transaction `manager` to enlist in its DB transaction. - * - * NOTE: the booking flow settles via {@link payInvoice} + the `payment.succeeded` - * event ({@link settleByPaymentId}); this source-keyed settle is a generic helper - * for callers that settle by source rather than by gateway intent id. + * Pass `type` to select a specific invoice when a source carries several (e.g. + * a booking's up-front vs final charge); omit it to settle whichever single + * invoice is currently open. Returns the most recent matching open (unpaid, + * non-cancelled) invoice. */ - async settlePayable( + findInvoice( source: Freight.InvoiceSource, sourceId: string, - paymentId: string | null, - manager?: EntityManager, + type?: string, ): Promise { - const mg = manager ?? this.dataSource.manager; - const invoice = await mg.findOne(Invoice, { - where: { source, sourceId, status: In(OPEN_STATUSES) }, + return this.dataSource.getRepository(Invoice).findOne({ + where: { + source, + sourceId, + ...(type ? { type } : {}), + }, order: { issuedAt: "DESC" }, }); - if (!invoice) return null; - - return this.markInvoiceAsPaid(invoice.id, paymentId, mg); - } - - /** - * 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. - * - * Pass the caller's transaction `manager` (e.g. from `payment.service.refund`) - * to enlist in its DB transaction. - */ - async refundPayable( - source: Freight.InvoiceSource, - sourceId: string, - manager?: EntityManager, - ): Promise { - const mg = manager ?? this.dataSource.manager; - const invoice = await mg.findOne(Invoice, { - where: { source, sourceId, status: Freight.InvoiceStatus.Paid }, - order: { issuedAt: "DESC" }, - }); - if (!invoice) return null; - - return this.markInvoiceAsRefunded(invoice.id, mg); } /** @@ -693,11 +671,17 @@ export class BillingService { async expirePayable( 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: In(OPEN_STATUSES) }, + where: { + source, + sourceId, + status: In(OPEN_STATUSES), + ...(type ? { type } : {}), + }, order: { issuedAt: "DESC" }, }); if (!invoice) return null; @@ -721,11 +705,17 @@ export class BillingService { source: Freight.InvoiceSource, sourceId: string, dueAt: Date, + type?: string, 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, + status: In(OPEN_STATUSES), + ...(type ? { type } : {}), + }, order: { issuedAt: "DESC" }, }); if (!invoice) return; @@ -749,19 +739,20 @@ export class BillingService { // ── Payment initiation & settlement (the gateway boundary) ─────────────────── /** - * Charge a source's open invoice through the payment gateway. Billing is the - * single place that turns "what is owed" (the invoice) into a payment intent — - * the domain never talks to the payment service directly. Resolves the open - * invoice, opens an intent for `invoice.totalAmount`, records the intent id on - * the invoice (the settlement correlation key), and returns the client action. + * Charge an invoice through the payment gateway. Billing is the single place + * that turns "what is owed" (the invoice) into a payment intent — the domain + * never talks to the payment service directly. Resolves the invoice by ID, + * opens an intent for `invoice.balanceAmount` (so partial payments are honored), + * records the intent id on the invoice (the settlement correlation key), and + * returns the client action. * * When the provider settles synchronously, the invoice is settled inline here — * after the intent id is stored — so the `payment.succeeded` correlation can - * never fire before the link exists. Throws when the source has no open invoice. + * never fire before the link exists. Throws when the invoice is not found or + * not in an open/payable status. */ async payInvoice( - source: Freight.InvoiceSource, - sourceId: string, + invoiceId: string, opts: { method?: string; platform?: "web" | "mobile"; @@ -770,15 +761,17 @@ export class BillingService { failureUrl?: string; } = {}, ): Promise { - const invoice = await this.findPayable(source, sourceId); + const invoice = await this.dataSource.getRepository(Invoice).findOne({ + where: { id: invoiceId, status: In(OPEN_STATUSES) }, + }); if (!invoice) { throw new NotFoundException( - `No open invoice to charge for ${source}:${sourceId}`, + `Invoice ${invoiceId} not found or not in a payable status`, ); } const result = await this.payment.initiate({ - referenceId: sourceId, + referenceId: invoice.sourceId, source: invoice.source, // Freight payments settle under the generic SHIPMENT reference — how the // payment service attributes them to the freight API. The payment ↔ invoice @@ -787,7 +780,7 @@ export class BillingService { // service branches on a domain-specific reference type. referenceType: PaymentReferenceType.SHIPMENT, orderRef: invoice.invoiceNumber, - amountMinor: Math.round(Number(invoice.totalAmount)), + amountMinor: Math.round(Number(invoice.balanceAmount)), currency: invoice.currency, reason: `Payment for invoice ${invoice.invoiceNumber}`, method: opts.method ?? "TELEBIRR", diff --git a/apps/edr-freight-api/src/modules/bookings/booking-payment.controller.ts b/apps/edr-freight-api/src/modules/billing/payment.controller.ts similarity index 83% rename from apps/edr-freight-api/src/modules/bookings/booking-payment.controller.ts rename to apps/edr-freight-api/src/modules/billing/payment.controller.ts index ae01ebc36..543c84201 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-payment.controller.ts +++ b/apps/edr-freight-api/src/modules/billing/payment.controller.ts @@ -16,9 +16,8 @@ import { } from "@nestjs/swagger"; import { Response } from "express"; import { Public } from "@edr/api-common"; -import { Freight } from "@edr/types"; -import { BillingService } from "../billing/billing.service"; +import { BillingService } from "./billing.service"; import { InitiatePaymentDto, InitiateResponseDto, @@ -27,25 +26,24 @@ import { } from "../payment/payments.dto"; /** - * Booking-payment entrypoints. This is the ONE place that knows a payment is for a - * booking — it maps the request to {@link Freight.InvoiceSource.Booking} and hands - * off to billing, which resolves the invoice/amount and drives the gateway. Billing - * and payment stay source-agnostic; the booking knowledge lives here, in the domain. + * Central payment entrypoints. Domain-agnostic — the caller supplies an + * invoice ID and the billing service resolves the amount and drives the + * gateway. The domain never talks to the payment service directly. * Routes are unchanged (`/payments/*`) so the portal is unaffected. */ @ApiTags("Payment") @Controller("payments") -export class BookingPaymentController { +export class PaymentController { constructor(private readonly billing: BillingService) { } @Post("initiate") @ApiOperation({ - summary: "Initiate payment for a freight booking", - description: "Charges the booking's open invoice through the payment gateway.", + summary: "Initiate payment for an invoice", + description: "Charges the invoice through the payment gateway.", }) @ApiOkResponse({ type: InitiateResponseDto }) initiate(@Body() dto: InitiatePaymentDto): Promise { - return this.billing.payInvoice(Freight.InvoiceSource.Booking, dto.bookingId, { + return this.billing.payInvoice(dto.invoiceId, { method: dto.method, platform: dto.platform, payerAccount: dto.payerAccount, @@ -59,23 +57,23 @@ export class BookingPaymentController { @ApiOperation({ summary: "Browser checkout redirect", description: - "Charges the booking's invoice and returns an HTML page that auto-redirects to the provider checkout URL. Open directly in a browser tab.", + "Charges the invoice and returns an HTML page that auto-redirects to the provider checkout URL. Open directly in a browser tab.", }) - @ApiQuery({ name: "bookingId", required: true }) + @ApiQuery({ name: "invoiceId", required: true }) @ApiQuery({ name: "method", enum: PaymentMethodTypeEnum, required: true }) @ApiQuery({ name: "platform", enum: ["web", "mobile"], required: false }) @ApiProduces("text/html") async checkout( - @Query("bookingId") bookingId: string, + @Query("invoiceId") invoiceId: string, @Query("method") method: PaymentMethodTypeEnum, @Query("platform") platform: PaymentPlatformDto = "web", @Res() res: Response, ) { - if (!bookingId) { + if (!invoiceId) { return res .status(HttpStatus.BAD_REQUEST) .type("html") - .send(this.buildErrorHtml("Missing required query parameter: bookingId")); + .send(this.buildErrorHtml("Missing required query parameter: invoiceId")); } if (!method || !Object.values(PaymentMethodTypeEnum).includes(method)) { return res @@ -86,8 +84,7 @@ export class BookingPaymentController { try { const result = await this.billing.payInvoice( - Freight.InvoiceSource.Booking, - bookingId, + invoiceId, { method, platform }, ); const url = diff --git a/apps/edr-freight-api/src/modules/bookings/booking-payment.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-payment.service.ts deleted file mode 100644 index 1fbe34e1f..000000000 --- a/apps/edr-freight-api/src/modules/bookings/booking-payment.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { Freight } from '@edr/types'; -import { BookingsRepository } from './bookings.repository'; -import { Booking } from './entities/booking.entity'; -import { assertBookingStatus } from './booking-status.util'; -import { InAppPaymentReceiptDto } from './dto/pay-booking.dto'; -import { BillingService } from '../billing/billing.service'; -import { PaymentMethodTypeEnum } from '../payment/payments.dto'; -export interface InAppPaymentReceipt extends InAppPaymentReceiptDto { } - -@Injectable() -export class BookingPaymentService { - constructor( - private readonly bookingsRepository: BookingsRepository, - private readonly billing: BillingService, - ) { } - - /** - * Start payment for a booking. The booking never touches the payment gateway - * directly — it charges its invoice through billing, which resolves the amount - * and drives the provider. Returns the provider redirect URL (empty when none). - */ - async pay(bookingId: string): Promise<{ redirectUrl: string }> { - const booking = await this.requireBooking(bookingId); - assertBookingStatus(booking, ['FULLY_EXECUTED', 'SELECTED_FOR_BATCH', 'AWAITING_PAYMENT', '']); - - const resp = await this.billing.payInvoice(Freight.InvoiceSource.Booking, bookingId, { - method: PaymentMethodTypeEnum.TELEBIRR, - platform: 'web', - }); - - const action = resp.clientAction as { type?: string; url?: string } | undefined; - return { - redirectUrl: action?.type === 'REDIRECT' ? (action.url ?? '') : '', - }; - } - - private async requireBooking(id: string): Promise { - const booking = await this.bookingsRepository.findById(id); - if (!booking) throw new NotFoundException(`Booking ${id} not found`); - return booking; - } -} diff --git a/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts index 90ccd0118..cedc85f5f 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts @@ -447,6 +447,7 @@ export class BookingTransitionService { "CHANGES_REQUESTED", "PENDING_APPROVAL", "CONTRACT_READY", + "OPERATION_REQUEST_PENDING", ]); await this.bookingsRepository.createReviewNote( diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts index 4cd5d10df..5a7a0947f 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts @@ -14,13 +14,10 @@ import { BillingModule } from "../billing/billing.module"; import { FirstMileModule } from "../first-mile/first-mile.module"; import { BookingContractService } from "./booking-contract.service"; import { BookingInvoiceService } from "./booking-invoice.service"; -import { BookingPaymentController } from "./booking-payment.controller"; -import { BookingPaymentService } from "./booking-payment.service"; import { BookingPricingService } from "./booking-pricing.service"; import { BookingReferenceDataService } from "./booking-reference-data.service"; import { BookingTransitionService } from "./booking-transition.service"; import { BookingsController } from "./bookings.controller"; -import { PayController } from "./pay.controller"; import { BookingsRepository } from "./bookings.repository"; import { ConsolidationService } from "./consolidation.service"; import { BookingsService } from "./bookings.service"; @@ -69,7 +66,7 @@ import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.modu config.get("app.cbeExchange") ?? {}, }), ], - controllers: [BookingsController, PayController, BookingPaymentController], + controllers: [BookingsController], providers: [ BookingsService, BookingsRepository, @@ -79,7 +76,6 @@ import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.modu BookingTransitionService, BookingContractService, BookingInvoiceService, - BookingPaymentService, ContractTemplateResolver, ContractViewModelBuilder, ContractPricingScheduleBuilder, diff --git a/apps/edr-freight-api/src/modules/bookings/pay.controller.ts b/apps/edr-freight-api/src/modules/bookings/pay.controller.ts deleted file mode 100644 index 25f1927d3..000000000 --- a/apps/edr-freight-api/src/modules/bookings/pay.controller.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Controller, Param, ParseUUIDPipe, Post } from '@nestjs/common'; -import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { BookingPaymentService } from './booking-payment.service'; -// import { BookingTransitionService } from './booking-transition.service'; -// import { InAppPaymentReceiptDto } from './dto/pay-booking.dto'; -// import { Booking } from './entities/booking.entity'; -// import { BookingNextStep } from './booking-next-step.util'; - -@ApiTags('payments') -@ApiBearerAuth() -@Controller('bookings') -export class PayController { - constructor( - private readonly paymentService: BookingPaymentService, - // private readonly transitionService: BookingTransitionService, - ) { } - - @Post(':id/payment/pay') - @ApiOperation({ summary: 'Complete in-app payment (mock)' }) - @ApiOkResponse({ description: 'Enriched booking with ephemeral payment receipt' }) - async pay(@Param('id', ParseUUIDPipe) id: string) { - return await this.paymentService.pay(id); - // const abstract = await this.transitionService.enrichBookingResponse(booking); - // return { ...abstract, paymentReceipt: receipt }; - } -} diff --git a/apps/edr-freight-api/src/modules/payment/payments.dto.ts b/apps/edr-freight-api/src/modules/payment/payments.dto.ts index 67ca68e87..3b86a940c 100644 --- a/apps/edr-freight-api/src/modules/payment/payments.dto.ts +++ b/apps/edr-freight-api/src/modules/payment/payments.dto.ts @@ -15,9 +15,9 @@ export enum PaymentMethodTypeEnum { } export class InitiatePaymentDto { - @ApiProperty({ example: "booking-uuid" }) + @ApiProperty({ example: "invoice-uuid" }) @IsString() - bookingId!: string; + invoiceId!: string; @ApiProperty({ enum: PaymentMethodTypeEnum, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index 112c044fc..543646a7d 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -1069,6 +1069,7 @@ export class BookingBatchService implements OnModuleInit { Freight.InvoiceSource.Booking, booking.id, deadline, + "PREPAID", ); await this.notifier.payNow(booking, deadline); } @@ -1120,7 +1121,7 @@ export class BookingBatchService implements OnModuleInit { // Pay window closed before settlement → expire the booking's open invoice too // (emits `booking.invoice.expired`). Domain owns the reaction; billing stays // source-agnostic. - await this.billing.expirePayable(Freight.InvoiceSource.Booking, booking.id); + await this.billing.expirePayable(Freight.InvoiceSource.Booking, booking.id, "PREPAID"); this.notifier.expired(booking); } @@ -1167,6 +1168,7 @@ export class BookingBatchService implements OnModuleInit { await this.billing.expirePayable( Freight.InvoiceSource.Booking, victim.id, + "PREPAID", manager, ); });