From 0c7cd5b7d5b2b1beda2e4ac984954377780fc902 Mon Sep 17 00:00:00 2001 From: DEls Date: Sun, 30 Aug 2026 12:34:51 +0200 Subject: [PATCH] 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 --- src/lib/metrics/delivery-note-value.test.ts | 47 +++++++++++++++++++++ src/lib/metrics/delivery-note-value.ts | 24 +++++++++++ src/lib/metrics/index.ts | 3 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/lib/metrics/delivery-note-value.test.ts create mode 100644 src/lib/metrics/delivery-note-value.ts diff --git a/src/lib/metrics/delivery-note-value.test.ts b/src/lib/metrics/delivery-note-value.test.ts new file mode 100644 index 0000000..77ddb7b --- /dev/null +++ b/src/lib/metrics/delivery-note-value.test.ts @@ -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); + }); +}); diff --git a/src/lib/metrics/delivery-note-value.ts b/src/lib/metrics/delivery-note-value.ts new file mode 100644 index 0000000..f225949 --- /dev/null +++ b/src/lib/metrics/delivery-note-value.ts @@ -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); +} diff --git a/src/lib/metrics/index.ts b/src/lib/metrics/index.ts index 92c1577..eb67e00 100644 --- a/src/lib/metrics/index.ts +++ b/src/lib/metrics/index.ts @@ -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"; \ No newline at end of file +export * from "./fuel-attention"; +export * from "./delivery-note-value";