import { describe, expect, it } from "vitest"; import { stockOf } from "@/lib/server/stock"; import { StockTransfer } from "@/lib/server/models/StockTransfer"; import { StockLock } from "@/lib/server/models/StockLock"; import * as adjustments from "@/app/api/stock/adjustments/route"; import * as transfers from "@/app/api/stock/transfers/route"; import { makeTenant, req, uuid, type Tenant } from "./helpers"; async function openingStock(t: Tenant, quantity: number) { await adjustments.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), branchId: t.branchA, productId: t.water20, mode: "DELTA", quantity }, }) ); } const transferOut = (t: Tenant, quantity: number) => transfers.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), fromBranchId: t.branchA, toBranchId: t.branchB, productId: t.water20, quantity }, }) ); describe("concurrent stock changes", () => { it("never lets simultaneous transfers send more than the branch holds", async () => { const t = await makeTenant(); await openingStock(t, 10); // Five managers press "Record transfer" for 4 units at the same moment. const statuses = (await Promise.all(Array.from({ length: 5 }, () => transferOut(t, 4)))).map((r) => r.status); expect(statuses.filter((s) => s === 201)).toHaveLength(2); expect(statuses.filter((s) => s === 409)).toHaveLength(3); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(2); expect(await stockOf(t.businessId, t.branchB, t.water20)).toBe(8); expect(await StockTransfer.countDocuments({})).toBe(2); }); it("holds across repeated races, not just once", async () => { for (let round = 0; round < 5; round++) { const t = await makeTenant("Round" + round); await openingStock(t, 9); const statuses = (await Promise.all(Array.from({ length: 4 }, () => transferOut(t, 3)))).map((r) => r.status); expect(statuses.filter((s) => s === 201)).toHaveLength(3); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(0); } }); it("applies a double-tapped transfer once even when both taps race", async () => { const t = await makeTenant(); await openingStock(t, 10); const body = { clientUuid: uuid(), fromBranchId: t.branchA, toBranchId: t.branchB, productId: t.water20, quantity: 6 }; const statuses = ( await Promise.all([ transfers.POST(req("POST", "/x", { as: t.owner, body })), transfers.POST(req("POST", "/x", { as: t.owner, body })), ]) ).map((r) => r.status); expect(statuses.sort()).toEqual([200, 201]); expect(await stockOf(t.businessId, t.branchA, t.water20)).toBe(4); }); it("takes over a lock left behind by a crashed request once it expires", async () => { const t = await makeTenant(); await openingStock(t, 10); await StockLock.create({ _id: `${t.businessId}:${t.branchA}:${t.water20}`, owner: "crashed-request", lockedUntil: new Date(Date.now() - 1000), }); expect((await transferOut(t, 3)).status).toBe(201); expect(await StockLock.countDocuments({})).toBe(0); }); it("serialises a physical count with a transfer, so the count is never applied to a stale balance", async () => { const t = await makeTenant(); await openingStock(t, 10); // A count of 10 and a transfer of 6 race. Whichever runs second must // see the first: either the count resets 4 back to 10 after the // transfer, or the transfer moves 6 out of the counted 10. const [count, transfer] = await Promise.all([ adjustments.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), branchId: t.branchA, productId: t.water20, mode: "COUNT", quantity: 10 }, }) ), transferOut(t, 6), ]); expect([count.status, transfer.status]).toEqual([201, 201]); const a = await stockOf(t.businessId, t.branchA, t.water20); expect([4, 10]).toContain(a); expect(await stockOf(t.businessId, t.branchB, t.water20)).toBe(6); }); it("does not block transfers of different products or branches on each other", async () => { const t = await makeTenant(); await openingStock(t, 10); await adjustments.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), branchId: t.branchA, productId: t.water5, mode: "DELTA", quantity: 10 }, }) ); const results = await Promise.all([ transferOut(t, 5), transfers.POST( req("POST", "/x", { as: t.owner, body: { clientUuid: uuid(), fromBranchId: t.branchA, toBranchId: t.branchB, productId: t.water5, quantity: 5 }, }) ), ]); expect(results.map((r) => r.status)).toEqual([201, 201]); }); });