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,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);
|
||||
}
|
||||
Reference in new issue
Block a user