Files
Wabenhain/docs/superpowers/plans/2026-10-16-bienenpatenschaft.md
Nick SzaunigandClaude Opus 5.5 f227aeb8f9 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]>
2026-10-16 14:07:30 +02:00

12 KiB
Raw Permalink Blame History

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 unter UPLOAD_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

  1. Zwei Patenschaften desselben Pakets im Warenkorb dürfen nicht verschmelzen → Tests in A7 (Warenkorbschlüssel) und A8 (Checkout mit doppelter productId).
  2. Webhook mehrfach oder parallel: genau eine Patenschaft, genau eine Mail → Test in A9.
  3. Manipulierte Personalisierung (überlange Widmung, fremde Zeichen, <script>): Checkout lehnt ab, Urkunde zeigt nur Text → Tests in A2 und A8.
  4. Abo gekündigt, während eine Lieferung fällig wird: keine Lieferung über bezahlte Monate hinaus → Test in D4.
  5. 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" }; giverNames wird bei SELBST verworfen

  • 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üssel serif, 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/certificate grü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 (Checkout mode: "subscription", eingebettet; excluded_payment_method_types: ["sepa_debit"] nur bei Sperre).
  • D3 Webhooks Abo/Rechnung/Rückbuchung idempotent; invoice.paid → Zahlung + Bestellung DIGITAL + 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.ts und Livekonto über die Stripe-Anbindung (Webhook-Ereignisse, Kundenportal nur Zahlungsart), Checkliste in docs/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 build grün
  • Browserprüfung Desktop + 390 px, reduced motion, Tastatur
  • Code-Review und code-simplifier auf den Diff
  • Commits, Push, CI grün; Rollout nur nach Freigabe