From 523352d053fd5852748b8b1dbf73ad8c287a9254 Mon Sep 17 00:00:00 2001 From: DanielS Date: Tue, 22 Sep 2026 21:53:22 +0200 Subject: [PATCH] fix(pos): CASPOS.EXEC Vorbereitung, alle VK1-VK6 Parameter und XML/JSON Parsing optimiert --- posClient.js | 262 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 178 insertions(+), 84 deletions(-) diff --git a/posClient.js b/posClient.js index b347716..e7e8cfe 100644 --- a/posClient.js +++ b/posClient.js @@ -12,11 +12,15 @@ function getConfig() { return config; } -function generateHeaders(appId, passId) { +// 1. Authentifizierung & WWSVC-Headers nach Spezifikation +function generateHeaders(appId, passId, algo = 'md5') { + // Zeitstempel Format: yyyy-MM-ddTHH:mm:ss oder epoch/ISO const ts = new Date().toISOString(); const reqId = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2); - // POS Cloud standard hash: MD5/SHA256 über appId + passId + ts - const hash = crypto.createHash('sha256').update(`${appId}${passId || ''}${ts}`).digest('hex'); + + // WWSVC Hash: MD5 oder SHA256 über appId + passId + ts + const rawString = `${appId || ''}${passId || ''}${ts}`; + const hash = crypto.createHash(algo === 'sha256' ? 'sha256' : 'md5').update(rawString).digest('hex'); return { 'WWSVC-HASH': hash, @@ -26,26 +30,76 @@ function generateHeaders(appId, passId) { }; } +function formatDate(d) { + const pad = n => String(n).padStart(2, '0'); + return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()}`; +} + +function formatTime(d) { + const pad = n => String(n).padStart(2, '0'); + return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + +// 2. Datenabruf-Ablauf: CASPOS.EXEC (Vorbereitung) & CASPOS.GET (Download) async function performSync(syncType = 'ERSTEINRICHTUNG') { const config = getConfig(); - const { pos_url, pos_pass_id, pos_app_id, pos_cash_register } = config; + const { pos_url, pos_pass_id, pos_app_id, pos_cash_register, last_sync_time } = config; if (!pos_url) { throw new Error('POS Cloud URL nicht konfiguriert'); } - const endpoint = new URL(pos_url); - endpoint.searchParams.set('ART', syncType); - if (pos_cash_register) { - endpoint.searchParams.set('MDCDEVICEID', pos_cash_register); + // Basis-URL sauber aufbereiten + let baseUrl = pos_url.trim(); + + // A) Differenzabgleich Vorbereitung wenn ART=AENDERUNGEN + if (syncType === 'AENDERUNGEN') { + let fromDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // Default: 24h zurück + if (last_sync_time) { + const parsed = new Date(last_sync_time); + if (!isNaN(parsed.getTime())) fromDate = parsed; + } + + const execUrl = new URL(baseUrl.replace(/CASPOS\.GET/i, 'CASPOS.EXEC')); + if (!execUrl.pathname.includes('CASPOS.EXEC') && !baseUrl.includes('CASPOS.')) { + execUrl.pathname = execUrl.pathname.replace(/\/$/, '') + '/CASPOS.EXEC'; + } + + execUrl.searchParams.set('ART', 'AENDERUNGEN'); + execUrl.searchParams.set('AENDERUNGEN_ABDATUM', formatDate(fromDate)); + execUrl.searchParams.set('AENDERUNGEN_ABUHRZEIT', formatTime(fromDate)); + if (pos_cash_register) execUrl.searchParams.set('MDCDEVICEID', pos_cash_register); + + const execHeaders = generateHeaders(pos_app_id, pos_pass_id); + try { + await fetch(execUrl.toString(), { method: 'GET', headers: execHeaders }); + } catch(e) { + console.warn('CASPOS.EXEC Hinweis:', e.message); + } } - endpoint.searchParams.set('LAGER', '1'); - endpoint.searchParams.set('LAGERORTE', '1'); - endpoint.searchParams.set('VK1', '1'); + + // B) Download mit CASPOS.GET + const getUrl = new URL(baseUrl.replace(/CASPOS\.EXEC/i, 'CASPOS.GET')); + if (!getUrl.pathname.includes('CASPOS.GET') && !baseUrl.includes('CASPOS.')) { + getUrl.pathname = getUrl.pathname.replace(/\/$/, '') + '/CASPOS.GET'; + } + + getUrl.searchParams.set('ART', syncType); + if (pos_cash_register) { + getUrl.searchParams.set('MDCDEVICEID', pos_cash_register); + } + getUrl.searchParams.set('LAGER', '1'); + getUrl.searchParams.set('LAGERORTE', '1'); + getUrl.searchParams.set('VK1', '1'); + getUrl.searchParams.set('VK2', '1'); + getUrl.searchParams.set('VK3', '1'); + getUrl.searchParams.set('VK4', '1'); + getUrl.searchParams.set('VK5', '1'); + getUrl.searchParams.set('VK6', '1'); const headers = generateHeaders(pos_app_id, pos_pass_id); - const response = await fetch(endpoint.toString(), { + const response = await fetch(getUrl.toString(), { method: 'GET', headers: headers }); @@ -57,15 +111,23 @@ async function performSync(syncType = 'ERSTEINRICHTUNG') { const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); - return parseAndStoreZip(buffer); + const result = await parseAndStoreZip(buffer); + + // Letzten erfolgreichen Sync-Zeitstempel speichern + db.prepare('INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value') + .run('last_sync_time', new Date().toISOString()); + + return result; } +// 3. Inhalt ZIP & Dateitypen (JSON & XML Parsing für Products, Groups, Deletions) async function parseAndStoreZip(buffer) { const zip = new AdmZip(buffer); const zipEntries = zip.getEntries(); let productsCount = 0; let groupsCount = 0; + let deletionsCount = 0; const insertGroup = db.prepare(` INSERT INTO product_groups (id, name, sort_order) @@ -90,87 +152,119 @@ async function parseAndStoreZip(buffer) { UPDATE products SET active = 0 WHERE id = ? `); - const transaction = db.transaction(() => { - for (const entry of zipEntries) { - const fileName = entry.entryName.toLowerCase(); - - // Gruppen - if (fileName.includes('productgroup') || fileName.includes('warengruppe')) { - const content = entry.getData().toString('utf8'); - let groups = []; - if (fileName.endsWith('.json')) { - try { groups = JSON.parse(content); } catch(e) {} - } - if (Array.isArray(groups)) { - for (const g of groups) { - const id = String(g.id || g.groupId || g.nr || ''); - const name = g.name || g.text || g.description || 'Kategorie ' + id; - if (id) { - insertGroup.run(id, name, g.sortOrder || 0); - groupsCount++; - } - } - } - } + const parser = new xml2js.Parser({ explicitArray: false, ignoreAttrs: false }); - // Artikel - if (fileName.includes('product') && !fileName.includes('productgroup')) { - const content = entry.getData().toString('utf8'); - let items = []; - if (fileName.endsWith('.json')) { - try { items = JSON.parse(content); } catch(e) {} - } - if (Array.isArray(items)) { - for (const item of items) { - const id = String(item.id || item.articleNo || item.barcode || ''); - const text1 = item.text1 || item.name || item.title || 'Unbekannt'; - const text2 = item.text2 || item.description || ''; - const groupId = String(item.groupId || item.warengruppe || ''); - - // Preis 1 ermitteln - let preis1 = 0; - if (typeof item.prices === 'object' && item.prices !== null) { - preis1 = parseFloat(item.prices.vk1 || item.prices.price1 || item.prices.standard || 0); - } else if (item.preis1 !== undefined) { - preis1 = parseFloat(item.preis1); - } else if (item.price !== undefined) { - preis1 = parseFloat(item.price); - } + for (const entry of zipEntries) { + const fileName = entry.entryName.toLowerCase(); + const rawContent = entry.getData().toString('utf8'); - const unit = item.contentUnit || item.unit || ''; - - if (id) { - insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, unit); - productsCount++; - } - } - } - } - - // Deletions - if (fileName.includes('deletion')) { - const content = entry.getData().toString('utf8'); + // A) Warengruppen (Casposdata_ProductGroups) + if (fileName.includes('productgroup') || fileName.includes('warengruppe')) { + let groups = []; + if (fileName.endsWith('.json')) { try { - const deletions = JSON.parse(content); - if (Array.isArray(deletions)) { - for (const del of deletions) { - const id = String(del.id || del.articleNo || del); - deactivateProduct.run(id); - } - } + const parsed = JSON.parse(rawContent); + groups = Array.isArray(parsed) ? parsed : (parsed.groups || parsed.ProductGroups || []); } catch(e) {} + } else if (fileName.endsWith('.xml')) { + try { + const parsed = await parser.parseStringPromise(rawContent); + const root = parsed.ProductGroups || parsed.groups || parsed.root || parsed; + groups = root.ProductGroup || root.group || []; + if (!Array.isArray(groups)) groups = [groups]; + } catch(e) {} + } + + for (const g of groups) { + const id = String(g.id || g.groupId || g.nr || g.Id || g.GroupId || ''); + const name = g.name || g.text || g.text1 || g.description || g.Name || g.Text1 || ('Kategorie ' + id); + const sortOrder = parseInt(g.sortOrder || g.sort || g.SortOrder || 0, 10); + if (id) { + insertGroup.run(id, name, isNaN(sortOrder) ? 0 : sortOrder); + groupsCount++; + } } } - db.prepare('INSERT INTO sync_logs (status, message, items_count) VALUES (?, ?, ?)') - .run('SUCCESS', `Importiert: ${productsCount} Artikel, ${groupsCount} Gruppen`, productsCount); - }); + // B) Artikelstamm & Preise (Casposdata_Products) + if (fileName.includes('product') && !fileName.includes('productgroup') && !fileName.includes('group')) { + let items = []; + if (fileName.endsWith('.json')) { + try { + const parsed = JSON.parse(rawContent); + items = Array.isArray(parsed) ? parsed : (parsed.products || parsed.Products || parsed.articles || []); + } catch(e) {} + } else if (fileName.endsWith('.xml')) { + try { + const parsed = await parser.parseStringPromise(rawContent); + const root = parsed.Products || parsed.products || parsed.root || parsed; + items = root.Product || root.product || root.article || []; + if (!Array.isArray(items)) items = [items]; + } catch(e) {} + } - transaction(); - return { productsCount, groupsCount }; + for (const item of items) { + const id = String(item.id || item.articleNo || item.barcode || item.Id || item.ArticleNo || ''); + const text1 = item.text1 || item.name || item.title || item.Text1 || item.Name || 'Unbekannt'; + const text2 = item.text2 || item.description || item.zutaten || item.Text2 || item.Description || ''; + const groupId = String(item.groupId || item.warengruppe || item.GroupId || item.ProductGroupId || ''); + + // Preisermittlung (preis1 / vk1 / standard brutto) + let preis1 = 0; + if (typeof item.prices === 'object' && item.prices !== null) { + preis1 = parseFloat(item.prices.vk1 || item.prices.VK1 || item.prices.price1 || item.prices.standard || 0); + } else if (item.preis1 !== undefined || item.Preis1 !== undefined) { + preis1 = parseFloat(item.preis1 || item.Preis1); + } else if (item.price !== undefined || item.Price !== undefined) { + preis1 = parseFloat(item.price || item.Price); + } else if (item.VK1 !== undefined || item.vk1 !== undefined) { + preis1 = parseFloat(item.VK1 || item.vk1); + } + + const unit = item.contentUnit || item.unit || item.ContentUnit || item.Unit || ''; + + if (id) { + insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, unit); + productsCount++; + } + } + } + + // C) Gelöschte / inaktive Artikel (Casposdata_Deletions) + if (fileName.includes('deletion') || fileName.includes('delete')) { + let deletions = []; + if (fileName.endsWith('.json')) { + try { + const parsed = JSON.parse(rawContent); + deletions = Array.isArray(parsed) ? parsed : (parsed.deletions || parsed.Deletions || []); + } catch(e) {} + } else if (fileName.endsWith('.xml')) { + try { + const parsed = await parser.parseStringPromise(rawContent); + const root = parsed.Deletions || parsed.deletions || parsed.root || parsed; + deletions = root.Deletion || root.deletion || root.id || []; + if (!Array.isArray(deletions)) deletions = [deletions]; + } catch(e) {} + } + + for (const del of deletions) { + const id = String(del.id || del.articleNo || del.Id || del); + if (id) { + deactivateProduct.run(id); + deletionsCount++; + } + } + } + } + + const msg = `Import: ${productsCount} Artikel, ${groupsCount} Gruppen, ${deletionsCount} Deaktiviert`; + db.prepare('INSERT INTO sync_logs (status, message, items_count) VALUES (?, ?, ?)') + .run('SUCCESS', msg, productsCount); + + return { productsCount, groupsCount, deletionsCount, message: msg }; } -// Manueller/Mock Import für Testzwecke & manuelle Pflege +// Manueller/Mock Import für UI Testing function importJsonData(data) { const insertProduct = db.prepare(` INSERT INTO products (id, text1, text2, group_id, preis1, unit, active)