Unterschriftenservice, Extern-Modus, Adressmodell, Alt-Mail, Team-Mails
skrift-app (Konfigurator): - Datei-Produkte (Unterschriftenservice): Mehrfach-Upload (PDF/Word/Excel), komplett preisfrei (keine Preisleiste/Aufpreise/Kostenübersicht/Zahlung) - Extern-/Etsy-Modus (?extern=1&versand&kuvert&beschriftung&menge): vom Betreiber vorkonfiguriert, Kunde füllt nur Empfänger/Text/Rechnungsdaten/Übersicht, keine Preise/Zahlung; Menge greift immer (auch >25 / ohne Empfänger); eigener Danke-Text - Adressmodell: zwei feste Adressen (Rechnung/Lieferung) ohne Namen statt Dropdown; Auto-Vorbefüllung; Lieferadresse nur bei Sammelversand; abweichende Lieferadresse per Default aus; Pflichtfelder erzwungen - Platzhalter-Tabelle immer im Text-Schritt (auch Sammelversand ohne Kuvert) + Excel; Tabellen-Eingabe nur bis 25, „Ab 26"-Hinweis entfernt - Stammdaten: zwei Adressblöcke + Erfolgsmeldung Directus/Backend: - Signatur-Endpunkte /signature-preview + /signature-render (Proxy zum signature-service, Marker + Konto-Offsets), Modul-Reiter „Unterschriften" (aus Auftrag oder manuell), Tabelle über volle Breite - Generierung läuft bei jeder Bestellung; Kuverts immer (Datei-Produkt: nur Kuvert, DIN Lang als Default); source_files am Auftrag - Produktions-Board: „Dateien"-Button (hochgeladene Originale, keine SVGs) - Alt-Mail-Login: Zweit-Mails melden im selben Konto an (skrift-auth) - Team-Mails schlank + konfigurierbar (email_settings.team_email) für Bestellung/Muster/Anfrage; keine Bestellbestätigung bei Datei-Produkten, separate Vorlagen unterschriften_eingang/extern_eingang - Rechnungsadresse wird ins Konto übernommen, wenn dort noch keine steht - Kunden-Policy: directus_files create/read (Upload); Bootstrap: users billing_address/shipping_address/alt_emails, orders.source_files/source=extern, signatures marker/offsets, unterschriftenservice input_mode=datei/pricing_mode=formel Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,7 +153,87 @@ async function paypalToken() {
|
||||
}
|
||||
|
||||
const handler = (router, { services, getSchema, logger }) => {
|
||||
const { ItemsService, MailService, UsersService, RolesService } = services;
|
||||
const { ItemsService, MailService, UsersService, RolesService, AssetsService } = services;
|
||||
|
||||
// Signatur-Plotter-Service (intern über das npm-Netz per container_name erreichbar).
|
||||
const SIGN_URL = (process.env.SKRIFT_SIGN_URL || 'http://skrift-signature-service:8000').replace(/\/$/, '');
|
||||
|
||||
/** Liest eine Directus-Datei (Upload) serverseitig als Buffer. */
|
||||
async function dateiBytes(schema, id) {
|
||||
const assets = new AssetsService({ schema, accountability: null });
|
||||
const { stream } = await assets.getAsset(id, {});
|
||||
const chunks = [];
|
||||
for await (const c of stream) chunks.push(c);
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
/** Baut die Multipart-Anfrage an den Signatur-Service (Vorschau/Render). */
|
||||
async function signatureForm(schema, pdfId, sigList) {
|
||||
const pdfBytes = await dateiBytes(schema, pdfId);
|
||||
const form = new FormData();
|
||||
form.append('pdf', new Blob([pdfBytes], { type: 'application/pdf' }), 'dokument.pdf');
|
||||
const jobs = [];
|
||||
for (let i = 0; i < sigList.length; i += 1) {
|
||||
const s = sigList[i] || {};
|
||||
if (!s.file_id) continue;
|
||||
const svgBytes = await dateiBytes(schema, s.file_id);
|
||||
const svgName = `sig${i + 1}.svg`;
|
||||
form.append('svgs', new Blob([svgBytes], { type: 'image/svg+xml' }), svgName);
|
||||
const marker = String(s.marker_key || `SIG${i + 1}`).replace(/§/g, '').trim();
|
||||
jobs.push({
|
||||
token: `§§${marker}§§`,
|
||||
svg_id: svgName,
|
||||
position: ['genau', 'ueber', 'unter'].includes(s.position) ? s.position : 'genau',
|
||||
breite_mm: Number(s.width_mm) > 0 ? Number(s.width_mm) : 45,
|
||||
versatz_x_mm: Number(s.offset_x_mm) || 0,
|
||||
versatz_y_mm: Number(s.offset_y_mm) || 0,
|
||||
});
|
||||
}
|
||||
form.append('job', JSON.stringify(jobs));
|
||||
return { form, jobCount: jobs.length };
|
||||
}
|
||||
|
||||
/** Gemeinsame Vorbereitung für /signature-preview und /signature-render. */
|
||||
async function signatureAufruf(req, res, pfad, contentType, key) {
|
||||
const acc = req.accountability;
|
||||
if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' });
|
||||
const body = req.body || {};
|
||||
// Entweder aus einem Auftrag (order) ODER manuell mit direkt hochgeladenem
|
||||
// Dokument (document_id) – eins von beiden muss vorhanden sein.
|
||||
if (!body.order && !body.document_id) return res.status(400).json({ error: 'order oder document_id erforderlich.' });
|
||||
try {
|
||||
const schema = await getSchema();
|
||||
const svc = (c) => new ItemsService(c, { schema, accountability: null });
|
||||
let ord = null;
|
||||
if (body.order) {
|
||||
ord = await svc('orders').readOne(body.order, { fields: ['id', 'order_number', 'source_file', 'source_files'] });
|
||||
if (!ord) return res.status(404).json({ error: 'Auftrag nicht gefunden.' });
|
||||
}
|
||||
const pdfId = body.document_id || (ord ? (ord.source_file || (Array.isArray(ord.source_files) ? ord.source_files[0] : null)) : null);
|
||||
if (!pdfId) return res.status(400).json({ error: 'Kein Dokument (Auftrag ohne Upload bzw. keine document_id).' });
|
||||
const sigList = Array.isArray(body.signatures) ? body.signatures : [];
|
||||
if (!sigList.length) return res.status(400).json({ error: 'Mindestens eine Signatur wählen.' });
|
||||
|
||||
const { form, jobCount } = await signatureForm(schema, pdfId, sigList);
|
||||
if (!jobCount) return res.status(400).json({ error: 'Keine gültige Signatur (SVG-Datei fehlt).' });
|
||||
|
||||
const r = await fetch(`${SIGN_URL}${pfad}`, { method: 'POST', body: form });
|
||||
if (!r.ok) {
|
||||
const t = await r.text();
|
||||
return res.status(502).json({ error: 'Signatur-Service-Fehler.', detail: `${r.status}: ${String(t).slice(0, 400)}` });
|
||||
}
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
return res.json({ [key]: buf.toString('base64'), content_type: contentType, order_number: ord?.order_number || 'manuell' });
|
||||
} catch (err) {
|
||||
logger.error(`[skrift-orders] ${pfad}: ${err.stack || err.message}`);
|
||||
return res.status(500).json({ error: 'Signatur-Verarbeitung fehlgeschlagen.' });
|
||||
}
|
||||
}
|
||||
|
||||
// Vorschau-PDF (Dokument mit eingefügten Unterschriften).
|
||||
router.post('/signature-preview', (req, res) => signatureAufruf(req, res, '/preview', 'application/pdf', 'pdf_base64'));
|
||||
// A4-Signatur-SVGs als ZIP (zum Plotten in der lokalen Produktion).
|
||||
router.post('/signature-render', (req, res) => signatureAufruf(req, res, '/render', 'application/zip', 'zip_base64'));
|
||||
|
||||
// Policy-konformes Zufallspasswort (Kunde meldet sich ohnehin passwortlos an).
|
||||
const zufallsPasswort = () =>
|
||||
@@ -189,7 +269,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
|
||||
const product = (await svc('products').readByQuery({
|
||||
filter: { key: { _eq: input.product } }, limit: 1,
|
||||
fields: ['id', 'key', 'name', 'type', 'pricing_mode', 'base_price', 'norm_qty', 'price_factor_a', 'price_factor_e', 'active'],
|
||||
fields: ['id', 'key', 'name', 'type', 'pricing_mode', 'base_price', 'norm_qty', 'price_factor_a', 'price_factor_e', 'input_mode', 'active'],
|
||||
}))?.[0];
|
||||
if (!product || !product.active) throw new Error('Unbekanntes oder inaktives Produkt.');
|
||||
if (product.pricing_mode === 'auf_anfrage')
|
||||
@@ -964,6 +1044,52 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schlanke interne Benachrichtigung (neue Bestellung/Muster/Anfrage). Empfänger
|
||||
* ist konfigurierbar (email_settings.team_email, Fallback SKRIFT_TEAM_EMAIL).
|
||||
* Leer → keine Team-Mail.
|
||||
*/
|
||||
async function teamMail(schema, subject, html) {
|
||||
try {
|
||||
let to = TEAM_EMAIL;
|
||||
try {
|
||||
const es = await new ItemsService('email_settings', { schema, accountability: null }).readSingleton({ fields: ['team_email'] });
|
||||
if (es && es.team_email !== undefined && es.team_email !== null) to = String(es.team_email).trim();
|
||||
} catch { /* Feld evtl. noch nicht gebootstrapt → Fallback */ }
|
||||
if (!to) return; // bewusst leer = keine Team-Mails
|
||||
const mail = new MailService({ schema, accountability: null });
|
||||
const von = await absender(schema);
|
||||
await mail.send({ ...(von ? { from: von } : {}), to, subject, html });
|
||||
} catch (e) {
|
||||
logger.warn(`[skrift-orders] Team-Mail nicht versendet: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine im Admin editierbare E-Mail-Vorlage (email_templates) an den
|
||||
* Kunden. Platzhalter {{feld}} werden aus `vars` gefüllt. Fehlt/deaktiviert die
|
||||
* Vorlage, passiert nichts (kein Fehler).
|
||||
*/
|
||||
async function sendeVorlage(schema, key, to, vars) {
|
||||
try {
|
||||
const tpl = (await new ItemsService('email_templates', { schema, accountability: null }).readByQuery({
|
||||
filter: { key: { _eq: key }, enabled: { _eq: true } }, limit: 1, fields: ['subject', 'body'],
|
||||
}))?.[0];
|
||||
if (!tpl) return;
|
||||
const fuelle = (t) => String(t || '').replace(/\{\{(\w+)\}\}/g, (_, k) => (vars[k] ?? ''));
|
||||
let signature = '';
|
||||
try {
|
||||
const es = await new ItemsService('email_settings', { schema, accountability: null }).readSingleton({ fields: ['signature'] });
|
||||
if (es?.signature) signature = es.signature;
|
||||
} catch { /* optional */ }
|
||||
const mail = new MailService({ schema, accountability: null });
|
||||
const von = await absender(schema);
|
||||
await mail.send({ ...(von ? { from: von } : {}), to, subject: fuelle(tpl.subject), html: fuelle(tpl.body) + signature });
|
||||
} catch (e) {
|
||||
logger.warn(`[skrift-orders] Vorlagen-Mail „${key}" nicht versendet: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Angebotsanfrage („Individuelles Angebot") ────────────────────────────
|
||||
router.post('/offer-request', async (req, res) => {
|
||||
const body = req.body || {};
|
||||
@@ -1004,25 +1130,11 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
customer: customer?.id ?? null,
|
||||
});
|
||||
|
||||
// Benachrichtigung – darf den Erfolg der Anfrage nicht gefährden.
|
||||
try {
|
||||
const mail = new MailService({ schema, accountability: null });
|
||||
const von = await absender(schema);
|
||||
// Angefragtes Produkt (z. B. Unterschriftenservice, Follow-ups) mit ausweisen.
|
||||
const produkt = body.product ? clean(String(body.product)) : null;
|
||||
await mail.send({
|
||||
...(von ? { from: von } : {}),
|
||||
to: TEAM_EMAIL,
|
||||
subject: produkt ? `Angebotsanfrage: ${produkt} – ${name}` : `Angebotsanfrage von ${name}`,
|
||||
html:
|
||||
`<p><strong>${name}</strong> (${email}${body.phone ? `, ${clean(String(body.phone))}` : ''})</p>` +
|
||||
`<p>Anfrage betrifft: <strong>${produkt || 'Individuelles Angebot'}</strong></p>` +
|
||||
`<p>Menge: ${Number.isFinite(quantity) ? quantity : 'k. A.'}</p>` +
|
||||
`<p style="white-space:pre-wrap">${clean(message)}</p>`,
|
||||
});
|
||||
} catch (mailErr) {
|
||||
logger.warn(`[skrift-orders] Angebots-Mail nicht versendet: ${mailErr.message}`);
|
||||
}
|
||||
// Schlanke interne Benachrichtigung.
|
||||
const produkt = body.product ? clean(String(body.product)) : null;
|
||||
await teamMail(schema, `Neue Angebotsanfrage: ${name}`,
|
||||
`<p>Neue Angebotsanfrage von <strong>${name}</strong> (${email}${body.phone ? `, ${clean(String(body.phone))}` : ''})`
|
||||
+ `${produkt ? ` · ${produkt}` : ''} – im Board unter „Neu".</p>`);
|
||||
|
||||
return res.json({ ok: true });
|
||||
} catch (err) {
|
||||
@@ -1070,22 +1182,9 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
customer: customer?.id ?? null,
|
||||
});
|
||||
|
||||
try {
|
||||
const mail = new MailService({ schema, accountability: null });
|
||||
const von = await absender(schema);
|
||||
await mail.send({
|
||||
...(von ? { from: von } : {}),
|
||||
to: TEAM_EMAIL,
|
||||
subject: `Musterbestellung: ${clean(String(body.product))} – ${body.name}`,
|
||||
html:
|
||||
`<p><strong>${clean(String(body.name))}</strong> (${email}${body.phone ? `, ${clean(String(body.phone))}` : ''})</p>` +
|
||||
`<p>Produkt: <strong>${clean(String(body.product))}</strong></p>` +
|
||||
`<p>${clean(String(body.street))}, ${clean(String(body.zip))} ${clean(String(body.city))}, ` +
|
||||
`${clean(String(body.country || ''))}</p>`,
|
||||
});
|
||||
} catch (mailErr) {
|
||||
logger.warn(`[skrift-orders] Muster-Mail nicht versendet: ${mailErr.message}`);
|
||||
}
|
||||
await teamMail(schema, `Neue Musteranfrage: ${clean(String(body.name))}`,
|
||||
`<p>Neue Musteranfrage von <strong>${clean(String(body.name))}</strong> (${email}${body.phone ? `, ${clean(String(body.phone))}` : ''})`
|
||||
+ ` · Produkt: ${clean(String(body.product))} – im Board unter „Neu".</p>`);
|
||||
|
||||
return res.json({ ok: true });
|
||||
} catch (err) {
|
||||
@@ -1171,7 +1270,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
},
|
||||
status: 'neu', customer: customer?.id ?? null,
|
||||
};
|
||||
subject = `Musteranfrage (Web): ${produkt || '—'} – ${name}`;
|
||||
subject = `Neue Musteranfrage: ${name}`;
|
||||
mailHtml =
|
||||
`<p><strong>${clean(name)}</strong> (${email}${phone ? `, ${phone}` : ''})</p>` +
|
||||
`<p>Interesse: <strong>${produkt || 'k. A.'}</strong></p>` +
|
||||
@@ -1185,7 +1284,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
type: 'angebot', name: clean(name), email, phone, product: null, message,
|
||||
status: 'neu', customer: customer?.id ?? null,
|
||||
};
|
||||
subject = `Angebotsanfrage (Web) von ${name}`;
|
||||
subject = `Neue Angebotsanfrage: ${name}`;
|
||||
mailHtml =
|
||||
`<p><strong>${clean(name)}</strong>${unternehmen ? ` · ${unternehmen}` : ''} (${email}${phone ? `, ${phone}` : ''})</p>` +
|
||||
`<p style="white-space:pre-wrap">${nachricht || '—'}</p>`;
|
||||
@@ -1194,14 +1293,8 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
await svc('offer_requests').createOne(record);
|
||||
logger.info(`[skrift-orders] inbound/${kind}: Anfrage von ${email} gespeichert.`);
|
||||
|
||||
// Interne Team-Benachrichtigung (Kunden-Autoresponder macht JetFormBuilder).
|
||||
try {
|
||||
const mail = new MailService({ schema, accountability: null });
|
||||
const von = await absender(schema);
|
||||
await mail.send({ ...(von ? { from: von } : {}), to: TEAM_EMAIL, subject, html: mailHtml });
|
||||
} catch (mailErr) {
|
||||
logger.warn(`[skrift-orders] Webhook-Mail nicht versendet: ${mailErr.message}`);
|
||||
}
|
||||
// Schlanke interne Benachrichtigung (Kunden-Autoresponder macht JetFormBuilder).
|
||||
await teamMail(schema, subject, mailHtml);
|
||||
|
||||
return res.json({ ok: true });
|
||||
} catch (err) {
|
||||
@@ -1250,8 +1343,12 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
if (body.format) {
|
||||
formatId = (await svc('formats').readByQuery({ filter: { key: { _eq: body.format } }, limit: 1, fields: ['id'] }))?.[0]?.id ?? null;
|
||||
}
|
||||
// Kuvertformat: explizite Angabe gewinnt; Datei-Produkte (Unterschriften)
|
||||
// immer DIN Lang; A6/Postkarten → C6; sonst Standard DIN Lang.
|
||||
const envelopeFormat = body.needs_envelope
|
||||
? (body.envelope_format || (String(body.format || '').startsWith('a4') ? 'dinlang' : 'c6'))
|
||||
? (body.envelope_format
|
||||
|| (quote.product?.input_mode === 'datei' ? 'dinlang'
|
||||
: (String(body.format || '').startsWith('a6') ? 'c6' : 'dinlang')))
|
||||
: null;
|
||||
|
||||
const istInland = (e) => !e.country || /^(de|deutschland|germany)$/i.test(String(e.country).trim());
|
||||
@@ -1344,7 +1441,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
// Kunde = Login-Konto (directus_users). Finden oder anlegen – per E-Mail.
|
||||
const users = new UsersService({ schema, accountability: null });
|
||||
let customer = (await users.readByQuery({
|
||||
filter: { email: { _eq: email } }, limit: 1, fields: ['id'],
|
||||
filter: { email: { _eq: email } }, limit: 1, fields: ['id', 'billing_address'],
|
||||
}))?.[0];
|
||||
if (!customer) {
|
||||
const rolesSvc = new RolesService({ schema, accountability: null });
|
||||
@@ -1362,9 +1459,25 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
company: clean(body.company) ?? null,
|
||||
phone: clean(body.phone) ?? null,
|
||||
});
|
||||
customer = { id };
|
||||
customer = { id, billing_address: null };
|
||||
}
|
||||
|
||||
// Rechnungsadresse automatisch ins Konto übernehmen, wenn dort noch KEINE
|
||||
// hinterlegt ist (bestehende Adresse wird nicht überschrieben).
|
||||
try {
|
||||
const b = body.billing && typeof body.billing === 'object' ? body.billing : null;
|
||||
const hatAdresse = customer.billing_address
|
||||
&& (customer.billing_address.street || customer.billing_address.city);
|
||||
if (b && (b.street || b.city) && !hatAdresse) {
|
||||
await users.updateOne(customer.id, {
|
||||
billing_address: {
|
||||
street: clean(b.street) || '', zip: clean(b.zip) || '',
|
||||
city: clean(b.city) || '', country: clean(b.country) || '',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) { logger.warn(`[skrift-orders] Konto-Adresse nicht übernommen: ${e.message}`); }
|
||||
|
||||
// Spezial-/Kontaktprodukte (z. B. Unterschriftenservice) sind nur
|
||||
// bestellbar, wenn sie für dieses Konto freigeschaltet wurden.
|
||||
if (quote.product.type === 'contact') {
|
||||
@@ -1385,9 +1498,9 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
}))?.[0]?.id ?? null;
|
||||
}
|
||||
|
||||
// Umschlagformat automatisch: A4 → DIN Lang, A6 → C6
|
||||
// Umschlagformat automatisch: A6/Postkarte → C6, sonst Standard DIN Lang.
|
||||
const envelopeFormat = body.needs_envelope
|
||||
? (String(body.format || '').startsWith('a4') ? 'dinlang' : 'c6')
|
||||
? (String(body.format || '').startsWith('a6') ? 'c6' : 'dinlang')
|
||||
: null;
|
||||
|
||||
// Empfänger nach Inland/Ausland aufteilen (für die Auftragsübersicht).
|
||||
@@ -1414,8 +1527,9 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
format: formatId,
|
||||
person_type: body.person_type === 'unternehmen' ? 'unternehmen' : 'privat',
|
||||
font: ['tilda', 'alva', 'ellie'].includes(body.font) ? body.font : 'tilda',
|
||||
source: body.source === 'operator' ? 'operator' : (body.source === 'portal' ? 'portal' : 'configurator'),
|
||||
status: 'wartet_auf_zahlung',
|
||||
source: ['operator', 'portal', 'extern'].includes(body.source) ? body.source : 'configurator',
|
||||
// Extern (z. B. Etsy) wird über die Plattform bezahlt → nicht auf Zahlung warten.
|
||||
status: body.source === 'extern' ? 'bezahlt' : 'wartet_auf_zahlung',
|
||||
shipping_type: body.shipping_type === 'einzeln' ? 'einzeln' : 'sammel',
|
||||
needs_envelope: !!body.needs_envelope,
|
||||
envelope_labeling: body.envelope_labeling ?? null,
|
||||
@@ -1425,6 +1539,9 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
motif: Number.isFinite(Number(body.motif)) && body.motif ? Number(body.motif) : null,
|
||||
motif_upload: body.motif_upload || null,
|
||||
source_file: body.source_file || null,
|
||||
source_files: Array.isArray(body.source_files) && body.source_files.length
|
||||
? body.source_files
|
||||
: (body.source_file ? [body.source_file] : null),
|
||||
billing: body.billing && typeof body.billing === 'object' ? body.billing : null,
|
||||
shipping_address: body.shipping_address && typeof body.shipping_address === 'object' ? body.shipping_address : null,
|
||||
own_cards: body.motif_mode === 'eigene_karten',
|
||||
@@ -1439,7 +1556,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
gross_total: quote.gross_total,
|
||||
voucher: quote.voucher?.id ?? null,
|
||||
payment_method: paymentMethod,
|
||||
payment_status: 'offen',
|
||||
payment_status: body.source === 'extern' ? 'bezahlt' : 'offen',
|
||||
};
|
||||
|
||||
let orderId, orderNumber;
|
||||
@@ -1496,19 +1613,25 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
order: orderId, status: 'wartet_auf_zahlung', note: 'Auftrag über den Konfigurator angelegt.',
|
||||
});
|
||||
|
||||
// SVGs automatisch erzeugen, sobald der Kunde den Auftrag abschließt.
|
||||
// Handschrift-Produkte werden gerendert (gezeigte Vorschau-Dokumente 1:1);
|
||||
// Datei-Produkte (Upload) nicht. Feuern & vergessen – blockiert die Antwort
|
||||
// an den Kunden nicht (das Backend generiert im Hintergrund und schreibt
|
||||
// Status/artifact_path selbst zurück).
|
||||
if (quote.product.type === 'letter' || quote.product.type === 'postcard') {
|
||||
fetch(`${BACKEND_URL}/api/order/from-directus`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN },
|
||||
body: JSON.stringify({ orderId }),
|
||||
}).catch((e) => logger.warn(`[skrift-orders] Auto-Generierung nicht gestartet: ${e.message}`));
|
||||
// Separate, im Admin editierbare Kunden-Mail für Sonderfälle.
|
||||
const kundeEmail = norm(body.email);
|
||||
if (isEmail(kundeEmail)) {
|
||||
const vars = { order_number: orderNumber, name: [body.first_name, body.last_name].filter(Boolean).join(' ') };
|
||||
if (quote.product?.input_mode === 'datei') sendeVorlage(schema, 'unterschriften_eingang', kundeEmail, vars);
|
||||
else if (body.source === 'extern') sendeVorlage(schema, 'extern_eingang', kundeEmail, vars);
|
||||
}
|
||||
|
||||
// SVGs automatisch erzeugen, sobald der Kunde den Auftrag abschließt – IMMER
|
||||
// und für jedes Produkt (das Backend entscheidet, was zu erzeugen ist:
|
||||
// Schriftstücke bei Handschrift-Produkten, Kuverts immer wenn gewünscht;
|
||||
// bei Datei-Produkten NUR die Kuverts, nicht das hochgeladene Dokument).
|
||||
// Feuern & vergessen – blockiert die Antwort an den Kunden nicht.
|
||||
fetch(`${BACKEND_URL}/api/order/from-directus`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN },
|
||||
body: JSON.stringify({ orderId }),
|
||||
}).catch((e) => logger.warn(`[skrift-orders] Auto-Generierung nicht gestartet: ${e.message}`));
|
||||
|
||||
return res.json({
|
||||
order_number: orderNumber,
|
||||
net_total: quote.net_total, vat_amount: quote.vat_amount, gross_total: quote.gross_total,
|
||||
|
||||
Reference in New Issue
Block a user