import * as mssqlArticle from '../../providers/mssql/queries/article.js'; import * as sqliteArticle from '../../providers/sqlite3/queries/article.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 { 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 listChildren from './listChildren.js'; import makeRunner from '../runner.js'; export default function attachChild(provider) { const resolve = resolveArticleBySku(provider); const load = getBySku(provider); const list = listChildren(provider); const isSqlite = provider?.dialect === 'sqlite3'; const queries = isSqlite ? sqliteArticle : mssqlArticle; const attrValQueries = isSqlite ? sqliteAttrVal : mssqlAttrVal; const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal; const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE; const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; /** * Attaches an existing article as a child 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 {string} options.sku - The child article SKU to attach. * @param {string} [options.childSku] - Alias for options.sku. * @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 {boolean} [options.reparent=false] - Whether to allow re-parenting if child belongs to another father. * @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<{article: object, children: Array}>} */ return async (googleAuthId, fatherSku, options = {}, tx) => { const run = async (runner) => { const childSku = (typeof options.sku === 'string' && options.sku.trim()) || (typeof options.childSku === 'string' && options.childSku.trim()) || null; if (!childSku) { throw new Error('sku is required to attach an existing article'); } 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, is_father, 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'); } // Check child article exists in company const childRes = await runner.query( 'SELECT id, sku, company_id, fatherarticle_id, is_father FROM article WHERE sku = @childSku AND company_id = @companyId', { childSku, companyId }, ); const child = childRes.rows[0]; if (!child) { throw new Error('Article not found'); } if (child.id === fatherId) { throw new Error('An article cannot be a child of itself'); } // 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'); } const isAlreadyChildOfThisFather = child.fatherarticle_id === fatherId; if (child.fatherarticle_id != null && !isAlreadyChildOfThisFather && !options.reparent) { throw new Error('Article already belongs to another father'); } // Check if father already has a different child with this attribute value const existingVariantRes = await runner.query( `SELECT a.id FROM article a INNER JOIN articleattributevalue aav ON aav.article_id = a.id WHERE a.fatherarticle_id = @fatherId AND a.id != @childId AND aav.attributevalue_id = @finalValueId`, { fatherId, childId: child.id, finalValueId }, ); if (existingVariantRes.rows.length > 0) { throw new Error('Variant with this attribute value already exists'); } // If re-parenting from another father, unparent and recompact previous father if (child.fatherarticle_id != null && !isAlreadyChildOfThisFather && options.reparent) { const prevFatherId = child.fatherarticle_id; await runner.query(insertHistorySql, { articleId: prevFatherId, companyuserId, text: `Variante (${childSku}) getrennt (umgehängt zu "${father.sku}")`, }); // Recompact previous father's children const prevRemainingRes = await runner.query( 'SELECT id, fathersort FROM article WHERE fatherarticle_id = @prevFatherId AND id != @childId ORDER BY fathersort ASC, id ASC', { prevFatherId, childId: child.id }, ); for (let i = 0; i < prevRemainingRes.rows.length; i++) { const row = prevRemainingRes.rows[i]; if (Number(row.fathersort) !== i) { await runner.query(queries.SET_FATHER_SORT, { articleId: row.id, fatherArticleId: prevFatherId, fathersort: i, }); } } } // Assign fathersort if not already child of this father let fathersort; if (isAlreadyChildOfThisFather) { const currentSortRes = await runner.query( 'SELECT fathersort FROM article WHERE id = @childId', { childId: child.id }, ); fathersort = Number(currentSortRes.rows[0]?.fathersort ?? 0); } else { const existingSortsRes = await runner.query( 'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC', { fatherId }, ); const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort))); fathersort = 0; while (usedSorts.has(fathersort)) { fathersort++; } } // Link child to father await runner.query(queries.UPDATE_FATHER_BY_SKU, { sku: childSku, companyId, fatherArticleId: fatherId, fathersort, }); // Apply optional field overrides if (options.unitamount != null) { await runner.query('UPDATE article SET unitamount = @unitamount WHERE id = @childId', { childId: child.id, unitamount: options.unitamount, }); } if (options.weight != null) { await runner.query(queries.UPDATE_WEIGHT, { articleId: child.id, weight: options.weight, }); } const rawTrack = options.track_stock ?? options.trackStock; if (rawTrack != null) { await runner.query(queries.UPDATE_TRACK_STOCK, { articleId: child.id, trackStock: rawTrack ? 1 : 0, }); } // Remove existing attribute value for this father's attribute on child (if any) if (father.fatherattributename_id) { await runner.query( `DELETE FROM articleattributevalue WHERE article_id = @childId AND attributevalue_id IN ( SELECT id FROM attributevalue WHERE attribute_id = @attributeId )`, { childId: child.id, attributeId: father.fatherattributename_id }, ); } await runner.query(articleAttrValQueries.INSERT_ROW, { articleId: child.id, attributeValueId: finalValueId, sort: 0, }); // Copy ancestor attributes from father (if missing on child) 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) { const hasAttrRes = await runner.query( 'SELECT 1 FROM articleattributevalue WHERE article_id = @childId AND attributevalue_id = @attrValId', { childId: child.id, attrValId: faRow.attributevalue_id }, ); if (hasAttrRes.rows.length === 0) { await runner.query(articleAttrValQueries.INSERT_ROW, { articleId: child.id, attributeValueId: faRow.attributevalue_id, sort: faRow.sort, }); } } } // Copy father's categories (if missing on child) const catRes = await runner.query( 'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId', { fatherId }, ); for (const catRow of catRes.rows) { const hasCatRes = await runner.query( 'SELECT 1 FROM categoryarticle WHERE article_id = @childId AND category_id = @categoryId', { childId: child.id, categoryId: catRow.category_id }, ); if (hasCatRes.rows.length === 0) { await runner.query(addCategoryArticleSql, { categoryId: catRow.category_id, articleId: child.id, }); } } // Fetch child name for history const childTextRes = await runner.query( 'SELECT name FROM articletext WHERE article_id = @childId AND language_id = @languageId', { childId: child.id, languageId }, ); const childName = childTextRes.rows[0]?.name || childSku; // History only if not already child if (!isAlreadyChildOfThisFather) { await runner.query(insertHistorySql, { articleId: child.id, companyuserId, text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" zugewiesen`, }); await runner.query(insertHistorySql, { articleId: fatherId, companyuserId, text: `Variante "${childName}" (${childSku}) hinzugefügt`, }); } const updated = await load(googleAuthId, childSku, runner); const children = await list(googleAuthId, fatherSku, runner); return { article: updated.article, children, }; }; if (tx) { return run(makeRunner(provider, tx)); } if (typeof provider?.transaction === 'function') { return provider.transaction(run); } return run(makeRunner(provider)); }; }