// MARK: - Imports // #region Imports import { cleanValue, parseGermanFloat } from './csvParser.js'; import { createLogger } from '../../util/logger.js'; // #endregion const log = createLogger('import'); // MARK: - Order / Manifest Importer // #region Order / Manifest Importer /** * Imports order manifests and line items, resolving customer entities, payments, and shippings. * * @param {object} params * @param {object} params.store - Store singleton * @param {number} params.googleAuthId * @param {Array>} params.manRecords * @param {Array>} params.itemRecords * @param {Map} [params.jtlArtIdToSku] * @param {Set} [params.parentSkus] * @param {object} params.progressTracker * @returns {Promise<{ ordersImported: number, ordersSkipped: number, paymentsImported: number, shippingsImported: number }>} */ export async function importOrders({ store, googleAuthId, manRecords, itemRecords, jtlArtIdToSku = new Map(), parentSkus = new Set(), progressTracker, }) { let ordersImported = 0; let ordersSkipped = 0; let paymentsImported = 0; let shippingsImported = 0; if (manRecords.length === 0 || itemRecords.length === 0) { return { ordersImported, ordersSkipped, paymentsImported, shippingsImported }; } const itemsByAuftrag = new Map(); for (const it of itemRecords) { const kAuftrag = String(it.manifestid ?? it.kAuftrag ?? '').trim(); if (!kAuftrag) continue; if (!itemsByAuftrag.has(kAuftrag)) { itemsByAuftrag.set(kAuftrag, []); } itemsByAuftrag.get(kAuftrag).push(it); } const existingOrdersResult = await store.order.list(googleAuthId); const existingOrderList = Array.isArray(existingOrdersResult) ? existingOrdersResult : (existingOrdersResult?.orders || []); const existingOrderNumbers = new Set( existingOrderList.map((o) => (o.number || '').trim()).filter(Boolean) ); let defaultCustomerGroupId = null; try { const groups = await store.customergroup.list(googleAuthId); const posGroup = groups.find((g) => g.name === 'POS' || g.name?.toUpperCase() === 'POS'); defaultCustomerGroupId = posGroup?.id || groups[0]?.id || null; } catch (err) { log.warn('Could not list customer groups:', err.message); } let defaultWarehouseId = null; try { const warehouses = await store.warehouse.list(googleAuthId); if (warehouses && warehouses.length > 0) { defaultWarehouseId = warehouses[0].id; } else { const wh = await store.warehouse.create(googleAuthId, { name: 'Standardlager' }); defaultWarehouseId = wh.id; } } catch (err) { log.warn('Could not ensure warehouse:', err.message); } let customersImported = 0; const entityMap = new Map(); const entityByNumber = new Map(); try { const entities = await store.entity.list(googleAuthId); for (const ent of entities) { if (ent.name) { entityMap.set(ent.name.trim().toLowerCase(), ent.id); } if (ent.number) { entityByNumber.set(String(ent.number).trim().toLowerCase(), ent.id); } } } catch (err) { log.warn('Could not list existing entities:', err.message); } const resolveEntityId = async (name, firstName, number) => { const cleanName = (name || '').trim(); const cleanNr = (number || '').trim(); if (!cleanName || !defaultCustomerGroupId) return null; const nrKey = cleanNr ? cleanNr.toLowerCase() : null; if (nrKey && entityByNumber.has(nrKey)) { return entityByNumber.get(nrKey); } const key = cleanName.toLowerCase(); if (!nrKey && entityMap.has(key)) return entityMap.get(key); try { const ent = await store.entity.create(googleAuthId, { customergroupId: defaultCustomerGroupId, name: cleanName, firstName: (firstName || '').trim() || undefined, number: cleanNr || undefined, }); entityMap.set(key, ent.id); if (ent.number) { entityByNumber.set(String(ent.number).trim().toLowerCase(), ent.id); } if (nrKey) { entityByNumber.set(nrKey, ent.id); } customersImported++; return ent.id; } catch (err) { if (cleanNr && err.message?.includes('Number already in use')) { try { const refreshed = await store.entity.list(googleAuthId).catch(() => []); const found = refreshed.find( (e) => String(e.number).trim().toLowerCase() === nrKey ); if (found) { entityByNumber.set(nrKey, found.id); if (found.name) entityMap.set(found.name.trim().toLowerCase(), found.id); return found.id; } } catch {} try { const ent = await store.entity.create(googleAuthId, { customergroupId: defaultCustomerGroupId, name: cleanName, firstName: (firstName || '').trim() || undefined, }); entityMap.set(key, ent.id); if (ent.number) { entityByNumber.set(String(ent.number).trim().toLowerCase(), ent.id); } customersImported++; return ent.id; } catch (retryErr) { log.warn(`Could not create entity "${cleanName}":`, retryErr.message); return null; } } log.warn(`Could not create entity "${cleanName}":`, err.message); return null; } }; const accountMap = new Map(); try { const accounts = await store.account.list(googleAuthId); for (const acc of accounts) { if (acc.name) { accountMap.set(acc.name.trim().toLowerCase(), acc.id); } } } catch (err) { log.warn('Could not list accounts:', err.message); } const resolveAccountId = async (accountName) => { const cleanAccName = (accountName || 'Bar').trim(); const key = cleanAccName.toLowerCase(); if (accountMap.has(key)) return accountMap.get(key); try { const created = await store.account.create(googleAuthId, { type: cleanAccName, name: cleanAccName, }); accountMap.set(key, created.id); return created.id; } catch (err) { log.warn(`Could not create account "${cleanAccName}":`, err.message); return accountMap.values().next().value || null; } }; const carrierMap = new Map(); try { const carriers = await store.carrier.list(googleAuthId); for (const c of carriers) { if (c.name) { carrierMap.set(c.name.trim().toLowerCase(), c.id); } } } catch (err) { log.warn('Could not list carriers:', err.message); } const resolveCarrierId = async (name) => { const cleanName = (name || '').trim() || 'Standard'; const key = cleanName.toLowerCase(); if (carrierMap.has(key)) return carrierMap.get(key); try { const carrier = await store.carrier.create(googleAuthId, { name: cleanName, hasPackages: true, }); carrierMap.set(key, carrier.id); return carrier.id; } catch (err) { const list = await store.carrier.list(googleAuthId).catch(() => []); const found = list.find((c) => c.name.trim().toLowerCase() === key); if (found) { carrierMap.set(key, found.id); return found.id; } log.warn(`Could not create carrier "${cleanName}":`, err.message); return null; } }; const processManifest = async (m) => { const orderNumber = cleanValue(m.nr ?? m.cAuftragsNr); if (!orderNumber || existingOrderNumbers.has(orderNumber)) { ordersSkipped++; progressTracker.counts.receiptsdone++; progressTracker.emitProgress(); return; } const manifestKey = String(m.id ?? m.kAuftrag ?? '').trim(); const rawItems = itemsByAuftrag.get(manifestKey) || []; if (rawItems.length === 0) { ordersSkipped++; progressTracker.counts.receiptsdone++; progressTracker.emitProgress(); return; } rawItems.sort((a, b) => (Number(a.sort ?? a.nSort) || 0) - (Number(b.sort ?? b.nSort) || 0)); const lines = rawItems.map((it) => { const articleId = cleanValue(it.articleid ?? it.kArtikel); let sku = cleanValue(it.sku ?? it.cArtNr) || (articleId && jtlArtIdToSku?.get?.(articleId)) || null; if (sku && parentSkus.has(sku)) { sku = null; } const rawName = cleanValue(it.name ?? it.cName ?? it.cNameStandard); const name = rawName || (sku ? `Artikel ${sku}` : 'Artikel'); const amount = parseGermanFloat(it.amount ?? it.fAnzahl, 1); const net = parseGermanFloat(it.net ?? it.fVKNetto, 0); const taxrate = parseGermanFloat(it.taxrate ?? it.fMwSt, 19); const rebate = parseGermanFloat(it.rebate ?? it.fRabatt, 0); const comment = cleanValue(it.comment ?? it.cHinweis) || null; let shippable = true; if (it.shippable !== undefined && it.shippable !== null && String(it.shippable).trim() !== '') { const s = String(it.shippable).trim().toLowerCase(); shippable = s !== '0' && s !== 'false'; } else if (/^(summe glätten|shippingcost|versandkosten)$/i.test(name.trim())) { shippable = false; } return { sku, name, amount, net, taxrate, rebate, comment, shippable, }; }); const custName = cleanValue(m.customerName ?? m.name ?? m.cName) || 'Laufkunde'; const custFirstName = cleanValue(m.firstName ?? m.cVorname); const custNr = cleanValue(m.customerNr ?? m.cKundenNr); const entityId = await resolveEntityId(custName, custFirstName, custNr); const dateStr = cleanValue(m.created_at ?? m.dErstellt); const parsedDate = dateStr ? new Date(dateStr) : new Date(); const validDate = !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : new Date().toISOString(); let createdOrder = null; try { createdOrder = await store.order.create(googleAuthId, { number: orderNumber, externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null, date: validDate, createdAt: validDate, entityId: entityId || undefined, lines, }); existingOrderNumbers.add(orderNumber); ordersImported++; } catch (err) { const parentMatch = err.message?.match(/A parent article cannot be booked on documents \(([^)]+)\)/); if (parentMatch) { const offendingSku = parentMatch[1]; parentSkus.add(offendingSku); for (const l of lines) { if (l.sku === offendingSku) l.sku = null; } try { createdOrder = await store.order.create(googleAuthId, { number: orderNumber, externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null, date: validDate, createdAt: validDate, entityId: entityId || undefined, lines, }); existingOrderNumbers.add(orderNumber); ordersImported++; } catch (retryErr) { log.warn(`Failed to create order ${orderNumber} after stripping parent SKU ${offendingSku}:`, retryErr.message); } } else { log.warn(`Failed to create order ${orderNumber}:`, err.message); } } if (createdOrder) { // Handle Payment const isPayed = String(m.payed ?? '').trim() === '1'; const paymentType = cleanValue(m.paymentType); if (isPayed && paymentType) { const orderTotal = Math.round( lines.reduce( (sum, l) => sum + (l.amount * l.net * (1 - (l.rebate || 0) / 100) * (1 + (l.taxrate || 0) / 100)), 0 ) * 100 ) / 100; if (orderTotal > 0) { try { const accountId = await resolveAccountId(paymentType); if (accountId) { await store.payment.create(googleAuthId, { accountId, manifestId: createdOrder.id, type: paymentType, amount: orderTotal, }); paymentsImported++; } } catch (err) { log.warn(`Failed to create payment for order ${orderNumber}:`, err.message); } } } // Handle Delivery / Shipping const shippingType = cleanValue(m.shippingType); const isShipped = String(m.shipped ?? '').trim() === '1'; if (isShipped || shippingType) { const orderLines = createdOrder?.lines || lines; const shippableLines = orderLines .filter((l) => l.shippable !== false) .map((l) => { if (l.sku) return { sku: l.sku, amount: l.amount }; if (l.id) return { orderLineId: l.id, amount: l.amount }; return null; }) .filter(Boolean); if (shippableLines.length > 0) { try { const isSelbstabholer = (shippingType || '').trim().toLowerCase() === 'selbstabholer'; const deliveryState = isShipped ? 'delivered' : 'new'; const shipment = await store.shipment.create(googleAuthId, { orderId: createdOrder.id, date: validDate, state: deliveryState, lines: shippableLines, }); if (!isSelbstabholer) { const trackingCode = cleanValue(m.trackingCode ?? m.tracking_code ?? m.cTracking); const carrierName = cleanValue(m.carrier ?? m.cCarrier) || 'Standard'; const carrierId = await resolveCarrierId(carrierName); const shippingState = isShipped ? 'shipped' : 'draft'; if (carrierId) { await store.shipping.create(googleAuthId, { manifestId: shipment.id, carrierId, type: shippingType || 'standard', state: shippingState, trackingCode: trackingCode || undefined, warehouseId: defaultWarehouseId || undefined, }); shippingsImported++; } } } catch (err) { log.warn(`Failed to create shipment/shipping for order ${orderNumber}:`, err.message); } } } } progressTracker.counts.receiptsdone++; progressTracker.emitProgress(); }; const runTx = async (fn) => { if (typeof store.transaction === 'function') { return store.transaction(fn); } if (typeof store.provider?.transaction === 'function') { return store.provider.transaction(fn); } return fn(); }; const BATCH_SIZE = 25; for (let i = 0; i < manRecords.length; i += BATCH_SIZE) { const batch = manRecords.slice(i, i + BATCH_SIZE); try { await runTx(async () => { for (const m of batch) { await processManifest(m); } }); } catch (batchErr) { log.warn(`Batch of ${batch.length} manifests failed in transaction, falling back to one-by-one:`, batchErr.message); for (const m of batch) { try { await processManifest(m); } catch (singleErr) { log.warn(`Single manifest failed:`, singleErr.message); progressTracker.counts.receiptsdone++; progressTracker.emitProgress(); } } } await new Promise((resolve) => setImmediate(resolve)); } progressTracker.emitProgress(true); log.info(`Orders complete: ${ordersImported} created, ${ordersSkipped} skipped, ${paymentsImported} payments, ${shippingsImported} shippings, ${customersImported} customers`); return { ordersImported, ordersSkipped, paymentsImported, shippingsImported, customersImported, }; } // #endregion