mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
chore: updating billing logic
This commit is contained in:
147
apps/edr-freight-api/src/modules/billing/billing.service.spec.ts
Normal file
147
apps/edr-freight-api/src/modules/billing/billing.service.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Freight } from "@edr/types";
|
||||
|
||||
import { BillingService } from "./billing.service";
|
||||
import type { Invoice } from "./entities/invoice.entity";
|
||||
|
||||
/**
|
||||
* Minimal in-memory EntityManager stand-in covering the methods
|
||||
* `generateForBooking` / `markBookingInvoicePaid` call on the transaction manager.
|
||||
*/
|
||||
function makeManager(savedLines: unknown[]) {
|
||||
return {
|
||||
create: (_entity: unknown, data: Record<string, unknown>) => data,
|
||||
save: (data: Record<string, unknown>) => {
|
||||
const row = { id: data.id ?? `gen-${Math.round(0)}`, ...data };
|
||||
if (data.invoiceId) savedLines.push(row);
|
||||
return Promise.resolve(row);
|
||||
},
|
||||
query: () => Promise.resolve([{ seq: 0 }]),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
}
|
||||
|
||||
function makeBooking(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "booking-1",
|
||||
reference: "BK-001",
|
||||
companyId: "company-1",
|
||||
companyProfileId: "profile-1",
|
||||
paymentCurrency: "ETB",
|
||||
totalAmount: 1500,
|
||||
pricingBreakdown: {
|
||||
currency: "ETB",
|
||||
totalAmount: 1500,
|
||||
lineItems: [
|
||||
{ code: "RAIL_FREIGHT", description: "Rail freight", amount: 1000, unitAmount: 500, unit: "PER_CONTAINER", quantity: 2, currency: "ETB" },
|
||||
{ code: "HAZARD_SURCHARGE", description: "Hazard surcharge", amount: 500, unitAmount: 250, unit: "PER_CONTAINER", quantity: 2, currency: "ETB" },
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("BillingService.generateForBooking", () => {
|
||||
let invoices: { findAll: jest.Mock; findById: jest.Mock };
|
||||
let invoiceLines: { findAll: jest.Mock };
|
||||
let savedLines: unknown[];
|
||||
let manager: ReturnType<typeof makeManager>;
|
||||
let bookingRow: Record<string, unknown> | null;
|
||||
let dataSource: {
|
||||
getRepository: jest.Mock;
|
||||
transaction: jest.Mock;
|
||||
manager: unknown;
|
||||
};
|
||||
let service: BillingService;
|
||||
|
||||
beforeEach(() => {
|
||||
savedLines = [];
|
||||
manager = makeManager(savedLines);
|
||||
invoices = { findAll: jest.fn().mockResolvedValue([]), findById: jest.fn() };
|
||||
invoiceLines = { findAll: jest.fn().mockResolvedValue([]) };
|
||||
bookingRow = makeBooking();
|
||||
dataSource = {
|
||||
getRepository: jest.fn().mockReturnValue({
|
||||
findOne: jest.fn().mockImplementation(() => Promise.resolve(bookingRow)),
|
||||
}),
|
||||
transaction: jest.fn().mockImplementation((cb: (mg: unknown) => unknown) => cb(manager)),
|
||||
manager,
|
||||
};
|
||||
service = new BillingService(dataSource as never, invoices as never, invoiceLines as never);
|
||||
});
|
||||
|
||||
it("creates a PENDING invoice with one line per pricing line item", async () => {
|
||||
const invoice = (await service.generateForBooking("booking-1")) as Invoice;
|
||||
|
||||
expect(invoice).toBeTruthy();
|
||||
expect(invoice.status).toBe(Freight.InvoiceStatus.Pending);
|
||||
expect(invoice.companyId).toBe("company-1");
|
||||
expect(invoice.companyProfileId).toBe("profile-1");
|
||||
expect(invoice.source).toBe("booking");
|
||||
expect(invoice.sourceId).toBe("booking-1");
|
||||
expect(invoice.totalAmount).toBe(1500);
|
||||
expect(invoice.invoiceNumber).toMatch(/^FRT-\d{8}-00001$/);
|
||||
expect(savedLines).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns the existing active invoice instead of creating a duplicate", async () => {
|
||||
const existing = { id: "inv-existing", status: Freight.InvoiceStatus.Pending } as Invoice;
|
||||
invoices.findAll.mockResolvedValueOnce([existing]);
|
||||
|
||||
const invoice = await service.generateForBooking("booking-1");
|
||||
|
||||
expect(invoice).toBe(existing);
|
||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips generation (returns null) when the booking has no company to bill", async () => {
|
||||
bookingRow = makeBooking({ companyId: null, companyProfileId: null });
|
||||
|
||||
const invoice = await service.generateForBooking("booking-1");
|
||||
|
||||
expect(invoice).toBeNull();
|
||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to a single freight line when no pricing breakdown exists", async () => {
|
||||
bookingRow = makeBooking({ pricingBreakdown: null });
|
||||
|
||||
const invoice = (await service.generateForBooking("booking-1")) as Invoice;
|
||||
|
||||
expect(invoice.totalAmount).toBe(1500);
|
||||
expect(savedLines).toHaveLength(1);
|
||||
expect((savedLines[0] as { chargeType: string }).chargeType).toBe("FREIGHT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("BillingService.markBookingInvoicePaid", () => {
|
||||
it("marks the open booking invoice PAID and links the payment", async () => {
|
||||
const open = { id: "inv-1", status: Freight.InvoiceStatus.Pending };
|
||||
const mg = {
|
||||
findOne: jest.fn().mockResolvedValue(open),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const dataSource = { manager: mg } as never;
|
||||
const service = new BillingService(dataSource, {} as never, {} as never);
|
||||
|
||||
await service.markBookingInvoicePaid("booking-1", "pay-1", mg as never);
|
||||
|
||||
expect(mg.update).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{ id: "inv-1" },
|
||||
{ status: Freight.InvoiceStatus.Paid, paymentId: "pay-1" },
|
||||
);
|
||||
});
|
||||
|
||||
it("is a no-op when the booking has no open invoice", async () => {
|
||||
const mg = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const service = new BillingService({ manager: mg } as never, {} as never, {} as never);
|
||||
|
||||
await service.markBookingInvoicePaid("booking-1", "pay-1", mg as never);
|
||||
|
||||
expect(mg.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user