feat(stock): write stock_ledger audit row when cost_price changes
This commit is contained in:
parent
0cab704840
commit
202f5e73e2
2 changed files with 127 additions and 0 deletions
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue