Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m57s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m7s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 0s
Docker Image bauen und veroeffentlichen / test (push) Canceled after 1m22s
- Oeffentliche Kuendigung: exakte Identifikation, Limits je Vertrag und Adresse, Bestaetigung nur an die Kontoadresse mit Ruecknahme-Link (HMAC) - Vorschau: Body-Grenze beim Lesen, Parallelitaets- und Minutenlimit, Cache - Rabatte gelten nicht fuer Patenschaftspositionen - Abo aktiv erst nach bestaetigter Zahlung, offene Abschluesse zaehlen zum Kontingent und werden nach 24 h aufgeraeumt, Sitzung 58 Minuten - Konto-Loeschung nur ohne laufendes Abo, Urkunden-Dateien werden entfernt - Abo-Lieferungen mit Zahlungszeile (DHL-Marke moeglich) - Shop zeigt freien Bestand nach Abo-Reservierung - Storno/volle Erstattung widerruft die Patenschaft (Status WIDERRUFEN) - Umsatzsteuer der Patenschaft per VAT_RATE_PERCENT_PATENSCHAFT (19 %) - Digitale Bestellungen archivieren sich, Kalendertage in Europe/Berlin, Monatsenden begrenzt, Datenauskunft um Patenschaften ergaenzt Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
/**
|
|
* Abo-Reservierung im Stapel: Produktlisten brauchen den freien Bestand
|
|
* vieler Produkte auf einmal — mit festen drei Abfragen statt drei je Produkt.
|
|
*/
|
|
import { describe, expect, test, vi } from "vitest";
|
|
|
|
vi.mock("@/lib/prisma", () => ({ prisma: {} }));
|
|
|
|
const { reservedJarsByProduct, reservedJarsForProduct, availableForSale, availableForSaleByProduct } = await import("./reservation");
|
|
|
|
const NOW = new Date("2026-10-16T12:00:00.000Z");
|
|
|
|
function fakeDb(data: {
|
|
quarterly?: { offer: { honeyProductId: string | null; honeyJars: number } }[];
|
|
windows?: { offerId: string; startsOn: Date; endsOn: Date; plannedJars: number; offer: { honeyProductId: string | null } }[];
|
|
deliveries?: { jars: number; dueAt: Date; subscription: { offerId: string } }[];
|
|
inventory?: { productId: string; quantity: number }[];
|
|
}) {
|
|
return {
|
|
sponsorshipSubscription: { findMany: vi.fn(async () => data.quarterly ?? []) },
|
|
harvestWindow: { findMany: vi.fn(async () => data.windows ?? []) },
|
|
subscriptionDelivery: { findMany: vi.fn(async () => data.deliveries ?? []) },
|
|
inventory: {
|
|
findMany: vi.fn(async () => data.inventory ?? []),
|
|
findUnique: vi.fn(async ({ where }: { where: { productId: string } }) =>
|
|
(data.inventory ?? []).find((row) => row.productId === where.productId) ?? null
|
|
),
|
|
},
|
|
};
|
|
}
|
|
|
|
type Db = Parameters<typeof reservedJarsByProduct>[2];
|
|
|
|
describe("reservedJarsByProduct", () => {
|
|
test("summiert vierteljaehrliche Abos und offene Erntefenster je Produkt in drei Abfragen", async () => {
|
|
const db = fakeDb({
|
|
quarterly: [
|
|
{ offer: { honeyProductId: "wald", honeyJars: 2 } },
|
|
{ offer: { honeyProductId: "wald", honeyJars: 2 } },
|
|
{ offer: { honeyProductId: "raps", honeyJars: 3 } },
|
|
],
|
|
windows: [
|
|
{
|
|
offerId: "ernte",
|
|
startsOn: new Date("2026-10-01T00:00:00.000Z"),
|
|
endsOn: new Date("2026-10-31T00:00:00.000Z"),
|
|
plannedJars: 10,
|
|
offer: { honeyProductId: "wald" },
|
|
},
|
|
],
|
|
deliveries: [
|
|
{ jars: 4, dueAt: new Date("2026-10-10T08:00:00.000Z"), subscription: { offerId: "ernte" } },
|
|
// Ausserhalb des Fensters: zaehlt nicht.
|
|
{ jars: 6, dueAt: new Date("2026-09-10T08:00:00.000Z"), subscription: { offerId: "ernte" } },
|
|
],
|
|
});
|
|
|
|
const result = await reservedJarsByProduct(["wald", "raps", "linde"], NOW, db as unknown as Db);
|
|
|
|
expect(result.get("wald")).toBe(2 + 2 + (10 - 4));
|
|
expect(result.get("raps")).toBe(3);
|
|
expect(result.get("linde")).toBe(0);
|
|
expect(db.sponsorshipSubscription.findMany).toHaveBeenCalledOnce();
|
|
expect(db.harvestWindow.findMany).toHaveBeenCalledOnce();
|
|
expect(db.subscriptionDelivery.findMany).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
test("ein ueberliefertes Erntefenster reserviert nichts mehr (nie negativ)", async () => {
|
|
const db = fakeDb({
|
|
windows: [
|
|
{
|
|
offerId: "ernte",
|
|
startsOn: new Date("2026-10-01T00:00:00.000Z"),
|
|
endsOn: new Date("2026-10-31T00:00:00.000Z"),
|
|
plannedJars: 3,
|
|
offer: { honeyProductId: "wald" },
|
|
},
|
|
],
|
|
deliveries: [{ jars: 5, dueAt: new Date("2026-10-31T20:00:00.000Z"), subscription: { offerId: "ernte" } }],
|
|
});
|
|
|
|
const result = await reservedJarsByProduct(["wald"], NOW, db as unknown as Db);
|
|
|
|
expect(result.get("wald")).toBe(0);
|
|
});
|
|
|
|
test("ohne Produkte keine Abfrage", async () => {
|
|
const db = fakeDb({});
|
|
|
|
const result = await reservedJarsByProduct([], NOW, db as unknown as Db);
|
|
|
|
expect(result.size).toBe(0);
|
|
expect(db.sponsorshipSubscription.findMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("Einzelabfrage und freier Bestand bauen auf dem Stapel auf", async () => {
|
|
const db = fakeDb({
|
|
quarterly: [{ offer: { honeyProductId: "wald", honeyJars: 2 } }],
|
|
inventory: [{ productId: "wald", quantity: 5 }],
|
|
});
|
|
|
|
expect(await reservedJarsForProduct("wald", NOW, db as unknown as Db)).toBe(2);
|
|
expect(await availableForSale("wald", NOW, db as unknown as Db)).toBe(3);
|
|
});
|
|
|
|
test("freier Bestand im Stapel: Lager minus Reservierung, ohne Lagerzeile 0", async () => {
|
|
const db = fakeDb({
|
|
quarterly: [{ offer: { honeyProductId: "wald", honeyJars: 4 } }],
|
|
inventory: [
|
|
{ productId: "wald", quantity: 3 },
|
|
{ productId: "raps", quantity: 7 },
|
|
],
|
|
});
|
|
|
|
const result = await availableForSaleByProduct(["wald", "raps", "linde"], NOW, db as unknown as Db);
|
|
|
|
expect(Object.fromEntries(result)).toEqual({ wald: 0, raps: 7, linde: 0 });
|
|
});
|
|
});
|