From 0294015ba27ebc9e2b9a36ee70ea0d85f85db973 Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 24 Oct 2026 15:44:19 +0200 Subject: [PATCH] fix(orders): clamp the AI fill's markup at 0 like every other order line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A line that sells below the quote's cost produced a negative markup. Every other write to an order line's markup stores Math.max(0, computedMarkup), so the fill now does the same. The margin chip is unaffected — it works from cost and selling, not from this field, so a loss-making line still shows its real margin. Co-Authored-By: Claude Opus 5.5 --- src/lib/server/order-assistant-core.test.ts | 13 +++++++++++++ src/lib/server/order-assistant-core.ts | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/lib/server/order-assistant-core.test.ts b/src/lib/server/order-assistant-core.test.ts index 762f5f8..323a891 100644 --- a/src/lib/server/order-assistant-core.test.ts +++ b/src/lib/server/order-assistant-core.test.ts @@ -550,6 +550,19 @@ describe("buildOrderLineFillPatch", () => { expect(patch?.markup_percent).toBe(50); }); + it("clamps a loss-making markup at 0, like every other order-line write", () => { + // The customer's price is below the quote's cost. The margin chip + // shows the real margin from cost + selling; the stored markup + // follows the app's convention of never going negative. + const patch = buildOrderLineFillPatch( + fillLine({ selling_price: 8 }), + fillQuoteLine({ cost_price: 10, selling_price: 12 }), + FILL_CTX, + ); + expect(patch?.markup).toBe(0); + expect(patch?.markup_percent).toBe(0); + }); + it("adds the price doubt when the customer price differs by more than 5%", () => { const patch = buildOrderLineFillPatch( fillLine({ selling_price: 16.64 }), diff --git a/src/lib/server/order-assistant-core.ts b/src/lib/server/order-assistant-core.ts index 243855f..eefbcf5 100644 --- a/src/lib/server/order-assistant-core.ts +++ b/src/lib/server/order-assistant-core.ts @@ -260,7 +260,11 @@ export function buildOrderLineFillPatch( // The resulting selling price: the customer's own price when there is // one, otherwise the quoted price. const selling = customerPrice > 0 ? customerPrice : quotedPrice; - const markup = cost > 0 && selling > 0 ? round2(((selling - cost) / cost) * 100) : 0; + // Clamped at 0, like every other write to an order line's markup + // (`orders-service.ts` stores `Math.max(0, computedMarkup)`). A line + // that sells below cost still shows its true margin on screen, because + // the margin chip works from cost and selling, not from this field. + const markup = cost > 0 && selling > 0 ? Math.max(0, round2(((selling - cost) / cost) * 100)) : 0; const supplier = (line.supplier ?? "").trim() || (quoteLine.supplier ?? ""); const sku = (line.sku ?? "").trim() || (quoteLine.sku ?? "");