import * as mssqlArticle from '../../providers/mssql/queries/article.js'; import * as sqliteArticle from '../../providers/sqlite3/queries/article.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 * as mssqlAttrVal from '../../providers/mssql/queries/attributevalue.js'; import * as sqliteAttrVal from '../../providers/sqlite3/queries/attributevalue.js'; import * as mssqlArticleAttrVal from '../../providers/mssql/queries/articleattributevalue.js'; import * as sqliteArticleAttrVal from '../../providers/sqlite3/queries/articleattributevalue.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 { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js'; import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js'; import resolveArticleBySku from './resolveArticleBySku.js'; import getBySku from './getBySku.js'; import makeRunner from '../runner.js'; import slugify from '../slugify.js'; export default function addChild(provider) { const resolve = resolveArticleBySku(provider); const load = getBySku(provider); const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteArticle : mssqlArticle; const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT; const attrValQueries = isSqlite ? sqliteAttrVal : mssqlAttrVal; const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal; const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE; const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT; const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; /** * Adds a child article to a father article. * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} fatherSku - The father article SKU. * @param {object} options * @param {number} [options.attributeValueId] - Existing attribute value ID for father's attribute. * @param {string} [options.value] - String value to find or create under father's attribute. * @param {string} [options.name] - Custom name for child article (defaults to father name + value). * @param {number} [options.unitamount] - Optional unitamount override. * @param {number} [options.weight] - Optional weight override. * @param {boolean|number} [options.track_stock] - Optional track_stock override. * @param {object} [tx] - Optional transaction. * @returns {Promise} The created child article detail. */ return async (googleAuthId, fatherSku, options = {}, tx) => { const run = async (runner) => { const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve( googleAuthId, fatherSku, runner, ); // Fetch father article fields const fatherRes = await runner.query( 'SELECT id, sku, company_id, unit_id, unitamount, weight, track_stock, brand_id, is_father, fatherarticle_id, fatherattributename_id FROM article WHERE id = @fatherId', { fatherId }, ); const father = fatherRes.rows[0]; if (!father) { throw new Error('Father article not found'); } if (!father.is_father || !father.fatherattributename_id) { throw new Error('Article is not a father article'); } // Fetch father's text / name const fatherTextRes = await runner.query( 'SELECT name FROM articletext WHERE article_id = @fatherId AND language_id = @languageId', { fatherId, languageId }, ); const fatherName = fatherTextRes.rows[0]?.name || father.sku; // Determine attribute value let finalValueId = null; let finalValue = null; if (options.attributeValueId != null) { if (typeof options.attributeValueId !== 'number' || !Number.isInteger(options.attributeValueId) || options.attributeValueId <= 0) { throw new Error('Invalid attributeValueId'); } const valRes = await runner.query(attrValQueries.GET_BY_ID_AND_COMPANY, { id: options.attributeValueId, companyId, }); const row = valRes.rows[0]; if (!row || row.attributeId !== father.fatherattributename_id) { throw new Error('Attribute value not found or does not match father attribute'); } finalValueId = row.id; finalValue = row.value; } else if (typeof options.value === 'string' && options.value.trim()) { const valStr = options.value.trim(); const existingValRes = await runner.query(attrValQueries.GET_BY_ATTRIBUTE_AND_VALUE, { attributeId: father.fatherattributename_id, value: valStr, }); if (existingValRes.rows[0]) { finalValueId = existingValRes.rows[0].id; finalValue = existingValRes.rows[0].value; } else { const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, { attributeId: father.fatherattributename_id, }); const sort = sortRes.rows[0]?.sort ?? 0; const createdValRes = await runner.query(attrValQueries.CREATE, { attributeId: father.fatherattributename_id, value: valStr, sort, }); finalValueId = createdValRes.rows[0]?.id ?? createdValRes.insertId; finalValue = valStr; } } else { throw new Error('Attribute value is required for child article'); } // Compute child SKU: father.sku + '-' + finalValueId const childSku = `${father.sku}-${finalValueId}`; // Check if child SKU already exists const existingChildRes = await runner.query( 'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId', { childSku, companyId }, ); if (existingChildRes.rows[0]) { throw new Error(`Child article "${childSku}" already exists`); } // Next fathersort const countRes = await runner.query(getChildCountSql, { articleId: fatherId }); const fathersort = countRes.rows[0]?.count ?? 0; // Create child article const rawTrack = options.track_stock ?? options.trackStock; const finalTrackStock = rawTrack == null ? father.track_stock : (rawTrack ? 1 : 0); const childRes = await runner.query(queries.CREATE_CHILD, { companyId, sku: childSku, unitId: father.unit_id, unitamount: options.unitamount ?? father.unitamount, weight: options.weight ?? father.weight, trackStock: finalTrackStock, brandId: father.brand_id, fatherArticleId: fatherId, fathersort, }); const childId = childRes.rows[0]?.id ?? childRes.insertId; // Copy existing attribute values from father (ancestor attributes that father has) const fatherAttrsRes = await runner.query( 'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @fatherId', { fatherId }, ); for (const faRow of fatherAttrsRes.rows) { if (faRow.attributevalue_id !== finalValueId) { await runner.query(articleAttrValQueries.INSERT_ROW, { articleId: childId, attributeValueId: faRow.attributevalue_id, sort: faRow.sort, }); } } // Link articleattributevalue await runner.query(articleAttrValQueries.INSERT_ROW, { articleId: childId, attributeValueId: finalValueId, sort: 0, }); // Copy father's categories const catRes = await runner.query( 'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId', { fatherId }, ); for (const catRow of catRes.rows) { await runner.query(addCategoryArticleSql, { categoryId: catRow.category_id, articleId: childId, }); } // Create article text const childName = options.name?.trim() || `${fatherName} - ${finalValue}`; const childSlug = slugify(childName); await runner.query(createArticleTextSql, { articleId: childId, languageId, name: childName, slug: childSlug, }); // History await runner.query(insertHistorySql, { articleId: childId, companyuserId, text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`, }); return load(googleAuthId, childSku, runner); }; if (tx) { return run(makeRunner(provider, tx)); } if (typeof provider?.transaction === 'function') { return provider.transaction(run); } return run(makeRunner(provider)); }; }