const assert = require('node:assert/strict'); const test = require('node:test'); const { pool } = require('../db'); const productionOrderRouter = require('../routes/productionOrderRoutes'); const analyticsRouter = require('../routes/analyticsRoutes'); const getRouteHandler = (router, method, path) => { const route = router.stack.find(layer => layer.route?.path === path && layer.route.methods[method]); if (!route) throw new Error(`Route not found: ${method.toUpperCase()} ${path}`); return route.route.stack.at(-1).handle; }; const tinySyncHandler = getRouteHandler(productionOrderRouter, 'post', '/production-orders/tiny-sync'); const tinyCompositionImportHandler = getRouteHandler(productionOrderRouter, 'post', '/production-orders/tiny-compositions/import'); const productCompositionHandler = getRouteHandler(analyticsRouter, 'get', '/analytics/products/:productId/composition'); const productCompositionsHandler = getRouteHandler(analyticsRouter, 'get', '/analytics/product-compositions'); const invokeHandler = async (handler, req) => { let statusCode = 200; let body; const res = { status(code) { statusCode = code; return this; }, json(payload) { body = payload; return this; } }; await handler(req, res); return { statusCode, body }; }; const compositionPayload = (components = [ { componentTinyId: '919498232', componentName: 'ETIQUETA DE TAMANHO GG', componentSku: 'ETIQ.T.GG', quantityPerUnit: '1', totalQuantity: '1', unit: 'UN' }, { componentTinyId: '976059144', componentName: '6118 MALHA CAMISETA 30OE COR CAFE', componentSku: 'MC.30.CAF', quantityPerUnit: '0.1851', totalQuantity: '0.1851', unit: 'KG' } ]) => ({ order: { tinyId: 'STRUCTURE-V3-976058813', number: 'STRUCTURE-V3-BLCS.CAF.GG', status: 'completed', productSku: 'BLCS.CAF.GG', productDescription: 'BASE LISA CAMISETA COR CAFE TAMANHO - GG', quantity: '1', unit: 'UN', notes: 'Composição sincronizada da API pública Olist V3.' }, components, steps: [] }); const withFakeDatabase = async (run) => { const originalConnect = pool.connect; const originalQuery = pool.query; const state = { nextCompositionId: 1, nextOrderId: 1, nextCategoryId: 1, nextCatalogProductId: 1, nextConsumptionReferenceId: 1, compositions: [], compositionComponents: [], productionOrders: [], categories: [], catalogProducts: [], consumptionReferences: [] }; const query = async (sql, params = []) => { const normalizedSql = String(sql).replace(/\s+/g, ' ').trim(); if (/^(BEGIN|COMMIT|ROLLBACK);?$/.test(normalizedSql)) return { rows: [] }; if (normalizedSql.startsWith('INSERT INTO catalog_categories')) { let category = state.categories.find(item => item.name === params[0]); if (!category) { category = { id: state.nextCategoryId++, name: params[0], description: params[1] }; state.categories.push(category); } return { rows: [{ id: category.id }] }; } if (normalizedSql.startsWith('INSERT INTO catalog_products')) { let product = state.catalogProducts.find(item => item.sku === params[1]); if (!product) { product = { id: state.nextCatalogProductId++, type: params[0], sku: params[1], name: params[2], category_id: params[3], notes: params[4] }; state.catalogProducts.push(product); } else { product.type = params[0]; product.name = params[2]; product.category_id = product.category_id || params[3]; product.notes = product.notes || params[4]; } return { rows: [{ id: product.id }] }; } if (normalizedSql.startsWith('SELECT id FROM consumption_references')) { const reference = state.consumptionReferences.find(item => ( item.product_id === params[0] && (item.material_product_id || 0) === params[1] && (item.color || '') === '' && (item.source || 'manual') === params[2] )); return { rows: reference ? [{ id: reference.id }] : [] }; } if (normalizedSql.startsWith('UPDATE consumption_references')) { const reference = state.consumptionReferences.find(item => item.id === params[5]); if (reference) { reference.general_yield = params[0]; reference.consumption_quantity = params[1]; reference.consumption_unit = params[2]; reference.last_production_order_id = params[3]; reference.source = params[4]; } return { rows: [] }; } if (normalizedSql.startsWith('INSERT INTO consumption_references')) { state.consumptionReferences.push({ id: state.nextConsumptionReferenceId++, product_id: params[0], material_product_id: params[1], general_yield: params[2], consumption_quantity: params[3], consumption_unit: params[4], source: params[5], last_production_order_id: params[6] }); return { rows: [] }; } if (normalizedSql.startsWith('SELECT id FROM product_compositions WHERE source')) { const composition = state.compositions.find(item => item.source === params[0] && item.finished_product_identity === params[1]); return { rows: composition ? [{ id: composition.id }] : [] }; } if (normalizedSql.startsWith('INSERT INTO product_compositions')) { const composition = { id: state.nextCompositionId++, source: params[0], external_source_id: params[1], finished_product_identity: params[2], finished_product_sku: params[3], finished_product_description: params[4], finished_product_unit: params[5], finished_tiny_product_id: params[6], source_metadata: JSON.parse(params[7]), last_synced_at: '2026-07-29T12:00:00.000Z' }; state.compositions.push(composition); return { rows: [{ id: composition.id }] }; } if (normalizedSql.startsWith('UPDATE product_compositions')) { const composition = state.compositions.find(item => item.id === params[7]); composition.external_source_id = params[0]; composition.finished_product_sku = params[2]; composition.finished_product_description = params[3]; composition.finished_product_unit = params[4]; composition.finished_tiny_product_id = params[5]; composition.source_metadata = JSON.parse(params[6]); return { rows: [] }; } if (normalizedSql.startsWith('DELETE FROM product_composition_components')) { state.compositionComponents = state.compositionComponents.filter(item => item.product_composition_id !== params[0]); return { rows: [] }; } if (normalizedSql.startsWith('INSERT INTO product_composition_components')) { state.compositionComponents.push({ id: state.compositionComponents.length + 1, product_composition_id: params[0], component_identity: params[1], component_tiny_id: params[2], component_sku: params[3], component_name: params[4], quantity_per_unit: params[5], unit: params[6] }); return { rows: [] }; } if (normalizedSql.includes('FROM product_compositions composition')) { const compositions = state.compositions.filter(item => ( item.source === params[0] && (params[1] === '' || item.finished_tiny_product_id === params[1] || item.finished_product_sku === String(params[1]).toUpperCase()) )); return { rows: compositions.map(composition => ({ ...composition, components: state.compositionComponents .filter(item => item.product_composition_id === composition.id) .map(item => ({ ...item, component_product_id: null })) })) }; } if (normalizedSql.startsWith('SELECT id FROM production_orders')) { return { rows: [] }; } if (normalizedSql.startsWith('INSERT INTO production_orders')) { const order = { id: state.nextOrderId++, tiny_id: params[0], number: params[1], status: params[2], order_reference: params[3], issue_date: params[4], expected_date: params[5], product_sku: params[6], product_description: params[7], quantity: params[8], unit: params[9], integration_status: params[10], notes: params[11], supplier: params[12], lot_code: params[13], roll_quantity: params[14], fabric_kg: params[15], rib_kg: params[16], yield_pieces_per_kg: params[17], created_at: null, updated_at: null }; state.productionOrders.push(order); return { rows: [{ id: order.id }] }; } if (normalizedSql.startsWith('DELETE FROM production_order_components') || normalizedSql.startsWith('DELETE FROM production_order_steps')) { return { rows: [] }; } if (normalizedSql.includes('FROM production_orders po') && normalizedSql.includes('WHERE po.id = $1')) { const order = state.productionOrders.find(item => item.id === params[0]); return { rows: order ? [{ ...order, markers: [], components: [], steps: [] }] : [] }; } throw new Error(`Unexpected SQL in test double: ${normalizedSql.slice(0, 100)}`); }; pool.query = query; pool.connect = async () => ({ query, release() {} }); try { await run(state); } finally { pool.connect = originalConnect; pool.query = originalQuery; } }; test('Tiny/Olist V3 first import creates a composition without creating an OP', async () => { await withFakeDatabase(async (state) => { const response = await invokeHandler(tinySyncHandler, { body: compositionPayload() }); const result = response.body; assert.equal(response.statusCode, 201); assert.equal(result.componentCount, 2); assert.equal(state.compositions.length, 1); assert.equal(state.compositionComponents.length, 2); assert.equal(state.productionOrders.length, 0); assert.equal(state.consumptionReferences.length, 2); assert.equal(result.composition.finishedTinyProductId, '976058813'); }); }); test('Tiny/Olist V3 bulk import stores compositions and consumption references', async () => { await withFakeDatabase(async (state) => { const response = await invokeHandler(tinyCompositionImportHandler, { body: { exportedAt: '2026-07-31T13:37:01.340Z', compositions: [{ finishedProductSku: 'BLCS.CAF.GG', finishedProductDescription: 'BASE LISA CAMISETA COR CAFE TAMANHO - GG', finishedProductUnit: 'UN', finishedTinyProductId: '976058813', sourceMetadata: { payload: compositionPayload() }, components: [] }] } }); assert.equal(response.statusCode, 201); assert.equal(response.body.imported, 1); assert.equal(response.body.referenceCount, 2); assert.equal(state.compositions.length, 1); assert.equal(state.consumptionReferences.length, 2); assert.equal(state.consumptionReferences[0].source, 'tiny_structure'); }); }); test('Tiny/Olist V3 re-sync replaces composition components without duplicates', async () => { await withFakeDatabase(async (state) => { await invokeHandler(tinySyncHandler, { body: compositionPayload() }); await invokeHandler(tinySyncHandler, { body: compositionPayload([ { componentTinyId: '976059144', componentName: 'MALHA ATUALIZADA', componentSku: 'MC.30.CAF', quantityPerUnit: '0.2', totalQuantity: '0.2', unit: 'KG' }, { componentTinyId: '976059144', componentName: 'MALHA ATUALIZADA', componentSku: 'MC.30.CAF', quantityPerUnit: '0.2', totalQuantity: '0.2', unit: 'KG' } ]) }); assert.equal(state.compositions.length, 1); assert.equal(state.compositionComponents.length, 1); assert.equal(state.compositionComponents[0].quantity_per_unit, 0.2); assert.equal(state.productionOrders.length, 0); }); }); test('product composition detail lookup returns stored components', async () => { await withFakeDatabase(async () => { await invokeHandler(tinySyncHandler, { body: compositionPayload() }); const response = await invokeHandler(productCompositionHandler, { params: { productId: '976058813' } }); const composition = response.body.composition; assert.equal(response.statusCode, 200); assert.equal(composition.finishedProductSku, 'BLCS.CAF.GG'); assert.equal(composition.components.length, 2); assert.deepEqual(composition.components[0], { id: 1, componentTinyId: '919498232', componentSku: 'ETIQ.T.GG', componentName: 'ETIQUETA DE TAMANHO GG', quantityPerUnit: 1, unit: 'UN', productId: null }); }); }); test('product composition export endpoint returns every stored composition', async () => { await withFakeDatabase(async () => { await invokeHandler(tinySyncHandler, { body: compositionPayload() }); const response = await invokeHandler(productCompositionsHandler, { params: {} }); assert.equal(response.statusCode, 200); assert.equal(response.body.compositions.length, 1); assert.equal(response.body.compositions[0].components.length, 2); }); }); test('ordinary Tiny production-order payloads continue to create production orders', async () => { await withFakeDatabase(async (state) => { const response = await invokeHandler(tinySyncHandler, { body: { order: { tinyId: '12345', number: 'OP-12345', status: 'in_progress', productSku: 'BLCS.CAF.GG', productDescription: 'BASE LISA CAMISETA COR CAFE TAMANHO - GG', quantity: '10', unit: 'UN' }, components: [], steps: [] } }); const result = response.body; assert.equal(response.statusCode, 201); assert.equal(state.compositions.length, 0); assert.equal(state.productionOrders.length, 1); assert.equal(result.order.tinyId, '12345'); assert.equal(result.order.number, 'OP-12345'); }); });