mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 09:35:44 +00:00
eims integration master test complete
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
import { EimsConfig } from "../../config/eims.config";
|
||||
import { Invoice } from "../billing/entities/invoice.entity";
|
||||
import { NotificationsService } from "../notifications/notifications.service";
|
||||
import { EimsAuthService } from "./eims-auth.service";
|
||||
import { EimsClientService } from "./eims-client.service";
|
||||
import { EimsApiException } from "./eims.errors";
|
||||
import { EimsReceiptStatus } from "./entities/eims-receipt.entity";
|
||||
import { EimsReceiptService } from "./eims-receipt.service";
|
||||
|
||||
const INVOICE_ID = "11111111-1111-4111-8111-111111111111";
|
||||
const IRN = "9fe9bbbece6ab76c112b617534e6aac7aa8b819d5be79f4d3d088ed2e887b2e0";
|
||||
const SESSION = { systemNumber: "B0360154BA", systemType: "SYS" };
|
||||
|
||||
const invoiceRow = (over: Partial<Invoice> = {}): Invoice =>
|
||||
({
|
||||
id: INVOICE_ID,
|
||||
invoiceNumber: "INV-20260807-00042",
|
||||
companyId: "company-1",
|
||||
currency: "ETB",
|
||||
totalAmount: "10000.00",
|
||||
paidAmount: "10000.00",
|
||||
balanceAmount: "0.00",
|
||||
eimsIrn: IRN,
|
||||
...over,
|
||||
}) as unknown as Invoice;
|
||||
|
||||
/** In-memory stand-in: Invoice lookups + EimsReceipt save/update/find. */
|
||||
class FakeDb {
|
||||
invoices = new Map<string, Invoice>();
|
||||
receipts = new Map<string, Record<string, unknown>>();
|
||||
companyContact: { phone: string | null; email: string | null } | null = null;
|
||||
private seq = 0;
|
||||
|
||||
constructor(invoices: Invoice[]) {
|
||||
for (const inv of invoices) this.invoices.set(inv.id, inv);
|
||||
}
|
||||
|
||||
private manager = {
|
||||
findOne: async (entity: unknown, options: { where: { id: string } }) =>
|
||||
entity === Invoice
|
||||
? (this.invoices.get(options.where.id) ?? null)
|
||||
: (this.receipts.get(options.where.id) ?? null),
|
||||
find: async (_entity: unknown, options: { where: { invoiceId: string } }) =>
|
||||
[...this.receipts.values()].filter((r) => r.invoiceId === options.where.invoiceId),
|
||||
save: async (_entity: unknown, data: Record<string, unknown>) => {
|
||||
const id = `receipt-${++this.seq}`;
|
||||
const row = { id, ...data };
|
||||
this.receipts.set(id, row);
|
||||
return row;
|
||||
},
|
||||
update: async (_entity: unknown, id: string, patch: Record<string, unknown>) => {
|
||||
Object.assign(this.receipts.get(id)!, patch);
|
||||
},
|
||||
};
|
||||
|
||||
asDataSource(): DataSource {
|
||||
return {
|
||||
manager: this.manager,
|
||||
query: async () => (this.companyContact ? [this.companyContact] : []),
|
||||
} as unknown as DataSource;
|
||||
}
|
||||
}
|
||||
|
||||
const config = (): EimsConfig => ({ tin: "0053481357" }) as EimsConfig;
|
||||
|
||||
const build = (
|
||||
db: FakeDb,
|
||||
postBearer: jest.Mock,
|
||||
directSend: jest.Mock = jest.fn().mockResolvedValue(undefined),
|
||||
) =>
|
||||
new EimsReceiptService(
|
||||
db.asDataSource(),
|
||||
{ get: () => config() } as unknown as ConfigService,
|
||||
{ postBearer } as unknown as EimsClientService,
|
||||
{ getSessionContext: jest.fn().mockResolvedValue(SESSION) } as unknown as EimsAuthService,
|
||||
{ directSend } as unknown as NotificationsService,
|
||||
);
|
||||
|
||||
const okResponse = (over: Record<string, unknown> = {}) => ({
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
body: { status: "A", rrn: "rrn-value", qr: "iVBORw0KGgo...", ...over },
|
||||
});
|
||||
|
||||
describe("EimsReceiptService.registerSalesReceipt", () => {
|
||||
it("registers a sales receipt and persists the RRN/QR", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postBearer = jest.fn().mockResolvedValue(okResponse());
|
||||
|
||||
const receipt = await build(db, postBearer).registerSalesReceipt(INVOICE_ID, {
|
||||
modeOfPayment: "CASH",
|
||||
} as never);
|
||||
|
||||
expect(postBearer).toHaveBeenCalledTimes(1);
|
||||
expect(postBearer.mock.calls[0][0]).toBe("/v1/receipt/sales");
|
||||
const request = postBearer.mock.calls[0][1];
|
||||
expect(request.SellerTIN).toBe("0053481357");
|
||||
expect(request.SourceSystemNumber).toBe("B0360154BA");
|
||||
expect(request.Invoices[0].InvoiceIRN).toBe(IRN);
|
||||
expect(request.TransactionDetails.ModeOfPayment).toBe("CASH");
|
||||
expect(receipt.status).toBe(EimsReceiptStatus.Registered);
|
||||
expect(receipt.rrn).toBe("rrn-value");
|
||||
expect(receipt.qr).toBe("iVBORw0KGgo...");
|
||||
});
|
||||
|
||||
it("defaults PaymentCoverage to FULL when the invoice balance is 0, PARTIAL otherwise", async () => {
|
||||
const db = new FakeDb([invoiceRow({ balanceAmount: 500 })]);
|
||||
const postBearer = jest.fn().mockResolvedValue(okResponse());
|
||||
|
||||
await build(db, postBearer).registerSalesReceipt(INVOICE_ID, { modeOfPayment: "CASH" } as never);
|
||||
|
||||
expect(postBearer.mock.calls[0][1].Invoices[0].PaymentCoverage).toBe("PARTIAL");
|
||||
});
|
||||
|
||||
it("rejects an unconfirmed receipt currency locally, with zero HTTP calls", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postBearer = jest.fn();
|
||||
|
||||
await expect(
|
||||
build(db, postBearer).registerSalesReceipt(INVOICE_ID, {
|
||||
modeOfPayment: "CASH",
|
||||
currency: "GBP",
|
||||
} as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(postBearer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refuses to file a receipt against an unregistered invoice", async () => {
|
||||
const db = new FakeDb([invoiceRow({ eimsIrn: null })]);
|
||||
const postBearer = jest.fn();
|
||||
|
||||
await expect(
|
||||
build(db, postBearer).registerSalesReceipt(INVOICE_ID, { modeOfPayment: "CASH" } as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(postBearer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks the receipt FAILED on a deterministic rejection and rethrows", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postBearer = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new EimsApiException("RULE_VALIDATION", "EIMS receipt failed (406)", 406));
|
||||
|
||||
await expect(
|
||||
build(db, postBearer).registerSalesReceipt(INVOICE_ID, { modeOfPayment: "CASH" } as never),
|
||||
).rejects.toBeInstanceOf(EimsApiException);
|
||||
const [receipt] = [...db.receipts.values()];
|
||||
expect(receipt.status).toBe(EimsReceiptStatus.Failed);
|
||||
});
|
||||
|
||||
it("marks the receipt UNKNOWN on an ambiguous failure (never auto-retried)", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postBearer = jest.fn().mockRejectedValue(new EimsApiException("TIMEOUT", "EIMS receipt timed out"));
|
||||
|
||||
await expect(
|
||||
build(db, postBearer).registerSalesReceipt(INVOICE_ID, { modeOfPayment: "CASH" } as never),
|
||||
).rejects.toBeInstanceOf(EimsApiException);
|
||||
const [receipt] = [...db.receipts.values()];
|
||||
expect(receipt.status).toBe(EimsReceiptStatus.Unknown);
|
||||
});
|
||||
|
||||
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(okResponse());
|
||||
|
||||
const receipt = await build(db, postBearer, directSend).registerSalesReceipt(INVOICE_ID, {
|
||||
modeOfPayment: "CASH",
|
||||
} as never);
|
||||
|
||||
expect(receipt.status).toBe(EimsReceiptStatus.Registered);
|
||||
expect(directSend).toHaveBeenCalledWith("sms", "+251911000000", expect.stringContaining("sales"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("EimsReceiptService.registerWithholdingReceipt", () => {
|
||||
it("registers a withholding receipt and persists the RRN", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postBearer = jest.fn().mockResolvedValue(okResponse());
|
||||
|
||||
const receipt = await build(db, postBearer).registerWithholdingReceipt(INVOICE_ID, {
|
||||
type: "TWHT",
|
||||
preTaxAmount: 6000,
|
||||
withholdingAmount: 120,
|
||||
} as never);
|
||||
|
||||
expect(postBearer.mock.calls[0][0]).toBe("/v1/receipt/withholding");
|
||||
const request = postBearer.mock.calls[0][1];
|
||||
expect(request.InvoiceDetail.InvoiceIRN).toBe(IRN);
|
||||
expect(request.WithholdDetail).toMatchObject({ Type: "TWHT", PreTaxAmount: 6000, WithholdingAmount: 120 });
|
||||
expect(receipt.status).toBe(EimsReceiptStatus.Registered);
|
||||
expect(receipt.rrn).toBe("rrn-value");
|
||||
});
|
||||
|
||||
it("requires an exchangeRate for a non-ETB invoice", async () => {
|
||||
const db = new FakeDb([invoiceRow({ currency: "USD" })]);
|
||||
const postBearer = jest.fn();
|
||||
|
||||
await expect(
|
||||
build(db, postBearer).registerWithholdingReceipt(INVOICE_ID, {
|
||||
type: "TWHT",
|
||||
preTaxAmount: 6000,
|
||||
withholdingAmount: 120,
|
||||
} as never),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(postBearer).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("EimsReceiptService.listReceipts", () => {
|
||||
it("returns every receipt filed against the invoice", async () => {
|
||||
const db = new FakeDb([invoiceRow()]);
|
||||
const postBearer = jest.fn().mockResolvedValue(okResponse());
|
||||
const service = build(db, postBearer);
|
||||
|
||||
await service.registerSalesReceipt(INVOICE_ID, { modeOfPayment: "CASH" } as never);
|
||||
await service.registerWithholdingReceipt(INVOICE_ID, {
|
||||
type: "TWHT",
|
||||
preTaxAmount: 100,
|
||||
withholdingAmount: 2,
|
||||
} as never);
|
||||
|
||||
const list = await service.listReceipts(INVOICE_ID);
|
||||
expect(list).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user