From 202f5e73e2d25e1b2c8e21cc6bbf4bfdf4d365ce Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 31 Aug 2026 16:43:13 +0200 Subject: [PATCH] feat(stock): write stock_ledger audit row when cost_price changes --- src/lib/server/stock-pricing-service.test.ts | 97 ++++++++++++++++++++ src/lib/server/stock-pricing-service.ts | 30 ++++++ 2 files changed, 127 insertions(+) diff --git a/src/lib/server/stock-pricing-service.test.ts b/src/lib/server/stock-pricing-service.test.ts index 875bbcf..4997001 100644 --- a/src/lib/server/stock-pricing-service.test.ts +++ b/src/lib/server/stock-pricing-service.test.ts @@ -47,3 +47,100 @@ describe("updateStockItemsPricing — happy path", () => { }); }); }); + +describe("updateStockItemsPricing — stock_ledger audit on cost change", () => { + const stubPb = (before: Record, updated: Record) => { + const ledgerCreates: Array> = []; + return { + pb: { + collection: (name: string) => { + if (name === "stock_items") { + return { + getOne: vi.fn().mockResolvedValue(before), + update: vi.fn().mockResolvedValue(updated), + create: vi.fn(), + }; + } + if (name === "stock_ledger") { + return { + create: vi.fn().mockImplementation(async (row) => { + ledgerCreates.push(row); + return { id: "ledger1", ...row }; + }), + _creates: ledgerCreates, + }; + } + throw new Error(`unexpected ${name}`); + }, + }, + ledgerCreates, + } as const; + }; + + it("writes a stock_ledger row with reason=adjustment when costPrice changes", async () => { + const before = { + id: "abcdefghijklmno", + cost_price: 2.5, + selling_price: 3.25, + markup: 30, + }; + const updated = { ...before, cost_price: 3.2 }; + const { pb, ledgerCreates } = stubPb(before, updated); + + await updateStockItemsPricing({ + pb: pb as never, + updates: [{ id: "abcdefghijklmno", costPrice: 3.2 }], + actor: { id: "useropid1234567", name: "Gary" }, + } as UpdateStockItemsPricingInput); + + expect(ledgerCreates).toEqual([ + expect.objectContaining({ + stock_item: "abcdefghijklmno", + change_qty: 0, + unit_cost: 3.2, + reason: "adjustment", + reference_type: "manual", + }), + ]); + }); + + it("does NOT write a stock_ledger row when costPrice is omitted", async () => { + const before = { + id: "abcdefghijklmno", + cost_price: 2.5, + selling_price: 3.25, + markup: 30, + }; + const updated = { ...before, selling_price: 4.16 }; + const { pb, ledgerCreates } = stubPb(before, updated); + + await updateStockItemsPricing({ + pb: pb as never, + updates: [{ id: "abcdefghijklmno", sellingPrice: 4.16 }], + actor: { id: "useropid1234567", name: "Gary" }, + } as UpdateStockItemsPricingInput); + + expect(ledgerCreates).toEqual([]); + }); + + it("does NOT write a stock_ledger row when costPrice equals the existing cost", async () => { + const before = { + id: "abcdefghijklmno", + cost_price: 2.5, + selling_price: 3.25, + markup: 30, + }; + const updated = before; + const { pb, ledgerCreates } = stubPb(before, updated); + + await updateStockItemsPricing({ + pb: pb as never, + updates: [ + { id: "abcdefghijklmno", costPrice: 2.5, sellingPrice: 5 }, + ], + actor: { id: "useropid1234567", name: "Gary" }, + } as UpdateStockItemsPricingInput); + + expect(ledgerCreates).toEqual([]); + }); +}); diff --git a/src/lib/server/stock-pricing-service.ts b/src/lib/server/stock-pricing-service.ts index 0eb4bb0..c3ffe2b 100644 --- a/src/lib/server/stock-pricing-service.ts +++ b/src/lib/server/stock-pricing-service.ts @@ -151,6 +151,36 @@ export async function updateStockItemsPricing( continue; } + // 3.5. Cost audit — write a stock_ledger row ONLY when costPrice was + // supplied and differs from the existing cost_price. Side-effect: + // a failed ledger write aborts THIS row (we won't half-write the + // patch without recording the audit trail). + if ( + update.costPrice !== undefined && + before.cost_price !== update.costPrice + ) { + try { + await input.pb.collection("stock_ledger").create({ + stock_item: update.id, + change_qty: 0, + unit_cost: update.costPrice, + reason: "adjustment", + reference_type: "manual", + notes: `${input.actor.name} edited cost from /stock`, + created_by: input.actor.id, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + results.push({ + id: update.id, + ok: false, + reason: "pb_error", + message: `Patched row but failed to write stock_ledger: ${message}`, + }); + continue; + } + } + // 4. Capture previous snapshot for the response. const previous: StockItemPricingSnapshot = { cost_price: before.cost_price,