import { getProvider } from './store.js'; export async function getCompanyCatalogMeta(companyId) { const provider = await getProvider(); const result = await provider.query( `SELECT id, name, root_category_id AS rootCategoryId, default_language_id AS languageId, poscustomergroup_id AS posCustomerGroupId FROM company WHERE id = @companyId`, { companyId } ); return result.rows[0] ?? null; } export async function listCategories(companyId) { const company = await getCompanyCatalogMeta(companyId); if (!company) { throw new Error('Company not found'); } if (!company.rootCategoryId) { return []; } const provider = await getProvider(); const isSqlite = provider.dialect === 'sqlite3'; const sql = isSqlite ? `WITH RECURSIVE tree AS ( SELECT id, parent_id, sort FROM category WHERE id = @rootId UNION ALL SELECT c.id, c.parent_id, c.sort FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT t.id, t.parent_id AS parentId, t.sort, ct.name, ct.slug, p.hash AS fullHash, p.thumbw AS thumbW, p.thumbh AS thumbH, p.thumbhash AS thumbHash FROM tree t LEFT JOIN categorytext ct ON ct.category_id = t.id AND ct.language_id = @languageId LEFT JOIN pic p ON p.id = ( SELECT cp.pic_id FROM categorypic cp WHERE cp.category_id = t.id ORDER BY cp.sort, cp.id LIMIT 1 ) ORDER BY t.sort, t.id` : `WITH tree AS ( SELECT id, parent_id, sort FROM category WHERE id = @rootId UNION ALL SELECT c.id, c.parent_id, c.sort FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT t.id, t.parent_id AS parentId, t.sort, ct.name, ct.slug, p.hash AS fullHash, p.thumbw AS thumbW, p.thumbh AS thumbH, p.thumbhash AS thumbHash FROM tree t LEFT JOIN categorytext ct ON ct.category_id = t.id AND ct.language_id = @languageId OUTER APPLY ( SELECT TOP 1 cp.pic_id FROM categorypic cp WHERE cp.category_id = t.id ORDER BY cp.sort ASC, cp.id ASC ) first_pic LEFT JOIN pic p ON p.id = first_pic.pic_id ORDER BY t.sort, t.id`; const result = await provider.query(sql, { rootId: company.rootCategoryId, languageId: company.languageId, }); return result.rows.map((row) => ({ id: row.id, parentId: row.parentId, sort: row.sort, name: row.name ?? '', slug: row.slug ?? '', fullHash: row.fullHash ?? null, thumbW: row.thumbW ?? null, thumbH: row.thumbH ?? null, thumbHash: row.thumbHash ?? null, })); } export async function listArticles(companyId, { categoryId, limit = 50, offset = 0, search } = {}) { const company = await getCompanyCatalogMeta(companyId); if (!company) { throw new Error('Company not found'); } const provider = await getProvider(); const isSqlite = provider.dialect === 'sqlite3'; const safeLimit = Math.min(Math.max(1, Number(limit) || 50), 200); const safeOffset = Math.max(0, Number(offset) || 0); const trimmedSearch = typeof search === 'string' ? search.trim() : ''; const params = { companyId, languageId: company.languageId, posCustomerGroupId: company.posCustomerGroupId, limit: safeLimit, offset: safeOffset, }; if (categoryId != null) { params.categoryId = Number(categoryId); } if (trimmedSearch) { params.searchPattern = `%${trimmedSearch}%`; } const whereClauses = [ 'a.company_id = @companyId', 'a.fatherarticle_id IS NULL', ]; if (categoryId != null) { whereClauses.push('ca.category_id = @categoryId'); } if (trimmedSearch) { whereClauses.push('(a.sku LIKE @searchPattern OR at.name LIKE @searchPattern)'); } const whereSql = whereClauses.join(' AND '); const fromClause = categoryId != null ? 'categoryarticle ca INNER JOIN article a ON a.id = ca.article_id' : 'article a'; const sql = isSqlite ? `SELECT a.id, a.sku, at.name, at.slug, pr.net, t.rate AS taxRate, a.track_stock AS trackStock, pic.hash AS fullHash, pic.thumbw AS thumbW, pic.thumbh AS thumbH, pic.thumbhash AS thumbHash, b.id AS brandId, b.name AS brandName FROM ${fromClause} LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId LEFT JOIN price pr ON pr.article_id = a.id AND pr.customergroup_id = @posCustomerGroupId AND pr.minmultiplier = 1 LEFT JOIN tax t ON t.id = pr.tax_id LEFT JOIN pic ON pic.id = ( SELECT ap.pic_id FROM articlepic ap WHERE ap.article_id = a.id ORDER BY ap.sort, ap.id LIMIT 1 ) LEFT JOIN brand b ON b.id = a.brand_id WHERE ${whereSql} ORDER BY a.id LIMIT @limit OFFSET @offset` : `SELECT a.id, a.sku, at.name, at.slug, pr.net, t.rate AS taxRate, a.track_stock AS trackStock, pic.hash AS fullHash, pic.thumbw AS thumbW, pic.thumbh AS thumbH, pic.thumbhash AS thumbHash, b.id AS brandId, b.name AS brandName FROM ${fromClause} LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId LEFT JOIN price pr ON pr.article_id = a.id AND pr.customergroup_id = @posCustomerGroupId AND pr.minmultiplier = 1 LEFT JOIN tax t ON t.id = pr.tax_id OUTER APPLY ( SELECT TOP 1 ap.pic_id FROM articlepic ap WHERE ap.article_id = a.id ORDER BY ap.sort ASC, ap.id ASC ) first_pic LEFT JOIN pic ON pic.id = first_pic.pic_id LEFT JOIN brand b ON b.id = a.brand_id WHERE ${whereSql} ORDER BY a.id OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`; const result = await provider.query(sql, params); return result.rows.map((row) => { const net = row.net != null ? Number(row.net) : null; const taxRate = row.taxRate != null ? Number(row.taxRate) : null; const gross = net != null && taxRate != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null; return { id: row.id, sku: row.sku, name: row.name ?? '', slug: row.slug ?? '', net, taxRate, gross, trackStock: Boolean(row.trackStock ?? 1), brand: row.brandName ? { id: row.brandId ?? null, name: row.brandName, } : null, fullHash: row.fullHash ?? null, thumbW: row.thumbW ?? null, thumbH: row.thumbH ?? null, thumbHash: row.thumbHash ?? null, }; }); } export async function getArticleBySku(companyId, sku) { const company = await getCompanyCatalogMeta(companyId); if (!company) { throw new Error('Company not found'); } const provider = await getProvider(); const isSqlite = provider.dialect === 'sqlite3'; const sql = isSqlite ? `SELECT a.id, a.sku, at.name, at.slug, at.desc AS description, a.unitamount, a.weight, a.track_stock AS trackStock, u.si AS unitSi, u.basesi AS unitBasesi, u.basesifactor AS unitBasesifactor, ut.symbol AS unitSymbol, ut.name AS unitName, b.id AS brandId, b.name AS brandName, brand_pic.hash AS brandFullHash, brand_pic.thumbw AS brandThumbW, brand_pic.thumbh AS brandThumbH, brand_pic.thumbhash AS brandThumbHash, a.is_father AS isFather, a.fatherarticle_id AS fatherArticleId, father.sku AS fatherSku FROM article a LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId LEFT JOIN unit u ON u.id = a.unit_id LEFT JOIN unittext ut ON ut.unit_id = u.id AND ut.language_id = @languageId LEFT JOIN brand b ON b.id = a.brand_id LEFT JOIN pic brand_pic ON brand_pic.id = ( SELECT bp.pic_id FROM brandpic bp WHERE bp.brand_id = b.id ORDER BY bp.sort, bp.id LIMIT 1 ) LEFT JOIN article father ON father.id = a.fatherarticle_id WHERE a.sku = @sku AND a.company_id = @companyId` : `SELECT a.id, a.sku, at.name, at.slug, at.desc AS description, a.unitamount, a.weight, a.track_stock AS trackStock, u.si AS unitSi, u.basesi AS unitBasesi, u.basesifactor AS unitBasesifactor, ut.symbol AS unitSymbol, ut.name AS unitName, b.id AS brandId, b.name AS brandName, brand_pic.hash AS brandFullHash, brand_pic.thumbw AS brandThumbW, brand_pic.thumbh AS brandThumbH, brand_pic.thumbhash AS brandThumbHash, a.is_father AS isFather, a.fatherarticle_id AS fatherArticleId, father.sku AS fatherSku FROM article a LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId LEFT JOIN unit u ON u.id = a.unit_id LEFT JOIN unittext ut ON ut.unit_id = u.id AND ut.language_id = @languageId LEFT JOIN brand b ON b.id = a.brand_id OUTER APPLY ( SELECT TOP 1 bp.pic_id FROM brandpic bp WHERE bp.brand_id = b.id ORDER BY bp.sort ASC, bp.id ASC ) first_brand_pic LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id LEFT JOIN article father ON father.id = a.fatherarticle_id WHERE a.sku = @sku AND a.company_id = @companyId`; const result = await provider.query(sql, { sku, companyId, languageId: company.languageId, }); const row = result.rows[0]; if (!row) { throw new Error('Article not found'); } const articleId = row.id; const [categoriesRes, photosRes, eansRes, pricesRes, variantsRes] = await Promise.all([ provider.query( `SELECT ca.category_id AS categoryId, ct.name, ct.slug FROM categoryarticle ca LEFT JOIN categorytext ct ON ct.category_id = ca.category_id AND ct.language_id = @languageId WHERE ca.article_id = @articleId ORDER BY ca.category_id`, { articleId, languageId: company.languageId } ), provider.query( `SELECT ap.sort, p.hash AS fullHash, p.w AS fullW, p.h AS fullH, p.size AS fullSize, p.thumbw AS thumbW, p.thumbh AS thumbH, p.thumbhash AS thumbHash FROM articlepic ap INNER JOIN pic p ON p.id = ap.pic_id WHERE ap.article_id = @articleId ORDER BY ap.sort ASC, ap.id ASC`, { articleId } ), provider.query( `SELECT ae.ean FROM articleean ae WHERE ae.article_id = @articleId ORDER BY ae.ean`, { articleId } ), provider.query( `SELECT pr.minmultiplier, pr.net, t.rate AS taxRate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = @articleId AND pr.customergroup_id = @posCustomerGroupId ORDER BY pr.minmultiplier ASC`, { articleId, posCustomerGroupId: company.posCustomerGroupId } ), row.isFather ? provider.query( `SELECT child.id, child.sku, child.fathersort, child.track_stock AS trackStock, child.unitamount, child.weight, at_child.name, at_child.slug, av.id AS attributeValueId, av.value AS attributeValue FROM article child LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId LEFT JOIN articleattributevalue aav ON aav.article_id = child.id LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id WHERE child.fatherarticle_id = @articleId ORDER BY child.fathersort ASC, child.id ASC`, { articleId, languageId: company.languageId } ) : Promise.resolve({ rows: [] }), ]); const prices = pricesRes.rows.map((p) => { const net = p.net != null ? Number(p.net) : null; const taxRate = p.taxRate != null ? Number(p.taxRate) : null; const gross = net != null && taxRate != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null; return { minmultiplier: p.minmultiplier, net, taxRate, gross, }; }); const primaryPrice = prices[0] ?? null; return { id: row.id, sku: row.sku, name: row.name ?? '', slug: row.slug ?? '', description: row.description ?? '', unitamount: row.unitamount, weight: row.weight, trackStock: Boolean(row.trackStock ?? 1), unit: row.unitSi ? { si: row.unitSi, basesi: row.unitBasesi, basesifactor: row.unitBasesifactor, symbol: row.unitSymbol ?? null, name: row.unitName ?? null, } : null, brand: row.brandName ? { id: row.brandId ?? null, name: row.brandName, fullHash: row.brandFullHash ?? null, thumbW: row.brandThumbW ?? null, thumbH: row.brandThumbH ?? null, thumbHash: row.brandThumbHash ?? null, } : null, price: primaryPrice, prices, categories: categoriesRes.rows.map((c) => ({ id: c.categoryId, name: c.name ?? '', slug: c.slug ?? '', })), photos: photosRes.rows.map((p) => ({ sort: p.sort, fullHash: p.fullHash, fullW: p.fullW, fullH: p.fullH, fullSize: p.fullSize, thumbW: p.thumbW, thumbH: p.thumbH, thumbHash: p.thumbHash, })), eans: eansRes.rows.map((e) => e.ean), isFather: Boolean(row.isFather), fatherSku: row.fatherSku ?? null, variants: variantsRes.rows.map((v) => ({ id: v.id, sku: v.sku, sort: v.fathersort, name: v.name ?? '', slug: v.slug ?? '', attributeValueId: v.attributeValueId ?? null, attributeValue: v.attributeValue ?? null, unitamount: v.unitamount, weight: v.weight, trackStock: Boolean(v.trackStock ?? 1), })), }; }