mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: rm the hardcoded invoice types
This commit is contained in:
@@ -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<Invoice | null> {
|
||||
async ensureInvoiceForBooking(
|
||||
booking: Booking,
|
||||
invoiceOptions: InvoiceOptions = {},
|
||||
): Promise<Invoice | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user