// MARK: - Imports // #region Imports import fs from 'fs'; import path from 'path'; import { getStore } from '../db/store.js'; import { createLogger } from '../util/logger.js'; import { cleanValue, parseGermanFloat, parseCsvRecords, parseSuppliers } from './companyImport/csvParser.js'; import { computeSkeleHash } from './companyImport/imageHasher.js'; import { normalizeAttributeName, extractVariantValue } from './companyImport/variantHelpers.js'; import { createProgressTracker } from './companyImport/progressTracker.js'; import { filterCatalogRecords, prepareImportTasks } from './companyImport/catalogPreprocessor.js'; import { importCategories } from './companyImport/categoryImporter.js'; import { importBrands } from './companyImport/brandImporter.js'; import { importArticles } from './companyImport/articleImporter.js'; import { importVariants } from './companyImport/variantImporter.js'; import { importPfand, extractPfandValue, formatPfandSku, formatPfandName } from './companyImport/pfandImporter.js'; import { importArticleImages } from './companyImport/articleImageImporter.js'; import { importOrders } from './companyImport/orderImporter.js'; // #endregion // MARK: - Re-exports // #region Re-exports export { cleanValue, parseGermanFloat, parseCsvRecords, parseSuppliers, computeSkeleHash, normalizeAttributeName, extractVariantValue, extractPfandValue, formatPfandSku, formatPfandName, importPfand, }; // #endregion const log = createLogger('import'); // MARK: - Company Import Orchestrator // #region Company Import Orchestrator /** * Executes full catalog import from preserved files in importDir. * * @param {object} opts * @param {number} opts.googleAuthId * @param {number} opts.companyId * @param {string} opts.importDir * @param {function} [opts.onProgress] - (counts) => void; throttled to 250ms * @param {function} [opts.onImages] - (skelehashes: string[]) => void; batched every 50ms * @param {number} [opts.progressThrottleMs=250] - Progress emission throttle/debounce interval in ms * @returns {Promise} Summary outcome with statistics */ export async function executeCompanyImport({ googleAuthId, companyId, importDir, onProgress = () => {}, onImages = () => {}, progressThrottleMs = 250, }) { const store = await getStore(); // MARK: - Verify and Read Input Files // #region Read CSVs const catCsvPath = path.join(importDir, 'categories.csv'); const artCsvPath = path.join(importDir, 'articles.csv'); if (!fs.existsSync(catCsvPath) || !fs.existsSync(artCsvPath)) { throw new Error('Import files missing: categories.csv and articles.csv are required'); } const catRecords = parseCsvRecords(fs.readFileSync(catCsvPath, 'utf8')); const artRecords = parseCsvRecords(fs.readFileSync(artCsvPath, 'utf8')); log.info(`Read ${catRecords.length} category rows and ${artRecords.length} article rows`); // #endregion // MARK: - Filter Records & Prepare Tasks // #region Preprocessing & Task Discovery const { includedCategories, includedArticles } = filterCatalogRecords(catRecords, artRecords); const { categoryImageTasks, brandMap, brandImageTasks, articleImageTasks, totalImagesCount, manRecords, itemRecords, totalReceiptsCount, } = prepareImportTasks({ importDir, includedCategories, includedArticles }); // #endregion // MARK: - Progress Tracker Initialization // #region Progress Setup const progressTracker = createProgressTracker({ initialCounts: { categoriestotal: includedCategories.length, categoriesdone: 0, articlestotal: includedArticles.length, articlesdone: 0, imagestotal: totalImagesCount, imagesdone: 0, receiptstotal: totalReceiptsCount, receiptsdone: 0, }, onProgress, onImages, progressThrottleMs, }); // Emit baseline counts immediately progressTracker.emitProgress(true); // #endregion // MARK: - Phase 1: Categories // #region Phase 1: Categories const categoriesResult = await importCategories({ store, googleAuthId, includedCategories, categoryImageTasks, progressTracker, }); // #endregion // MARK: - Phase 2: Brands // #region Phase 2: Brands const brandsResult = await importBrands({ store, googleAuthId, brandMap, brandImageTasks, progressTracker, }); // #endregion // MARK: - Phase 3: Articles (base data, variants, pfand, pictures) // Articles with stock tracking require at least one warehouse; create a default one implicitly. const existingWarehouses = await store.warehouse.list(googleAuthId); if (!existingWarehouses || existingWarehouses.length === 0) { await store.warehouse.create(googleAuthId, { name: 'Standardlager' }); } // #region Phase 3: Articles const articlesResult = await importArticles({ store, googleAuthId, includedArticles, articleImageTasks, jtlCatIdToDashCatId: categoriesResult.jtlCatIdToDashCatId, hauptDashId: categoriesResult.hauptDashId, brandNameToId: brandsResult.brandNameToId, progressTracker, }); // #endregion // MARK: - Phase 4: Orders / Receipts // #region Phase 4: Orders / Receipts const allArtIdToSku = new Map(); const parentSkus = new Set(articlesResult.parentSkus || []); const parentIds = new Set(); for (const a of artRecords) { const kArt = cleanValue(a.id ?? a.kArtikel); const sku = cleanValue(a.sku ?? a.cArtNr); if (kArt && sku) allArtIdToSku.set(kArt, sku); const pid = cleanValue(a.parentid ?? a.kVaterArtikel); if (pid && pid !== '0') parentIds.add(pid); const isParent = a.is_parent === '1' || a.nIstVater === '1'; if (isParent) { if (kArt) parentIds.add(kArt); if (sku) parentSkus.add(sku); } } for (const a of artRecords) { const kArt = cleanValue(a.id ?? a.kArtikel); if (kArt && parentIds.has(kArt)) { const sku = cleanValue(a.sku ?? a.cArtNr); if (sku) parentSkus.add(sku); } } const ordersResult = await importOrders({ store, googleAuthId, manRecords, itemRecords, jtlArtIdToSku: allArtIdToSku, parentSkus, progressTracker, }); // #endregion // Flush any pending images and progress updates progressTracker.flushSkeleHashes(); progressTracker.emitProgress(true); return { ok: true, message: 'Import erfolgreich abgeschlossen.', stats: { categories: categoriesResult.jtlCatIdToDashCatId.size, categoriesCreated: categoriesResult.createdCatCount, categoryPictures: categoriesResult.catPicCount, brands: brandsResult.brandNameToId.size, brandsCreated: brandsResult.createdBrandCount, brandPictures: brandsResult.brandPicCount, articlesTotal: includedArticles.length, articlesImported: articlesResult.articlesImported, articlesUpdated: articlesResult.articlesUpdated, variantsLinked: articlesResult.variantsLinked, pfandArticlesCreated: articlesResult.pfandArticlesCreated, pfandArticlesUpdated: articlesResult.pfandArticlesUpdated, pfandOptionsLinked: articlesResult.pfandOptionsLinked, suppliersLinked: articlesResult.suppliersLinked || 0, articlePictures: articlesResult.totalImagesImported, ordersTotal: totalReceiptsCount, ordersImported: ordersResult.ordersImported, ordersSkipped: ordersResult.ordersSkipped, paymentsImported: ordersResult.paymentsImported, shippingsImported: ordersResult.shippingsImported, customersImported: ordersResult.customersImported || 0, }, }; } // #endregion