mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +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 { 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user