mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
chore: type updates on the billing service
This commit is contained in:
@@ -33,8 +33,20 @@ function generateInput(overrides: Record<string, unknown> = {}) {
|
||||
companyProfileId: "profile-1",
|
||||
currency: "ETB",
|
||||
lines: [
|
||||
{ chargeType: "RAIL_FREIGHT", description: "Rail freight", quantity: 2, unitRate: 500, amount: 1000 },
|
||||
{ chargeType: "HAZARD_SURCHARGE", description: "Hazard surcharge", quantity: 2, unitRate: 250, amount: 500 },
|
||||
{
|
||||
chargeType: "RAIL_FREIGHT",
|
||||
description: "Rail freight",
|
||||
quantity: 2,
|
||||
unitRate: 500,
|
||||
amount: 1000,
|
||||
},
|
||||
{
|
||||
chargeType: "HAZARD_SURCHARGE",
|
||||
description: "Hazard surcharge",
|
||||
quantity: 2,
|
||||
unitRate: 250,
|
||||
amount: 500,
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
@@ -52,10 +64,17 @@ describe("BillingService.generateInvoice", () => {
|
||||
manager = makeManager(savedLines);
|
||||
events = makeEvents();
|
||||
dataSource = {
|
||||
transaction: jest.fn().mockImplementation((cb: (mg: unknown) => unknown) => cb(manager)),
|
||||
transaction: jest
|
||||
.fn()
|
||||
.mockImplementation((cb: (mg: unknown) => unknown) => cb(manager)),
|
||||
manager,
|
||||
};
|
||||
service = new BillingService(dataSource as never, {} as never, {} as never, events as never);
|
||||
service = new BillingService(
|
||||
dataSource as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
events as never,
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a PENDING invoice with one line per input line", async () => {
|
||||
@@ -122,18 +141,31 @@ describe("BillingService.markInvoiceAsPaid", () => {
|
||||
);
|
||||
expect(events.emit).toHaveBeenCalledWith(
|
||||
"booking.invoice.paid",
|
||||
expect.objectContaining({ invoiceId: "inv-1", status: Freight.InvoiceStatus.Paid, paymentId: "pay-1" }),
|
||||
expect.objectContaining({
|
||||
invoiceId: "inv-1",
|
||||
status: Freight.InvoiceStatus.Paid,
|
||||
paymentId: "pay-1",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("is a no-op (no event) when the invoice is already paid", async () => {
|
||||
const paid = { id: "inv-1", status: Freight.InvoiceStatus.Paid, source: "booking" };
|
||||
const paid = {
|
||||
id: "inv-1",
|
||||
status: Freight.InvoiceStatus.Paid,
|
||||
source: "booking",
|
||||
};
|
||||
const mg = {
|
||||
findOne: jest.fn().mockResolvedValue(paid),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const events = makeEvents();
|
||||
const service = new BillingService({ manager: mg } as never, {} as never, {} as never, events as never);
|
||||
const service = new BillingService(
|
||||
{ manager: mg } as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
events as never,
|
||||
);
|
||||
|
||||
await service.markInvoiceAsPaid("inv-1", "pay-1", mg as never);
|
||||
|
||||
@@ -155,12 +187,16 @@ describe("BillingService.settlePayable", () => {
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const events = makeEvents();
|
||||
const service = new BillingService({ manager: mg } as never, {} as never, {} as never, events as never);
|
||||
const service = new BillingService(
|
||||
{ manager: mg } as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
events as never,
|
||||
);
|
||||
|
||||
const settled = await service.settlePayable(
|
||||
Freight.InvoiceSource.Booking,
|
||||
"booking-1",
|
||||
Freight.InvoiceType.Prepaid,
|
||||
"pay-1",
|
||||
mg as never,
|
||||
);
|
||||
@@ -171,7 +207,10 @@ describe("BillingService.settlePayable", () => {
|
||||
{ id: "inv-1" },
|
||||
{ status: Freight.InvoiceStatus.Paid, paymentId: "pay-1" },
|
||||
);
|
||||
expect(events.emit).toHaveBeenCalledWith("booking.invoice.paid", expect.anything());
|
||||
expect(events.emit).toHaveBeenCalledWith(
|
||||
"booking.invoice.paid",
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("is a no-op (returns null) when the source has no open invoice", async () => {
|
||||
@@ -180,12 +219,16 @@ describe("BillingService.settlePayable", () => {
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const events = makeEvents();
|
||||
const service = new BillingService({ manager: mg } as never, {} as never, {} as never, events as never);
|
||||
const service = new BillingService(
|
||||
{ manager: mg } as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
events as never,
|
||||
);
|
||||
|
||||
const settled = await service.settlePayable(
|
||||
Freight.InvoiceSource.Booking,
|
||||
"booking-1",
|
||||
Freight.InvoiceType.Prepaid,
|
||||
"pay-1",
|
||||
mg as never,
|
||||
);
|
||||
|
||||
@@ -308,42 +308,47 @@ export class BillingService {
|
||||
* against it rather than recomputing from the source's own total, so
|
||||
* discounts/penalties/adjustments carried on the invoice are honored.
|
||||
*
|
||||
* `type` selects which invoice when a source carries several (e.g. a booking's
|
||||
* up-front vs final charge). Returns the most recent open (unpaid,
|
||||
* non-cancelled) invoice of that type.
|
||||
* Pass `type` to select a specific invoice when a source carries several (e.g.
|
||||
* a booking's up-front vs final charge); omit it to settle whichever single
|
||||
* invoice is currently open. Returns the most recent matching open (unpaid,
|
||||
* non-cancelled) invoice.
|
||||
*/
|
||||
findPayable(
|
||||
source: Freight.InvoiceSource,
|
||||
sourceId: string,
|
||||
type: string,
|
||||
type?: string,
|
||||
): Promise<Invoice | null> {
|
||||
return this.dataSource.getRepository(Invoice).findOne({
|
||||
where: { source, sourceId, type, status: In(OPEN_STATUSES) },
|
||||
where: {
|
||||
source,
|
||||
sourceId,
|
||||
status: In(OPEN_STATUSES),
|
||||
...(type ? { type } : {}),
|
||||
},
|
||||
order: { issuedAt: "DESC" },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle a source's open invoice of `type` as paid and link the gateway
|
||||
* Settle a source's currently-open invoice as paid and link the gateway
|
||||
* payment, then emit `${source}.invoice.paid`. Resolves the open invoice then
|
||||
* delegates to {@link markInvoiceAsPaid}. Full-payment only — no partial
|
||||
* settlement. No-op (returns null) when the source has no open invoice of that
|
||||
* type.
|
||||
* settlement. No-op (returns null) when the source has no open invoice.
|
||||
*
|
||||
* `type` selects which invoice when a source carries several (e.g. a booking's
|
||||
* up-front vs final charge). Pass the caller's transaction `manager` (e.g. from
|
||||
* Type-blind by design: the payment process settles whichever invoice is due;
|
||||
* any per-type reaction belongs in the `${source}.invoice.paid` handler, which
|
||||
* reads `invoice.type`. Pass the caller's transaction `manager` (e.g. from
|
||||
* `payment.service.finalizePaymentSuccess`) to enlist in its DB transaction.
|
||||
*/
|
||||
async settlePayable(
|
||||
source: Freight.InvoiceSource,
|
||||
sourceId: string,
|
||||
type: string,
|
||||
paymentId: string | null,
|
||||
manager?: EntityManager,
|
||||
): Promise<Invoice | null> {
|
||||
const mg = manager ?? this.dataSource.manager;
|
||||
const invoice = await mg.findOne(Invoice, {
|
||||
where: { source, sourceId, type, status: In(OPEN_STATUSES) },
|
||||
where: { source, sourceId, status: In(OPEN_STATUSES) },
|
||||
order: { issuedAt: "DESC" },
|
||||
});
|
||||
if (!invoice) return null;
|
||||
@@ -352,10 +357,9 @@ export class BillingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund a source's paid invoice of `type`, then emit
|
||||
* `${source}.invoice.refunded`. Resolves the paid invoice then delegates to
|
||||
* {@link markInvoiceAsRefunded}. No-op (returns null) when the source has no
|
||||
* paid invoice of that type.
|
||||
* Refund a source's paid invoice, then emit `${source}.invoice.refunded`.
|
||||
* Resolves the paid invoice then delegates to {@link markInvoiceAsRefunded}.
|
||||
* No-op (returns null) when the source has no paid invoice.
|
||||
*
|
||||
* Pass the caller's transaction `manager` (e.g. from `payment.service.refund`)
|
||||
* to enlist in its DB transaction.
|
||||
@@ -363,12 +367,11 @@ export class BillingService {
|
||||
async refundPayable(
|
||||
source: Freight.InvoiceSource,
|
||||
sourceId: string,
|
||||
type: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<Invoice | null> {
|
||||
const mg = manager ?? this.dataSource.manager;
|
||||
const invoice = await mg.findOne(Invoice, {
|
||||
where: { source, sourceId, type, status: Freight.InvoiceStatus.Paid },
|
||||
where: { source, sourceId, status: Freight.InvoiceStatus.Paid },
|
||||
order: { issuedAt: "DESC" },
|
||||
});
|
||||
if (!invoice) return null;
|
||||
|
||||
Reference in New Issue
Block a user