feat(orders): expose tiered quote line pairing
This commit is contained in:
parent
88fa8e978e
commit
f2a1cbf586
2 changed files with 92 additions and 1 deletions
|
|
@ -7,6 +7,9 @@ import {
|
|||
scoreOrderVsQuote,
|
||||
RULE_POINTS,
|
||||
MATCH_CONFIDENCE_THRESHOLD,
|
||||
pairQuoteLines,
|
||||
LINE_PAIR_CONFIDENT,
|
||||
LINE_PAIR_POSSIBLE,
|
||||
} from "@/lib/server/order-quote-match";
|
||||
|
||||
/**
|
||||
|
|
@ -424,3 +427,48 @@ describe("scoreOrderVsQuote — normalization & thresholding", () => {
|
|||
expect(r.lines[0].points).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pairQuoteLines", () => {
|
||||
it("tiers a near-identical line as confident", () => {
|
||||
const pairs = pairQuoteLines(
|
||||
[{ description: "M20 HEX BOLT GR8.8 ZINC" }],
|
||||
[{ description: "Hex Bolt M20 Gr8.8 Zinc" }],
|
||||
);
|
||||
expect(pairs).toHaveLength(1);
|
||||
expect(pairs[0].orderLineIdx).toBe(0);
|
||||
expect(pairs[0].quoteLineIdx).toBe(0);
|
||||
expect(pairs[0].tier).toBe("confident");
|
||||
});
|
||||
|
||||
it("tiers a partial-overlap line as possible", () => {
|
||||
const pairs = pairQuoteLines(
|
||||
[{ description: "cable gland brass M20" }],
|
||||
[{ description: "cable gland stainless M25" }],
|
||||
);
|
||||
expect(pairs).toHaveLength(1);
|
||||
expect(pairs[0].tier).toBe("possible");
|
||||
expect(pairs[0].jaccard).toBeGreaterThanOrEqual(LINE_PAIR_POSSIBLE);
|
||||
expect(pairs[0].jaccard).toBeLessThan(LINE_PAIR_CONFIDENT);
|
||||
});
|
||||
|
||||
it("returns no pair when descriptions do not overlap", () => {
|
||||
const pairs = pairQuoteLines(
|
||||
[{ description: "galvanised threaded rod 3 metre" }],
|
||||
[{ description: "silicone sealant clear cartridge" }],
|
||||
);
|
||||
expect(pairs).toEqual([]);
|
||||
});
|
||||
|
||||
it("never pairs one quote line to two order lines", () => {
|
||||
const pairs = pairQuoteLines(
|
||||
[
|
||||
{ description: "hex bolt m20 gr8.8 zinc" },
|
||||
{ description: "hex bolt m20 gr8.8 zinc" },
|
||||
],
|
||||
[{ description: "hex bolt m20 gr8.8 zinc" }],
|
||||
);
|
||||
expect(pairs).toHaveLength(1);
|
||||
const quoteIdxs = pairs.map((p) => p.quoteLineIdx);
|
||||
expect(new Set(quoteIdxs).size).toBe(quoteIdxs.length);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export interface ScoreBreakdown {
|
|||
* from the pool, repeat. Lines that don't have any meaningful match
|
||||
* fall through with `descMatched=false` so the matcher can still
|
||||
* see the line-by-line picture. */
|
||||
function pairLinesByDescription(
|
||||
export function pairLinesByDescription(
|
||||
orderItems: ReadonlyArray<{ description: string }>,
|
||||
quoteItems: ReadonlyArray<{ description: string }>
|
||||
): Array<{ oi: number; qi: number; jaccard: number }> {
|
||||
|
|
@ -384,3 +384,46 @@ export function scoreCandidate(
|
|||
{ items: quoteItems }
|
||||
).confidence;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Line-pairing tiers
|
||||
*
|
||||
* `pairLinesByDescription` already greedily pairs order lines to quote
|
||||
* lines and drops anything below 0.3 Jaccard. These constants split
|
||||
* what survives into two tiers:
|
||||
*
|
||||
* >= 0.5 confident — safe to persist onto `order_items.quote_item`
|
||||
* 0.3–0.5 possible — shown to the operator with a confirm button,
|
||||
* never written without their say-so
|
||||
*
|
||||
* Both numbers already existed in this file (0.5 is the
|
||||
* `descriptionFull` award boundary, 0.3 is the candidate floor) — they
|
||||
* are named here rather than reintroduced.
|
||||
* ========================================================================== */
|
||||
|
||||
export const LINE_PAIR_CONFIDENT = 0.5;
|
||||
export const LINE_PAIR_POSSIBLE = 0.3;
|
||||
|
||||
export type LinePairTier = "confident" | "possible";
|
||||
|
||||
export interface QuoteLinePair {
|
||||
orderLineIdx: number;
|
||||
quoteLineIdx: number;
|
||||
jaccard: number;
|
||||
tier: LinePairTier;
|
||||
}
|
||||
|
||||
/** Pair order lines to quote lines by description overlap, tiered by
|
||||
* confidence. Delegates to `pairLinesByDescription` so there is
|
||||
* exactly one pairing algorithm in the codebase. */
|
||||
export function pairQuoteLines(
|
||||
orderItems: ReadonlyArray<{ description: string }>,
|
||||
quoteItems: ReadonlyArray<{ description: string }>,
|
||||
): QuoteLinePair[] {
|
||||
return pairLinesByDescription(orderItems, quoteItems).map((p) => ({
|
||||
orderLineIdx: p.oi,
|
||||
quoteLineIdx: p.qi,
|
||||
jaccard: p.jaccard,
|
||||
tier: p.jaccard >= LINE_PAIR_CONFIDENT ? "confident" : "possible",
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue