feat(metrics): compute delivery note value from received items
delivery_notes stores no value; a note is worth the quantity received on each line times the matched PO item's rate. Unmatched lines contribute zero rather than a guessed rate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
818a679922
commit
0c7cd5b7d5
3 changed files with 73 additions and 1 deletions
47
src/lib/metrics/delivery-note-value.test.ts
Normal file
47
src/lib/metrics/delivery-note-value.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { computeDeliveryNoteValue } from "./delivery-note-value";
|
||||
|
||||
describe("computeDeliveryNoteValue", () => {
|
||||
it("is zero for no items", () => {
|
||||
expect(computeDeliveryNoteValue([])).toBe(0);
|
||||
});
|
||||
|
||||
it("multiplies quantity received by the PO item rate", () => {
|
||||
expect(
|
||||
computeDeliveryNoteValue([{ qty_received: 3, expand: { purchase_order_item: { rate: 100 } } }]),
|
||||
).toBe(300);
|
||||
});
|
||||
|
||||
it("sums across items", () => {
|
||||
expect(
|
||||
computeDeliveryNoteValue([
|
||||
{ qty_received: 2, expand: { purchase_order_item: { rate: 50 } } },
|
||||
{ qty_received: 1, expand: { purchase_order_item: { rate: 25 } } },
|
||||
]),
|
||||
).toBe(125);
|
||||
});
|
||||
|
||||
it("treats a missing quantity as zero", () => {
|
||||
expect(
|
||||
computeDeliveryNoteValue([{ expand: { purchase_order_item: { rate: 100 } } }]),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("treats an unmatched PO item as zero rather than guessing a rate", () => {
|
||||
// A delivery note line that never matched a PO item has no price.
|
||||
// Inventing one would silently inflate the note's value.
|
||||
expect(computeDeliveryNoteValue([{ qty_received: 5 }])).toBe(0);
|
||||
});
|
||||
|
||||
it("treats a missing rate as zero", () => {
|
||||
expect(
|
||||
computeDeliveryNoteValue([{ qty_received: 5, expand: { purchase_order_item: {} } }]),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("handles fractional quantities", () => {
|
||||
expect(
|
||||
computeDeliveryNoteValue([{ qty_received: 2.5, expand: { purchase_order_item: { rate: 10 } } }]),
|
||||
).toBe(25);
|
||||
});
|
||||
});
|
||||
24
src/lib/metrics/delivery-note-value.ts
Normal file
24
src/lib/metrics/delivery-note-value.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* What a delivery note is worth.
|
||||
*
|
||||
* `delivery_notes` stores no value — the money lives on the purchase
|
||||
* order. A note's value is the quantity actually received on each line
|
||||
* multiplied by the rate of the PO item it was matched to.
|
||||
*
|
||||
* A line with no matched PO item contributes ZERO, not a guess. Those
|
||||
* lines are exactly the ones a human still has to price, and inventing
|
||||
* a rate would silently inflate the note.
|
||||
*/
|
||||
|
||||
export interface ReceivedItemLike {
|
||||
qty_received?: number;
|
||||
expand?: { purchase_order_item?: { rate?: number } };
|
||||
}
|
||||
|
||||
export function computeDeliveryNoteValue(items: ReceivedItemLike[]): number {
|
||||
return items.reduce((sum, item) => {
|
||||
const qty = item.qty_received ?? 0;
|
||||
const rate = item.expand?.purchase_order_item?.rate ?? 0;
|
||||
return sum + qty * rate;
|
||||
}, 0);
|
||||
}
|
||||
|
|
@ -9,4 +9,5 @@ export * from "./site-start-date";
|
|||
export * from "./site-spend";
|
||||
export * from "./site-projection";
|
||||
export * from "./income-vs-expenses";
|
||||
export * from "./fuel-attention";
|
||||
export * from "./fuel-attention";
|
||||
export * from "./delivery-note-value";
|
||||
|
|
|
|||
Loading…
Reference in a new issue