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:
DEls 2026-08-30 12:34:51 +02:00
parent 818a679922
commit 0c7cd5b7d5
3 changed files with 73 additions and 1 deletions

View 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);
});
});

View 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);
}

View file

@ -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";