diff --git a/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts index 47338f196..49d184b48 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts @@ -1,20 +1,20 @@ -import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; -import { Freight } from '@edr/types'; -import { DataSource } from 'typeorm'; +import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; +import { OnEvent } from "@nestjs/event-emitter"; +import { Freight } from "@edr/types"; +import { DataSource } from "typeorm"; import { BillingService, GenerateInvoiceInput, InvoiceEventPayload, InvoiceLineInput, -} from '../billing/billing.service'; -import { Invoice } from '../billing/entities/invoice.entity'; -import { FirstMileService } from '../first-mile/first-mile.service'; -import { BookingBatchService } from '../train-scheduling/booking-batch.service'; -import { PriceLineItemDto } from './dto/generate-price-response.dto'; -import { BookingsRepository } from './bookings.repository'; -import { Booking } from './entities/booking.entity'; +} from "../billing/billing.service"; +import { Invoice } from "../billing/entities/invoice.entity"; +import { FirstMileService } from "../first-mile/first-mile.service"; +import { BookingBatchService } from "../train-scheduling/booking-batch.service"; +import { PriceLineItemDto } from "./dto/generate-price-response.dto"; +import { BookingsRepository } from "./bookings.repository"; +import { Booking } from "./entities/booking.entity"; /** Snapshot written onto `booking.pricingBreakdown` by the pricing service. */ interface StoredPricingBreakdown { @@ -23,6 +23,12 @@ interface StoredPricingBreakdown { currency?: string; } +export interface InvoiceOptions { + dueDate?: Date; + invoiceType?: string; + invoiceStatus?: Freight.InvoiceStatus; +} + /** Round to 2 decimals, avoiding binary float drift. */ const round2 = (n: number): number => Math.round(n * 100) / 100; @@ -56,11 +62,14 @@ export class BookingInvoiceService { * bill (e.g. government bookings whose `companyId` is null, which the invoices * FK requires), or no priced amount. */ - async ensureInvoiceForBooking(booking: Booking): Promise { + async ensureInvoiceForBooking( + booking: Booking, + invoiceOptions: InvoiceOptions = {}, + ): Promise { const existing = await this.billing.findPayable( Freight.InvoiceSource.Booking, booking.id, - Freight.InvoiceType.Prepaid, + "PREPAID", ); if (existing) return existing; @@ -71,7 +80,7 @@ export class BookingInvoiceService { return null; } - const input = this.buildInput(booking); + const input = this.buildInput(booking, invoiceOptions); if (!input) { this.logger.warn( `Skipping invoice for booking ${booking.reference} (${booking.id}): no priced amount.`, @@ -87,10 +96,10 @@ export class BookingInvoiceService { * reactions live here (not in the payment process): each invoice type advances * the booking its own way. Only PREPAID exists today. */ - @OnEvent('booking.invoice.paid') + @OnEvent("booking.invoice.paid") async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise { switch (payload.type) { - case Freight.InvoiceType.Prepaid: + case "PREPAID": await this.advanceBookingOnPayment(payload.sourceId); break; default: @@ -114,16 +123,18 @@ export class BookingInvoiceService { private async advanceBookingOnPayment(bookingId: string): Promise { const booking = await this.bookingsRepository.findById(bookingId); if (!booking) { - this.logger.warn(`Cannot advance unknown booking ${bookingId} on payment.`); + this.logger.warn( + `Cannot advance unknown booking ${bookingId} on payment.`, + ); return; } - if (booking.paymentStatus === 'PAID') return; + if (booking.paymentStatus === "PAID") return; await this.dataSource.transaction(async (mg) => { await mg.update( Booking, { id: bookingId }, - { paymentStatus: 'PAID', status: 'PAID' }, + { paymentStatus: "PAID", status: "PAID" }, ); await this.firstMile.acceptBooking(bookingId); }); @@ -138,9 +149,13 @@ export class BookingInvoiceService { } /** Map a booking's pricing snapshot into a generic invoice request. */ - private buildInput(booking: Booking): GenerateInvoiceInput | null { - const breakdown = (booking.pricingBreakdown ?? {}) as StoredPricingBreakdown; - const currency = breakdown.currency ?? booking.paymentCurrency ?? 'ETB'; + private buildInput( + booking: Booking, + invoiceOptions: InvoiceOptions = {}, + ): GenerateInvoiceInput | null { + const breakdown = (booking.pricingBreakdown ?? + {}) as StoredPricingBreakdown; + const currency = breakdown.currency ?? booking.paymentCurrency ?? "ETB"; const lines: InvoiceLineInput[] = (breakdown.lineItems ?? []).map((l) => ({ chargeType: l.code, @@ -157,8 +172,8 @@ export class BookingInvoiceService { const amount = Number(booking.totalAmount); if (!Number.isFinite(amount) || amount <= 0) return null; lines.push({ - chargeType: 'FREIGHT', - description: 'Rail freight', + chargeType: "FREIGHT", + description: "Rail freight", quantity: 1, unitRate: amount, amount, @@ -166,7 +181,9 @@ export class BookingInvoiceService { }); } - const subtotal = round2(lines.reduce((sum, l) => sum + Number(l.amount), 0)); + const subtotal = round2( + lines.reduce((sum, l) => sum + Number(l.amount), 0), + ); let totalAmount = subtotal; // Honor a staff price override: bill the adjusted total, recording the delta @@ -176,8 +193,8 @@ export class BookingInvoiceService { const delta = round2(Number(adjusted) - subtotal); if (delta !== 0) { lines.push({ - chargeType: 'ADJUSTMENT', - description: 'Staff price adjustment', + chargeType: "ADJUSTMENT", + description: "Staff price adjustment", quantity: 1, unitRate: delta, amount: delta, @@ -190,12 +207,14 @@ export class BookingInvoiceService { return { source: Freight.InvoiceSource.Booking, sourceId: booking.id, - type: Freight.InvoiceType.Prepaid, companyId: booking.companyId, companyProfileId: booking.companyProfileId, currency, lines, totalAmount, + dueAt: invoiceOptions.dueDate, + type: invoiceOptions.invoiceType ?? "PREPAID", + status: invoiceOptions.invoiceStatus ?? Freight.InvoiceStatus.Draft, }; } } diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index 5cffecf70..3d92a5652 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -151,14 +151,6 @@ export enum InvoiceSource { Demurrage = "demurrage", } -/** - * What an invoice bills for within its source — the discriminator when one - * entity carries several invoices (e.g. a booking's up-front vs final charge). - */ -export enum InvoiceType { - Prepaid = "PREPAID", -} - export enum SchedulingStatus { NotScheduled = "NOT_SCHEDULED", Holding = "HOLDING",