Files
Wabenhain/src/lib/sponsorship/honey-selection.test.ts
T
Nick.RosenloecherandClaude Opus 5.5 1d88bac52e
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m18s
Docker Image bauen und veroeffentlichen / test (push) Successful in 5m32s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m41s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 20m38s
feat: Patenschafts-Honig automatisch waehlen (nie cremig, neueste Ernte)
Pakete, Abo-Reservierung und Abo-Lieferungen nehmen immer den aktiven,
nicht cremigen Honig der neuesten Ernte mit genug freiem Bestand, bei
gleichem Jahrgang den mit dem meisten Bestand. Feste Honig-Zuordnung
im Admin entfaellt (Spalte bleibt, wird nicht mehr genutzt).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 18:35:04 +02:00

177 lines
6.7 KiB
TypeScript

/**
* 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<HoneyCandidate> & Pick<HoneyCandidate, "id">): 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");
});
});