/** * Automatische Honigwahl fuer Pakete und Abos (Entscheidung des Inhabers): * nie cremiger Honig, die neueste Ernte zuerst, bei gleich neuer Ernte der * groessere freie Bestand — und nur, was fuer die Glaeser auch reicht. */ import { describe, expect, test } from "vitest"; import { allocateReservedJars, harvestYearOf, honeyLabel, isCreamy, pickHoneyProduct, rankHoneyCandidates, withTakenStock, type HoneyCandidate, } from "./honey-selection"; function candidate(overrides: Partial & Pick): HoneyCandidate { return { name: overrides.id, weightGrams: 500, harvestYear: 2026, consistency: "flüssig", inventory: 10, freeStock: 10, ...overrides, }; } describe("isCreamy", () => { test("erkennt jede Schreibweise von cremig", () => { expect(isCreamy("cremig")).toBe(true); expect(isCreamy("feincremig")).toBe(true); expect(isCreamy("Feincremig")).toBe(true); expect(isCreamy("CREMIG gerührt")).toBe(true); }); test("laesst fluessigen und anderen Honig durch", () => { expect(isCreamy("flüssig")).toBe(false); expect(isCreamy("fest")).toBe(false); expect(isCreamy("")).toBe(false); }); }); describe("harvestYearOf", () => { const createdAt = new Date("2024-03-01T10:00:00.000Z"); test("nimmt das Erntejahr des Produkts", () => { expect(harvestYearOf({ harvest: { year: 2026 }, batch: { fillDate: new Date("2025-08-01"), harvest: { year: 2025 } }, createdAt })).toBe(2026); }); test("faellt auf die Ernte der Charge, dann ihr Abfuelljahr, dann das Anlagejahr zurueck", () => { expect(harvestYearOf({ harvest: null, batch: { fillDate: new Date("2025-08-01"), harvest: { year: 2025 } }, createdAt })).toBe(2025); expect(harvestYearOf({ harvest: null, batch: { fillDate: new Date("2025-08-01T00:00:00.000Z"), harvest: null }, createdAt })).toBe(2025); expect(harvestYearOf({ harvest: null, batch: null, createdAt })).toBe(2024); }); }); describe("pickHoneyProduct", () => { test("schliesst cremigen Honig aus, auch wenn er neuer ist und mehr Bestand hat", () => { const picked = pickHoneyProduct( [ candidate({ id: "fein", consistency: "feincremig", harvestYear: 2027, freeStock: 99 }), candidate({ id: "wald", consistency: "flüssig", harvestYear: 2025, freeStock: 3 }), ], 2 ); expect(picked?.id).toBe("wald"); }); test("die neueste Ernte gewinnt vor dem groesseren Bestand", () => { const picked = pickHoneyProduct( [candidate({ id: "alt", harvestYear: 2025, freeStock: 50 }), candidate({ id: "neu", harvestYear: 2026, freeStock: 4 })], 3 ); expect(picked?.id).toBe("neu"); }); test("bei gleich neuer Ernte entscheidet der groessere freie Bestand", () => { const picked = pickHoneyProduct( [candidate({ id: "linde", freeStock: 6, inventory: 20 }), candidate({ id: "raps", freeStock: 9, inventory: 9 })], 1 ); expect(picked?.id).toBe("raps"); }); test("ueberspringt Honig, dessen freier Bestand nicht fuer alle Glaeser reicht", () => { const picked = pickHoneyProduct( [candidate({ id: "neu", harvestYear: 2026, freeStock: 2 }), candidate({ id: "alt", harvestYear: 2025, freeStock: 3 })], 3 ); expect(picked?.id).toBe("alt"); }); test("ohne passenden Honig null — dann ist das Angebot nicht bestellbar", () => { expect(pickHoneyProduct([], 1)).toBeNull(); expect(pickHoneyProduct([candidate({ id: "fein", consistency: "cremig" })], 1)).toBeNull(); expect(pickHoneyProduct([candidate({ id: "knapp", freeStock: 2 })], 3)).toBeNull(); }); test("bleibt bei Gleichstand stabil (nach id)", () => { const a = candidate({ id: "a" }); const b = candidate({ id: "b" }); expect(pickHoneyProduct([b, a], 1)?.id).toBe("a"); }); }); describe("rankHoneyCandidates", () => { test("sortiert ohne die Eingabe zu veraendern und laesst cremigen Honig weg", () => { const input = [ candidate({ id: "alt", harvestYear: 2024 }), candidate({ id: "fein", consistency: "feincremig", harvestYear: 2027 }), candidate({ id: "neu", harvestYear: 2026 }), ]; expect(rankHoneyCandidates(input).map((c) => c.id)).toEqual(["neu", "alt"]); expect(input.map((c) => c.id)).toEqual(["alt", "fein", "neu"]); }); }); describe("withTakenStock", () => { test("zieht bereits verplante Glaeser vom freien Bestand ab, ohne die Eingabe zu veraendern", () => { const input = [candidate({ id: "wald", freeStock: 5 }), candidate({ id: "raps", freeStock: 4 })]; const result = withTakenStock(input, new Map([["wald", 3]])); expect(result.map((c) => [c.id, c.freeStock])).toEqual([ ["wald", 2], ["raps", 4], ]); expect(input[0].freeStock).toBe(5); }); test("nacheinander gewaehlte Pakete weichen auf den naechsten Honig aus, wenn der erste nicht mehr reicht", () => { const candidates = [candidate({ id: "neu", harvestYear: 2026, freeStock: 4 }), candidate({ id: "alt", harvestYear: 2025, freeStock: 10 })]; const first = pickHoneyProduct(candidates, 3); const second = pickHoneyProduct(withTakenStock(candidates, new Map([["neu", 3]])), 3); expect(first?.id).toBe("neu"); expect(second?.id).toBe("alt"); }); }); describe("allocateReservedJars", () => { test("legt alle Abo-Glaeser auf den Honig, der fuer alle reicht", () => { const result = allocateReservedJars( [candidate({ id: "neu", harvestYear: 2026, freeStock: 5 }), candidate({ id: "alt", harvestYear: 2025, freeStock: 30 })], 8 ); expect(Object.fromEntries(result)).toEqual({ alt: 8 }); }); test("reicht keiner allein, wird in der Rangfolge aufgefuellt", () => { const result = allocateReservedJars( [candidate({ id: "neu", harvestYear: 2026, freeStock: 5 }), candidate({ id: "alt", harvestYear: 2025, freeStock: 4 })], 8 ); expect(Object.fromEntries(result)).toEqual({ neu: 5, alt: 3 }); }); test("ein Fehlbestand landet beim erstgewaehlten Honig, damit er sichtbar wird", () => { const result = allocateReservedJars( [candidate({ id: "neu", harvestYear: 2026, freeStock: 2 }), candidate({ id: "alt", harvestYear: 2025, freeStock: 1 })], 6 ); expect(Object.fromEntries(result)).toEqual({ neu: 5, alt: 1 }); }); test("ohne Bedarf oder ohne fluessigen Honig keine Reservierung", () => { expect(allocateReservedJars([candidate({ id: "wald" })], 0).size).toBe(0); expect(allocateReservedJars([candidate({ id: "fein", consistency: "cremig" })], 4).size).toBe(0); }); }); describe("honeyLabel", () => { test("haengt das Erntejahr an, ausser es steht schon im Namen", () => { expect(honeyLabel({ name: "Waldhonig", harvestYear: 2026 })).toBe("Waldhonig 2026"); expect(honeyLabel({ name: "Waldhonig 2026", harvestYear: 2026 })).toBe("Waldhonig 2026"); }); });