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]>
36 lines
1.4 KiB
TypeScript
36 lines
1.4 KiB
TypeScript
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);
|
|
}
|