fix(inventory): sluta gissa utgangsdatum + las 2-siffriga bast-fore-stamplar
CI / Typecheck, test & build (push) Failing after 46s
CI / Typecheck, test & build (push) Failing after 46s
BUG: forseglad vara utan tryckt datum fick fabricerat utgang (purchasedAt + shelfLifeGuidance, som ar hallbarhet EFTER oppning) -> falska 'gar ut snart'-varningar OCH gav sken av att datum inte lastes. Nu: ingen data -> unknown (hedrar appens egen princip 'aldrig gissa'). parseDate laser aven YY-MM-DD/YYMMDD/YYYYMMDD (svenska stamplar) som forr slangdes. Regressionstester tillagda. 36/36 gront.
This commit is contained in:
@@ -182,13 +182,30 @@ const geminiFridgeResponseSchema = z.object({
|
||||
imageQualityIssues: z.array(z.enum(["dark", "blurry", "occlusion", "too_far"])).default([]),
|
||||
});
|
||||
|
||||
function parseDate(value: string | null | undefined): string | null {
|
||||
export function parseDate(value: string | null | undefined): string | null {
|
||||
if (!value) return null;
|
||||
const normalized = value.trim();
|
||||
if (!normalized) return null;
|
||||
// Accept ISO dates (YYYY-MM-DD) only.
|
||||
// Accept ISO dates (YYYY-MM-DD).
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) return normalized;
|
||||
// Try common Swedish/EU formats.
|
||||
// Kompakta stämplar utan separator (vanligt på svensk förpackning): YYYYMMDD / YYMMDD.
|
||||
if (/^\d{8}$/.test(normalized)) {
|
||||
const y = +normalized.slice(0, 4),
|
||||
m = +normalized.slice(4, 6),
|
||||
d = +normalized.slice(6, 8);
|
||||
if (y > 2000 && m >= 1 && m <= 12 && d >= 1 && d <= 31)
|
||||
return `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
||||
return null;
|
||||
}
|
||||
if (/^\d{6}$/.test(normalized)) {
|
||||
const y = 2000 + +normalized.slice(0, 2),
|
||||
m = +normalized.slice(2, 4),
|
||||
d = +normalized.slice(4, 6);
|
||||
if (m >= 1 && m <= 12 && d >= 1 && d <= 31)
|
||||
return `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
||||
return null;
|
||||
}
|
||||
// Separatorformat (- / .).
|
||||
const parts = normalized.split(/[-/.]/);
|
||||
if (parts.length === 3) {
|
||||
const [aStr, bStr, cStr] = parts;
|
||||
@@ -203,6 +220,9 @@ function parseDate(value: string | null | undefined): string | null {
|
||||
// DD-MM-YYYY
|
||||
if (c > 2000 && b <= 12 && a <= 31)
|
||||
return `${c}-${String(b).padStart(2, "0")}-${String(a).padStart(2, "0")}`;
|
||||
// YY-MM-DD (2-siffrigt år först — vanligast på svensk bäst-före-stämpel).
|
||||
if (a <= 99 && b >= 1 && b <= 12 && c >= 1 && c <= 31)
|
||||
return `${2000 + a}-${String(b).padStart(2, "0")}-${String(c).padStart(2, "0")}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { GeminiAamosClient, MemoryBudgetStore } from "../src/gemini.js";
|
||||
import { GeminiAamosClient, MemoryBudgetStore, parseDate } from "../src/gemini.js";
|
||||
import type { AamosRequestEnvelope } from "../src/tasks.js";
|
||||
|
||||
const FIXTURE_RESPONSE = {
|
||||
@@ -159,3 +159,14 @@ describe("GeminiAamosClient", () => {
|
||||
expect(result.error).toContain("Kontraktsfel");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseDate (svenska bäst-före-stämplar)", () => {
|
||||
it("läser 2-siffrigt år och kompakta format", () => {
|
||||
expect(parseDate("25-08-30")).toBe("2025-08-30"); // YY-MM-DD
|
||||
expect(parseDate("250830")).toBe("2025-08-30"); // YYMMDD
|
||||
expect(parseDate("20250830")).toBe("2025-08-30"); // YYYYMMDD
|
||||
expect(parseDate("2025-08-30")).toBe("2025-08-30"); // ISO
|
||||
expect(parseDate("30-08-2025")).toBe("2025-08-30"); // DD-MM-YYYY
|
||||
expect(parseDate("skräp")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,15 +79,11 @@ export function classifyExpiry(input: ExpiryInput, today: Date = new Date()): Ex
|
||||
factor: "opened_guidance",
|
||||
date: new Date(opened.getTime() + guidanceDays * DAY_MS),
|
||||
});
|
||||
} else if (!useBy && !bestBefore && guidanceDays != null) {
|
||||
const purchased = parse(input.purchasedAt);
|
||||
if (purchased) {
|
||||
candidates.push({
|
||||
factor: "purchased_guidance",
|
||||
date: new Date(purchased.getTime() + guidanceDays * DAY_MS),
|
||||
});
|
||||
}
|
||||
}
|
||||
// shelfLifeGuidance = hållbarhet EFTER öppning (entities.ts). Vi gissar ALDRIG
|
||||
// utgång från inköpsdatum på en oöppnad vara — utan riktigt tryckt datum blir
|
||||
// status "unknown" (spec: "ingen data → unknown, aldrig gissning"). Den gamla
|
||||
// purchased_guidance-grenen gav falska "går ut snart"-varningar på förseglade varor.
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return { status: "unknown", daysLeft: null, limitingFactor: "none", pastBestBefore: false };
|
||||
|
||||
@@ -54,6 +54,16 @@ describe("bäst före-klassning (spec §13)", () => {
|
||||
it("ingen data → unknown, aldrig gissning", () => {
|
||||
expect(classifyExpiry({}, TODAY).status).toBe("unknown");
|
||||
});
|
||||
it("förseglad vara: inköpsdatum + riktvärde men INGET tryckt datum → unknown", () => {
|
||||
// Regressionsvakt: gamla purchased_guidance-grenen gissade utgång från inköp
|
||||
// på oöppnade varor → falska "går ut snart"-varningar (Bug B).
|
||||
const result = classifyExpiry(
|
||||
{ purchasedAt: "2026-08-01", storageLocationType: "fridge", shelfLifeGuidance: { fridge: 7 } },
|
||||
TODAY,
|
||||
);
|
||||
expect(result.status).toBe("unknown");
|
||||
expect(result.limitingFactor).toBe("none");
|
||||
});
|
||||
it("mjölkprincipen: gick ut igår → använd sinnena, inte soptunnan", () => {
|
||||
// Bäst före är en KVALITETSgräns: mjölken som gick ut igår är inte
|
||||
// automatiskt dålig – användaren ska lukta/smaka. Appen får aldrig
|
||||
|
||||
Reference in New Issue
Block a user