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
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]>
177 lines
6.7 KiB
TypeScript
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");
|
|
});
|
|
});
|