Derive consumption references from Tiny OP sync
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m16s

This commit is contained in:
Cauê Faleiros
2026-07-23 11:53:18 -03:00
parent c07938c94e
commit 800eb976ab
7 changed files with 310 additions and 50 deletions

View File

@@ -65,6 +65,157 @@ const normalizeInteger = (value) => {
return Number.isInteger(number) ? number : null;
};
const normalizeSku = (value) => normalizeText(value).toUpperCase();
const normalizeUnit = (value) => normalizeText(value).toLowerCase();
const normalizeCategoryName = (name) => normalizeText(name)
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.toUpperCase();
const classifyCatalogCategoryName = (name, type) => {
const normalizedName = normalizeCategoryName(name);
if (type === 'finished_product') {
if (/\bDTF\b/.test(normalizedName)) return 'DTF';
if (/\b(?:MOLETOM|CANGURU)\b/.test(normalizedName)) return 'Moletom';
if (/\b(?:OVERSIZE|OVERSIZED)\b/.test(normalizedName)) return 'Oversized';
if (/\bINFANTIL\b/.test(normalizedName)) return 'Camiseta infantil';
if (/\b(?:BONE|ACESSORIO|ACESSORIOS)\b/.test(normalizedName)) return 'Acessórios';
return 'Camiseta regular';
}
if (/\b(?:MALHA|TECIDO|RIBANA|FIO)\b/.test(normalizedName)) return 'Malha';
if (/\b(?:ETIQUETA|TAG|EMBALAGEM|SACO|SACOLA)\b/.test(normalizedName)) return 'Embalagens';
if (/\b(?:ATACADOR|ILHOS|LINHA|FITA)\b/.test(normalizedName)) return 'Aviamentos';
if (/\bDTF\b/.test(normalizedName)) return 'DTF';
return 'Insumos gerais';
};
const ensureCategoryId = async (client, name) => {
const result = await client.query(`
INSERT INTO catalog_categories (name, description, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP)
ON CONFLICT (name) DO UPDATE
SET updated_at = catalog_categories.updated_at
RETURNING id;
`, [name, 'Categoria criada automaticamente pela sincronização de produção.']);
return result.rows[0].id;
};
const upsertCatalogProductFromSync = async (client, { sku, name, type, categoryName, notes }) => {
const normalizedSku = normalizeSku(sku);
const normalizedName = normalizeText(name);
if (!normalizedSku || !normalizedName) return null;
const categoryId = await ensureCategoryId(client, categoryName || classifyCatalogCategoryName(normalizedName, type));
const result = await client.query(`
INSERT INTO catalog_products (
type, sku, name, category_id, notes, updated_at
)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
ON CONFLICT (sku) DO UPDATE
SET type = EXCLUDED.type,
name = EXCLUDED.name,
category_id = COALESCE(catalog_products.category_id, EXCLUDED.category_id),
notes = COALESCE(NULLIF(catalog_products.notes, ''), EXCLUDED.notes),
updated_at = CURRENT_TIMESTAMP
RETURNING id;
`, [
type,
normalizedSku,
normalizedName,
categoryId,
normalizeText(notes) || null
]);
return result.rows[0].id;
};
const upsertConsumptionReferencesFromComponents = async (client, orderId, order, components) => {
if (!order.productSku || !components.length) {
return { referenceCount: 0, skippedReferenceCount: components.length };
}
const productId = await upsertCatalogProductFromSync(client, {
sku: order.productSku,
name: order.productDescription,
type: 'finished_product',
categoryName: classifyCatalogCategoryName(order.productDescription, 'finished_product'),
notes: `Sincronizado da OP ${order.number || order.tinyId || orderId}.`
});
if (!productId) return { referenceCount: 0, skippedReferenceCount: components.length };
let referenceCount = 0;
let skippedReferenceCount = 0;
for (const component of components) {
const materialId = await upsertCatalogProductFromSync(client, {
sku: component.componentSku,
name: component.componentName,
type: 'raw_material',
categoryName: classifyCatalogCategoryName(component.componentName, 'raw_material'),
notes: `Sincronizado da composição da OP ${order.number || order.tinyId || orderId}.`
});
if (!materialId || !component.quantityPerUnit) {
skippedReferenceCount += 1;
continue;
}
const unit = normalizeUnit(component.unit);
const generalYield = unit === 'kg' ? 1 / component.quantityPerUnit : null;
const existingReferenceResult = await client.query(`
SELECT id
FROM consumption_references
WHERE product_id = $1
AND COALESCE(material_product_id, 0) = $2
AND COALESCE(color, '') = ''
AND COALESCE(source, 'manual') = 'tiny_op'
LIMIT 1;
`, [productId, materialId]);
if (existingReferenceResult.rows.length) {
await client.query(`
UPDATE consumption_references
SET general_yield = $1,
consumption_quantity = $2,
consumption_unit = $3,
last_production_order_id = $4,
updated_at = CURRENT_TIMESTAMP
WHERE id = $5;
`, [
generalYield,
component.quantityPerUnit,
component.unit || null,
orderId,
existingReferenceResult.rows[0].id
]);
} else {
await client.query(`
INSERT INTO consumption_references (
product_id, material_product_id, general_yield, consumption_quantity,
consumption_unit, source, last_production_order_id, updated_at
)
VALUES ($1, $2, $3, $4, $5, 'tiny_op', $6, CURRENT_TIMESTAMP);
`, [
productId,
materialId,
generalYield,
component.quantityPerUnit,
component.unit || null,
orderId
]);
}
referenceCount += 1;
}
return { referenceCount, skippedReferenceCount };
};
const mapComponent = (component) => ({
id: component.id,
componentTinyId: component.component_tiny_id || '',
@@ -640,11 +791,15 @@ const upsertTinyProductionOrderDetail = async (payload = {}) => {
]);
}
const referenceSync = await upsertConsumptionReferencesFromComponents(client, orderId, order, components);
await client.query('COMMIT');
return {
order: await getOrderById(orderId),
componentCount: components.length,
stepCount: steps.length
stepCount: steps.length,
referenceCount: referenceSync.referenceCount,
skippedReferenceCount: referenceSync.skippedReferenceCount
};
} catch (error) {
await client.query('ROLLBACK');