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]>
115 lines
4.6 KiB
TypeScript
115 lines
4.6 KiB
TypeScript
import type { Locale } from "@/i18n/config";
|
|
import { CertificateLayoutError, type CertificateField, type CertificateLayout } from "@/lib/certificate/elements";
|
|
import { FONT_KEYS, findUnsupportedCharacters } from "@/lib/certificate/fonts";
|
|
import { fixedTermFor, type CertificateTerm, type Personalization } from "@/lib/certificate/schema";
|
|
import { composeCertificateText, type CertificateText } from "@/lib/certificate/text";
|
|
import { CURRENT_TEMPLATE_VERSION, getTemplate } from "@/lib/certificate/templates";
|
|
|
|
/**
|
|
* Einstieg in die Layout-Berechnung (Spec "Ein Layout, mehrere Ausgaben"):
|
|
*
|
|
* composeCertificateText() → aufgeloester Text
|
|
* layoutCertificate() → positionierte Elemente (diese Datei)
|
|
* renderCertificatePdf() / renderCertificateSvg() / renderCertificateImage()
|
|
*
|
|
* Reine Funktionen ohne Datenbank: dieselbe Eingabe ergibt dasselbe Layout,
|
|
* auf dem Server fuer Vorschau, Textur und PDF gleichermassen.
|
|
*/
|
|
|
|
export {
|
|
CertificateLayoutError,
|
|
PAGE_HEIGHT,
|
|
PAGE_WIDTH,
|
|
SAFE_CONTENT,
|
|
SAFE_FRAME,
|
|
MIN_TEXT_SIZE,
|
|
MIN_BODY_SIZE,
|
|
textStartX,
|
|
elementBounds,
|
|
type CertificateField,
|
|
type CertificateLayout,
|
|
type LayoutElement,
|
|
type TextElement,
|
|
type PathElement,
|
|
type ImageElement,
|
|
type TextRole,
|
|
} from "@/lib/certificate/elements";
|
|
export { CURRENT_TEMPLATE_VERSION, getTemplate } from "@/lib/certificate/templates";
|
|
|
|
export function layoutCertificate(text: CertificateText, templateVersion: string = CURRENT_TEMPLATE_VERSION): CertificateLayout {
|
|
return getTemplate(templateVersion).layout(text);
|
|
}
|
|
|
|
/** Alle Textzeilen in Zeichenreihenfolge als "rolle: text" — Grundlage fuer den Zeilenvergleich der Ausgaben. */
|
|
export function layoutLines(layout: CertificateLayout): string[] {
|
|
return layout.elements.flatMap((el) => (el.kind === "text" ? [`${el.role}: ${el.text}`] : []));
|
|
}
|
|
|
|
export type FitCheck =
|
|
| { ok: true; layout: CertificateLayout }
|
|
| { ok: false; field: CertificateField; reason: "glyphs"; characters: string[]; message: string }
|
|
| { ok: false; field: CertificateField; reason: "fit"; message: string };
|
|
|
|
const FIELD_LABELS: Record<CertificateField, string> = {
|
|
beeName: "Der Name der Patenbiene",
|
|
recipientName: "Der Name auf der Urkunde",
|
|
giverNames: "Die Angabe der Schenkenden",
|
|
dedication: "Die Widmung",
|
|
occasion: "Die Anlasszeile",
|
|
layout: "Der Urkundentext",
|
|
};
|
|
|
|
/** Deutsche Meldung fuer Formular und Checkout; die Felder field/reason erlauben eine eigene Uebersetzung. */
|
|
function fitMessage(field: CertificateField, reason: "glyphs" | "fit", characters: string[] = []): string {
|
|
if (reason === "glyphs") {
|
|
return `${FIELD_LABELS[field]} enthält Zeichen, die auf der Urkunde nicht gedruckt werden können: ${characters.join(" ")}`;
|
|
}
|
|
return `${FIELD_LABELS[field]} passt so nicht auf die Urkunde. Bitte kürzen oder an einer Leerstelle teilen.`;
|
|
}
|
|
|
|
// Breiteste plausible Nummer fuer die Pruefung vor der Bestellung (die echte
|
|
// Nummer entsteht erst nach der Zahlung).
|
|
const SAMPLE_NUMBER = "WH-P-2026-00000";
|
|
|
|
const CUSTOMER_FIELDS: ReadonlyArray<[CertificateField, keyof Personalization]> = [
|
|
["beeName", "beeName"],
|
|
["recipientName", "recipientName"],
|
|
["giverNames", "giverNames"],
|
|
["dedication", "dedication"],
|
|
];
|
|
|
|
/**
|
|
* Serverseitige Pruefung einer bereits per zod validierten Personalisierung:
|
|
* 1. jedes Zeichen der Kundenfelder hat in allen Urkundenschriften eine
|
|
* Glyphe (bewusst alle Schnitte, nicht nur die der aktuellen Vorlage —
|
|
* so bleibt eine Bestellung auch mit einer spaeteren Vorlage druckbar);
|
|
* 2. das Layout passt in die kleinste Stufe.
|
|
* Vorschau-API und Checkout nutzen dieselbe Funktion.
|
|
*/
|
|
export function checkCertificateFits(
|
|
personalization: Personalization,
|
|
locale: Locale,
|
|
opts?: { term?: CertificateTerm; templateVersion?: string; number?: string }
|
|
): FitCheck {
|
|
for (const [field, key] of CUSTOMER_FIELDS) {
|
|
const value = personalization[key];
|
|
if (!value) continue;
|
|
const characters = findUnsupportedCharacters(value, FONT_KEYS);
|
|
if (characters.length > 0) return { ok: false, field, reason: "glyphs", characters, message: fitMessage(field, "glyphs", characters) };
|
|
}
|
|
const text = composeCertificateText({
|
|
personalization,
|
|
locale,
|
|
term: opts?.term ?? fixedTermFor(personalization.startDate),
|
|
number: opts?.number ?? SAMPLE_NUMBER,
|
|
});
|
|
try {
|
|
return { ok: true, layout: layoutCertificate(text, opts?.templateVersion) };
|
|
} catch (error: unknown) {
|
|
if (error instanceof CertificateLayoutError) {
|
|
return { ok: false, field: error.field, reason: "fit", message: fitMessage(error.field, "fit") };
|
|
}
|
|
throw error;
|
|
}
|
|
}
|