feat: rm the hardcoded invoice types

This commit is contained in:
ghost2023
2026-07-01 15:10:04 +03:00
parent 1754c18ede
commit 7e96fa65b9
2 changed files with 47 additions and 36 deletions

View File

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

View File

@@ -151,14 +151,6 @@ export enum InvoiceSource {
Demurrage = "demurrage", 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 { export enum SchedulingStatus {
NotScheduled = "NOT_SCHEDULED", NotScheduled = "NOT_SCHEDULED",
Holding = "HOLDING", Holding = "HOLDING",