Files
dashlibstorage/src/store/article/attachChild.js
2026-10-04 18:56:55 +02:00

322 lines
13 KiB
JavaScript

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<object>}>}
*/
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));
};
}