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 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-03 10:31:23 +02:00
parent ca0b3a845a
commit 2b575fa540

View File

@@ -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;