feat: Bienenpatenschaft mit Urkunde, Abo, Medaillen und 3D-Konto
Setzt Teilprojekte 1-5 der Spec 2026-10-16 um: - Urkunden-Engine (lib/certificate): ein Layout fuer PDF, Vorschau und 3D-Textur, Vorlage classic@1, eingebettete OFL-Schriften, Testmatrix - Einmal-Patenschaft ueber Traegerprodukte (Kategorie PATENSCHAFT), Paket wird im Checkout in Patenschaft (Regelsatz) und Honig aufgeteilt, Kontopflicht, rein digitale Bestellungen (FulfillmentMethod DIGITAL) - Ausstellung nach Zahlung idempotent, Wiederholung im App-Prozess (instrumentation + background-jobs), Ablage in CERTIFICATE_DIR - Verkaufsseite /patenschaft mit Konfigurator und Live-Vorschau, Warenkorb mit Schluessel je Position - Kundenkonto: 3D-Ansicht von Urkunde und Medaille, Downloads, ZIP, Uebertragung per befristetem Einmal-Link, Treuestufen bis VIP - Admin: Angebote, Patenschaften, Druck, Korrektur mit Revisionen, Abos, Erntefenster, Deckung, SEPA-Frist, Treuestufen - Abo ueber Stripe Billing: Lieferung erst nach bezahlten Monaten, SEPA-Neukunden-Frist und Rueckbuchungsschutz, Honigreservierung, Kuendigungsbutton, Monatsrechnungen, Stripe-Einrichtungsskript Startangebote werden inaktiv angelegt; Livebetrieb erst nach Freigabe. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
dcb578f062
commit
f227aeb8f9
269 files changed
+28054
-141
No files matched your search
@@ -0,0 +1,90 @@
|
||||
/** Abo-Abschluss: nur mit verifiziertem Konto, nur fuer verfuegbare Abo-Angebote, mit gepruefter Personalisierung. */
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
const m = vi.hoisted(() => ({
|
||||
getCurrentCustomer: vi.fn(),
|
||||
getOfferBySlug: vi.fn(),
|
||||
createSubscriptionCheckout: vi.fn(),
|
||||
isTrustedOrigin: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({ getCurrentCustomer: m.getCurrentCustomer }));
|
||||
vi.mock("@/lib/csrf", () => ({ isTrustedOrigin: m.isTrustedOrigin }));
|
||||
vi.mock("@/lib/rate-limit", () => ({ rateLimit: () => true, getClientIp: async () => "127.0.0.1" }));
|
||||
vi.mock("@/i18n/locale", () => ({ getLocale: async () => "de" }));
|
||||
vi.mock("@/lib/sponsorship/offers", () => ({ getOfferBySlug: m.getOfferBySlug }));
|
||||
vi.mock("@/lib/sponsorship-subscription/checkout", () => ({ createSubscriptionCheckout: m.createSubscriptionCheckout }));
|
||||
vi.mock("@/lib/certificate/layout", () => ({ checkCertificateFits: () => ({ ok: true }) }));
|
||||
|
||||
const { POST } = await import("./route");
|
||||
|
||||
const CUSTOMER = { id: "cust_1", email: "[email protected]", emailVerifiedAt: new Date(), stripeCustomerId: null, sepaBlockedAt: null };
|
||||
const PERSONALIZATION = { beeName: "Summsi", recipientName: "Erika Mustermann", occasion: "SELBST", startDate: "2030-01-01" };
|
||||
|
||||
function post(body: unknown): Promise<Response> {
|
||||
return POST(
|
||||
new Request("https://wabenhain.test/api/patenschaft/abo", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Origin: "https://wabenhain.test" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
m.isTrustedOrigin.mockReturnValue(true);
|
||||
m.getCurrentCustomer.mockResolvedValue(CUSTOMER);
|
||||
m.getOfferBySlug.mockResolvedValue({ id: "offer_1", slug: "vierteljaehrlich", kind: "ABO", isAvailable: true });
|
||||
m.createSubscriptionCheckout.mockResolvedValue({ clientSecret: "cs_secret", subscriptionId: "sub_local" });
|
||||
});
|
||||
|
||||
describe("POST /api/patenschaft/abo", () => {
|
||||
test("liefert das client_secret und ersetzt das Startdatum durch heute", async () => {
|
||||
const response = await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({ clientSecret: "cs_secret" });
|
||||
const input = m.createSubscriptionCheckout.mock.calls[0][0];
|
||||
expect(input.personalization.startDate).toBe(new Date().toISOString().slice(0, 10));
|
||||
expect(input.offerId).toBe("offer_1");
|
||||
});
|
||||
|
||||
test("weist fremde Herkunft ab", async () => {
|
||||
m.isTrustedOrigin.mockReturnValue(false);
|
||||
expect((await post({ offerSlug: "x", personalization: PERSONALIZATION })).status).toBe(403);
|
||||
});
|
||||
|
||||
test("ohne Konto 401, ohne bestaetigte Adresse 403", async () => {
|
||||
m.getCurrentCustomer.mockResolvedValueOnce(null);
|
||||
expect((await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION })).status).toBe(401);
|
||||
m.getCurrentCustomer.mockResolvedValueOnce({ ...CUSTOMER, emailVerifiedAt: null });
|
||||
expect((await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION })).status).toBe(403);
|
||||
expect(m.createSubscriptionCheckout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("ungueltige Personalisierung wird abgelehnt", async () => {
|
||||
const response = await post({ offerSlug: "vierteljaehrlich", personalization: { ...PERSONALIZATION, beeName: "" } });
|
||||
expect(response.status).toBe(400);
|
||||
expect((await response.json()).field).toBe("beeName");
|
||||
});
|
||||
|
||||
test("Einmal-Angebote laufen nicht ueber den Abo-Weg", async () => {
|
||||
m.getOfferBySlug.mockResolvedValue({ id: "offer_2", slug: "bienenfreund", kind: "EINMALIG", isAvailable: true });
|
||||
expect((await post({ offerSlug: "bienenfreund", personalization: PERSONALIZATION })).status).toBe(404);
|
||||
});
|
||||
|
||||
test("ausgebuchtes Abo (Kontingent oder Honig) nimmt keine Abschluesse an", async () => {
|
||||
m.getOfferBySlug.mockResolvedValue({ id: "offer_1", slug: "vierteljaehrlich", kind: "ABO", isAvailable: false });
|
||||
expect((await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION })).status).toBe(409);
|
||||
expect(m.createSubscriptionCheckout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("ein Stripe-Fehler wird ohne Details gemeldet", async () => {
|
||||
m.createSubscriptionCheckout.mockRejectedValue(new Error("Invalid API Key provided: sk_test_***"));
|
||||
const response = await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION });
|
||||
expect(response.status).toBe(502);
|
||||
expect(JSON.stringify(await response.json())).not.toContain("sk_test");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user