Unterschriftenservice als echter Auftrag: signature-order (Seiten-SVGs schreiben + Auftrag anlegen), Kundenmail-Feld + 'Auftrag anlegen'-Button, Output-Mount schreibbar

This commit is contained in:
Lucas Orth
2026-08-27 11:05:45 +02:00
parent aef08dbbff
commit 1b600ba150
3 changed files with 113 additions and 3 deletions

View File

@@ -245,6 +245,96 @@ const handler = (router, { services, getSchema, logger }) => {
// 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'));
// Unterschriftenservice als ECHTEN Auftrag anlegen: erzeugt je PDF-Seite eine
// A4-SVG (leer, wo kein Marker), schreibt sie als Schriftstück in den Auftrags-
// ordner und legt den Auftrag an. Danach läuft alles über die normale Produktion
// (Drucken → Plotter). KEIN neuer Job-Typ. Setzt den schreibbaren Output-Mount voraus.
router.post('/signature-order', async (req, res) => {
const acc = req.accountability;
if (!acc || !acc.user || !(acc.admin || acc.app)) return res.status(403).json({ error: 'Kein Zugriff.' });
const body = req.body || {};
if (!body.order && !body.document_id) return res.status(400).json({ error: 'order oder document_id erforderlich.' });
const sigList = Array.isArray(body.signatures) ? body.signatures : [];
if (!sigList.length) return res.status(400).json({ error: 'Mindestens eine Unterschrift wählen.' });
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', '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 (document_id fehlt).' });
const prodKey = body.product || 'unterschriftenservice';
let product = (await svc('products').readByQuery({ filter: { key: { _eq: prodKey } }, limit: 1, fields: ['id', 'key', 'name'] }))?.[0];
if (!product) product = (await svc('products').readByQuery({ filter: { input_mode: { _eq: 'datei' } }, limit: 1, fields: ['id', 'key', 'name'] }))?.[0];
if (!product) return res.status(400).json({ error: 'Kein Unterschriftenservice-/Datei-Produkt gefunden.' });
let customerId = null, customerEmail = null;
if (body.customer_email && isEmail(norm(body.customer_email))) {
const u = (await new UsersService({ schema, accountability: null }).readByQuery({
filter: { email: { _eq: norm(body.customer_email) } }, limit: 1, fields: ['id', 'email'],
}))?.[0];
if (u) { customerId = u.id; customerEmail = u.email; } else customerEmail = norm(body.customer_email);
}
// A4-Signaturen erzeugen (eine je Seite) – JSON-Variante.
const { form, jobCount } = await signatureForm(schema, pdfId, sigList);
if (!jobCount) return res.status(400).json({ error: 'Keine gültige Signatur (SVG-Datei fehlt).' });
let r;
try { r = await fetch(`${SIGN_URL}/render-json`, { method: 'POST', body: form }); }
catch (e) { return res.status(502).json({ error: 'Signatur-Service nicht erreichbar.', detail: `${SIGN_URL} – ${e.message}` }); }
if (!r.ok) {
let d = ''; try { d = (await r.json())?.detail || ''; } catch { /* ignore */ }
return res.status(502).json({ error: 'Signatur-Service-Fehler (Marker im PDF?).', detail: String(Array.isArray(d) ? d.join('; ') : d).slice(0, 400) });
}
const pages = (await r.json())?.pages || [];
if (!pages.length) return res.status(422).json({ error: 'Keine Seiten erzeugt.' });
const jt = new Date();
const praefix = `${String(jt.getDate()).padStart(2, '0')}-${String(jt.getMonth() + 1).padStart(2, '0')}-${String(jt.getFullYear()).slice(2)}`;
const naechsteNummer = async () => {
const heutige = await svc('orders').readByQuery({ filter: { order_number: { _starts_with: `${praefix}-` } }, limit: -1, fields: ['id'] });
return `${praefix}-${String((heutige?.length ?? 0) + 1).padStart(3, '0')}`;
};
const orderData = {
customer: customerId, customer_email: customerEmail, product: product.id,
person_type: 'privat', source: 'operator', status: 'in_queue', production_status: 'eingegangen',
payment_status: 'bezahlt', payment_method: 'rechnung', shipping_type: 'sammel',
needs_envelope: false, envelope_labeling: 'keine',
source_file: pdfId, source_files: [pdfId],
entries_count: pages.length, count_inland: pages.length, count_ausland: 0,
};
let orderId, orderNumber;
for (let v = 0; v < 6; v += 1) {
orderNumber = await naechsteNummer();
try { orderId = await svc('orders').createOne({ order_number: orderNumber, ...orderData }); break; }
catch (e) { if (v === 5 || !/unique|duplicate|bereits|exists/i.test(String(e?.message || ''))) throw e; }
}
const entriesSvc = svc('order_entries');
for (let i = 0; i < pages.length; i += 1) {
await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` });
}
await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Unterschriftenservice – manuell angelegt.' }).catch(() => {});
// Seiten-SVGs als Schriftstück in den Auftragsordner schreiben.
const ordner = path.join(OUTPUT_DIR, orderNumber);
fs.mkdirSync(ordner, { recursive: true });
pages.forEach((p, i) => {
fs.writeFileSync(path.join(ordner, `letter_${String(i + 1).padStart(3, '0')}.svg`), p.svg, 'utf8');
});
return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length });
} catch (err) {
logger.error(`[skrift-orders] signature-order: ${err.stack || err.message}`);
return res.status(500).json({ error: 'Auftrag konnte nicht angelegt werden.', detail: String(err.message || err).slice(0, 400) });
}
});
// Policy-konformes Zufallspasswort (Kunde meldet sich ohnehin passwortlos an).
const zufallsPasswort = () =>
`Aa1!${crypto.randomBytes(24).toString('base64url').replace(/[^A-Za-z0-9]/g, '')}`;