Files
Wabenhain/scripts/render-certificate-samples.ts
T
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

142 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Rendert Beispiel-Urkunden fuer die Sichtpruefung (Spec "Visuelle
* Kontrolle"): je Vorlage und Datensatz ein PNG mit 300 dpi (2480 px breit)
* und die Druck-PDF, dazu eine Vorschau mit Wasserzeichen.
*
* Aufruf: pnpm tsx scripts/render-certificate-samples.ts <ausgabeordner> [--templates=a@1,b@1] [--samples=standard,lang]
*
* Schreibt nur in den angegebenen Ordner; keine Datenbank, keine Uploads.
*/
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { layoutCertificate } from "../src/lib/certificate/layout";
import { renderCertificateImage } from "../src/lib/certificate/render-image";
import { certificatePdfTitle, renderCertificatePdf } from "../src/lib/certificate/render-pdf";
import { fixedTermFor, type CertificateTerm, type Personalization } from "../src/lib/certificate/schema";
import { listTemplateVersions } from "../src/lib/certificate/templates";
import { composeCertificateText } from "../src/lib/certificate/text";
import type { Locale } from "../src/i18n/config";
interface Sample {
personalization: Personalization;
locale: Locale;
term: CertificateTerm;
number: string;
}
const START = "2026-12-24";
const NUMBER = "WH-P-2026-00042";
function sample(personalization: Personalization, extra: Partial<Omit<Sample, "personalization">> = {}): Sample {
return { personalization, locale: "de", term: fixedTermFor(personalization.startDate), number: NUMBER, ...extra };
}
const MAX_DEDICATION =
"Für die Frau, die jeden Garten zum Summen bringt.\nMöge diese kleine Biene dich an all die sonnigen Tage erinnern, die wir gemeinsam zwischen Lavendel und Linden verbracht haben.\n\nIn Liebe und Dankbarkeit,\nAnna, Ben und Carla";
const SAMPLES: Record<string, Sample> = {
// Identische Beispieldaten fuer den Vergleich der drei Varianten.
standard: sample({
beeName: "Summsi",
recipientName: "Marie-Luise Schmidt",
giverNames: "Anna und Ben",
dedication: "Für die beste Gärtnerin der Welt –\nmögen deine Blüten immer summen.",
occasion: "WEIHNACHTEN",
startDate: START,
}),
kurz: sample({ beeName: "Bo", recipientName: "Li", occasion: "SELBST", startDate: START }),
lang: sample({
beeName: "Hildegard-Wilhelmine",
recipientName: "Marie-Luise Schmidt-Habermann von Hohenlinden",
giverNames: "Anna, Ben, Carla und Familie Özdemir aus dem Nachbarhaus",
dedication: MAX_DEDICATION,
occasion: "GEBURTSTAG",
startDate: START,
}),
// An den Eingabelimits (CERTIFICATE_LIMITS): 23/24, 57/60, 80/80, 277/280 Zeichen, 5 Umbrueche.
maximal: sample(
{
beeName: "Wilhelmine-Mümmelwunder",
recipientName: "Marie-Luise Schmidt-Habermann von Hohenlinden-Württemberg",
giverNames: "Anna, Ben, Carla, Dominik, Emilia, Friedrich und die ganze Familie Özdemir-Lange",
dedication: [
"Möge diese kleine Biene dich an sonnige Tage im Garten erinnern,",
"an Lindenblüten, Lavendel und an den Duft von frischem Honigbrot,",
"an lange Abende auf der Bank vor dem Haus und an alle Gespräche.",
"Danke für alles, was du für uns getan hast, jeden einzelnen Tag!",
"Alles Liebe",
"Anna",
].join("\n"),
occasion: "DANKESCHOEN",
startDate: START,
},
{ locale: "de" }
),
sonderzeichen: sample(
{
beeName: "Zoë",
recipientName: "Jürgen Weiß & Ñúñez O’Brien",
giverNames: "Anna, Ben, Carla und Familie Özdemir",
occasion: "DANKESCHOEN",
startDate: START,
},
{ term: { kind: "OPEN", start: START } }
),
englisch: sample(
{
beeName: "Honeybell",
recipientName: "Charlotte Ashworth-Blake",
giverNames: "Oliver and Grace",
dedication: "For the kindest gardener we know —\nmay your days always hum with sunshine.",
occasion: "GEBURTSTAG",
startDate: START,
},
{ locale: "en" }
),
beendet: sample(
{ beeName: "Lindenblüte", recipientName: "Familie Wagner", occasion: "GESCHENK", startDate: START },
{ term: { kind: "ENDED", start: START, end: "2027-05-31" } }
),
};
function parseList(flag: string): string[] | null {
const arg = process.argv.find((value) => value.startsWith(`--${flag}=`));
return arg ? arg.slice(flag.length + 3).split(",").filter(Boolean) : null;
}
async function main(): Promise<void> {
const outDir = process.argv[2];
if (!outDir || outDir.startsWith("--")) {
throw new Error("Aufruf: pnpm tsx scripts/render-certificate-samples.ts <ausgabeordner> [--templates=…] [--samples=…]");
}
await mkdir(outDir, { recursive: true });
const templates = parseList("templates") ?? listTemplateVersions();
const samples = parseList("samples") ?? Object.keys(SAMPLES);
for (const version of templates) {
for (const name of samples) {
const data = SAMPLES[name];
if (!data) throw new Error(`Unbekannter Datensatz "${name}".`);
const text = composeCertificateText(data);
const layout = layoutCertificate(text, version);
const base = path.join(outDir, `${version.replace("@", "-v")}_${name}`);
const [png, pdf] = await Promise.all([
renderCertificateImage(layout, { widthPx: 2480, format: "png" }),
renderCertificatePdf(layout, { title: certificatePdfTitle(text), creationDate: new Date(`${START}T00:00:00Z`) }),
]);
await writeFile(`${base}.png`, png);
await writeFile(`${base}.pdf`, pdf);
if (name === "standard") {
const preview = await renderCertificateImage(layout, { widthPx: 1240, format: "png", watermark: data.locale });
await writeFile(`${base}_vorschau.png`, preview);
}
process.stdout.write(`${base}.png / .pdf\n`);
}
}
}
main().catch((error: unknown) => {
process.stderr.write(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
process.exit(1);
});