Initial commit
This commit is contained in:
283
Docker Backend/src/services/placeholder-service.js
Normal file
283
Docker Backend/src/services/placeholder-service.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Ersetzt Platzhalter im Text
|
||||
* @param {string} text - Text mit Platzhaltern wie [[Vorname]]
|
||||
* @param {object} placeholders - Objekt mit Platzhalter-Werten {Vorname: 'Max', ...}
|
||||
* @returns {string} - Text mit ersetzten Platzhaltern
|
||||
*/
|
||||
function replacePlaceholders(text, placeholders) {
|
||||
if (!placeholders || typeof placeholders !== 'object') {
|
||||
return text;
|
||||
}
|
||||
|
||||
let result = text;
|
||||
|
||||
for (const [key, value] of Object.entries(placeholders)) {
|
||||
const placeholder = `[[${key}]]`;
|
||||
// Case-insensitive replacement: 'i' flag
|
||||
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
||||
result = result.replace(regex, value || '');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaped einen Wert für CSV
|
||||
*/
|
||||
function escapeCSV(value) {
|
||||
const str = String(value || '');
|
||||
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV aus Platzhalter-Daten (für Briefe)
|
||||
* @param {Array} letters - Array von Letter-Objekten mit placeholders
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generatePlaceholderCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Briefe filtern (keine Umschläge)
|
||||
const letterItems = letters.filter(l => l.type !== 'envelope');
|
||||
if (letterItems.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sammle alle unique Keys
|
||||
const allKeys = new Set();
|
||||
letterItems.forEach(letter => {
|
||||
if (letter.placeholders) {
|
||||
Object.keys(letter.placeholders).forEach(key => allKeys.add(key));
|
||||
}
|
||||
});
|
||||
|
||||
if (allKeys.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = ['BriefNr', ...Array.from(allKeys).sort()];
|
||||
|
||||
// Header
|
||||
const header = keys.join(',');
|
||||
|
||||
// Rows
|
||||
const rows = letterItems.map((letter, index) => {
|
||||
const briefNr = String(letter.index !== undefined ? letter.index : index).padStart(3, '0');
|
||||
const values = [briefNr];
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = letter.placeholders?.[key] || '';
|
||||
values.push(escapeCSV(value));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV für Empfängerdaten (Umschläge im recipientData-Modus)
|
||||
* @param {Array} letters - Array von Letter-Objekten
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generateRecipientCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Umschläge im recipient-Modus filtern
|
||||
const envelopes = letters.filter(l => l.type === 'envelope' && l.envelopeType === 'recipient');
|
||||
if (envelopes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Prüfe ob Empfängerdaten vorhanden sind (vorname, name, strasse, etc.)
|
||||
const recipientKeys = ['vorname', 'name', 'strasse', 'hausnummer', 'plz', 'ort', 'land'];
|
||||
const hasRecipientData = envelopes.some(env =>
|
||||
env.placeholders && recipientKeys.some(key => env.placeholders[key])
|
||||
);
|
||||
|
||||
if (!hasRecipientData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = ['UmschlagNr', ...recipientKeys];
|
||||
const header = keys.join(',');
|
||||
|
||||
const rows = envelopes.map((envelope, index) => {
|
||||
const umschlagNr = String(envelope.index !== undefined ? envelope.index : index).padStart(3, '0');
|
||||
const values = [umschlagNr];
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = envelope.placeholders?.[key] || '';
|
||||
values.push(escapeCSV(value));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV für freie Adressen (Umschläge im free-Modus)
|
||||
* @param {Array} letters - Array von Letter-Objekten
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generateFreeAddressCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Umschläge im free-Modus filtern
|
||||
const envelopes = letters.filter(l => l.type === 'envelope' && l.envelopeType === 'free');
|
||||
if (envelopes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Freie Adressen haben bis zu 5 Zeilen
|
||||
const keys = ['UmschlagNr', 'Zeile1', 'Zeile2', 'Zeile3', 'Zeile4', 'Zeile5'];
|
||||
const header = keys.join(',');
|
||||
|
||||
const rows = envelopes.map((envelope, index) => {
|
||||
const umschlagNr = String(envelope.index !== undefined ? envelope.index : index).padStart(3, '0');
|
||||
|
||||
// Text in Zeilen aufteilen
|
||||
const textLines = (envelope.text || '').split('\n');
|
||||
const values = [umschlagNr];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
values.push(escapeCSV(textLines[i] || ''));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert CSV für Umschlag-Platzhalter (Umschläge im customText-Modus)
|
||||
* @param {Array} letters - Array von Letter-Objekten
|
||||
* @returns {string} - CSV-String
|
||||
*/
|
||||
function generateEnvelopePlaceholderCSV(letters) {
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
console.log('[CSV] generateEnvelopePlaceholderCSV: No letters');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Nur Umschläge im custom-Modus filtern
|
||||
const envelopes = letters.filter(l => l.type === 'envelope' && l.envelopeType === 'custom');
|
||||
console.log('[CSV] Found custom envelopes:', envelopes.length);
|
||||
console.log('[CSV] Envelope details:', envelopes.map(e => ({ type: e.type, envelopeType: e.envelopeType, placeholders: e.placeholders })));
|
||||
|
||||
if (envelopes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sammle alle unique Keys (außer Standard-Empfängerfelder)
|
||||
const recipientKeys = ['vorname', 'name', 'strasse', 'hausnummer', 'plz', 'ort', 'land'];
|
||||
const allKeys = new Set();
|
||||
envelopes.forEach(envelope => {
|
||||
if (envelope.placeholders) {
|
||||
console.log('[CSV] Envelope placeholders keys:', Object.keys(envelope.placeholders));
|
||||
Object.keys(envelope.placeholders).forEach(key => {
|
||||
// Nur nicht-Empfängerfelder sammeln
|
||||
if (!recipientKeys.includes(key.toLowerCase())) {
|
||||
allKeys.add(key);
|
||||
console.log('[CSV] Added key:', key);
|
||||
} else {
|
||||
console.log('[CSV] Skipped recipient key:', key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[CSV] All collected keys:', Array.from(allKeys));
|
||||
|
||||
if (allKeys.size === 0) {
|
||||
console.log('[CSV] No custom placeholder keys found for envelopes');
|
||||
return '';
|
||||
}
|
||||
|
||||
const keys = ['UmschlagNr', ...Array.from(allKeys).sort()];
|
||||
const header = keys.join(',');
|
||||
|
||||
const rows = envelopes.map((envelope, index) => {
|
||||
const umschlagNr = String(envelope.index !== undefined ? envelope.index : index).padStart(3, '0');
|
||||
const values = [umschlagNr];
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = envelope.placeholders?.[key] || '';
|
||||
values.push(escapeCSV(value));
|
||||
}
|
||||
|
||||
return values.join(',');
|
||||
});
|
||||
|
||||
return [header, ...rows].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert alle CSV-Dateien für eine Bestellung
|
||||
* @param {Array} letters - Array von Letter-Objekten (Briefe und Umschläge)
|
||||
* @returns {Object} - Objekt mit CSV-Inhalten { placeholders, recipients, envelopePlaceholders, freeAddresses }
|
||||
*/
|
||||
function generateAllCSVs(letters) {
|
||||
const result = {
|
||||
placeholders: null, // Brief-Platzhalter
|
||||
recipients: null, // Empfängerdaten für Umschläge (recipientData-Modus)
|
||||
envelopePlaceholders: null, // Umschlag-Platzhalter (customText-Modus)
|
||||
freeAddresses: null // Freie Adressen für Umschläge (free-Modus)
|
||||
};
|
||||
|
||||
if (!Array.isArray(letters) || letters.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Brief-Platzhalter CSV
|
||||
const placeholderCSV = generatePlaceholderCSV(letters);
|
||||
if (placeholderCSV) {
|
||||
result.placeholders = placeholderCSV;
|
||||
}
|
||||
|
||||
// Empfänger CSV (für recipientData-Modus)
|
||||
const recipientCSV = generateRecipientCSV(letters);
|
||||
if (recipientCSV) {
|
||||
result.recipients = recipientCSV;
|
||||
}
|
||||
|
||||
// Umschlag-Platzhalter CSV (für customText-Modus)
|
||||
const envelopePlaceholderCSV = generateEnvelopePlaceholderCSV(letters);
|
||||
if (envelopePlaceholderCSV) {
|
||||
result.envelopePlaceholders = envelopePlaceholderCSV;
|
||||
}
|
||||
|
||||
// Freie Adressen CSV (für free-Modus)
|
||||
const freeAddressCSV = generateFreeAddressCSV(letters);
|
||||
if (freeAddressCSV) {
|
||||
result.freeAddresses = freeAddressCSV;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
replacePlaceholders,
|
||||
generatePlaceholderCSV,
|
||||
generateRecipientCSV,
|
||||
generateEnvelopePlaceholderCSV,
|
||||
generateFreeAddressCSV,
|
||||
generateAllCSVs,
|
||||
escapeCSV
|
||||
};
|
||||
230
Docker Backend/src/services/scriptalizer-service.js
Normal file
230
Docker Backend/src/services/scriptalizer-service.js
Normal file
@@ -0,0 +1,230 @@
|
||||
const https = require("https");
|
||||
const { parseStringPromise } = require("xml2js");
|
||||
const config = require("../config");
|
||||
|
||||
// API Request Counter (resets daily)
|
||||
const stats = {
|
||||
requestsToday: 0,
|
||||
textsToday: 0,
|
||||
lastResetDate: new Date().toDateString(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Prüft ob der Zähler zurückgesetzt werden muss (neuer Tag)
|
||||
*/
|
||||
function checkAndResetDailyStats() {
|
||||
const today = new Date().toDateString();
|
||||
if (stats.lastResetDate !== today) {
|
||||
console.log(`[Scriptalizer] 📅 New day detected, resetting counters`);
|
||||
console.log(`[Scriptalizer] 📊 Yesterday's stats: ${stats.requestsToday} API calls, ${stats.textsToday} texts processed`);
|
||||
stats.requestsToday = 0;
|
||||
stats.textsToday = 0;
|
||||
stats.lastResetDate = today;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen Stats zurück
|
||||
*/
|
||||
function getStats() {
|
||||
checkAndResetDailyStats();
|
||||
return { ...stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft die Scriptalizer API auf
|
||||
* @param {string} inputText - Text zum Scriptalisieren
|
||||
* @param {string} fontName - Font-Name (z.B. 'PremiumUltra79')
|
||||
* @param {number} errFrequency - Fehlerfrequenz
|
||||
* @returns {Promise<{status: string, outputText: string}>}
|
||||
*/
|
||||
async function callScriptalizer(inputText, fontName, errFrequency = 10) {
|
||||
// Prüfe ob neuer Tag
|
||||
checkAndResetDailyStats();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!config.scriptalizer.licenseKey) {
|
||||
reject(new Error("Scriptalizer License Key fehlt in Konfiguration"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use x-www-form-urlencoded format
|
||||
const params = new URLSearchParams();
|
||||
params.append("LicenseKey", config.scriptalizer.licenseKey);
|
||||
params.append("FontName", fontName);
|
||||
params.append("ErrFrequency", String(errFrequency));
|
||||
params.append("InputText", inputText);
|
||||
|
||||
const body = params.toString();
|
||||
|
||||
// Check input size (48KB limit)
|
||||
if (Buffer.byteLength(body) > config.scriptalizer.maxInputSize) {
|
||||
reject(
|
||||
new Error(
|
||||
`Input size exceeds 48KB limit (${Buffer.byteLength(body)} bytes)`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
hostname: "www.scriptalizer.co.uk",
|
||||
port: 443,
|
||||
path: "/QuantumScriptalize.asmx/Scriptalize",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
},
|
||||
timeout: 30000, // 30 second timeout
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = "";
|
||||
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on("end", async () => {
|
||||
try {
|
||||
const result = await parseStringPromise(data, {
|
||||
explicitArray: false,
|
||||
});
|
||||
const response = result.ScriptalizerResponse;
|
||||
|
||||
if (!response || response.Status !== "OK") {
|
||||
reject(new Error(response?.Status || "UNKNOWN_STATUS"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Zähler erhöhen bei erfolgreichem Request
|
||||
stats.requestsToday++;
|
||||
console.log(`[Scriptalizer] 📊 API Request #${stats.requestsToday} today completed`);
|
||||
|
||||
resolve({ status: "OK", outputText: response.OutputText });
|
||||
} catch (err) {
|
||||
reject(
|
||||
new Error(`Failed to parse Scriptalizer response: ${err.message}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", (err) => {
|
||||
reject(new Error(`Scriptalizer request failed: ${err.message}`));
|
||||
});
|
||||
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("Scriptalizer request timed out"));
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scriptalisiert mehrere Texte in 25er Batches
|
||||
* @param {string[]} texts - Array von Texten
|
||||
* @param {string} font - Font-Key (tilda, alva, ellie)
|
||||
* @param {number} errFrequency - Fehlerfrequenz
|
||||
* @returns {Promise<string[]>} - Array von scriptalisierten Texten
|
||||
*/
|
||||
async function scriptalizeBatch(texts, font = "tilda", errFrequency = 10) {
|
||||
if (!Array.isArray(texts) || texts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fontName =
|
||||
config.scriptalizer.fontMap[font.toLowerCase()] ||
|
||||
config.scriptalizer.fontMap.tilda;
|
||||
const separator = config.scriptalizer.separator;
|
||||
const BATCH_SIZE = 25; // 25 Texte pro Scriptalizer API Call
|
||||
|
||||
console.log(
|
||||
`[Scriptalizer] Processing ${texts.length} texts in batches of ${BATCH_SIZE}`
|
||||
);
|
||||
|
||||
const allScriptalized = [];
|
||||
|
||||
// Teile in 25er Batches auf
|
||||
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
||||
const batch = texts.slice(i, i + BATCH_SIZE);
|
||||
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
|
||||
const totalBatches = Math.ceil(texts.length / BATCH_SIZE);
|
||||
|
||||
console.log(
|
||||
`[Scriptalizer] Processing batch ${batchNumber}/${totalBatches} (${
|
||||
batch.length
|
||||
} texts, indices ${i}-${i + batch.length - 1})`
|
||||
);
|
||||
|
||||
// Kombiniere Texte mit Separator
|
||||
const combinedText = batch.join(separator);
|
||||
|
||||
try {
|
||||
const result = await callScriptalizer(
|
||||
combinedText,
|
||||
fontName,
|
||||
errFrequency
|
||||
);
|
||||
|
||||
// Trenne Ergebnis wieder
|
||||
const scriptalized = result.outputText.split(separator);
|
||||
|
||||
if (scriptalized.length !== batch.length) {
|
||||
console.warn(
|
||||
`[Scriptalizer] Batch ${batchNumber}: returned ${scriptalized.length} parts, expected ${batch.length}`
|
||||
);
|
||||
}
|
||||
|
||||
allScriptalized.push(...scriptalized);
|
||||
console.log(
|
||||
`[Scriptalizer] Batch ${batchNumber}/${totalBatches} completed successfully`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`[Scriptalizer] Batch ${batchNumber} failed:`, err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Textzähler erhöhen
|
||||
stats.textsToday += allScriptalized.length;
|
||||
|
||||
console.log(
|
||||
`[Scriptalizer] All batches completed. Total scriptalized: ${allScriptalized.length}`
|
||||
);
|
||||
console.log(
|
||||
`[Scriptalizer] 📊 Daily stats: ${stats.requestsToday} API calls, ${stats.textsToday} texts processed`
|
||||
);
|
||||
return allScriptalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scriptalisiert einen einzelnen Text
|
||||
* @param {string} text - Text zum Scriptalisieren
|
||||
* @param {string} font - Font-Key (tilda, alva, ellie)
|
||||
* @param {number} errFrequency - Fehlerfrequenz
|
||||
* @returns {Promise<string>} - Scriptalisierter Text
|
||||
*/
|
||||
async function scriptalizeSingle(text, font = "tilda", errFrequency = 10) {
|
||||
const fontName =
|
||||
config.scriptalizer.fontMap[font.toLowerCase()] ||
|
||||
config.scriptalizer.fontMap.tilda;
|
||||
|
||||
try {
|
||||
const result = await callScriptalizer(text, fontName, errFrequency);
|
||||
return result.outputText;
|
||||
} catch (err) {
|
||||
console.error("Scriptalizer single call failed:", err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scriptalizeBatch,
|
||||
scriptalizeSingle,
|
||||
getStats,
|
||||
};
|
||||
Reference in New Issue
Block a user