All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m16s
276 lines
11 KiB
JavaScript
276 lines
11 KiB
JavaScript
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 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,
|
|
compositions: [],
|
|
compositionComponents: [],
|
|
productionOrders: []
|
|
};
|
|
|
|
const query = async (sql, params = []) => {
|
|
const normalizedSql = String(sql).replace(/\s+/g, ' ').trim();
|
|
|
|
if (/^(BEGIN|COMMIT|ROLLBACK);?$/.test(normalizedSql)) 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(result.composition.finishedTinyProductId, '976058813');
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|