Support Tiny Olist V3 product compositions
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m57s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m57s
This commit is contained in:
@@ -182,6 +182,40 @@ const initDB = async () => {
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS product_compositions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
source VARCHAR(60) NOT NULL,
|
||||
external_source_id VARCHAR(120) NOT NULL,
|
||||
finished_product_identity VARCHAR(255) NOT NULL,
|
||||
finished_product_sku VARCHAR(255),
|
||||
finished_product_description TEXT NOT NULL,
|
||||
finished_product_unit VARCHAR(30) NOT NULL DEFAULT 'UN',
|
||||
finished_tiny_product_id VARCHAR(100),
|
||||
source_metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
last_synced_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (source, finished_product_identity)
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS product_composition_components (
|
||||
id SERIAL PRIMARY KEY,
|
||||
product_composition_id INTEGER NOT NULL REFERENCES product_compositions(id) ON DELETE CASCADE,
|
||||
component_identity VARCHAR(255) NOT NULL,
|
||||
component_tiny_id VARCHAR(100),
|
||||
component_sku VARCHAR(255),
|
||||
component_name TEXT NOT NULL,
|
||||
quantity_per_unit NUMERIC(14, 4) NOT NULL DEFAULT 0,
|
||||
unit VARCHAR(30),
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (product_composition_id, component_identity)
|
||||
);
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS cutting_family_rules (
|
||||
family_key VARCHAR(20) PRIMARY KEY,
|
||||
@@ -504,6 +538,8 @@ const initDB = async () => {
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_components_order_id ON production_order_components (production_order_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_components_sku ON production_order_components (component_sku);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_production_order_steps_order_id ON production_order_steps (production_order_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_product_compositions_finished_tiny_id ON product_compositions (finished_tiny_product_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_product_composition_components_tiny_id ON product_composition_components (component_tiny_id);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_cutting_product_overrides_family_key ON cutting_product_overrides (family_key);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_type ON catalog_products (type);`);
|
||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_catalog_products_category_id ON catalog_products (category_id);`);
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
getProductDetailsAnalytics,
|
||||
getRfmAnalytics
|
||||
} = require('../services/analyticsService');
|
||||
const { getProductComposition } = require('../services/productionOrderService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -58,6 +59,16 @@ router.get('/analytics/products/:productId/details', verifyToken, async (req, re
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/analytics/products/:productId/composition', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const composition = await getProductComposition(req.params.productId);
|
||||
res.json({ composition });
|
||||
} catch (error) {
|
||||
console.error('Error fetching product composition:', error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/analytics/clients', verifyToken, async (req, res) => {
|
||||
try {
|
||||
res.json(await getClientAnalytics(getClientAnalyticsFilters(req.query)));
|
||||
|
||||
@@ -650,9 +650,223 @@ const normalizeSteps = (steps = []) => (
|
||||
color: normalizeText(step.color || step.cor)
|
||||
})).filter(step => step.name);
|
||||
|
||||
const TINY_OLIST_V3_COMPOSITION_SOURCE = 'tiny_olist_v3';
|
||||
const TINY_OLIST_V3_COMPOSITION_PREFIX = 'STRUCTURE-V3-';
|
||||
|
||||
const isTinyOlistV3Composition = (order) => order.tinyId.startsWith(TINY_OLIST_V3_COMPOSITION_PREFIX);
|
||||
|
||||
const getTinyProductIdFromCompositionReference = (tinyId) => (
|
||||
tinyId.slice(TINY_OLIST_V3_COMPOSITION_PREFIX.length)
|
||||
);
|
||||
|
||||
const normalizeCompositionComponents = (components) => {
|
||||
const byIdentity = new Map();
|
||||
|
||||
for (const component of components) {
|
||||
const componentIdentity = normalizeSku(component.componentSku)
|
||||
|| normalizeText(component.componentTinyId)
|
||||
|| normalizeText(component.componentName).toUpperCase();
|
||||
if (!componentIdentity) continue;
|
||||
|
||||
byIdentity.set(componentIdentity, {
|
||||
...component,
|
||||
componentSku: normalizeSku(component.componentSku),
|
||||
componentIdentity
|
||||
});
|
||||
}
|
||||
|
||||
return [...byIdentity.values()];
|
||||
};
|
||||
|
||||
const mapProductCompositionRow = (row) => ({
|
||||
id: Number(row.id),
|
||||
source: row.source,
|
||||
externalSourceId: row.external_source_id,
|
||||
finishedProductSku: row.finished_product_sku || '',
|
||||
finishedProductDescription: row.finished_product_description || '',
|
||||
finishedProductUnit: row.finished_product_unit || 'UN',
|
||||
finishedTinyProductId: row.finished_tiny_product_id || '',
|
||||
sourceMetadata: row.source_metadata || {},
|
||||
lastSyncedAt: row.last_synced_at || null,
|
||||
components: Array.isArray(row.components) ? row.components.map(component => ({
|
||||
id: Number(component.id),
|
||||
componentTinyId: component.component_tiny_id || '',
|
||||
componentSku: component.component_sku || '',
|
||||
componentName: component.component_name || '',
|
||||
quantityPerUnit: Number(component.quantity_per_unit || 0),
|
||||
unit: component.unit || '',
|
||||
productId: component.component_product_id || null
|
||||
})) : []
|
||||
});
|
||||
|
||||
const getProductComposition = async (productId, client = pool) => {
|
||||
const identity = normalizeText(productId);
|
||||
if (!identity) return null;
|
||||
|
||||
const result = await client.query(`
|
||||
SELECT
|
||||
composition.id,
|
||||
composition.source,
|
||||
composition.external_source_id,
|
||||
composition.finished_product_sku,
|
||||
composition.finished_product_description,
|
||||
composition.finished_product_unit,
|
||||
composition.finished_tiny_product_id,
|
||||
composition.source_metadata,
|
||||
composition.last_synced_at,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', component.id,
|
||||
'component_tiny_id', component.component_tiny_id,
|
||||
'component_sku', component.component_sku,
|
||||
'component_name', component.component_name,
|
||||
'quantity_per_unit', component.quantity_per_unit,
|
||||
'unit', component.unit,
|
||||
'component_product_id', COALESCE(
|
||||
(
|
||||
SELECT stock_match.produto_id
|
||||
FROM stock stock_match
|
||||
WHERE stock_match.produto_id = component.component_tiny_id
|
||||
OR stock_match.produto_id = component.component_sku
|
||||
LIMIT 1
|
||||
),
|
||||
(
|
||||
SELECT order_match.produto_id
|
||||
FROM orders order_match
|
||||
WHERE order_match.produto_id = component.component_tiny_id
|
||||
OR order_match.produto_id = component.component_sku
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY component.id
|
||||
)
|
||||
FROM product_composition_components component
|
||||
WHERE component.product_composition_id = composition.id
|
||||
),
|
||||
'[]'::json
|
||||
) AS components
|
||||
FROM product_compositions composition
|
||||
WHERE composition.source = $1
|
||||
AND (
|
||||
composition.finished_tiny_product_id = $2
|
||||
OR composition.finished_product_sku = UPPER($2)
|
||||
)
|
||||
LIMIT 1;
|
||||
`, [TINY_OLIST_V3_COMPOSITION_SOURCE, identity]);
|
||||
|
||||
return result.rows[0] ? mapProductCompositionRow(result.rows[0]) : null;
|
||||
};
|
||||
|
||||
const upsertTinyProductComposition = async (order, components) => {
|
||||
const finishedTinyProductId = getTinyProductIdFromCompositionReference(order.tinyId);
|
||||
const finishedProductSku = normalizeSku(order.productSku);
|
||||
const finishedProductIdentity = finishedProductSku || finishedTinyProductId;
|
||||
|
||||
if (!finishedProductIdentity) {
|
||||
const error = new Error('SKU ou ID Tiny do produto acabado é obrigatório para a composição.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const normalizedComponents = normalizeCompositionComponents(components);
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const existingResult = await client.query(`
|
||||
SELECT id
|
||||
FROM product_compositions
|
||||
WHERE source = $1 AND finished_product_identity = $2
|
||||
LIMIT 1;
|
||||
`, [TINY_OLIST_V3_COMPOSITION_SOURCE, finishedProductIdentity]);
|
||||
|
||||
let compositionId = existingResult.rows[0]?.id;
|
||||
const values = [
|
||||
order.tinyId,
|
||||
finishedProductIdentity,
|
||||
finishedProductSku || null,
|
||||
order.productDescription,
|
||||
order.unit,
|
||||
finishedTinyProductId || null,
|
||||
JSON.stringify({
|
||||
provider: 'Tiny/Olist V3',
|
||||
notes: order.notes || null,
|
||||
payload: order.rawPayload
|
||||
})
|
||||
];
|
||||
|
||||
if (compositionId) {
|
||||
await client.query(`
|
||||
UPDATE product_compositions
|
||||
SET external_source_id = $1,
|
||||
finished_product_sku = $3,
|
||||
finished_product_description = $4,
|
||||
finished_product_unit = $5,
|
||||
finished_tiny_product_id = $6,
|
||||
source_metadata = $7::jsonb,
|
||||
last_synced_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $8;
|
||||
`, [...values, compositionId]);
|
||||
} else {
|
||||
const insertResult = await client.query(`
|
||||
INSERT INTO product_compositions (
|
||||
source, external_source_id, finished_product_identity,
|
||||
finished_product_sku, finished_product_description, finished_product_unit,
|
||||
finished_tiny_product_id, source_metadata, last_synced_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, CURRENT_TIMESTAMP)
|
||||
RETURNING id;
|
||||
`, [
|
||||
TINY_OLIST_V3_COMPOSITION_SOURCE,
|
||||
...values
|
||||
]);
|
||||
compositionId = insertResult.rows[0].id;
|
||||
}
|
||||
|
||||
await client.query('DELETE FROM product_composition_components WHERE product_composition_id = $1;', [compositionId]);
|
||||
for (const component of normalizedComponents) {
|
||||
await client.query(`
|
||||
INSERT INTO product_composition_components (
|
||||
product_composition_id, component_identity, component_tiny_id,
|
||||
component_sku, component_name, quantity_per_unit, unit
|
||||
)
|
||||
VALUES ($1, $2, NULLIF($3, ''), NULLIF($4, ''), $5, $6, NULLIF($7, ''));
|
||||
`, [
|
||||
compositionId,
|
||||
component.componentIdentity,
|
||||
component.componentTinyId,
|
||||
component.componentSku,
|
||||
component.componentName,
|
||||
component.quantityPerUnit,
|
||||
component.unit
|
||||
]);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return {
|
||||
composition: await getProductComposition(finishedTinyProductId || finishedProductSku),
|
||||
componentCount: normalizedComponents.length
|
||||
};
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
const upsertTinyProductionOrderDetail = async (payload = {}) => {
|
||||
const order = resolveTinyOrderPayload(payload);
|
||||
const components = normalizeComponents(payload.components || payload.composition || payload.composicao, order.quantity);
|
||||
|
||||
if (isTinyOlistV3Composition(order)) {
|
||||
return upsertTinyProductComposition(order, components);
|
||||
}
|
||||
|
||||
const steps = normalizeSteps(payload.steps || payload.etapas);
|
||||
const client = await pool.connect();
|
||||
|
||||
@@ -836,6 +1050,7 @@ const updateProductionOrderStatus = async (id, status) => {
|
||||
|
||||
module.exports = {
|
||||
createProductionOrders,
|
||||
getProductComposition,
|
||||
listProductionOrders,
|
||||
normalizeStatus,
|
||||
upsertTinyProductionOrderDetail,
|
||||
|
||||
264
backend/test/productCompositionService.test.js
Normal file
264
backend/test/productCompositionService.test.js
Normal file
@@ -0,0 +1,264 @@
|
||||
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 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 composition = state.compositions.find(item => (
|
||||
item.source === params[0]
|
||||
&& (item.finished_tiny_product_id === params[1] || item.finished_product_sku === String(params[1]).toUpperCase())
|
||||
));
|
||||
if (!composition) return { rows: [] };
|
||||
return {
|
||||
rows: [{
|
||||
...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('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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user