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

+307
View File
@@ -0,0 +1,307 @@
import type { Locale } from "@/i18n/config";
import type { SubscriptionStatus } from "@/generated/prisma/client";
/**
* Texte des Abos: Dankeseite nach dem Abschluss, Abo-Seite im Konto mit
* Kuendigungsknopf (§ 312k BGB) und die oeffentliche Seite
* /vertraege-kuendigen. Eigenes Modul wie die uebrigen Patenschaftstexte,
* damit parallele Teile sich nicht in die Quere kommen. Ansprache: du.
*
* Die Beschriftungen "Verträge hier kündigen" und "Jetzt kündigen" sind vom
* Gesetz vorgegeben (§ 312k Abs. 2 BGB) und duerfen nicht umformuliert werden.
*/
export interface AboStrings {
footerLink: string;
status: Record<SubscriptionStatus, string>;
thanks: {
metaTitle: string;
activeTitle: string;
activeText: string;
processingTitle: string;
processingText: string;
notCompletedTitle: string;
notCompletedText: string;
toSubscription: string;
toAccount: string;
backToOffer: string;
};
detail: {
metaTitle: string;
back: string;
title: (offer: string) => string;
beeLine: (bee: string) => string;
contractNumber: string;
statusLabel: string;
monthlyPrice: string;
nextCharge: string;
endsOn: string;
paidMonths: string;
progressTitle: string;
progress: (paid: number, required: number) => string;
progressHarvest: (paid: number, required: number) => string;
sepaHoldNote: string;
deliveriesTitle: string;
deliveriesEmpty: string;
deliveryPrepared: (date: string, jars: number) => string;
deliveryDeferred: (date: string) => string;
delinquentNote: string;
paymentTitle: string;
paymentText: string;
paymentButton: string;
paymentUnavailable: string;
cancelTitle: string;
cancelText: string;
cancelButton: string;
cancelledNote: (date: string) => string;
endedNote: (date: string) => string;
certificateLink: string;
cancelDone: (date: string) => string;
cancelFailed: string;
};
confirm: {
metaTitle: string;
title: string;
intro: string;
kindLabel: string;
kindValue: string;
contractLabel: string;
effectiveLabel: string;
effectiveValue: (date: string) => string;
confirmationLabel: string;
confirmationValue: (email: string) => string;
submit: string;
back: string;
};
publicPage: {
metaTitle: string;
title: string;
intro: string;
accountHint: string;
nameLabel: string;
emailLabel: string;
emailHint: string;
referenceLabel: string;
referenceHint: string;
messageLabel: string;
requiredNote: string;
startButton: string;
reviewHeading: string;
reviewIntro: string;
confirmButton: string;
editButton: string;
submitting: string;
successHeading: string;
receivedAtLabel: string;
successMailSent: string;
successMailFailed: string;
errorMessage: string;
rateLimited: string;
invalid: string;
};
}
const de: AboStrings = {
footerLink: "Verträge hier kündigen",
status: {
AUSSTEHEND: "Abschluss offen",
AKTIV: "Aktiv",
IM_VERZUG: "Zahlung offen",
GEKUENDIGT: "Gekündigt",
BEENDET: "Beendet",
},
thanks: {
metaTitle: "Abo abgeschlossen",
activeTitle: "Danke für dein Abo!",
activeText: "Dein Abo ist aktiv – die Urkunde erscheint nach der ersten Zahlung in deinem Konto.",
processingTitle: "Deine Zahlung wird geprüft",
processingText:
"Dein Abo ist abgeschlossen. Bei Lastschrift dauert die Bestätigung der Bank einige Tage – die Urkunde erscheint nach der ersten Zahlung in deinem Konto.",
notCompletedTitle: "Das Abo wurde nicht abgeschlossen",
notCompletedText: "Es wurde nichts abgebucht. Du kannst es jederzeit erneut versuchen.",
toSubscription: "Zum Abo",
toAccount: "Zu meiner Patenschaft",
backToOffer: "Zurück zur Patenschaft",
},
detail: {
metaTitle: "Mein Abo",
back: "Zurück zur Patenschaft",
title: (offer) => `Abo „${offer}“`,
beeLine: (bee) => `Patenbiene ${bee}`,
contractNumber: "Vertragsnummer",
statusLabel: "Status",
monthlyPrice: "Monatlicher Betrag",
nextCharge: "Nächste Abbuchung",
endsOn: "Endet am",
paidMonths: "Bezahlte Monate",
progressTitle: "Bis zur nächsten Honiglieferung",
progress: (paid, required) => `${paid} von ${required} Monaten bezahlt`,
progressHarvest: (paid, required) =>
`${paid} von ${required} Monaten bezahlt – geliefert wird im nächsten Erntefenster`,
sepaHoldNote:
"Bei Lastschrift zählt die erste Zahlung erst nach acht Wochen für die Lieferung, weil die Bank sie so lange zurückholen kann.",
deliveriesTitle: "Honiglieferungen",
deliveriesEmpty: "Noch keine Lieferung – der Honig kommt, sobald genug Monate bezahlt sind.",
deliveryPrepared: (date, jars) => `${date}: ${jars} Gläser, in Vorbereitung oder unterwegs`,
deliveryDeferred: (date) => `${date}: zurückgestellt – wir melden uns bei dir`,
delinquentNote:
"Eine Zahlung ist offen. Solange sie nicht ausgeglichen ist, ruhen die Honiglieferungen.",
paymentTitle: "Zahlungsart",
paymentText: "Du kannst deine Zahlungsart sicher bei unserem Zahlungsdienstleister Stripe ändern.",
paymentButton: "Zahlungsart ändern",
paymentUnavailable: "Das Ändern der Zahlungsart ist gerade nicht möglich. Bitte versuche es später erneut.",
cancelTitle: "Kündigen",
cancelText: "Das Abo ist monatlich kündbar und endet zum Ende des bezahlten Monats.",
cancelButton: "Verträge hier kündigen",
cancelledNote: (date) => `Gekündigt – das Abo endet am ${date}. Danach wird nichts mehr abgebucht.`,
endedNote: (date) => `Das Abo ist am ${date} beendet. Urkunde, Medaille und Downloads bleiben in deinem Konto.`,
certificateLink: "Zur Urkunde",
cancelDone: (date) => `Deine Kündigung ist eingegangen. Das Abo endet am ${date}. Eine Bestätigung ist per Mail unterwegs.`,
cancelFailed: "Die Kündigung konnte gerade nicht verarbeitet werden. Bitte versuche es erneut oder nutze die Seite „Verträge hier kündigen“.",
},
confirm: {
metaTitle: "Kündigung bestätigen",
title: "Kündigung bestätigen",
intro: "Bitte prüfe deine Angaben. Mit dem Klick auf „Jetzt kündigen“ ist die Kündigung erklärt.",
kindLabel: "Art der Kündigung",
kindValue: "Ordentliche Kündigung zum nächstmöglichen Zeitpunkt",
contractLabel: "Vertrag",
effectiveLabel: "Wirksam zum",
effectiveValue: (date) => `${date} (Ende des bezahlten Monats)`,
confirmationLabel: "Bestätigung an",
confirmationValue: (email) => email,
submit: "Jetzt kündigen",
back: "Abbrechen",
},
publicPage: {
metaTitle: "Verträge hier kündigen",
title: "Verträge hier kündigen",
intro:
"Hier kündigst du dein Abo der Bienenpatenschaft ohne Anmeldung. Die Kündigung wirkt zum Ende des bezahlten Monats. Du bekommst sofort eine Eingangsbestätigung per Mail.",
accountHint: "Mit Kundenkonto geht es auch direkt dort: Konto → Patenschaft → Abo.",
nameLabel: "Vor- und Nachname *",
emailLabel: "E-Mail-Adresse *",
emailHint: "Die Adresse deines Kundenkontos – dorthin geht die Bestätigung.",
referenceLabel: "Vertragsnummer *",
referenceHint: "Die Patenschaftsnummer von der Urkunde (WH-P-…) oder die Vertragsnummer aus deinem Konto.",
messageLabel: "Nachricht (optional)",
requiredNote: "* Pflichtangaben",
startButton: "Verträge hier kündigen",
reviewHeading: "Kündigung bestätigen",
reviewIntro: "Bitte prüfe deine Angaben. Mit dem Klick auf „Jetzt kündigen“ ist die Kündigung erklärt.",
confirmButton: "Jetzt kündigen",
editButton: "Angaben ändern",
submitting: "Wird gesendet …",
successHeading: "Deine Kündigung ist eingegangen",
receivedAtLabel: "Eingegangen am",
successMailSent: "Wir haben dir eine Eingangsbestätigung per Mail geschickt.",
successMailFailed:
"Die Bestätigungsmail konnte gerade nicht versendet werden. Deine Kündigung ist trotzdem eingegangen; bitte bewahre diese Seite als Nachweis auf.",
errorMessage: "Deine Kündigung konnte nicht gesendet werden. Bitte versuche es erneut oder schreib uns eine Mail.",
rateLimited: "Zu viele Anfragen. Bitte versuche es später erneut oder schreib uns eine Mail.",
invalid: "Bitte überprüfe deine Angaben.",
},
};
const en: AboStrings = {
footerLink: "Cancel contracts here",
status: {
AUSSTEHEND: "Sign-up pending",
AKTIV: "Active",
IM_VERZUG: "Payment outstanding",
GEKUENDIGT: "Cancelled",
BEENDET: "Ended",
},
thanks: {
metaTitle: "Subscription confirmed",
activeTitle: "Thank you for subscribing!",
activeText: "Your subscription is active – the certificate appears in your account after the first payment.",
processingTitle: "Your payment is being confirmed",
processingText:
"Your subscription is set up. With direct debit, the bank takes a few days to confirm – the certificate appears in your account after the first payment.",
notCompletedTitle: "The subscription was not completed",
notCompletedText: "Nothing has been charged. You can try again at any time.",
toSubscription: "View subscription",
toAccount: "My sponsorship",
backToOffer: "Back to bee sponsorship",
},
detail: {
metaTitle: "My subscription",
back: "Back to sponsorship",
title: (offer) => `Subscription “${offer}”`,
beeLine: (bee) => `Sponsored bee ${bee}`,
contractNumber: "Contract number",
statusLabel: "Status",
monthlyPrice: "Monthly amount",
nextCharge: "Next charge",
endsOn: "Ends on",
paidMonths: "Paid months",
progressTitle: "Until the next honey delivery",
progress: (paid, required) => `${paid} of ${required} months paid`,
progressHarvest: (paid, required) => `${paid} of ${required} months paid – delivered in the next harvest window`,
sepaHoldNote:
"With direct debit, the first payment only counts towards a delivery after eight weeks, because the bank can reverse it until then.",
deliveriesTitle: "Honey deliveries",
deliveriesEmpty: "No delivery yet – the honey comes as soon as enough months are paid.",
deliveryPrepared: (date, jars) => `${date}: ${jars} jars, being prepared or on the way`,
deliveryDeferred: (date) => `${date}: on hold – we will get in touch`,
delinquentNote: "A payment is outstanding. Honey deliveries are paused until it is settled.",
paymentTitle: "Payment method",
paymentText: "You can change your payment method securely with our payment provider Stripe.",
paymentButton: "Change payment method",
paymentUnavailable: "Changing the payment method is not possible right now. Please try again later.",
cancelTitle: "Cancel",
cancelText: "The subscription can be cancelled monthly and ends at the end of the paid month.",
cancelButton: "Cancel contracts here",
cancelledNote: (date) => `Cancelled – the subscription ends on ${date}. Nothing will be charged afterwards.`,
endedNote: (date) => `The subscription ended on ${date}. Your certificate, medal and downloads remain in your account.`,
certificateLink: "View certificate",
cancelDone: (date) => `We have received your cancellation. The subscription ends on ${date}. A confirmation is on its way by email.`,
cancelFailed: "The cancellation could not be processed right now. Please try again or use the “Cancel contracts here” page.",
},
confirm: {
metaTitle: "Confirm cancellation",
title: "Confirm cancellation",
intro: "Please check your details. Clicking “Cancel now” submits your cancellation.",
kindLabel: "Type of cancellation",
kindValue: "Ordinary cancellation at the earliest possible date",
contractLabel: "Contract",
effectiveLabel: "Effective on",
effectiveValue: (date) => `${date} (end of the paid month)`,
confirmationLabel: "Confirmation to",
confirmationValue: (email) => email,
submit: "Cancel now",
back: "Back",
},
publicPage: {
metaTitle: "Cancel contracts here",
title: "Cancel contracts here",
intro:
"Cancel your bee sponsorship subscription here without logging in. The cancellation takes effect at the end of the paid month. You receive a confirmation of receipt by email right away.",
accountHint: "With a customer account you can also do this directly there: Account → Sponsorship → Subscription.",
nameLabel: "First and last name *",
emailLabel: "Email address *",
emailHint: "The address of your customer account – the confirmation goes there.",
referenceLabel: "Contract number *",
referenceHint: "The sponsorship number from the certificate (WH-P-…) or the contract number from your account.",
messageLabel: "Message (optional)",
requiredNote: "* Required",
startButton: "Cancel contracts here",
reviewHeading: "Confirm cancellation",
reviewIntro: "Please check your details. Clicking “Cancel now” submits your cancellation.",
confirmButton: "Cancel now",
editButton: "Edit details",
submitting: "Sending …",
successHeading: "We have received your cancellation",
receivedAtLabel: "Received on",
successMailSent: "We have sent you a confirmation of receipt by email.",
successMailFailed:
"The confirmation email could not be sent right now. Your cancellation has still been received; please keep this page as proof.",
errorMessage: "Your cancellation could not be sent. Please try again or email us.",
rateLimited: "Too many requests. Please try again later or email us.",
invalid: "Please check your details.",
},
};
export function getAboStrings(locale: Locale): AboStrings {
return locale === "en" ? en : de;
}