Public-API /v1/orders: Zeichen-Warnung + Ablehnung bei zu langem Text (A6)

Additiv, ohne Aenderung der bestehenden Antwortstruktur:
- Nicht erlaubte Zeichen werden wie bisher entfernt; zusaetzlich meldet
  die Antwort ein optionales "warnings":[{code:"unsupported_characters",
  characters:[...]}] (Brieftext + Empfaengerfelder).
- Nur EINSEITIGE Formate (A6 hoch/quer): passt der Brieftext nicht auf
  eine Karte, wird der Auftrag mit HTTP 422 abgelehnt (Backend-Umbruch-
  zaehlung /api/order/pagecount, kein Scriptalizer). A4 fliesst mehrseitig
  und wird nie abgelehnt. Backend nicht erreichbar => fail-open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-25 08:52:53 +02:00
parent 541483217d
commit 2438ecb491

View File

@@ -328,6 +328,53 @@ const handler = (router, { services, getSchema, logger }) => {
const envFormat = envMode === 'none' ? null : (fmtKey === 'a4' ? 'dinlang' : 'c6');
const hatDateien = files.length > 0;
// ── Zeichen prüfen (WARNEN, nicht ablehnen) ──────────────────────────────
// Nicht erlaubte Zeichen werden wie bisher entfernt; der Aufrufer bekommt
// zusätzlich eine Warnung, welche Zeichen entfernt wurden.
const unerlaubt = new Set();
const pruefeZeichen = (s) => {
for (const ch of new Set(String(s || ''))) if (clean(ch) === '') unerlaubt.add(ch);
};
if (!istDatei && !istKuvertOnly) pruefeZeichen(letter.text);
for (const e of recipients) {
for (const k of ['salutation', 'first_name', 'last_name', 'street', 'house_no', 'zip', 'city', 'country', 'free_text']) {
pruefeZeichen(e && e[k]);
}
}
const warnings = [];
if (unerlaubt.size) {
warnings.push({ code: 'unsupported_characters', characters: [...unerlaubt],
message: 'Some characters are not supported and were removed. See the allowed character set in the documentation.' });
}
// ── Textlänge prüfen (ABLEHNEN) – NUR einseitige Formate (A6) ────────────
// A4 fließt automatisch über mehrere Seiten und kann nicht „zu lang" sein;
// A6-Karten sind einseitig → passt der Text nicht auf eine Seite, wird der
// Auftrag abgelehnt (die reine Umbruch-Zählung des Backends, kein Scriptalizer).
if (!istDatei && !istKuvertOnly && (fmtKey === 'a6_hoch' || fmtKey === 'a6_quer')) {
const layoutKey = fmtKey === 'a6_hoch' ? 'a6p' : 'a6l';
try {
const r = await fetch(`${BACKEND_URL}/api/order/pagecount`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN },
body: JSON.stringify({ text: clean(String(letter.text || '')), format: layoutKey }),
signal: AbortSignal.timeout(8000),
});
if (r.ok) {
const d = await r.json();
const pages = Math.max(1, ...((d.counts || []).map((c) => Number(c.pages) || 1)));
if (pages > 1) {
return fehler(res, 422, `letter.text is too long for a single ${String(body.document_format)} card (needs ${pages} pages). Please shorten it.`);
}
} else {
logger.warn(`[skrift-api] Längenprüfung: pagecount HTTP ${r.status}`);
}
} catch (e) {
// Backend nicht erreichbar → Auftrag NICHT blockieren (fail-open).
logger.warn(`[skrift-api] Längenprüfung übersprungen: ${e.message}`);
}
}
const font = ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda';
// Versandart nur Englisch: "single" (direkt an Empfänger) / "bulk" (Sammelversand an dich).
const shippingType = String(body.shipping_type || '').toLowerCase() === 'single' ? 'einzeln' : 'sammel';
@@ -409,7 +456,7 @@ const handler = (router, { services, getSchema, logger }) => {
body: JSON.stringify({ orderId }),
}).catch((e) => logger.warn(`[skrift-api] Generierung nicht gestartet: ${e.message}`));
return res.status(201).json({ order_number: orderNumber, status: 'pending' });
return res.status(201).json({ order_number: orderNumber, status: 'pending', ...(warnings.length ? { warnings } : {}) });
} catch (err) {
logger.error(`[skrift-api] orders: ${err.stack || err.message}`);
return fehler(res, 500, 'Order could not be created.');