From 2b575fa540d269024a3316c6aa28938d9b35d610 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Thu, 3 Sep 2026 10:31:23 +0200 Subject: [PATCH] Laender-Split (manuelle Auftraege): DE/INT getrennt anlegen zielInland()-Erkennung (Land/Freitext/D-PLZ). manual-order: bei gemischten Empfaengern zwei Auftraege mit Suffix -DE / -INT (Empfaenger aufgeteilt), Generierung je Auftrag -> Schriftstuecke UND Kuverts je Zielgebiet getrennt. Einzelland unveraendert (kein Suffix). Co-Authored-By: Claude Opus 4.8 --- .../dist/index.js | 104 ++++++++++++------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index 6e5cb53..f9f691f 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -155,6 +155,21 @@ async function paypalToken() { return (await r.json()).access_token; } +/** + * Zielland-Erkennung je Empfänger für den Länder-Split. + * Deutschland, wenn: Land = DE/leer, „Deutschland"/„Germany" im Freitext, + * „D-" vor der PLZ, oder nur eine PLZ ohne Auslandshinweis. Sonst → weltweit (INT). + */ +function zielInland(e) { + const land = String(e?.country || '').trim(); + if (land) return /^(de|deu|d|deutschland|germany)$/i.test(land); + const txt = String(e?.free_text || ''); + if (/deutschland|germany/i.test(txt)) return true; + if (/\bd-\s?\d{4,5}\b/i.test(txt)) return true; + if (/(österreich|schweiz|austria|switzerland|frankreich|france|italien|italy|niederlande|netherlands|belgien|belgium|luxemburg|luxembourg|spanien|spain|portugal|usa|vereinigte staaten|united states|kanada|canada|england|großbritannien|grossbritannien|united kingdom|dänemark|denmark|schweden|sweden|norwegen|norway|finnland|finland|polen|poland|tschechien|czech|ungarn|hungary|griechenland|greece|irland|ireland|australien|australia|liechtenstein)/i.test(txt)) return false; + return true; +} + const handler = (router, { services, getSchema, logger }) => { const { ItemsService, MailService, UsersService, RolesService, AssetsService, FilesService } = services; @@ -1572,14 +1587,21 @@ const handler = (router, { services, getSchema, logger }) => { : (String(body.format || '').startsWith('a6') ? 'c6' : 'dinlang'))) : null; - const istInland = (e) => !e.country || /^(de|deutschland|germany)$/i.test(String(e.country).trim()); + const istInland = zielInland; const countAusland = entries.filter((e) => !istInland(e)).length; 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 () => { + // Freie Basis-Nummer; prüft auch die Split-Varianten -DE / -INT auf Kollision. + const freieBasis = 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')}`; + let n = (heutige?.length ?? 0) + 1; + for (let v = 0; v < 30; v += 1, n += 1) { + const basis = `${praefix}-${String(n).padStart(3, '0')}`; + const koll = await svc('orders').readByQuery({ filter: { order_number: { _in: [basis, `${basis}-DE`, `${basis}-INT`] } }, limit: 1, fields: ['id'] }); + if (!koll?.length) return basis; + } + throw new Error('Keine freie Auftragsnummer gefunden.'); }; const orderData = { @@ -1612,38 +1634,54 @@ const handler = (router, { services, getSchema, logger }) => { net_total: quote?.net_total ?? null, vat_amount: quote?.vat_amount ?? null, gross_total: quote?.gross_total ?? null, }; - 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 entryFelder = (e, nr) => ({ + letter_number: nr, + salutation: clean(e.salutation) ?? null, first_name: clean(e.first_name) ?? null, last_name: clean(e.last_name) ?? null, + street: clean(e.street) ?? null, house_no: clean(e.house_no) ?? null, zip: clean(e.zip) ?? null, + city: clean(e.city) ?? null, country: clean(e.country) ?? null, free_text: clean(e.free_text) ?? null, + placeholders: e.placeholders && typeof e.placeholders === 'object' + ? Object.fromEntries(Object.entries(e.placeholders).map(([k, v]) => [k, clean(String(v))])) : null, + }); const entriesSvc = svc('order_entries'); - let n = 0; - for (const e of entries) { - n += 1; - await entriesSvc.createOne({ - order: orderId, letter_number: n, - salutation: clean(e.salutation) ?? null, first_name: clean(e.first_name) ?? null, last_name: clean(e.last_name) ?? null, - street: clean(e.street) ?? null, house_no: clean(e.house_no) ?? null, zip: clean(e.zip) ?? null, - city: clean(e.city) ?? null, country: clean(e.country) ?? null, free_text: clean(e.free_text) ?? null, - placeholders: e.placeholders && typeof e.placeholders === 'object' - ? Object.fromEntries(Object.entries(e.placeholders).map(([k, v]) => [k, clean(String(v))])) : null, + + // Länder-Split: sobald DE- UND Welt-Empfänger vorkommen → zwei getrennte + // Aufträge (-DE / -INT), sonst ein Auftrag ohne Suffix. Gilt für Schriftstücke UND Kuverts. + const deEntries = entries.filter((e) => istInland(e)); + const intEntries = entries.filter((e) => !istInland(e)); + const gemischt = deEntries.length > 0 && intEntries.length > 0; + + const anlegen = async (subEntries, nummer, gebiet) => { + const auslandN = subEntries.filter((e) => !istInland(e)).length; + let q = null; + try { q = await calculatePrice(schema, { ...body, quantity: subEntries.length }); } catch { /* nur Aufzeichnung */ } + const id = await svc('orders').createOne({ + order_number: nummer, + ...orderData, + entries_count: subEntries.length, + count_inland: subEntries.length - auslandN, + count_ausland: auslandN, + net_total: q?.net_total ?? null, vat_amount: q?.vat_amount ?? null, gross_total: q?.gross_total ?? null, }); + let k = 0; + for (const e of subEntries) { k += 1; await entriesSvc.createOne({ order: id, ...entryFelder(e, k) }); } + await svc('status_history').createOne({ order: id, status: 'eingegangen', note: gebiet ? `Manuell angelegt (Länder-Split: ${gebiet}).` : 'Manuell im Admin angelegt.' }); + // Generierung IMMER anstoßen – das Backend entscheidet, was zu erzeugen ist. + fetch(`${BACKEND_URL}/api/order/from-directus`, { + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN }, + body: JSON.stringify({ orderId: id }), + }).catch((err) => logger.warn(`[skrift-orders] Manuell-Generierung nicht gestartet: ${err.message}`)); + return { order_number: nummer, id }; + }; + + if (gemischt) { + const basis = await freieBasis(); + const de = await anlegen(deEntries, `${basis}-DE`, 'Deutschland'); + const int = await anlegen(intEntries, `${basis}-INT`, 'weltweit'); + return res.json({ ok: true, split: true, orders: [de, int], order_number: de.order_number, id: de.id }); } - - await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Manuell im Admin angelegt.' }); - - // Generierung IMMER anstoßen – 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). Nichts = No-Op. - 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] Manuell-Generierung nicht gestartet: ${e.message}`)); - - return res.json({ ok: true, order_number: orderNumber, id: orderId }); + const basis = await freieBasis(); + const einzel = await anlegen(entries, basis, null); + return res.json({ ok: true, order_number: einzel.order_number, id: einzel.id }); } catch (err) { logger.error(`[skrift-orders] manual-order: ${err.stack || err.message}`); return res.status(400).json({ error: err.message }); @@ -1737,7 +1775,7 @@ const handler = (router, { services, getSchema, logger }) => { : null; // Empfänger nach Inland/Ausland aufteilen (für die Auftragsübersicht). - const istInland = (e) => !e.country || /^(de|deutschland|germany)$/i.test(String(e.country).trim()); + const istInland = zielInland; const countAusland = entries.filter((e) => !istInland(e)).length; const countInland = entries.length - countAusland;