import { describe, expect, it } from "vitest"; import { stockOf } from "@/lib/server/stock"; import * as stock from "@/app/api/stock/route"; import * as adjustments from "@/app/api/stock/adjustments/route"; import * as transfers from "@/app/api/stock/transfers/route"; import * as purchases from "@/app/api/purchases/route"; import { json, makeTenant, req, uuid, type Actor, type Tenant } from "./helpers"; async function addStock(t: Tenant, as: Actor, branchId: string, productId: string, quantity: number) { const res = await adjustments.POST( req("POST", "/x", { as, body: { clientUuid: uuid(), branchId, productId, mode: "DELTA", quantity, note: "opening" } }) ); expect(res.status).toBe(201); } describe("stock ledger", () => { it("adds purchased stock to the receiving branch", async () => { const t = await makeTenant(); const res = await purchases.POST( req("POST", "/x", { as: t.managerA, body: { supplierName: "Kiwanja Water", productId: t.water20, quantity: 40, unitCost: 120 }, }) ); expect(res.status).toBe(201); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(40); }); it("corrects stock to a physical count, once per submit", async () => { const t = await makeTenant(); await addStock(t, t.owner, t.branchA, t.water20, 50); const body = { clientUuid: uuid(), productId: t.water20, mode: "COUNT", quantity: 47, note: "evening count" }; const first = await json(await adjustments.POST(req("POST", "/x", { as: t.managerA, body }))); expect(first.body.quantity).toBe(47); // A double-submit of the same count changes nothing further. const again = await json(await adjustments.POST(req("POST", "/x", { as: t.managerA, body }))); expect(again.body.quantity).toBe(47); }); it("does not let a manager adjust another branch", async () => { const t = await makeTenant(); const res = await adjustments.POST( req("POST", "/x", { as: t.managerA, body: { clientUuid: uuid(), branchId: t.branchB, productId: t.water20, mode: "DELTA", quantity: 5 }, }) ); expect(res.status).toBe(403); }); it("reports stock per branch with low and out-of-stock flags", async () => { const t = await makeTenant(); await addStock(t, t.owner, t.branchA, t.water20, 8); // threshold defaults to 10 await addStock(t, t.owner, t.branchA, t.water5, 30); const { body } = await json(await stock.GET(req("GET", "/api/stock", { as: t.owner }))); const find = (b: string, p: string) => body.levels.find((l: any) => l.branchId === b && l.productId === p); expect(body.levels).toHaveLength(4); // 2 branches x 2 products, zero rows included expect(find(t.branchA, t.water20)).toMatchObject({ quantity: 8, status: "LOW" }); expect(find(t.branchA, t.water5)).toMatchObject({ quantity: 30, status: "OK" }); expect(find(t.branchB, t.water20)).toMatchObject({ quantity: 0, status: "OUT" }); }); it("shows a cashier only their own branch's stock", async () => { const t = await makeTenant(); const { body } = await json(await stock.GET(req("GET", "/api/stock?branchId=" + t.branchB, { as: t.cashierA }))); expect(new Set(body.levels.map((l: any) => l.branchId))).toEqual(new Set([t.branchA])); }); }); describe("inter-branch transfers", () => { it("moves stock out of one branch and into the other", async () => { const t = await makeTenant(); await addStock(t, t.owner, t.branchA, t.water20, 30); const res = await transfers.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), fromBranchId: t.branchA, toBranchId: t.branchB, productId: t.water20, quantity: 12 }, }) ); expect(res.status).toBe(201); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(18); expect(await stockOf(t.businessId, t.branchB, t.water20)).toBe(12); }); it("applies a double-submitted transfer once", async () => { const t = await makeTenant(); await addStock(t, t.owner, t.branchA, t.water20, 30); const body = { clientUuid: uuid(), toBranchId: t.branchB, productId: t.water20, quantity: 10 }; await transfers.POST(req("POST", "/x", { as: t.managerA, body })); const second = await transfers.POST(req("POST", "/x", { as: t.managerA, body })); expect(second.status).toBe(200); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(20); expect(await stockOf(t.businessId, t.branchB, t.water20)).toBe(10); }); it("refuses to send more than the branch has", async () => { const t = await makeTenant(); await addStock(t, t.owner, t.branchA, t.water20, 5); const res = await transfers.POST( req("POST", "/x", { as: t.managerA, body: { clientUuid: uuid(), toBranchId: t.branchB, productId: t.water20, quantity: 6 }, }) ); expect(res.status).toBe(409); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(5); }); it("only lets a manager send from their own branch", async () => { const t = await makeTenant(); await addStock(t, t.owner, t.branchB, t.water20, 5); const res = await transfers.POST( req("POST", "/x", { as: t.managerA, body: { clientUuid: uuid(), fromBranchId: t.branchB, toBranchId: t.branchA, productId: t.water20, quantity: 1 }, }) ); expect(res.status).toBe(403); }); it("cannot move stock into another tenant's branch", async () => { const t = await makeTenant("Alpha"); const other = await makeTenant("Beta"); await addStock(t, t.owner, t.branchA, t.water20, 5); const res = await transfers.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), fromBranchId: t.branchA, toBranchId: other.branchA, productId: t.water20, quantity: 1 }, }) ); expect(res.status).toBe(404); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(5); }); });