Import Tiny product compositions
This commit is contained in:
@@ -12,6 +12,7 @@ const getRouteHandler = (router, method, path) => {
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
@@ -70,9 +71,15 @@ const withFakeDatabase = async (run) => {
|
||||
const state = {
|
||||
nextCompositionId: 1,
|
||||
nextOrderId: 1,
|
||||
nextCategoryId: 1,
|
||||
nextCatalogProductId: 1,
|
||||
nextConsumptionReferenceId: 1,
|
||||
compositions: [],
|
||||
compositionComponents: [],
|
||||
productionOrders: []
|
||||
productionOrders: [],
|
||||
categories: [],
|
||||
catalogProducts: [],
|
||||
consumptionReferences: []
|
||||
};
|
||||
|
||||
const query = async (sql, params = []) => {
|
||||
@@ -80,6 +87,72 @@ const withFakeDatabase = async (run) => {
|
||||
|
||||
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 }] : [] };
|
||||
@@ -197,10 +270,36 @@ test('Tiny/Olist V3 first import creates a composition without creating an OP',
|
||||
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() });
|
||||
|
||||
Reference in New Issue
Block a user