Files
ERP/tests/retest-plan.test.ts
denis_ireri f865837b27 Address the 11 October test report findings
The initial test report (run before the pilot-readiness changes) raised
seven findings and a retest plan. This closes the ones still open.

- F-01: a business on its free trial may run 2 active branches whatever
  its tier, so an owner can try transfers and per-branch stock. Only
  active branches count towards any limit.
- F-06: owners can rename branches and deactivate/reactivate them from
  the Branches page. A branch can only be deactivated once it holds no
  stock and no active staff; its history stays in every report, and it
  leaves every picker.
- F-04: the Products page lists deactivated products under a toggle with
  Reactivate. Deactivating warns about stock still on hand, which stays
  on the Stock page so it can be transferred or counted out.
- F-05: products carry a unit (L, ml, kg, g, pcs, tray, pack) instead of
  everything being litres.
- CAT-12: product prices must be above zero and names are unique per
  business, ignoring case.
- F-03: a reversed report period is refused by the page and the API, and
  the page labels which period the cards show. Report dates are Nairobi
  business days.
- F-07: cost of goods sold is units sold x average buying price. Stock
  bought in the period is reported separately, with a cash surplus line,
  so unsold stock no longer shows as a loss.
- STA-04: staff and new branch managers can be invited with a one-time,
  7-day link; the person sets their own password. Managers can issue a
  new link for their own cashiers.
- OFF-02 / POS-02: the till shows each product's stock and deducts every
  sale immediately, online or offline; it warns before selling more than
  the recorded stock, and has a Clear sale button.
- npm run backfill:stock turns purchases recorded before the stock ledger
  into stock, so existing tenants (and the report's ZERRO fixtures) start
  from what they bought.

tests/retest-plan.test.ts runs the report's completion and retest plan
and checks every balance and total it predicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:29:21 +03:00

102 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Product } from "@/lib/server/models/Product";
import { Purchase } from "@/lib/server/models/Purchase";
import { Transaction } from "@/lib/server/models/Transaction";
import { backfillPurchaseMovements, stockOf } from "@/lib/server/stock";
import * as cash from "@/app/api/transactions/cash/route";
import * as transfers from "@/app/api/stock/transfers/route";
import * as profit from "@/app/api/reports/profit/route";
import * as summary from "@/app/api/dashboard/summary/route";
import { json, makeTenant, req, uuid, type Actor } from "./helpers";
/**
* The "Completion and retest plan" from the 11 October 2026 test report
* (AquaFlow_Initial_Test_Report, pages 9-10), with its expected numbers.
*/
describe("initial test report: completion and retest plan", () => {
it("matches every planned balance and total", async () => {
const t = await makeTenant("ZERRO");
const [refill3] = await Product.insertMany([
{ businessId: t.businessId, label: "QA 20261011 3L Refill", sizeLiters: 3, unitPrice: 40 },
]);
const l3 = refill3._id.toString();
const l5 = t.water5; // KES 60
// Fixtures recorded before the ledger existed (QA-20261011-STOCK-A-3L / -5L),
// brought into stock by the one-off backfill.
await Purchase.insertMany([
{ businessId: t.businessId, branchId: t.branchA, supplierName: "QA", productId: l3, productLabel: "3L", quantity: 20, unitCost: 10, totalCost: 200, recordedBy: t.owner.id },
{ businessId: t.businessId, branchId: t.branchA, supplierName: "QA", productId: l5, productLabel: "5L", quantity: 10, unitCost: 20, totalCost: 200, recordedBy: t.owner.id },
]);
await backfillPurchaseMovements(t.businessId);
const stock = async (branch: string, product: string) => stockOf(t.businessId, branch, product);
expect([await stock(t.branchA, l3), await stock(t.branchA, l5)]).toEqual([20, 10]);
const sell = async (as: Actor, lines: { productId: string; quantity: number }[], offline = false) =>
json(
await cash.POST(
req("POST", "/x", {
as,
body: {
sales: [
{
clientUuid: uuid(),
staffId: as.id,
branchId: as.branchId,
lines,
...(offline ? { clientSubmittedAt: new Date().toISOString() } : {}),
},
],
},
})
)
);
// Online sale: 2 x 3L + 1 x 5L = KES 140; A balances 18 and 9.
const online = await sell(t.cashierA, [{ productId: l3, quantity: 2 }, { productId: l5, quantity: 1 }]);
expect(online.body.results[0]).toMatchObject({ status: "saved", amountTotal: 140 });
expect([await stock(t.branchA, l3), await stock(t.branchA, l5)]).toEqual([18, 9]);
// Transfer 5 x 3L from A to B: A 13, B 5, combined 18.
const transfer = await transfers.POST(
req("POST", "/x", {
as: t.owner,
body: { clientUuid: uuid(), fromBranchId: t.branchA, toBranchId: t.branchB, productId: l3, quantity: 5 },
})
);
expect(transfer.status).toBe(201);
expect([await stock(t.branchA, l3), await stock(t.branchB, l3)]).toEqual([13, 5]);
// Two offline sales replayed on reconnect (sale 2: 1 x 3L; sale 3: 2 x 3L + 1 x 5L),
// then replayed again: exactly two new sales (three lines), A 10 x 3L and 8 x 5L.
const before = await Transaction.countDocuments({ businessId: t.businessId });
const queued = [
{ clientUuid: uuid(), staffId: t.cashierA.id, branchId: t.branchA, clientSubmittedAt: new Date().toISOString(), lines: [{ productId: l3, quantity: 1 }] },
{ clientUuid: uuid(), staffId: t.cashierA.id, branchId: t.branchA, clientSubmittedAt: new Date().toISOString(), lines: [{ productId: l3, quantity: 2 }, { productId: l5, quantity: 1 }] },
];
for (let pass = 0; pass < 2; pass++) {
const r = await json(await cash.POST(req("POST", "/x", { as: t.cashierA, body: { sales: queued } })));
expect(r.body.results.map((x: any) => x.status)).toEqual(pass === 0 ? ["saved", "saved"] : ["duplicate", "duplicate"]);
}
expect((await Transaction.countDocuments({ businessId: t.businessId })) - before).toBe(3);
expect([await stock(t.branchA, l3), await stock(t.branchA, l5)]).toEqual([10, 8]);
// Branch B sells 1 x 3L for KES 40.
await sell(t.cashierB, [{ productId: l3, quantity: 1 }]);
// Final: A 3L 10, B 3L 4, A 5L 8; combined 3L 14 = 20 - 6 sold, 5L 8 = 10 - 2 sold.
expect([await stock(t.branchA, l3), await stock(t.branchB, l3), await stock(t.branchA, l5)]).toEqual([10, 4, 8]);
// Revenue: A KES 320, B KES 40, total KES 360 — on the report and the owner dashboard.
const report = async (q: string) => (await json(await profit.GET(req("GET", "/api/reports/profit" + q, { as: t.owner })))).body;
expect((await report("")).revenue).toBe(360);
expect((await report("?branchId=" + t.branchA)).revenue).toBe(320);
expect((await report("?branchId=" + t.branchB)).revenue).toBe(40);
const dash = (await json(await summary.GET(req("GET", "/api/dashboard/summary", { as: t.owner })))).body;
expect(dash.totals.cashTotal).toBe(360);
const ranking = Object.fromEntries(dash.branchRanking.map((b: any) => [b.branchName, b.revenue]));
expect(ranking).toEqual({ "ZERRO A": 320, "ZERRO B": 40 });
});
});