Fas 2 steg 4: Quick Reconciliation (motor, API, app + 12-språks i18n)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildReconciliationCandidates } from "../src/reconciliation.js";
|
||||
|
||||
const PINNED = new Date("2026-08-07T00:00:00.000Z");
|
||||
|
||||
function baseItem(overrides: Partial<Parameters<typeof buildReconciliationCandidates>[0][number]> = {}) {
|
||||
return {
|
||||
id: "itm-1",
|
||||
displayName: "Mjölk",
|
||||
quantity: 1,
|
||||
unit: "l",
|
||||
locationName: "Kylen",
|
||||
confidence: 1,
|
||||
verifiedByUser: true,
|
||||
lastVerifiedAt: PINNED,
|
||||
updatedAt: PINNED,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildReconciliationCandidates", () => {
|
||||
it("returns empty when no items match", () => {
|
||||
const result = buildReconciliationCandidates(
|
||||
[baseItem({ quantity: 0, depletedAt: PINNED })],
|
||||
{ plannedRecipeIngredientIds: new Map(), daysLeftByItemId: new Map(), dailyConsumptionRate: new Map(), priceMinorByItemId: new Map() },
|
||||
PINNED,
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("prioritises planned recipe ingredients", () => {
|
||||
const a = baseItem({ id: "a", displayName: "Grädde", confidence: 1, verifiedByUser: true });
|
||||
const b = baseItem({ id: "b", displayName: "Ost", confidence: 1, verifiedByUser: true });
|
||||
const result = buildReconciliationCandidates(
|
||||
[b, a],
|
||||
{
|
||||
plannedRecipeIngredientIds: new Map([["a", ["rec-1"]]]),
|
||||
daysLeftByItemId: new Map(),
|
||||
dailyConsumptionRate: new Map(),
|
||||
priceMinorByItemId: new Map(),
|
||||
},
|
||||
PINNED,
|
||||
15,
|
||||
);
|
||||
expect(result.map((r) => r.itemId)).toEqual(["a"]);
|
||||
expect(result[0]?.reasons).toContainEqual({ kind: "planned_recipe", recipeIds: ["rec-1"] });
|
||||
});
|
||||
|
||||
it("flags expiring soon and sorts by urgency", () => {
|
||||
const a = baseItem({ id: "a", displayName: "Yoghurt", confidence: 0.5 });
|
||||
const b = baseItem({ id: "b", displayName: "Juice", confidence: 0.5 });
|
||||
const result = buildReconciliationCandidates(
|
||||
[b, a],
|
||||
{
|
||||
plannedRecipeIngredientIds: new Map(),
|
||||
daysLeftByItemId: new Map([
|
||||
["a", 1],
|
||||
["b", 6],
|
||||
]),
|
||||
dailyConsumptionRate: new Map(),
|
||||
priceMinorByItemId: new Map(),
|
||||
},
|
||||
PINNED,
|
||||
);
|
||||
expect(result.map((r) => r.itemId)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("flags low confidence", () => {
|
||||
const item = baseItem({ confidence: 0.3, verifiedByUser: false });
|
||||
const result = buildReconciliationCandidates(
|
||||
[item],
|
||||
{ plannedRecipeIngredientIds: new Map(), daysLeftByItemId: new Map(), dailyConsumptionRate: new Map(), priceMinorByItemId: new Map() },
|
||||
PINNED,
|
||||
);
|
||||
expect(result[0]?.reasons).toContainEqual({ kind: "low_confidence", confidence: 0.3 });
|
||||
});
|
||||
|
||||
it("suggests depleted when remaining days < 0.5", () => {
|
||||
const item = baseItem({ quantity: 0.1 });
|
||||
const result = buildReconciliationCandidates(
|
||||
[item],
|
||||
{
|
||||
plannedRecipeIngredientIds: new Map(),
|
||||
daysLeftByItemId: new Map(),
|
||||
dailyConsumptionRate: new Map([["itm-1", 1]]),
|
||||
priceMinorByItemId: new Map(),
|
||||
},
|
||||
PINNED,
|
||||
);
|
||||
expect(result[0]?.suggestedAction).toBe("depleted");
|
||||
});
|
||||
|
||||
it("caps results at maxItems", () => {
|
||||
const items = Array.from({ length: 20 }, (_, i) =>
|
||||
baseItem({ id: `itm-${i}`, displayName: `Vara ${String(i).padStart(2, "0")}`, confidence: 0.1 }),
|
||||
);
|
||||
const result = buildReconciliationCandidates(
|
||||
items,
|
||||
{ plannedRecipeIngredientIds: new Map(), daysLeftByItemId: new Map(), dailyConsumptionRate: new Map(), priceMinorByItemId: new Map() },
|
||||
PINNED,
|
||||
5,
|
||||
);
|
||||
expect(result).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user