Files
dashback/src/services/companyImport.js
2026-10-16 01:25:00 +02:00

222 lines
7.5 KiB
JavaScript

// 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 } 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,
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<object>} 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)
// #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,
articlePictures: articlesResult.totalImagesImported,
ordersTotal: totalReceiptsCount,
ordersImported: ordersResult.ordersImported,
ordersSkipped: ordersResult.ordersSkipped,
paymentsImported: ordersResult.paymentsImported,
shippingsImported: ordersResult.shippingsImported,
},
};
}
// #endregion