const { pool } = require('../db'); const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim(); const normalizeSku = (value) => normalizeText(value).toUpperCase(); const isRawOrService = (value) => /\b(?:MALHA|RIBANA|FIO|TECIDO|SERVICO|SERVIÇO|TINTURARIA|TECELAGEM|FRETE)\b/i.test(value); const getDataHealthSummary = async () => { const [productsResult, compositionsResult, componentsResult, stockResult] = await Promise.all([ pool.query(` SELECT produto_id AS id, MAX(NULLIF(produto_descricao, '')) AS name FROM orders GROUP BY produto_id UNION SELECT produto_id AS id, MAX(NULLIF(nome, '')) AS name FROM stock GROUP BY produto_id; `), pool.query(` SELECT id, finished_tiny_product_id, finished_product_sku, finished_product_description, finished_product_unit FROM product_compositions WHERE source = 'tiny_olist_v3'; `), pool.query(` SELECT product_composition_id, component_tiny_id, component_sku, component_name, quantity_per_unit, unit FROM product_composition_components; `), pool.query(`SELECT produto_id, nome, COALESCE(saldo, 0)::numeric AS saldo FROM stock;`) ]); const products = Array.from(new Map(productsResult.rows.map(row => [normalizeSku(row.id), row])).values()); const compositions = compositionsResult.rows; const components = componentsResult.rows; const stock = stockResult.rows; const stockKeys = new Set(stock.flatMap(item => [normalizeSku(item.produto_id), normalizeSku(item.nome)]).filter(Boolean)); const compositionKeys = new Set(compositions.flatMap(item => [normalizeSku(item.finished_tiny_product_id), normalizeSku(item.finished_product_sku)]).filter(Boolean)); const productKeys = new Set(products.map(item => normalizeSku(item.id)).filter(Boolean)); const linkedComponentKeys = new Set([...stockKeys, ...productKeys]); const materialKeys = new Set(components.map(item => normalizeSku(item.component_tiny_id) || normalizeSku(item.component_sku) || normalizeSku(item.component_name)).filter(Boolean)); const materialStockKeys = new Set([...materialKeys].filter(key => stockKeys.has(key))); const rowsByComposition = new Map(compositions.map(item => [Number(item.id), item])); const issues = []; const addIssue = (type, severity, title, detail, productSku = '', component = '') => { issues.push({ type, severity, title, detail, productSku, component }); }; compositions.filter(row => !normalizeText(row.finished_product_sku)).forEach(row => { addIssue('missing_product_sku', 'critical', 'Produto sem SKU', normalizeText(row.finished_product_description) || 'Composição sem descrição', '', ''); }); components.filter(row => { const key = normalizeSku(row.component_tiny_id) || normalizeSku(row.component_sku) || normalizeSku(row.component_name); return !key || !linkedComponentKeys.has(key); }).forEach(row => { const parent = rowsByComposition.get(Number(row.product_composition_id)); addIssue('component_without_stock_link', 'attention', 'Componente sem vínculo de estoque', normalizeText(row.component_name) || 'Componente sem nome', normalizeText(parent?.finished_product_sku), normalizeText(row.component_sku || row.component_tiny_id)); }); components.filter(row => { const quantity = Number(row.quantity_per_unit || 0); return !Number.isFinite(quantity) || quantity <= 0 || quantity > 100 || !normalizeText(row.unit); }).forEach(row => { const parent = rowsByComposition.get(Number(row.product_composition_id)); addIssue('suspicious_quantity', 'attention', 'Quantidade suspeita', `${normalizeText(row.component_name)} · ${row.quantity_per_unit || 0} ${normalizeText(row.unit) || '(sem unidade)'}`, normalizeText(parent?.finished_product_sku), normalizeText(row.component_sku || row.component_tiny_id)); }); compositions.filter(row => isRawOrService(row.finished_product_description) || normalizeText(row.finished_product_unit).toLowerCase() !== 'un').forEach(row => { addIssue('raw_or_service_structure', 'info', 'Estrutura de matéria-prima/serviço', normalizeText(row.finished_product_description), normalizeText(row.finished_product_sku), ''); }); products.filter(row => !compositionKeys.has(normalizeSku(row.id))).forEach(row => { addIssue('product_without_composition', 'attention', 'Produto sem composição', normalizeText(row.name) || normalizeText(row.id), normalizeText(row.id), ''); }); const productsWithComposition = products.filter(row => compositionKeys.has(normalizeSku(row.id))).length; const productsWithStockLink = products.filter(row => stockKeys.has(normalizeSku(row.id))).length; return { totals: { products: productKeys.size, productsWithComposition, productsWithStockLink, materials: materialKeys.size, materialsWithStock: materialStockKeys.size, compositions: compositions.length, components: components.length }, issues: issues.sort((a, b) => { const severityOrder = { critical: 0, attention: 1, info: 2 }; return severityOrder[a.severity] - severityOrder[b.severity] || a.title.localeCompare(b.title); }) }; }; module.exports = { getDataHealthSummary };