WFDASH-WEBAPP/src/lib/quote-assistant-save.test.ts
Dave 40094be3f3 feat(quote-assistant): purple AI lines on process and quote pages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:56:02 +02:00

152 lines
4.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { QuoteItem, QuoteItemAiFill } from "@/lib/types";
import { reviewerFromAuthRecord, withAiReview } from "./quote-assistant-save";
const NOW = new Date("2026-10-16T12:00:00.000Z");
const reviewer = { id: "u1", name: "Rep Rita" };
function fill(over: Partial<QuoteItemAiFill> = {}): QuoteItemAiFill {
return {
status: "filled",
stock_item_id: "s-1",
stock_item_name: "Bolt",
stock_item_sku: "B",
cost_price: 10,
selling_price: 15,
markup: 50,
supplier_id: "sup1",
confidence: "high",
reason: "",
doubts: [],
candidates: [],
description_snapshot: "bolt",
model: "m",
attempted_at: NOW.toISOString(),
...over,
};
}
const aiItem = { ai_fill: fill(), pricing_source: "ai_pos" } as unknown as QuoteItem;
const plainItem = { ai_fill: null, pricing_source: "" } as unknown as QuoteItem;
const saved = { cost_price: 10, selling_price: 15, supplier: "sup1" };
describe("withAiReview", () => {
it("keeps ai_pos and records the review on a pending AI line", () => {
const out = withAiReview(
aiItem,
{ cost_price: 10, selling_price: 15, pricing_source: "manual" },
saved,
reviewer,
NOW,
{ markProcessedWhenPriced: false },
);
expect(out.pricing_source).toBe("ai_pos");
const f = out.ai_fill as QuoteItemAiFill;
expect(f.reviewed_at).toBe(NOW.toISOString());
expect(f.reviewed_by).toBe("Rep Rita");
expect(f.accepted_unchanged).toBe(true);
expect(out.is_processed).toBeUndefined();
});
it("adds is_processed on a pending AI line when priced and asked to", () => {
const out = withAiReview(aiItem, { cost_price: 10, selling_price: 15 }, saved, reviewer, NOW, {
markProcessedWhenPriced: true,
});
expect(out.is_processed).toBe(true);
});
it("does not add is_processed on a pending AI line with a zero price", () => {
const out = withAiReview(
aiItem,
{ cost_price: 10, selling_price: 0 },
{ ...saved, selling_price: 0 },
reviewer,
NOW,
{ markProcessedWhenPriced: true },
);
expect(out.is_processed).toBeUndefined();
});
it("never adds is_processed or ai_fill to a non-AI line", () => {
const patch = { cost_price: 10, selling_price: 15 };
const out = withAiReview(plainItem, patch, saved, reviewer, NOW, {
markProcessedWhenPriced: true,
});
expect(out).toEqual(patch);
expect("is_processed" in out).toBe(false);
expect("ai_fill" in out).toBe(false);
});
it("maps pricing_source through nextPricingSourceOnSave on a non-AI line", () => {
const out = withAiReview(
plainItem,
{ cost_price: 1, pricing_source: "manual" },
saved,
reviewer,
NOW,
{ markProcessedWhenPriced: false },
);
expect(out.pricing_source).toBe("manual");
const cat = withAiReview(
{ ...plainItem, pricing_source: "catalogue" } as QuoteItem,
{ pricing_source: "manual" },
saved,
reviewer,
NOW,
{ markProcessedWhenPriced: false },
);
expect(cat.pricing_source).toBe("catalogue");
});
it("does not add pricing_source when the patch has none and the line is not AI", () => {
const out = withAiReview(plainItem, { cost_price: 1 }, saved, reviewer, NOW, {
markProcessedWhenPriced: false,
});
expect("pricing_source" in out).toBe(false);
});
it("keeps an explicit catalogue source but still records the review", () => {
const out = withAiReview(
aiItem,
{ pricing_source: "catalogue", cost_price: 12 },
{ ...saved, cost_price: 12 },
reviewer,
NOW,
{ markProcessedWhenPriced: false },
);
expect(out.pricing_source).toBe("catalogue");
expect((out.ai_fill as QuoteItemAiFill).accepted_unchanged).toBe(false);
});
it("does not touch a reviewed AI line", () => {
const reviewed = {
ai_fill: fill({ reviewed_at: NOW.toISOString() }),
pricing_source: "ai_pos",
} as unknown as QuoteItem;
const out = withAiReview(reviewed, { cost_price: 1 }, saved, reviewer, NOW, {
markProcessedWhenPriced: true,
});
expect(out).toEqual({ cost_price: 1 });
});
it("never removes keys from the patch and does not mutate it", () => {
const patch = { a: 1, b: "x", pricing_source: "manual" };
const copy = { ...patch };
const out = withAiReview(aiItem, patch, saved, reviewer, NOW, {
markProcessedWhenPriced: true,
});
for (const k of Object.keys(copy)) expect(k in out).toBe(true);
expect(patch).toEqual(copy);
});
});
describe("reviewerFromAuthRecord", () => {
it("prefers name, falls back to email, then empty", () => {
expect(reviewerFromAuthRecord({ id: "u1", name: "Rita", email: "r@x.co" })).toEqual({
id: "u1",
name: "Rita",
});
expect(reviewerFromAuthRecord({ id: "u1", name: "", email: "r@x.co" }).name).toBe("r@x.co");
expect(reviewerFromAuthRecord(null)).toEqual({ id: undefined, name: "" });
});
});