import { Prisma } from "@jorgecuadros/database"; import { BillingService } from "./billing.service"; /** * The statement is a *year* statement, like the EDO CUENTA report the office * prints: this year's movements, oldest-first, opening on the balance carried * in from before it. * * The carrying is the part worth testing. Dropping earlier rows from the list * is easy; dropping them from the arithmetic too would restart every balance at * zero on January 1st, and nothing would throw — the numbers would just be * wrong, which is exactly how the double-counting bug lived for years. */ describe("statement year scoping", () => { const YEAR = new Date().getUTCFullYear(); function d(iso: string) { return new Date(`${iso}T00:00:00.000Z`); } type RowSpec = { id: string; date: Date; amount: string; currency?: string; domain?: string; voidedAt?: Date | null; outstanding?: boolean; }; function row(r: RowSpec) { return { id: r.id, transactionDate: r.date, domain: r.domain ?? "UTILITY", amount: new Prisma.Decimal(r.amount), currency: r.currency ?? "MXN", reference: null, period: null, checkNumber: null, message: null, legacySourceTable: null, voidedAt: r.voidedAt ?? null, outstanding: r.outstanding ?? false, type: { nameEn: "WATER", nameEs: "AGUA" }, }; } /** No BALANCE FORWARD row, so the floor is null and every row is fetched. */ function serviceWith(rows: RowSpec[]) { const prisma = { customer: { findUnique: jest.fn().mockResolvedValue({ id: "c1", name: "CUADROS, JORGE H.", preferredCurrency: "MXN", _count: { properties: 0, policies: 0 }, }), }, transaction: { findFirst: jest.fn().mockResolvedValue(null), findMany: jest.fn().mockResolvedValue(rows.map(row)), }, }; return new BillingService(prisma as never); } it("lists the year's movements oldest-first", async () => { const s = await serviceWith([ { id: "a", date: d(`${YEAR}-01-02`), amount: "-100" }, { id: "b", date: d(`${YEAR}-03-04`), amount: "250" }, { id: "c", date: d(`${YEAR}-07-16`), amount: "-40" }, ]).statement("c1"); expect(s.movements.map((m) => m.id)).toEqual(["a", "b", "c"]); }); it("leaves earlier years off the list", async () => { const s = await serviceWith([ { id: "old", date: d(`${YEAR - 1}-11-30`), amount: "-500" }, { id: "new", date: d(`${YEAR}-02-11`), amount: "-100" }, ]).statement("c1"); expect(s.movements.map((m) => m.id)).toEqual(["new"]); }); it("carries the earlier years' balance instead of discarding it", async () => { // 1,000 credit left over from last year, 300 charged this year: the // customer is 700 in credit, not 300 in debt. const s = await serviceWith([ { id: "old", date: d(`${YEAR - 1}-12-15`), amount: "1000" }, { id: "new", date: d(`${YEAR}-02-11`), amount: "-300" }, ]).statement("c1"); const mxn = s.summary.find((x) => x.currency === "MXN"); expect(mxn?.opening).toBe("1000.00"); expect(mxn?.charges).toBe("-300.00"); expect(mxn?.balance).toBe("700.00"); // The running balance on the listed row picks up where last year left off. expect(s.movements[0].balanceAfter).toBe("700.00"); }); it("carries it per business line as well", async () => { const s = await serviceWith([ { id: "old", date: d(`${YEAR - 1}-12-15`), amount: "1000", domain: "INSURANCE" }, { id: "new", date: d(`${YEAR}-02-11`), amount: "-300", domain: "INSURANCE" }, ]).statement("c1"); const line = s.byDomain.find((x) => x.domain === "INSURANCE"); expect(line?.opening).toBe("1000.00"); expect(line?.balance).toBe("700.00"); }); it("still reports a currency that only moved in earlier years", async () => { // Otherwise a customer sitting on a dollar credit they haven't touched all // year would appear to have no dollar balance at all. const s = await serviceWith([ { id: "old", date: d(`${YEAR - 2}-05-01`), amount: "180.83", currency: "USD" }, { id: "new", date: d(`${YEAR}-02-11`), amount: "-300" }, ]).statement("c1"); const usd = s.summary.find((x) => x.currency === "USD"); expect(usd?.balance).toBe("180.83"); expect(usd?.count).toBe(0); }); it("does not carry a voided earlier row", async () => { const s = await serviceWith([ { id: "old", date: d(`${YEAR - 1}-12-15`), amount: "1000", voidedAt: d(`${YEAR - 1}-12-16`) }, { id: "new", date: d(`${YEAR}-02-11`), amount: "-300" }, ]).statement("c1"); const mxn = s.summary.find((x) => x.currency === "MXN"); expect(mxn?.opening).toBe("0.00"); expect(mxn?.balance).toBe("-300.00"); }); it("reports the year it covers", async () => { const s = await serviceWith([ { id: "a", date: d(`${YEAR}-01-02`), amount: "-100" }, ]).statement("c1"); expect(s.year).toBe(YEAR); }); });