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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 14:07:30 +02:00
1 parent dcb578f062
commit f227aeb8f9
269 files changed
+28054 -141

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import { getCurrentCustomer } from "@/lib/auth";
import { getOwnedSponsorship, type OwnedSponsorship } from "@/lib/sponsorship/access";
import { rateLimit } from "@/lib/rate-limit";
import { getLocale } from "@/i18n/locale";
import type { Locale } from "@/i18n/config";
import type { Customer } from "@/generated/prisma/client";
export interface OwnedContext {
customer: Customer;
sponsorship: OwnedSponsorship;
locale: Locale;
}
/**
* Anmeldung und Besitz in einem Schritt. null heisst fuer jede Route: 404 —
* auch ohne Anmeldung, damit eine Route nicht verraet, ob es die Patenschaft
* gibt.
*/
export async function loadOwnedSponsorship(id: string): Promise<OwnedContext | null> {
const customer = await getCurrentCustomer();
if (!customer) return null;
const locale = await getLocale();
const sponsorship = await getOwnedSponsorship(customer.id, id, locale);
return sponsorship ? { customer, sponsorship, locale } : null;
}
// Rendern (Textur, Medaille, ZIP) kostet spuerbar CPU. Zwischengespeicherte
// Ergebnisse zaehlen nicht; das Limit trifft nur echte Renderlaeufe. 60 in
// 10 Minuten reichen fuer jede normale Nutzung mehrerer Patenschaften.
const RENDERS_PER_WINDOW = 60;
const RENDER_WINDOW_MS = 10 * 60 * 1000;
export function allowRender(customerId: string): boolean {
return rateLimit(`sponsorship-render:${customerId}`, RENDERS_PER_WINDOW, RENDER_WINDOW_MS);
}