diff --git a/src/lib/inbox-three/save-lines.test.ts b/src/lib/inbox-three/save-lines.test.ts index a37c988..b2a1a3e 100644 --- a/src/lib/inbox-three/save-lines.test.ts +++ b/src/lib/inbox-three/save-lines.test.ts @@ -5,6 +5,8 @@ import type { LineEdit } from "./line-rows"; const MESH_ITEM = "meshitem1234567"; const BAILING_ITEM = "bailingitem12345"; +/** An order line the slip sells in metres. */ +const KERB_ITEM = "kerbitem12345678"; /** An order line from another purchase order. */ const FOREIGN_ITEM = "foreignitem12345"; @@ -15,6 +17,7 @@ const context = (): PreviewContext => ({ po: { id: "poabcdefghijklm", purchase_order_number: "MEGCPO9448", total_value: 11748.7 }, poItems: [ { id: MESH_ITEM, description: "MESH DIAMOND 600 x 2.5 ROLL", unit: "m", qty: 2, rate: 1124.35, disc_percent: 0, vat_percent: 15 }, + { id: KERB_ITEM, description: "KERB 150", unit: "m", qty: 100, rate: 150, disc_percent: 0, vat_percent: 15 }, { id: BAILING_ITEM, description: "1.6mm Bailing Wire", unit: "ea", qty: 100, rate: 95, disc_percent: 0, vat_percent: 15 }, ], lines: [ @@ -85,8 +88,25 @@ describe("saveLineEdits", () => { expect(rules.some((rule) => rule.receiving_match?.factor === 30)).toBe(true); }); - it("saves nothing when one edit in the batch fails", () => { + it("keeps a combined line even when the supplier rule cannot be remembered", () => { const c = context(); + // Two kerb lines on one order line, sold in metres: there is no single + // product to scope a supplier rule to, so the rule is skipped — but the + // edit itself has to stand. + const result = saveLineEdits(c, preview(c), [edit({ + lines: [1, 2], + poItemId: KERB_ITEM, + slipQty: 4, + slipUnit: "ea", + factor: 15, + reason: "One kerb is 15 m.", + })], "operator"); + expect(result.remembered).toBeNull(); + expect(result.preview.allocations.some((allocation) => + allocation.purchaseOrderItemId === KERB_ITEM && allocation.sourceLineIds.length === 2)).toBe(true); + }); + + it("saves nothing when one edit in the batch fails", () => { const c = context(); const before = preview(c); const untouched = structuredClone(before); expect(() => saveLineEdits(c, before, [ diff --git a/src/lib/inbox-three/save-lines.ts b/src/lib/inbox-three/save-lines.ts index 643fedd..db0de6a 100644 --- a/src/lib/inbox-three/save-lines.ts +++ b/src/lib/inbox-three/save-lines.ts @@ -101,15 +101,23 @@ export function saveLineEdits( const item = edit.poItemId ? working.poItems.find((candidate) => candidate.id === edit.poItemId) : null; const targetUnit = item && typeof item.unit === "string" ? item.unit : null; if (targetUnit && edit.factor !== null && edit.factor !== 1 && edit.slipUnit.trim().toLowerCase() !== targetUnit.trim().toLowerCase()) { - const guidance = rememberConnection( - { ...working, supplier: { ...working.supplier, reconciliation_rules: remembered ?? working.supplier.reconciliation_rules } }, - current, - current.allocations.findIndex((allocation) => sameLines(allocation.sourceLineIds, sourceLineIds)), - actor, - ); - remembered = guidance; - working = { ...working, supplier: { ...working.supplier, reconciliation_rules: guidance } }; - current = { ...current, snapshotHash: snapshotHash(working) }; + try { + const guidance = rememberConnection( + { ...working, supplier: { ...working.supplier, reconciliation_rules: remembered ?? working.supplier.reconciliation_rules } }, + current, + current.allocations.findIndex((allocation) => sameLines(allocation.sourceLineIds, sourceLineIds)), + actor, + ); + remembered = guidance; + working = { ...working, supplier: { ...working.supplier, reconciliation_rules: guidance } }; + current = { ...current, snapshotHash: snapshotHash(working) }; + } catch (error) { + // A rule that cannot be written must not lose the edit: a combined + // line has no single product to scope the rule to. The line still + // books and the office still sees it — the supplier rule is simply + // not remembered this time. + if (!(error instanceof Error) || !/can be remembered/i.test(error.message)) throw error; + } } }