mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
156 lines
6.4 KiB
TypeScript
156 lines
6.4 KiB
TypeScript
import { BadRequestException, ConflictException } from "@nestjs/common";
|
|
import { DataSource } from "typeorm";
|
|
|
|
import { Invoice } from "../billing/entities/invoice.entity";
|
|
import { NotificationsService } from "../notifications/notifications.service";
|
|
import { EimsCancellationService } from "./eims-cancellation.service";
|
|
import { EimsClientService } from "./eims-client.service";
|
|
import { EimsApiException } from "./eims.errors";
|
|
import { EimsInvoiceStatus } from "./eims-registration.types";
|
|
|
|
const INVOICE_ID = "11111111-1111-4111-8111-111111111111";
|
|
const IRN = "9fe9bbbece6ab76c112b617534e6aac7aa8b819d5be79f4d3d088ed2e887b2e0";
|
|
|
|
const invoiceRow = (over: Partial<Invoice> = {}): Invoice =>
|
|
({
|
|
id: INVOICE_ID,
|
|
invoiceNumber: "INV-20260807-00042",
|
|
companyId: "company-1",
|
|
eimsStatus: EimsInvoiceStatus.Registered,
|
|
eimsIrn: IRN,
|
|
eimsCancelledAt: null,
|
|
eimsCancellationDate: null,
|
|
eimsCancellationReasonCode: null,
|
|
eimsCancellationRemark: null,
|
|
...over,
|
|
}) as unknown as Invoice;
|
|
|
|
/** In-memory stand-in, same shape as the registration spec's FakeDb but with only what cancel needs. */
|
|
class FakeDb {
|
|
invoices = new Map<string, Invoice>();
|
|
companyContact: { phone: string | null; email: string | null } | null = null;
|
|
|
|
constructor(invoices: Invoice[]) {
|
|
for (const inv of invoices) this.invoices.set(inv.id, inv);
|
|
}
|
|
|
|
private manager = {
|
|
createQueryBuilder: (entity: unknown) => {
|
|
let id: string | undefined;
|
|
const builder = {
|
|
setLock: () => builder,
|
|
where: (_clause: string, params: Record<string, string>) => {
|
|
id = params.invoiceId;
|
|
return builder;
|
|
},
|
|
getOne: async () => (entity === Invoice ? (this.invoices.get(id!) ?? null) : null),
|
|
};
|
|
return builder;
|
|
},
|
|
findOne: async (_entity: unknown, options: { where: { id: string } }) =>
|
|
this.invoices.get(options.where.id) ?? null,
|
|
update: async (_entity: unknown, id: string, patch: Record<string, unknown>) => {
|
|
Object.assign(this.invoices.get(id)!, patch);
|
|
},
|
|
};
|
|
|
|
asDataSource(): DataSource {
|
|
return {
|
|
manager: this.manager,
|
|
query: async () => (this.companyContact ? [this.companyContact] : []),
|
|
transaction: async (body: (m: unknown) => Promise<unknown>) => body(this.manager),
|
|
} as unknown as DataSource;
|
|
}
|
|
}
|
|
|
|
const build = (db: FakeDb, postBearer: jest.Mock, directSend: jest.Mock = jest.fn().mockResolvedValue(undefined)) =>
|
|
new EimsCancellationService(
|
|
db.asDataSource(),
|
|
{ postBearer } as unknown as EimsClientService,
|
|
{ directSend } as unknown as NotificationsService,
|
|
);
|
|
|
|
describe("EimsCancellationService.cancelInvoiceWithEims", () => {
|
|
it("cancels a registered invoice and persists MoR's confirmation", async () => {
|
|
const db = new FakeDb([invoiceRow()]);
|
|
const postBearer = jest
|
|
.fn()
|
|
.mockResolvedValue({ statusCode: 200, message: "Success", body: { cancellationDate: "Sun Dec 22 21:55:03 EAT 2024" } });
|
|
|
|
const view = await build(db, postBearer).cancelInvoiceWithEims(INVOICE_ID, "1", "Duplicate");
|
|
|
|
expect(postBearer).toHaveBeenCalledWith("/v1/cancel", { Irn: IRN, ReasonCode: "1", Remark: "Duplicate" });
|
|
expect(view.eimsStatus).toBe(EimsInvoiceStatus.Cancelled);
|
|
expect(view.eimsCancellationDate).toBe("Sun Dec 22 21:55:03 EAT 2024");
|
|
expect(view.eimsCancellationReasonCode).toBe("1");
|
|
expect(view.eimsCancellationRemark).toBe("Duplicate");
|
|
expect(db.invoices.get(INVOICE_ID)?.eimsCancelledAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("defaults Remark to an empty string, matching the collection's request shape", async () => {
|
|
const db = new FakeDb([invoiceRow()]);
|
|
const postBearer = jest.fn().mockResolvedValue({ statusCode: 200, body: { cancellationDate: "x" } });
|
|
|
|
await build(db, postBearer).cancelInvoiceWithEims(INVOICE_ID, "1");
|
|
|
|
expect(postBearer).toHaveBeenCalledWith("/v1/cancel", { Irn: IRN, ReasonCode: "1", Remark: "" });
|
|
});
|
|
|
|
it("refuses re-cancelling an already-cancelled invoice, per IRC-N010 — no silent no-op", async () => {
|
|
const db = new FakeDb([
|
|
invoiceRow({ eimsStatus: EimsInvoiceStatus.Cancelled, eimsCancellationDate: "Sun Dec 22 2024" }),
|
|
]);
|
|
const postBearer = jest.fn();
|
|
|
|
await expect(build(db, postBearer).cancelInvoiceWithEims(INVOICE_ID, "1")).rejects.toBeInstanceOf(
|
|
ConflictException,
|
|
);
|
|
expect(postBearer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("refuses to cancel an invoice that was never registered", async () => {
|
|
const db = new FakeDb([invoiceRow({ eimsStatus: EimsInvoiceStatus.NotSubmitted, eimsIrn: null })]);
|
|
const postBearer = jest.fn();
|
|
|
|
await expect(build(db, postBearer).cancelInvoiceWithEims(INVOICE_ID, "1")).rejects.toBeInstanceOf(
|
|
BadRequestException,
|
|
);
|
|
expect(postBearer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("propagates a MoR rejection and leaves the invoice REGISTERED", async () => {
|
|
const db = new FakeDb([invoiceRow()]);
|
|
const postBearer = jest
|
|
.fn()
|
|
.mockRejectedValue(new EimsApiException("RULE_VALIDATION", "EIMS cancel failed (406)", 406));
|
|
|
|
await expect(build(db, postBearer).cancelInvoiceWithEims(INVOICE_ID, "1")).rejects.toBeInstanceOf(
|
|
EimsApiException,
|
|
);
|
|
expect(db.invoices.get(INVOICE_ID)?.eimsStatus).toBe(EimsInvoiceStatus.Registered);
|
|
});
|
|
|
|
it("notifies the buyer company on success, without blocking the result", async () => {
|
|
const db = new FakeDb([invoiceRow()]);
|
|
db.companyContact = { phone: "+251911000000", email: "buyer@abc.et" };
|
|
const directSend = jest.fn().mockResolvedValue(undefined);
|
|
const postBearer = jest.fn().mockResolvedValue({ statusCode: 200, body: { cancellationDate: "x" } });
|
|
|
|
const view = await build(db, postBearer, directSend).cancelInvoiceWithEims(INVOICE_ID, "1");
|
|
|
|
expect(view.eimsStatus).toBe(EimsInvoiceStatus.Cancelled);
|
|
expect(directSend).toHaveBeenCalledWith("sms", "+251911000000", expect.stringContaining("cancelled"));
|
|
});
|
|
|
|
it("does not fail cancellation when the buyer notification itself fails", async () => {
|
|
const db = new FakeDb([invoiceRow()]);
|
|
db.companyContact = { phone: "+251911000000", email: null };
|
|
const directSend = jest.fn().mockRejectedValue(new Error("sms provider down"));
|
|
const postBearer = jest.fn().mockResolvedValue({ statusCode: 200, body: { cancellationDate: "x" } });
|
|
|
|
const view = await build(db, postBearer, directSend).cancelInvoiceWithEims(INVOICE_ID, "1");
|
|
|
|
expect(view.eimsStatus).toBe(EimsInvoiceStatus.Cancelled);
|
|
});
|
|
});
|