fix(pos): Webware Slash-URL Endpunkte, JSON Struktur und Preis-Parsing (Komma-Format) vollständig integriert
All checks were successful
Build and Deploy / build (push) Successful in 56s
All checks were successful
Build and Deploy / build (push) Successful in 56s
This commit is contained in:
282
posClient.js
282
posClient.js
@@ -17,7 +17,8 @@ function generateHeaders(appId, passId, algo = 'md5') {
|
||||
const ts = new Date().toISOString();
|
||||
const reqId = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2);
|
||||
|
||||
const rawString = `${appId || ''}${passId || ''}${ts}`;
|
||||
// WWSVC Hash: MD5 über appId + ts
|
||||
const rawString = `${appId || ''}${ts}`;
|
||||
const hash = crypto.createHash(algo === 'sha256' ? 'sha256' : 'md5').update(rawString).digest('hex');
|
||||
|
||||
return {
|
||||
@@ -60,108 +61,65 @@ async function processQrSetup(qrPayload) {
|
||||
|
||||
const cleanUrl = url.replace(/\/$/, '');
|
||||
|
||||
// Endpunkt-Varianten für ServicePass Link (Cloud-Instanzen unterstützen @LNK... oder @...)
|
||||
const candidateUrls = [
|
||||
`${cleanUrl}/@${servicepasslink}`,
|
||||
`${cleanUrl}/@LNK${servicepasslink}`,
|
||||
`${cleanUrl}/LNK${servicepasslink}`,
|
||||
`${cleanUrl}/@LNK/${servicepasslink}`,
|
||||
`${cleanUrl}/WWSVC/@LNK${servicepasslink}`
|
||||
];
|
||||
// 1. GET ServicePass: GET /@LNK{servicepasslink}
|
||||
const linkUrl = `${cleanUrl}/@LNK${servicepasslink}`;
|
||||
const res = await fetch(linkUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': 'Getraenkekarte-POS-Client/1.0' }
|
||||
});
|
||||
|
||||
let resLink = null;
|
||||
let responseText = '';
|
||||
let usedUrl = '';
|
||||
|
||||
for (const cUrl of candidateUrls) {
|
||||
try {
|
||||
const res = await fetch(cUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': 'Getraenkekarte-POS-Client/1.0' }
|
||||
});
|
||||
if (res.ok) {
|
||||
resLink = res;
|
||||
responseText = await res.text();
|
||||
usedUrl = cUrl;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
if (!res.ok) {
|
||||
throw new Error(`Fehler beim Abruf von ServicePass (@LNK): ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
if (!resLink) {
|
||||
throw new Error(`Fehler beim Abruf von ServicePass (@LNK): 404 Resource not found`);
|
||||
}
|
||||
|
||||
let passId = '';
|
||||
let appId = '';
|
||||
|
||||
try {
|
||||
const passData = JSON.parse(responseText);
|
||||
passId = passData.passId || passData.PassId || passData.id;
|
||||
appId = passData.appId || passData.AppId || passData.key;
|
||||
} catch (e) {
|
||||
// Falls Text / Delimiter Format
|
||||
if (responseText.includes('APPKILL')) {
|
||||
throw new Error(`QR-Code Token abgelaufen oder bereits verbraucht (Server meldet: APPKILL). Bitte neuen QR-Code im Kassensystem erzeugen.`);
|
||||
}
|
||||
const parts = responseText.split('@)').map(s => s.trim()).filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
passId = parts[0];
|
||||
appId = parts[1];
|
||||
} else {
|
||||
throw new Error(`Unerwartete Antwort von ServicePass: ${responseText}`);
|
||||
}
|
||||
}
|
||||
const resJson = await res.json();
|
||||
const servicePass = resJson.SERVICEPASS || resJson.servicepass || resJson;
|
||||
const passId = servicePass.PASSID || servicePass.passId || servicePass.id;
|
||||
const appId = servicePass.APPID || servicePass.appId || servicePass.key;
|
||||
|
||||
if (!passId || !appId) {
|
||||
throw new Error('Antwort von ServicePass enthält keine gültige passId/appId');
|
||||
throw new Error(`Unerwartete ServicePass-Antwort: ${JSON.stringify(resJson)}`);
|
||||
}
|
||||
|
||||
// 2. ServicePass registrieren (lokal in DB)
|
||||
// 2. In lokaler SQLite Datenbank speichern
|
||||
const upsert = db.prepare('INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');
|
||||
upsert.run('pos_url', cleanUrl);
|
||||
upsert.run('pos_pass_id', passId);
|
||||
upsert.run('pos_app_id', appId);
|
||||
if (mdcDeviceId) upsert.run('pos_cash_register', String(mdcDeviceId));
|
||||
|
||||
// 3. Pass aktivieren
|
||||
const devId = mdcDeviceId || '1';
|
||||
const activatePath = `/WWSVC/EXECURL/${passId}/CASPOS.PUT/REGISTERID=${devId}/ENTITY=SERVICEPASS/SERVICEPASS_ID=${passId}`;
|
||||
const activateUrl = `${cleanUrl}${activatePath}`;
|
||||
|
||||
const activateHeaders = generateHeaders(appId, passId);
|
||||
try {
|
||||
const actRes = await fetch(activateUrl, {
|
||||
method: 'PUT',
|
||||
headers: activateHeaders
|
||||
});
|
||||
console.log(`ServicePass Activation Status: ${actRes.status}`);
|
||||
} catch (err) {
|
||||
console.warn('Aktivierungsaufruf Warnung:', err.message);
|
||||
}
|
||||
|
||||
db.prepare('INSERT INTO sync_logs (status, message) VALUES (?, ?)')
|
||||
.run('SUCCESS', `QR-Code Handshake erfolgreich. Pass-ID: ${passId}, Kasse: ${devId}`);
|
||||
.run('SUCCESS', `QR-Code Handshake erfolgreich. Pass-ID: ${passId}, Kasse: ${mdcDeviceId || '1'}`);
|
||||
|
||||
// 3. Direkt automatischen Initial-Sync ausführen
|
||||
let syncResult = null;
|
||||
try {
|
||||
syncResult = await performSync('ERSTEINRICHTUNG');
|
||||
} catch(syncErr) {
|
||||
console.warn('Initialer Sync nach Setup Fehler:', syncErr.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: cleanUrl,
|
||||
passId,
|
||||
appId,
|
||||
mdcDeviceId: devId
|
||||
mdcDeviceId: mdcDeviceId || '1',
|
||||
syncResult
|
||||
};
|
||||
}
|
||||
|
||||
// Datenabruf-Ablauf: CASPOS.EXEC (Vorbereitung) & CASPOS.GET (Download)
|
||||
// Datenabruf-Ablauf: CASPOS.EXEC (Vorbereitung) & CASPOS.GET (Download) via WEBWARE Slash-Format
|
||||
async function performSync(syncType = 'ERSTEINRICHTUNG') {
|
||||
const config = getConfig();
|
||||
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');
|
||||
if (!pos_url || !pos_pass_id || !pos_app_id) {
|
||||
throw new Error('POS Cloud Zugangsdaten unvollständig (URL, PassId oder AppId fehlt)');
|
||||
}
|
||||
|
||||
let baseUrl = pos_url.trim();
|
||||
const cleanUrl = pos_url.trim().replace(/\/$/, '');
|
||||
const devId = pos_cash_register || '1';
|
||||
|
||||
// A) Differenzabgleich Vorbereitung wenn ART=AENDERUNGEN
|
||||
if (syncType === 'AENDERUNGEN') {
|
||||
@@ -171,46 +129,22 @@ async function performSync(syncType = 'ERSTEINRICHTUNG') {
|
||||
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 execPath = `/WWSVC/EXECURL/${pos_pass_id}/CASPOS.EXEC/ART=AENDERUNGEN/AENDERUNGEN_ABDATUM=${formatDate(fromDate)}/AENDERUNGEN_ABUHRZEIT=${formatTime(fromDate)}/MDCDEVICEID=${devId}`;
|
||||
const execHeaders = generateHeaders(pos_app_id, pos_pass_id);
|
||||
try {
|
||||
await fetch(execUrl.toString(), { method: 'GET', headers: execHeaders });
|
||||
await fetch(`${cleanUrl}${execPath}`, { method: 'GET', headers: execHeaders });
|
||||
} catch(e) {
|
||||
console.warn('CASPOS.EXEC Hinweis:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
// B) Download mit CASPOS.GET im Webware-Format (/WWSVC/EXECURL/{passId}/CASPOS.GET/ART=.../MDCDEVICEID=...)
|
||||
const getPath = `/WWSVC/EXECURL/${pos_pass_id}/CASPOS.GET/ART=${syncType}/MDCDEVICEID=${devId}/LAGER=1/LAGERORTE=1/VK1=1/VK2=1/VK3=1/VK4=1/VK5=1/VK6=1`;
|
||||
const getUrl = `${cleanUrl}${getPath}`;
|
||||
|
||||
const headers = generateHeaders(pos_app_id, pos_pass_id);
|
||||
|
||||
const response = await fetch(getUrl.toString(), {
|
||||
const response = await fetch(getUrl, {
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
});
|
||||
@@ -230,7 +164,7 @@ async function performSync(syncType = 'ERSTEINRICHTUNG') {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Inhalt ZIP & Dateitypen (JSON & XML Parsing)
|
||||
// Inhalt ZIP & Dateitypen (JSON & XML Parsing für SoftEngine CASPOS Cloud)
|
||||
async function parseAndStoreZip(buffer) {
|
||||
const zip = new AdmZip(buffer);
|
||||
const zipEntries = zip.getEntries();
|
||||
@@ -268,102 +202,92 @@ async function parseAndStoreZip(buffer) {
|
||||
const fileName = entry.entryName.toLowerCase();
|
||||
const rawContent = entry.getData().toString('utf8');
|
||||
|
||||
// A) Warengruppen (Casposdata_ProductGroups)
|
||||
if (fileName.includes('productgroup') || fileName.includes('warengruppe')) {
|
||||
let groups = [];
|
||||
if (fileName.endsWith('.json')) {
|
||||
try {
|
||||
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) {}
|
||||
}
|
||||
// JSON Format von SoftEngine CASPOS (z.B. Casposdata_Products.json)
|
||||
if (fileName.endsWith('.json')) {
|
||||
let json = {};
|
||||
try { json = JSON.parse(rawContent); } 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);
|
||||
// 1. Warengruppen
|
||||
const groupsData = json.productGroups?.data || (Array.isArray(json.productGroups) ? json.productGroups : []);
|
||||
for (const g of groupsData) {
|
||||
const id = String(g.id || g.groupId || '');
|
||||
const name = g.name || g.text || g.text1 || ('Kategorie ' + id);
|
||||
if (id) {
|
||||
insertGroup.run(id, name, isNaN(sortOrder) ? 0 : sortOrder);
|
||||
insertGroup.run(id, name, 0);
|
||||
groupsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {}
|
||||
}
|
||||
// 2. Artikel
|
||||
const productsData = json.products?.data || (Array.isArray(json.products) ? json.products : (Array.isArray(json) ? json : []));
|
||||
for (const item of productsData) {
|
||||
const id = String(item.id || item.articleNo || '');
|
||||
const text1 = item.text1 || item.name || item.title || 'Unbekannt';
|
||||
const text2 = item.text2 || item.description || item.additionalText || '';
|
||||
const groupId = String(item.groupId || item.warengruppe || '');
|
||||
|
||||
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 || '');
|
||||
|
||||
// Preis 1 ermitteln (VK1)
|
||||
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);
|
||||
if (Array.isArray(item.prices) && item.prices.length > 0) {
|
||||
const vk1Obj = item.prices.find(p => String(p.level) === '1') || item.prices[0];
|
||||
if (vk1Obj && vk1Obj.value !== undefined) {
|
||||
// Deutsches Zahlenformat "2,86" -> 2.86
|
||||
preis1 = parseFloat(String(vk1Obj.value).replace(',', '.'));
|
||||
}
|
||||
} else if (typeof item.prices === 'object' && item.prices !== null) {
|
||||
preis1 = parseFloat(String(item.prices.vk1 || item.prices.price1 || 0).replace(',', '.'));
|
||||
} else if (item.preis1 !== undefined) {
|
||||
preis1 = parseFloat(String(item.preis1).replace(',', '.'));
|
||||
} else if (item.price !== undefined) {
|
||||
preis1 = parseFloat(String(item.price).replace(',', '.'));
|
||||
}
|
||||
|
||||
const unit = item.contentUnit || item.unit || item.ContentUnit || item.Unit || '';
|
||||
const unit = item.contentUnit || item.salesUnit || 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);
|
||||
// 3. Deletions
|
||||
const deletionsData = json.deletions?.data || (Array.isArray(json.deletions) ? json.deletions : []);
|
||||
for (const del of deletionsData) {
|
||||
const id = String(del.id || del.articleNo || del);
|
||||
if (id) {
|
||||
deactivateProduct.run(id);
|
||||
deletionsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XML Format Fallback
|
||||
if (fileName.endsWith('.xml')) {
|
||||
let parsed = {};
|
||||
try { parsed = await parser.parseStringPromise(rawContent); } catch(e) {}
|
||||
const root = parsed.root || parsed;
|
||||
|
||||
// Warengruppen
|
||||
let groups = root.ProductGroups?.ProductGroup || root.productGroups?.productGroup || [];
|
||||
if (!Array.isArray(groups)) groups = [groups];
|
||||
for (const g of groups) {
|
||||
const id = String(g.id || g.groupId || '');
|
||||
const name = g.name || g.text || ('Kategorie ' + id);
|
||||
if (id) { insertGroup.run(id, name, 0); groupsCount++; }
|
||||
}
|
||||
|
||||
// Artikel
|
||||
let items = root.Products?.Product || root.products?.product || [];
|
||||
if (!Array.isArray(items)) items = [items];
|
||||
for (const item of items) {
|
||||
const id = String(item.id || item.articleNo || '');
|
||||
const text1 = item.text1 || item.name || 'Unbekannt';
|
||||
const text2 = item.text2 || '';
|
||||
const groupId = String(item.groupId || '');
|
||||
let preis1 = parseFloat(String(item.preis1 || item.price || 0).replace(',', '.'));
|
||||
if (id) { insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, ''); productsCount++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const msg = `Import: ${productsCount} Artikel, ${groupsCount} Gruppen, ${deletionsCount} Deaktiviert`;
|
||||
@@ -407,7 +331,7 @@ function importJsonData(data) {
|
||||
p.text1 || 'Unbekannt',
|
||||
p.text2 || '',
|
||||
String(p.groupId || ''),
|
||||
parseFloat(p.preis1 || p.price || 0),
|
||||
parseFloat(String(p.preis1 || p.price || 0).replace(',', '.')),
|
||||
p.unit || ''
|
||||
);
|
||||
count++;
|
||||
|
||||
Reference in New Issue
Block a user