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;
|
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.
|
// 4. Capture previous snapshot for the response.
|
||||||
const previous: StockItemPricingSnapshot = {
|
const previous: StockItemPricingSnapshot = {
|
||||||
cost_price: before.cost_price,
|
cost_price: before.cost_price,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue