fix(pos): CASPOS.EXEC Vorbereitung, alle VK1-VK6 Parameter und XML/JSON Parsing optimiert
All checks were successful
Build and Deploy / build (push) Successful in 27s

This commit is contained in:
DanielS
2026-09-22 21:53:22 +02:00
parent 89915bbd1d
commit 523352d053

View File

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