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]>
12 KiB
Bienenpatenschaft Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Bienenpatenschaften als Einmalkauf und Abo verkaufen, mit deterministisch erzeugter Urkunde, Medaillen, 3D-Ansicht im Konto, Übertragung, Treuestufen und Stripe-Abo.
Architecture: Eine reine Layout-Engine (src/lib/certificate) erzeugt aus Personalisierung, Textbausteinen und Vorlagenversion positionierte Elemente; daraus entstehen PDF (pdfkit) und Bilder (SVG mit Glyphenpfaden → sharp). Einmal-Angebote hängen an je einem Product der neuen Kategorie PATENSCHAFT, damit Warenkorb, Checkout, Rechnung, ERP-Export und ApiCore-Buchung weiterlaufen; der Honig eines Pakets wird beim Checkout als eigene Position des verknüpften Honigprodukts angelegt (Steueraufteilung, Bestandsreservierung und ApiCore-Abbuchung ohne Sonderweg). Abos laufen über Stripe Billing; jede bezahlte Monatsrechnung wird zu einer Wabenhain-Bestellung mit Rechnung, jede fällige Lieferung zu einer 0-€-Bestellung mit Honigpositionen.
Tech Stack: Next.js 16 (App Router, Server Actions), Prisma 7 (Postgres + MySQL), pdfkit, fontkit, sharp, three + @react-three/fiber + drei, fflate, Stripe (API 2026-08-26.dahlia), vitest, Playwright.
Spec: docs/superpowers/specs/2026-10-16-bienenpatenschaft-design.md
Abweichung vom Standard „ein Plan je Teilprojekt“: Der Inhaber hat alle fünf Teilprojekte auf einmal freigegeben. Der Plan ist deshalb in Teile A–E gegliedert, jeder Teil ist für sich lauffähig und testbar und wird einzeln committet.
Global Constraints
- Kein Versprechen von Besuchen, Events, Videos, Fotos, Berichten, Umweltwirkungen – in keinem Text, keiner Mail, keiner Urkunde.
- Urkunde: A4 hochkant (595.28 × 841.89 pt), genau eine Seite, Inhalt ≥ 15 mm (42.52 pt) vom Rand, Rahmen ≥ 12 mm (34.02 pt), kleinste Schrift 8 pt, Fließtext ≥ 11 pt.
- Schriften Cormorant Garamond und Inter, statische TTF unter SIL OFL, mit Lizenzdatei in
assets/fonts/. - Farben: Forest
#24352b, Honey Dark#8a6531, Honey#b88a44, Sage#788774; auf Papier kein Verlauf, keine Folienimitation. - Eingabelimits (Start): Bienenname 2–24, Name 2–60, Schenkende ≤ 80, Widmung ≤ 280 Zeichen und ≤ 5 Zeilenumbrüche; NFC, Steuerzeichen raus, Glyphenprüfung.
- Keine Pronomen, keine abgeleiteten Anreden, keine Behauptung individueller Betreuung einer Biene.
- Patenschaftsnummer
WH-P-JJJJ-NNNNN. Vorlagenkennung<name>@<version>. - Patenschaften nur mit angemeldetem, verifiziertem Konto. Gastbestellungen anderer Produkte bleiben.
- PDFs in
CERTIFICATE_DIR(nie unterUPLOAD_DIR), Downloads nur nach Besitzprüfung, fremde IDs → 404. - Wasserzeichen der Vorschau: „Unverbindliche Vorschau“ / „Preview – not binding“.
- Abo: Lieferanspruch nach 3 (vierteljährlich) bzw. 6 (Ernte) bezahlten Monaten; SEPA-Neukunden-Frist 56 Tage (Admin, 0 = aus); Rückbuchung →
IM_VERZUG, nach 14 Tagen ohne Ausgleich Ende; Konto mit Rückbuchung → kein SEPA mehr. - Abo-Stufen 1–24 je bezahltem Monat, danach VIP; Stammkunden-Stufen ab 2/5/10/25 bezahlten Bestellungen; beides im Admin pflegbar.
- Übertragungslinks: 24 h / 7 Tage / 30 Tage, einmalig, widerrufbar, Token ≥ 128 Bit, nur Hash gespeichert.
- Zahlarten nie per
payment_method_types; Stripe-Livekonto nur über Einrichtungsskript bzw. Stripe-Anbindung ändern. - Codekommentare und Texte auf Deutsch, Stil wie im Repo (Warum-Kommentare, keine console.log zum Debuggen).
- Agents committen nicht; der Hauptlauf committet zentral.
Review Focus
- Zwei Patenschaften desselben Pakets im Warenkorb dürfen nicht verschmelzen → Tests in A7 (Warenkorbschlüssel) und A8 (Checkout mit doppelter productId).
- Webhook mehrfach oder parallel: genau eine Patenschaft, genau eine Mail → Test in A9.
- Manipulierte Personalisierung (überlange Widmung, fremde Zeichen,
<script>): Checkout lehnt ab, Urkunde zeigt nur Text → Tests in A2 und A8. - Abo gekündigt, während eine Lieferung fällig wird: keine Lieferung über bezahlte Monate hinaus → Test in D4.
- Honig knapp, während Abos reserviert sind: Einzelverkauf und Pakete können reservierte Gläser nicht kaufen → Test in D5.
Teil A – Urkunde und Einmal-Patenschaft (Teilprojekt 1)
Task A1: Schriften, Personalisierungsschema, Textbausteine
Files: Create assets/fonts/CormorantGaramond-{Regular,Italic,SemiBold,SemiBoldItalic}.ttf, assets/fonts/Inter-{Regular,Medium}.ttf, Lizenzdateien; src/lib/certificate/{schema,text,fonts}.ts, src/types/fontkit.d.ts; Tests src/lib/certificate/{schema,text}.test.ts
Produces:
-
OCCASIONS = ["SELBST","GEBURTSTAG","WEIHNACHTEN","DANKESCHOEN","GESCHENK"] as const,type Occasion -
CERTIFICATE_LIMITS = { beeName:{min:2,max:24}, recipientName:{min:2,max:60}, giverNames:{max:80}, dedication:{max:280,maxLineBreaks:5} } -
normalizeCertificateText(value: string, opts?: { multiline?: boolean }): string -
personalizationSchema(zod) →Personalization = { beeName; recipientName; giverNames?; dedication?; occasion: Occasion; startDate: "YYYY-MM-DD" };giverNameswird beiSELBSTverworfen -
CertificateTerm = {kind:"FIXED";start;end} | {kind:"OPEN";start} | {kind:"ENDED";start;end},fixedTermFor(start)(Ende = Start + 12 Monate − 1 Tag) -
composeCertificateText({ personalization, locale, term, number }): CertificateText={ title; subtitle; occasionLine|null; leadLine|null; beforeBee; beeName; afterBee; giverLine|null; dedication|null; termLine; numberLine; notice } -
loadCertificateFonts(): CertificateFonts(gecacht; Schlüsselserif,serifItalic,serifBold,serifBoldItalic,sans,sansMedium),findUnsupportedCharacters(text, fontKeys): string[] -
Tests (Schema-Grenzen, „Zoë“, Steuerzeichen, giverNames bei SELBST, 6 Umbrüche, Emoji; Text: eigene/verschenkte Variante, Anlasszeilen, leere Felder ohne Zeile, drei Laufzeitformen de/en, Hinweis wörtlich)
-
Implementieren,
pnpm vitest run src/lib/certificategrün
Task A2: Layout-Engine
Files: src/lib/certificate/layout.ts, src/lib/certificate/templates/{index,<vorlage>}.ts; Test layout.test.ts
Produces:
-
LayoutElement = {kind:"text"; font; size; color; x; y; width; text; align; role} | {kind:"path"; d; stroke?; fill?; strokeWidth?; opacity?} | {kind:"image"; src:"logo"; x; y; width; height} -
CertificateLayout = { templateVersion; width: 595.28; height: 841.89; elements } -
CertificateTemplate { version; layout(text, fonts): CertificateLayout },CURRENT_TEMPLATE_VERSION,getTemplate(version) -
layoutCertificate(text, templateVersion?),CertificateLayoutError { field },checkCertificateFits(p, locale) -
Testmatrix der Spec: keine Überlappung, Sicherheitszone, Mindestgrößen, Namen ≤ 2 Zeilen, Umbruch nur an Leerzeichen/Bindestrich
-
Implementieren, grün
Task A3: Renderer und Designvarianten
Files: src/lib/certificate/{render-pdf,render-image}.ts, scripts/render-certificate-samples.ts; Test render.test.ts
Produces: renderCertificatePdf(layout, { title, logo? }): Promise<Buffer>, renderCertificateSvg(layout, { watermark? }): string (Glyphenpfade), renderCertificateImage(layout, { widthPx, watermark?, format }): Promise<Buffer>, layoutLines(layout): string[]
- Tests:
%PDF, genau eine Seite, eingebettete Fonts; SVG ohne<text>; Wasserzeichen; Bildbreite - Drei Vorlagen rendern, ansehen, eine wählen, andere entfernen
Task A4: Datenmodell
Enums SponsorshipOfferKind, SponsorshipCycle, MedalTier, SponsorshipStatus; ProductCategory.PATENSCHAFT; FulfillmentMethod.DIGITAL; Modelle SponsorshipOffer, Sponsorship, SponsorshipRevision, SponsorshipCounter; OrderItem.personalization, OrderItem.sponsorshipOfferId, OrderItem.sponsorshipRole. Migration prisma/migrations/20261016010000_sponsorship. DIGITAL überall behandeln, pnpm typecheck grün.
Task A5: Angebote im Admin + Startdaten
src/lib/sponsorship/offers.ts (getActiveOffers, getOfferBySlug), Admin /admin/patenschaften/angebote, Upsert legt das Product (Kategorie PATENSCHAFT, Preis = Paketpreis − Honiganteil) mit an; Seed der drei Pakete; PATENSCHAFT aus Shop-Listen und Produktseite filtern.
Task A6: Vorschau-API und Verkaufsseite
/api/patenschaft/vorschau (Origin, Rate-Limit 60/10 min, Schema + Fit, WebP 900 px mit Wasserzeichen, no-store); /patenschaft mit Konfigurator (Einmalig/Abo, Schritte, Zähler, Debounce 400 ms, Bestätigung, Anmeldezwang mit Entwurf in sessionStorage wabenhain-sponsorship-draft).
Task A7: Warenkorb
CartItem.key, CartItem.sponsorship?: { offerSlug; personalization }, Operationen über key, persist Version 2 mit migrate, Patenschaft Menge fest 1. Tests: zwei gleiche Pakete bleiben zwei Positionen; alter Speicherstand wird migriert.
Task A8: Checkout
expandSponsorshipItems(...) in src/lib/sponsorship/checkout-expand.ts: Hauptposition ohne Reservierung, Honigposition aus verknüpftem Produkt (unitPriceCents = honeyShareCentsPerJar, Menge = Gläser, Steuer des Honigs), Kontopflicht, erneute Prüfung, reine Digitalbestellung → DIGITAL ohne Versand, doppelte productIds erlaubt. Tests inkl. Manipulation.
Task A9: Ausstellung nach Zahlung
createSponsorshipsForOrder(tx, orderId) im Claim-Lauf; issuePendingSponsorships({orderId?, limit?}); Mail-Claim über mailSentAt; Backoff min(2^n × 5 min, 12 h), 8 Versuche → FEHLER; scripts/sponsorship-jobs.ts + CronJob wabenhain-sponsorship; CERTIFICATE_DIR mit Startprüfung. Tests: doppelter Aufruf, Fehler beim Rendern, Wiederholung.
Task A10: Kontobereich und Download
/konto/patenschaft, /konto/patenschaft/[id], PDF-Route mit Besitzprüfung, getOwnedSponsorship(customerId, id).
Task A11: Admin Patenschaften, Druck, Korrektur
Liste/Filter, Detail, Druck-PDF, „gedruckt“, „erneut erzeugen“, „Mail erneut“, reviseSponsorship(...) mit Revision und Audit-Log; Aufmerksamkeitsliste.
Teil B – Medaillen, 3D, Gesamtdownload (Teilprojekt 2)
- B1
src/lib/medals/:renderMedalSvg(input: MedalInput),renderMedalPng(input, sizePx = 2048);MedalInput= Patenschaft (tier, beeName, year, number) | Stammkunde (level, name) | Abo (months) | VIP. - B2 Routen
textur.webp,medaille.png,medaille.svg,alles.zip(fflate) mit Besitzprüfung. - B3
src/components/patenschaft-3d/:<SponsorshipStage textureUrl backTextureUrl medalSvgUrl tier fallbackImageUrl />, dynamisch geladen, Wölbung, Reflex, Ziehen mit Nachschwung, Doppelklick wendet, Zoom begrenzt, Neigung nach Tipp, reduced motion, WebGL-Fallback, Tastatur, Pause außerhalb der Sicht.
Teil C – Übertragung (Teilprojekt 3)
SponsorshipTransfer, createTransferAction(id, validity), revokeTransferAction, /patenschaft/uebernehmen/[token], Audit-Log, neutrale Fehlerseite. Tests: Ablauf, Widerruf, Doppel-Einlösung, Abo nicht übertragbar.
Teil D – Abo (Teilprojekt 4)
- D1 Modelle
SponsorshipSubscription,SubscriptionPayment,SubscriptionDelivery,HarvestWindow;Customer.stripeCustomerId,Customer.sepaBlockedAt; Einstellung SEPA-Frist (56). - D2 Stripe Product/Price aus Admin;
POST /api/patenschaft/abo(Checkoutmode: "subscription", eingebettet;excluded_payment_method_types: ["sepa_debit"]nur bei Sperre). - D3 Webhooks Abo/Rechnung/Rückbuchung idempotent;
invoice.paid→ Zahlung + BestellungDIGITAL+ Rechnung + Mail; erste Zahlung → Patenschaft (OPEN). - D4
computeDeliveryEntitlement(...), täglicher Lauf erzeugt 0-€-Lieferbestellungen. - D5
availableForSale(productId)= Bestand − Abo-Reservierung; Kontingent; Deckungsanzeige; Stopp neuer Abos. - D6 Kündigung im Konto und unter
/vertraege-kuendigen,cancel_at_period_end, Mail, ENDED-Revision bei Wirksamkeit. - D7
scripts/stripe-setup.tsund Livekonto über die Stripe-Anbindung (Webhook-Ereignisse, Kundenportal nur Zahlungsart), Checkliste indocs/EINRICHTUNG.md.
Teil E – Treuestufen (Teilprojekt 5)
LoyaltyLevel, Seed (2/5/10/25; Abo 1–24 + VIP), getCustomerLoyalty(customerId), Medaillenleiste, Admin-Pflege. Tests: Erstattung senkt Stufe, Kündigung entfernt Abo-Stufe.
Abschluss
pnpm lint,pnpm typecheck,pnpm test,pnpm buildgrün- Browserprüfung Desktop + 390 px, reduced motion, Tastatur
- Code-Review und
code-simplifierauf den Diff - Commits, Push, CI grün; Rollout nur nach Freigabe