fix(stock): skip ledger row on first-time pricing + include cost delta in notes

Code-review feedback from Task 2:
- Code-review Issue #1 (Important): the strict-equality guard fired on
  'before.cost_price = undefined' vs 'update.costPrice = 0' — exactly
  the population this feature targets (fresh catalogue rows the
  operator is pricing for the first time). No real cost movement was
  happening, but a spurious ledger row was getting written. Gate the
  ledger write on Number.isFinite(before.cost_price) so first-time
  pricing doesn't pollute audit history with a 0 -> 0 'movement'.
- Code-review Issue #4 (Minor, audit-readability): add the actual
  before -> after delta to the ledger notes so operators scrolling
  the ledger history see 'Gary edited cost 2.50 -> 3.20 from /stock'
  rather than the prior opaque 'Gary edited cost from /stock'.
- Added two regression tests: one for the first-time-pricing case,
  one pinning the notes delta format.
This commit is contained in:
Claude 2026-08-31 16:46:42 +02:00
parent 202f5e73e2
commit 86eba97d33
2 changed files with 49 additions and 4 deletions

View file

@ -143,4 +143,46 @@ describe("updateStockItemsPricing — stock_ledger audit on cost change", () =>
expect(ledgerCreates).toEqual([]); expect(ledgerCreates).toEqual([]);
}); });
it("does NOT write a stock_ledger row when prior cost_price is undefined (first-time pricing)", async () => {
const before = {
id: "abcdefghijklmno",
// cost_price intentionally missing — fresh catalogue row.
selling_price: 0,
markup: 0,
};
const updated = { ...before, cost_price: 2.5 };
const { pb, ledgerCreates } = stubPb(before, updated);
await updateStockItemsPricing({
pb: pb as never,
updates: [{ id: "abcdefghijklmno", costPrice: 2.5 }],
actor: { id: "useropid1234567", name: "Gary" },
} as UpdateStockItemsPricingInput);
expect(ledgerCreates).toEqual([]);
});
it("records an old → new delta in the notes string 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({
notes: "Gary edited cost 2.5 → 3.2 from /stock",
}),
]);
});
}); });

View file

@ -152,11 +152,14 @@ export async function updateStockItemsPricing(
} }
// 3.5. Cost audit — write a stock_ledger row ONLY when costPrice was // 3.5. Cost audit — write a stock_ledger row ONLY when costPrice was
// supplied and differs from the existing cost_price. Side-effect: // supplied AND the prior cost was a real number AND it differs.
// a failed ledger write aborts THIS row (we won't half-write the // First-time pricing (before.cost_price undefined) is NOT a
// patch without recording the audit trail). // cost movement to audit — it's a field initialisation.
// Side-effect: a failed ledger write aborts THIS row (we won't
// half-write the patch without recording the audit trail).
if ( if (
update.costPrice !== undefined && update.costPrice !== undefined &&
Number.isFinite(before.cost_price) &&
before.cost_price !== update.costPrice before.cost_price !== update.costPrice
) { ) {
try { try {
@ -166,7 +169,7 @@ export async function updateStockItemsPricing(
unit_cost: update.costPrice, unit_cost: update.costPrice,
reason: "adjustment", reason: "adjustment",
reference_type: "manual", reference_type: "manual",
notes: `${input.actor.name} edited cost from /stock`, notes: `${input.actor.name} edited cost ${before.cost_price} → ${update.costPrice} from /stock`,
created_by: input.actor.id, created_by: input.actor.id,
}); });
} catch (err) { } catch (err) {