/** * Generierung auf Basis eines Directus-Auftrags. * * Ablauf: Directus meldet "Auftrag X ist bezahlt" → dieser Endpunkt holt den * Auftrag samt Empfängerzeilen aus Directus, baut daraus die Dokumentliste für * die bestehende Pipeline (runGeneration) und schreibt Ergebnis + Status zurück. * * Antwortet sofort mit 202 – die Generierung läuft im Hintergrund weiter, weil * sie je nach Menge deutlich länger dauern kann als ein HTTP-Timeout erlaubt. */ const { runGeneration } = require('./order-controller'); const directus = require('../../services/directus-service'); const { countLetterPages } = require('../../lib/svg-generator'); const { getFormatKind } = require('../../lib/page-layout'); const { replacePlaceholders } = require('../../services/placeholder-service'); // Directus-Formatschlüssel → Backend-Formate const LETTER_FORMAT = { a4: 'a4', a6_hoch: 'a6p', a6_quer: 'a6l' }; const ENVELOPE_FORMAT = { c6: 'c6', dinlang: 'din_lang' }; /** Baut den Adressblock für einen Umschlag aus einer Empfängerzeile. */ function addressText(entry) { if (entry.free_text) return String(entry.free_text).trim(); const name = [entry.first_name, entry.last_name].filter(Boolean).join(' '); const street = [entry.street, entry.house_no].filter(Boolean).join(' '); const city = [entry.zip, entry.city].filter(Boolean).join(' '); const isForeign = entry.country && !/^(de|deutschland|germany)$/i.test(String(entry.country).trim()); return [name, street, city, isForeign ? entry.country : null] .filter(Boolean).join('\n'); } /** Platzhalterwerte einer Zeile (strukturierte Felder + freie Platzhalter). */ function placeholdersOf(entry) { // Die 5 Adresszeilen (5-Zeilen-Modus) auch als [[Zeile1]]..[[Zeile5]] verfügbar. const zeilen = String(entry.free_text || '').split('\n'); return { Anrede: entry.salutation || '', Vorname: entry.first_name || '', Nachname: entry.last_name || '', Strasse: [entry.street, entry.house_no].filter(Boolean).join(' '), PLZ: entry.zip || '', Ort: entry.city || '', Land: entry.country || '', Briefnummer: entry.letter_number ?? '', Zeile1: zeilen[0] || '', Zeile2: zeilen[1] || '', Zeile3: zeilen[2] || '', Zeile4: zeilen[3] || '', Zeile5: zeilen[4] || '', ...(entry.placeholders && typeof entry.placeholders === 'object' ? entry.placeholders : {}), }; } /** Übersetzt einen Directus-Auftrag in die Dokumentliste für runGeneration. */ function buildDocuments(order, entries) { const font = order.font || 'tilda'; // Eigene Format-Schlüssel (freie Formate) unverändert durchreichen. const letterFormat = LETTER_FORMAT[order.format?.key] || order.format?.key || 'a4'; const envFormat = ENVELOPE_FORMAT[order.envelope_format] || 'c6'; const wantsEnvelope = !!order.needs_envelope && order.envelope_labeling !== 'keine'; const envelopeType = order.envelope_labeling === 'freitext' ? 'custom' : 'recipient'; // Datei-Produkte (z. B. Unterschriftenservice): das „Schriftstück" ist das vom // Kunden hochgeladene Dokument – es wird NICHT generiert. Kuverts hingegen schon. const istDatei = order.product?.input_mode === 'datei'; // Postkarte: Text links, Absender+Empfänger rechts, Briefmarkenfeld frei – ein SVG // je Karte (kein separater Umschlag). Erkennung über die render_format-Art. const istPostkarte = getFormatKind(letterFormat) === 'postkarte'; // Kuvert-only-Produkt: es wird KEIN Schriftstück generiert, nur die (adressierten) // Kuverts. Etwaige hochgeladene PDF-Briefe liegen als source_files am Auftrag. const istKuvertOnly = order.product?.type === 'envelope'; // Natürliche Handschrift-Variation (Default an), am Auftrag gespeichert. const realistisch = order.realistic !== false; // Relative Schriftgröße (getrennt Brief/Kuvert), aus dem Auftrag. 1 = Standard. const fontScaleLetter = Number(order.font_scale_letter) || 1; const fontScaleEnv = Number(order.font_scale_envelope) || 1; // Signatur(en) auf dem Brief (manuelle Aufträge): SVG je [[signaturN]] + Feinjustierung. const sigManual = order.signatures_manual && typeof order.signatures_manual === 'object' ? order.signatures_manual : null; const sigMap = sigManual?.signatures || {}; const sigOff = sigManual?.offsets || {}; // Mehrseitiges Schriftstück (Auto-Fluss über N Seiten) – nur wenn am Auftrag gesetzt. const istMehrseitig = !!order.multi_page; const documents = []; entries.forEach((entry, i) => { const placeholders = placeholdersOf(entry); // Dateinummer = Briefnummer (1-basiert), damit letter_001.svg zu // Briefnummer 1 gehört. Vorschau nummeriert identisch (siehe skrift-orders). const nr = Number(entry.letter_number) || (i + 1); // Abweichender Text je Schriftstück hat Vorrang; sonst Standardtext. const briefText = (entry.text && String(entry.text).trim()) ? entry.text : (order.text_template || ''); // Postkarte: EIN Dokument mit Text + Absender + Empfänger (über Marker getrennt). // Ohne Brieftext KEINE Karte erzeugen – dann wird (falls gewünscht) nur das Kuvert erzeugt. if (istPostkarte && String(briefText).trim()) { const recip = addressText(entry); const sender = order.envelope_sender ? String(order.envelope_sender).trim() : ''; let combined = String(briefText || ''); if (sender) combined += `[[pk_abs]]${sender}`; if (recip) combined += `[[pk_adr]]${recip}`; documents.push({ index: nr, type: 'postcard', text: combined, format: letterFormat, font, placeholders, fontScale: fontScaleLetter, fontScaleEnv, realisticHandwriting: realistisch, }); return; } // Kein Schriftstück erzeugen, wenn kein Text hinterlegt ist (z. B. „nur Kuverts drucken"). if (!istDatei && !istKuvertOnly && String(briefText).trim()) { documents.push({ index: nr, type: 'letter', text: briefText, format: letterFormat, font, placeholders, fontScale: fontScaleLetter, signatures: sigMap, signatureOffsets: sigOff, multiPage: istMehrseitig, realisticHandwriting: realistisch, }); } if (wantsEnvelope) { documents.push({ index: nr, type: 'envelope', envelopeType, text: envelopeType === 'recipient' ? addressText(entry) : (order.envelope_text || order.text_template || ''), format: envFormat, font, placeholders, fontScale: fontScaleEnv, realisticHandwriting: realistisch, }); } }); return { documents, letterFormat, envFormat, wantsEnvelope }; } /** * POST /api/order/from-directus * Body: { orderId } */ async function generateFromDirectus(req, res, next) { const { orderId } = req.body || {}; if (!orderId) { return res.status(400).json({ error: 'Invalid request', message: 'orderId is required' }); } let order; try { const loaded = await directus.fetchOrder(orderId); order = loaded.order; if (!loaded.entries.length) { return res.status(400).json({ error: 'Invalid order', message: `Auftrag ${order.order_number} hat keine Empfängerzeilen.` }); } // Sofort quittieren – der Rest läuft im Hintergrund. res.status(202).json({ accepted: true, orderId, orderNumber: order.order_number, documents: loaded.entries.length * (order.needs_envelope ? 2 : 1), }); const { documents, letterFormat, envFormat, wantsEnvelope } = buildDocuments(order, loaded.entries); // Nichts zu generieren (z. B. Datei-Produkt ohne Kuvert) → sauber überspringen. if (!documents.length) { console.log(`[Directus] ${order.order_number}: nichts zu generieren (Datei-Produkt ohne Kuvert).`); return; } console.log(`[Directus] Generiere ${order.order_number}: ${documents.length} Dokumente`); const result = await runGeneration({ orderNumber: order.order_number, letters: documents, metadata: { letterFormat, envelopeFormat: wantsEnvelope ? envFormat : null, envelopeBeschriftung: wantsEnvelope ? order.envelope_labeling : null, directusOrderId: orderId, customerEmail: order.customer_email, // Vorschau-Session → gezeigte Dokumente 1:1 übernehmen. previewSession: order.preview_session || null, // Eingegebener Brieftext → als brieftext.txt im Ausgabeordner ablegen. rawText: order.text_template || null, // Absender fürs Kuvert (manuelle Aufträge): oben links, Empfänger unten rechts. envelopeSender: order.envelope_sender || null, envelopeSenderScale: Number(order.envelope_sender_scale) || 1, }, }); await directus.updateOrder(orderId, { status: 'in_queue', artifact_path: result.outputPath, }); await directus.addStatusHistory(orderId, 'in_queue', `${result.letters.length} Schriftstücke, ${result.envelopes.length} Umschläge generiert.`); console.log(`[Directus] ${order.order_number} fertig → in_queue`); } catch (err) { console.error('[Directus] Generierung fehlgeschlagen:', err.message); // Fehler in Directus sichtbar machen, statt still zu scheitern. if (order?.id || orderId) { try { await directus.addStatusHistory(orderId, 'fehler', `Generierung fehlgeschlagen: ${err.message}`); } catch (e) { console.error('[Directus] Konnte Fehler nicht zurückschreiben:', e.message); } } // Antwort wurde ggf. schon gesendet (202) – dann nur noch loggen. if (!res.headersSent) { if (err.status) return res.status(err.status).json({ error: 'Generation failed', message: err.message }); return next(err); } } } /** * POST /api/order/pagecount * Body: { text, format, fontScale, entries: [{ placeholders }] } * Liefert je Dokument die Seitenzahl (nur Umbruch-Zählung, kein Scriptalizer/SVG). */ function pageCounts(req, res) { const { text, format, fontScale, entries } = req.body || {}; if (!text || !String(text).trim()) { return res.status(400).json({ error: 'Invalid request', message: 'text is required' }); } const fmt = format || 'a4'; const scale = Number(fontScale) || 1; const list = Array.isArray(entries) && entries.length ? entries : [{}]; const counts = list.map((e, i) => { const ph = { ...(e && typeof e.placeholders === 'object' ? e.placeholders : {}), Briefnummer: String(i + 1) }; let pages = 1; try { pages = countLetterPages(replacePlaceholders(String(text), ph), fmt, { fontScale: scale }); } catch { pages = 1; } return { index: i + 1, pages }; }); return res.json({ counts }); } module.exports = { generateFromDirectus, pageCounts };