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 mssqlAttr from '../../providers/mssql/queries/attributename.js'; import * as sqliteAttr from '../../providers/sqlite3/queries/attributename.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'; export default function toggleFather(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 attrQueries = isSqlite ? sqliteAttr : mssqlAttr; const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; /** * Toggles an article into or out of father mode. * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. * @param {object} options * @param {boolean} options.isFather - Whether the article should be a father article. * @param {number} [options.attributeNameId] - The attribute name id required when enabling father mode. * @param {number} [options.fatherAttributeNameId] - Alias for attributeNameId. * @param {object} [tx] - Optional transaction. * @returns {Promise} The updated article detail. */ return async (googleAuthId, sku, { isFather, attributeNameId, fatherAttributeNameId } = {}, tx) => { const run = async (runner) => { const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner); const targetIsFather = Boolean(isFather); const targetAttrId = attributeNameId ?? fatherAttributeNameId; if (targetIsFather) { if (!targetAttrId || Number(targetAttrId) <= 0) { throw new Error('attributeNameId is required to become a father article'); } const currentRes = await runner.query( 'SELECT fatherarticle_id, articlelist_id, optionslist_id FROM article WHERE id = @articleId', { articleId }, ); const currentArt = currentRes.rows[0]; if (currentArt?.articlelist_id != null) { throw new Error('A Stückliste article cannot become a father'); } if (currentArt?.optionslist_id != null) { throw new Error('An article with options cannot become a father'); } const stockRes = await runner.query( `SELECT COALESCE(SUM(l.amount), 0) AS totalStock FROM ledger l INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifest m ON m.id = ma.manifest_id WHERE m.company_id = @companyId AND ( ma.sku = @sku OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = @articleId) )`, { companyId, sku, articleId }, ); const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0); if (totalStock !== 0) { throw new Error('An article with existing stock cannot become a father article'); } const supRes = await runner.query( 'SELECT COUNT(*) AS count FROM articlesupplier WHERE article_id = @articleId', { articleId }, ); if ((supRes.rows[0]?.count ?? 0) > 0) { throw new Error('An article with assigned suppliers cannot become a father article'); } // Fetch existing prices before deletion to record in articlehistory const existingPricesRes = await runner.query( `SELECT pr.net, pr.minmultiplier, t.rate AS taxRate, cg.name AS customerGroupName FROM price pr LEFT JOIN tax t ON t.id = pr.tax_id LEFT JOIN customergroup cg ON cg.id = pr.customergroup_id WHERE pr.article_id = @articleId ORDER BY pr.customergroup_id, pr.minmultiplier`, { articleId }, ); const existingPrices = existingPricesRes.rows; await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId }); await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId }); // If article is a child, verify attribute is not used by father or any father up the chain let currentFatherId = currentArt?.fatherarticle_id; const visited = new Set(); while (currentFatherId != null && !visited.has(currentFatherId)) { visited.add(currentFatherId); const fRes = await runner.query( 'SELECT a.fatherarticle_id, a.fatherattributename_id, an.name AS fatherAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.fatherattributename_id WHERE a.id = @currentFatherId', { currentFatherId }, ); const fRow = fRes.rows[0]; if (!fRow) break; if (fRow.fatherattributename_id != null && Number(fRow.fatherattributename_id) === Number(targetAttrId)) { const attrName = fRow.fatherAttributeName || 'ancestor'; throw new Error(`Attribute "${attrName}" is already used by an ancestor father article`); } currentFatherId = fRow.fatherarticle_id; } // Verify attribute exists and belongs to company const attrRes = await runner.query(attrQueries.GET_BY_COMPANY_AND_ID, { id: Number(targetAttrId), companyId, }); if (!attrRes.rows[0]) { throw new Error('Attribute name not found'); } const attributeName = attrRes.rows[0].name; // Verify article does not have an attribute value for this attribute const existingValRes = await runner.query( `SELECT aav.id FROM articleattributevalue aav INNER JOIN attributevalue av ON av.id = aav.attributevalue_id WHERE aav.article_id = @articleId AND av.attribute_id = @attributeId`, { articleId, attributeId: Number(targetAttrId) }, ); if (existingValRes.rows.length > 0) { throw new Error('A father article cannot have attribute values for its variant attribute'); } await runner.query(queries.TOGGLE_FATHER, { articleId, isFather: 1, fatherAttributeNameId: Number(targetAttrId), }); // Add history entry recording father conversion and deleted prices const historyLines = [ `Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`, ]; if (existingPrices.length > 0) { const priceParts = existingPrices.map((p) => { let str = `${p.net} €`; if (p.taxRate != null) { str += ` (${p.taxRate}% MwSt.)`; } if (p.minmultiplier > 1) { str += ` ab ${p.minmultiplier} Stk.`; } if (p.customerGroupName) { str += ` [${p.customerGroupName}]`; } return str; }); const label = existingPrices.length === 1 ? 'Bisheriger Preis gelöscht' : 'Bisherige Preise gelöscht'; historyLines.push(`${label}: ${priceParts.join(', ')}`); } await runner.query(insertHistorySql, { articleId, companyuserId, text: historyLines.join('\n'), }); } else { // Disabling father: verify zero children const childCountRes = await runner.query(getChildCountSql, { articleId }); const childCount = childCountRes.rows[0]?.count ?? 0; if (childCount > 0) { throw new Error('Cannot disable father mode on an article that still has children'); } await runner.query(queries.TOGGLE_FATHER, { articleId, isFather: 0, fatherAttributeNameId: null, }); await runner.query(insertHistorySql, { articleId, companyuserId, text: 'Vaterartikel-Modus deaktiviert', }); } return load(googleAuthId, sku, runner); }; if (tx) { return run(makeRunner(provider, tx)); } if (typeof provider?.transaction === 'function') { return provider.transaction(run); } return run(makeRunner(provider)); }; }