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

+115
View File
@@ -83,6 +83,121 @@ async function main() {
"Siehe README, wie ein bestehendes Konto nachtraeglich zu ADMIN befoerdert wird."
);
}
await seedSponsorshipDefaults();
}
/**
* Startangebote der Bienenpatenschaft und Treuestufen — nur wenn es noch
* keine gibt, und die Angebote INAKTIV: dieser Seed laeuft bei jedem
* Containerstart auch in Produktion, und Preise, Honig und Rechtstexte muss
* der Inhaber vor der Veroeffentlichung pruefen (Spec, "Geschaeftliche
* Konfiguration"). Erst das Aktivieren im Admin macht /patenschaft sichtbar.
*/
async function seedSponsorshipDefaults(): Promise<void> {
if ((await prisma.sponsorshipOffer.count()) === 0) {
const packages = [
{
slug: "bienenfreund",
name: "Bienenfreund",
nameEn: "Bee Friend",
priceCents: 1900,
isPhysical: false,
honeyJars: 0,
medalTier: "BRONZE" as const,
description: "Symbolische Namensgebung mit individuell gestalteter Urkunde als PDF.",
descriptionEn: "Symbolic naming with an individually designed certificate as a PDF.",
features: ["Symbolische Namensgebung", "Individuell gestaltete PDF-Urkunde", "Medaille und 3D-Ansicht im Kundenkonto", "Sicherer Download"],
featuresEn: ["Symbolic naming", "Individually designed PDF certificate", "Medal and 3D view in your account", "Secure download"],
},
{
slug: "honigfreund",
name: "Honigfreund",
nameEn: "Honey Friend",
priceCents: 3900,
isPhysical: true,
honeyJars: 1,
medalTier: "SILBER" as const,
description: "Digitale Leistungen plus gedruckte Urkunde, ein Glas Wabenhain-Honig, Grußkarte und Geschenkverpackung.",
descriptionEn: "Digital items plus a printed certificate, a jar of Wabenhain honey, a greeting card and gift wrapping.",
features: ["Alle digitalen Leistungen", "Hochwertig gedruckte Urkunde", "1 × 500 g Wabenhain-Honig", "Grußkarte und Geschenkverpackung"],
featuresEn: ["All digital items", "High-quality printed certificate", "1 × 500 g Wabenhain honey", "Greeting card and gift wrapping"],
},
{
slug: "wabenfreund",
name: "Wabenfreund",
nameEn: "Comb Friend",
priceCents: 5900,
isPhysical: true,
honeyJars: 3,
medalTier: "GOLD" as const,
description: "Digitale Leistungen plus gedruckte Urkunde, drei Gläser Wabenhain-Honig, Grußkarte und Geschenkverpackung.",
descriptionEn: "Digital items plus a printed certificate, three jars of Wabenhain honey, a greeting card and gift wrapping.",
features: ["Alle digitalen Leistungen", "Hochwertig gedruckte Urkunde", "3 × 500 g Wabenhain-Honig", "Grußkarte und Geschenkverpackung"],
featuresEn: ["All digital items", "High-quality printed certificate", "3 × 500 g Wabenhain honey", "Greeting card and gift wrapping"],
},
];
for (const [index, pkg] of packages.entries()) {
// Honiganteil als Vorschlag (7 € je Glas) — der Inhaber prueft ihn mit
// der Steuerberatung, bevor er das Angebot aktiviert.
const honeyShareCentsPerJar = pkg.honeyJars > 0 ? 700 : 0;
const product = await prisma.product.create({
data: {
slug: `patenschaft-${pkg.slug}`,
name: `Bienenpatenschaft ${pkg.name}`,
category: "PATENSCHAFT",
description: pkg.description,
weightGrams: 0,
priceCents: pkg.priceCents - pkg.honeyJars * honeyShareCentsPerJar,
pricePerKgCents: 0,
isActive: true,
},
});
await prisma.sponsorshipOffer.create({
data: {
slug: pkg.slug,
kind: "EINMALIG",
name: pkg.name,
nameEn: pkg.nameEn,
description: pkg.description,
descriptionEn: pkg.descriptionEn,
features: pkg.features,
featuresEn: pkg.featuresEn,
priceCents: pkg.priceCents,
isActive: false,
sortOrder: index,
isPhysical: pkg.isPhysical,
productId: product.id,
honeyJars: pkg.honeyJars,
honeyShareCentsPerJar,
medalTier: pkg.medalTier,
},
});
}
console.log("Bienenpatenschaft: drei Startangebote angelegt (inaktiv, bitte im Admin pruefen und aktivieren).");
}
if ((await prisma.loyaltyLevel.count()) === 0) {
const regular = [
{ threshold: 2, name: "Blütenbesuch", nameEn: "Blossom Visitor" },
{ threshold: 5, name: "Wabenbau", nameEn: "Comb Builder" },
{ threshold: 10, name: "Honigschatz", nameEn: "Honey Treasure" },
{ threshold: 25, name: "Goldwabe", nameEn: "Golden Comb" },
];
const subscription = Array.from({ length: 24 }, (_, i) => ({
threshold: i + 1,
name: `Stufe ${i + 1}`,
nameEn: `Level ${i + 1}`,
}));
await prisma.loyaltyLevel.createMany({
data: [
...regular.map((level) => ({ kind: "STAMMKUNDE" as const, ...level })),
...subscription.map((level) => ({ kind: "ABO" as const, ...level })),
{ kind: "ABO" as const, threshold: null, name: "VIP", nameEn: "VIP" },
],
});
console.log("Treuestufen angelegt (Stammkunde 2/5/10/25, Abo 1–24 und VIP).");
}
}
main()