fix: Befunde aus Sicherheits- und Korrektheitsreview der Bienenpatenschaft
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
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]>
This commit is contained in:
1 parent
f227aeb8f9
commit
515fca1c16
97 files changed
+2979
-254
No files matched your search
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user