diff --git a/src/store/article/importRows.js b/src/store/article/importRows.js index a65389a..f6350d1 100644 --- a/src/store/article/importRows.js +++ b/src/store/article/importRows.js @@ -1,40 +1,13 @@ -import * as mssqlArticle from '../../providers/mssql/queries/article.js'; -import * as sqliteArticle from '../../providers/sqlite3/queries/article.js'; -import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js'; -import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js'; -import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js'; -import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js'; -import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js'; -import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js'; -import { - recordParentConvertedHistory, - recordVariantAddedOnParentHistory, - recordChildAssignedToParentHistory, - recordArticleImportedHistory, -} from '../articlehistory/record.js'; -import { - CREATE as MSSQL_CREATE_BRAND, - GET_BY_COMPANY_AND_NAME as MSSQL_GET_BRAND_BY_NAME, -} from '../../providers/mssql/queries/brand.js'; -import { - CREATE as SQLITE_CREATE_BRAND, - GET_BY_COMPANY_AND_NAME as SQLITE_GET_BRAND_BY_NAME, -} from '../../providers/sqlite3/queries/brand.js'; -import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js'; -import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js'; -import { - CREATE as MSSQL_CREATE_TAX, - GET_BY_RATE as MSSQL_GET_TAX_BY_RATE, -} from '../../providers/mssql/queries/tax.js'; -import { - CREATE as SQLITE_CREATE_TAX, - GET_BY_RATE as SQLITE_GET_TAX_BY_RATE, -} from '../../providers/sqlite3/queries/tax.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import ensureByPath from '../category/ensureByPath.js'; -import ensureDefaultUnit from '../unit/ensureDefault.js'; +import createUnderParent from './createUnderParent.js'; +import getBySku from './getBySku.js'; +import setBrandIdBySku from './setBrandIdBySku.js'; +import setPricesBySku from './setPricesBySku.js'; +import setEansBySku from './setEansBySku.js'; +import setParentBySku from './setParentBySku.js'; +import brandUpsert from '../brand/upsert.js'; import makeRunner from '../runner.js'; -import resolveUniqueSlug from './resolveUniqueSlug.js'; // Normalizes a numeric value that may arrive as a string using German decimal // notation (e.g. "11,00"), a JS number, or null/undefined. Returns null when empty. @@ -51,40 +24,14 @@ function toNumber(value) { export default function importRows(provider) { const companyAccess = resolveSelectedCompanyAccess(provider); - const ensureUnit = ensureDefaultUnit(provider); + const getArticleBySku = getBySku(provider); + const createArticleUnderParent = createUnderParent(provider); const ensureCategoryByPath = ensureByPath(provider); - const resolveSlug = resolveUniqueSlug(provider); - const isSqlite = provider?.dialect === 'sqlite3'; - - const articleQueries = isSqlite ? sqliteArticle : mssqlArticle; - const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE; - const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT; - const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT; - const getBrandByNameSql = isSqlite ? SQLITE_GET_BRAND_BY_NAME : MSSQL_GET_BRAND_BY_NAME; - const createBrandSql = isSqlite ? SQLITE_CREATE_BRAND : MSSQL_CREATE_BRAND; - const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE; - const getTaxByRateSql = isSqlite ? SQLITE_GET_TAX_BY_RATE : MSSQL_GET_TAX_BY_RATE; - const createTaxSql = isSqlite ? SQLITE_CREATE_TAX : MSSQL_CREATE_TAX; - - // Find or create a brand for a company by name (within the current tx). - async function ensureBrand(runner, companyId, name) { - const existing = await runner.query(getBrandByNameSql, { - companyId, - name, - }); - - if (existing.rows[0]) { - return existing.rows[0].id; - } - - const created = await runner.query(createBrandSql, { - companyId, - name, - gsprpinfo: null, - }); - - return created.rows[0]?.id ?? created.insertId; - } + const upsertBrand = brandUpsert(provider); + const setBrandId = setBrandIdBySku(provider); + const setPrices = setPricesBySku(provider); + const setEans = setEansBySku(provider); + const setParent = setParentBySku(provider); /** * Imports article rows, creating articles by their SKU and returning per-row @@ -125,13 +72,7 @@ export default function importRows(provider) { */ return async ({ googleAuthId, rows }, tx) => { const baseRunner = makeRunner(provider, tx); - const { - companyId, - languageId, - rootCategoryId, - posCustomerGroupId, - companyuserId, - } = await companyAccess(googleAuthId, baseRunner); + const { rootCategoryId } = await companyAccess(googleAuthId, baseRunner); if (!Array.isArray(rows)) { throw new Error('Invalid rows'); @@ -141,6 +82,7 @@ export default function importRows(provider) { const skipped = []; const errors = []; const usedEans = new Set(); + const seenSkus = new Set(); const runInTx = async (fn) => { if (tx) { @@ -162,38 +104,25 @@ export default function importRows(provider) { continue; } - const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId }); - if (existing.rows[0]) { + if (seenSkus.has(sku)) { skipped.push({ sku, name, reason: 'duplicate sku' }); continue; } + try { + await getArticleBySku(googleAuthId, sku, baseRunner); + seenSkus.add(sku); + skipped.push({ sku, name, reason: 'duplicate sku' }); + continue; + } catch (err) { + if (!err.message?.includes('not found')) { + errors.push({ sku, name, error: err.message ?? String(err) }); + continue; + } + } + try { await runInTx(async (runner) => { - const unitId = await ensureUnit(runner, companyId, languageId); - const slug = await resolveSlug(runner, { companyId, languageId, name }); - - const rawTrack = row.track_stock ?? row.trackStock; - const trackStock = rawTrack == null ? 0 : (rawTrack ? 1 : 0); - if (trackStock === 1) { - const whRes = await runner.query(articleQueries.CHECK_HAS_WAREHOUSE, { companyId }); - if (!whRes.rows || whRes.rows.length === 0) { - throw new Error('At least one warehouse is required to enable stock tracking'); - } - } - - const article = await runner.query(articleQueries.CREATE, { - companyId, - sku, - unitId, - unitamount: 1, - weight: 0, - trackStock, - }); - - const articleId = article.rows[0]?.id ?? article.insertId; - - // Resolve the parent category id (explicit id or breadcrumb path). let parentId = row.parentId; if (parentId == null && row.parentCategory) { parentId = await ensureCategoryByPath( @@ -205,81 +134,53 @@ export default function importRows(provider) { throw new Error('Parent category required'); } - await runner.query(addCategoryArticleSql, { - categoryId: parentId, - articleId, - }); - - await runner.query(createArticleTextSql, { - articleId, - languageId, + const rawTrack = row.track_stock ?? row.trackStock; + const created = await createArticleUnderParent({ + googleAuthId, + parentId, name, - slug, + sku, desc: row.description ?? row.desc ?? null, - seo: null, - }); + track_stock: rawTrack, + }, runner); - await recordArticleImportedHistory(runner, { - articleId, - companyuserId, - name, - }); - - // Brand (upsert by name). const brandName = row.brand != null ? String(row.brand).trim() : ''; if (brandName) { - const brandId = await ensureBrand(runner, companyId, brandName); - await runner.query(articleQueries.UPDATE_BRAND, { - articleId, - brandId, - }); + const brand = await upsertBrand(googleAuthId, { name: brandName }, runner); + await setBrandId(googleAuthId, sku, brand.id, runner); } - // POS price (net/tax). const net = toNumber(row.net); const taxRate = toNumber(row.tax) ?? 0; if (net != null) { - const taxResult = await runner.query(getTaxByRateSql, { - rate: taxRate, - }); - let taxId = taxResult.rows[0]?.id; - if (!taxId) { - const created = await runner.query(createTaxSql, { - rate: taxRate, - }); - taxId = created.rows[0]?.id ?? created.insertId; - } - await runner.query(createPriceSql, { - taxId, - articleId, - minmultiplier: 1, - customerGroupId: posCustomerGroupId, - net, - }); + await setPrices(googleAuthId, sku, [{ net, taxRate }], runner); } - // EANs (array, or a single value; skip duplicates within the import). const rawEans = row.eans; const eans = Array.isArray(rawEans) ? rawEans : rawEans == null || rawEans === '' ? [] : [rawEans]; + + const validEans = []; for (const ean of eans) { const clean = String(ean ?? '').trim(); if (!clean || usedEans.has(clean)) continue; - const used = await runner.query(articleQueries.EAN_EXISTS, { - ean: clean, - }); - if (used.rows[0]) continue; - await runner.query(articleQueries.ADD_EAN, { - articleId, - ean: clean, - }); - usedEans.add(clean); + try { + await setEans(googleAuthId, sku, [...validEans, clean], runner); + validEans.push(clean); + usedEans.add(clean); + } catch (err) { + if (err.message?.includes('in use') || err.message?.includes('unique')) { + continue; + } + throw err; + } } - imported.push({ id: articleId, sku, name }); + seenSkus.add(sku); + imported.push({ id: created.id, sku, name }); }); } catch (err) { errors.push({ sku, name, error: err.message ?? String(err) }); @@ -295,59 +196,7 @@ export default function importRows(provider) { try { await runInTx(async (runner) => { - const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId }); - const childId = child.rows[0]?.articleId; - if (childId) { - const parent = await runner.query( - 'SELECT id, is_parent FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)', - { sku: parentSku, companyId }, - ); - const parentRow = parent.rows[0]; - const parentId = parentRow?.id; - if (parentId && parentId !== childId) { - const wasParent = Boolean(parentRow.is_parent); - - const existingSortsRes = await runner.query( - 'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC', - { parentId }, - ); - const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort))); - let parentsort = 0; - while (usedSorts.has(parentsort)) { - parentsort++; - } - await runner.query(articleQueries.UPDATE_PARENT_BY_SKU, { - sku, - companyId, - parentArticleId: parentId, - parentsort, - }); - await runner.query(articleQueries.SET_IS_PARENT, { - articleId: parentId, - }); - - if (!wasParent) { - await recordParentConvertedHistory(runner, { - articleId: parentId, - companyuserId, - }); - } - const childName = String(row?.name ?? sku).trim(); - await recordVariantAddedOnParentHistory(runner, { - parentId, - companyuserId, - childName, - childSku: sku, - }); - await recordChildAssignedToParentHistory(runner, { - childId, - companyuserId, - childName, - childSku: sku, - parentSku, - }); - } - } + await setParent(googleAuthId, sku, parentSku, runner); }); } catch { // Non-fatal: article was already imported in pass 1. diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index eb3d2d7..e5644d3 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -221,7 +221,7 @@ export default function updateBySku(provider) { } if (hasTrackStock) { - await setTrackStock(googleAuthId, sku, Boolean(rawTrackStock), activeTx); + await setTrackStock(googleAuthId, sku, Boolean(rawTrackStock), runner); } if (historyLines.length) {