feat(stock): write stock_ledger audit row when cost_price changes

This commit is contained in:
Dave 2026-08-31 16:43:13 +02:00
parent 0cab704840
commit 202f5e73e2
2 changed files with 127 additions and 0 deletions

View file

@ -47,3 +47,100 @@ describe("updateStockItemsPricing — happy path", () => {
});
});
});
describe("updateStockItemsPricing — stock_ledger audit on cost change", () => {
const stubPb = (before: Record<string, unknown>, updated: Record<string, unknown>) => {
const ledgerCreates: Array<Record<string, unknown>> = [];
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([]);
});
});

View file

@ -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,