Manage manual product compositions

This commit is contained in:
Cauê Faleiros
2026-08-14 14:28:47 -03:00
parent 2fb9092e05
commit 36981c9ac5
6 changed files with 305 additions and 14 deletions

View File

@@ -661,6 +661,7 @@ const normalizeSteps = (steps = []) => (
const TINY_OLIST_V3_COMPOSITION_SOURCE = 'tiny_olist_v3';
const TINY_OLIST_V3_COMPOSITION_PREFIX = 'STRUCTURE-V3-';
const MANUAL_COMPOSITION_SOURCE = 'manual';
const isTinyOlistV3Composition = (order) => order.tinyId.startsWith(TINY_OLIST_V3_COMPOSITION_PREFIX);
@@ -718,7 +719,7 @@ const mapProductCompositionRow = (row) => ({
})) : []
});
const findProductCompositions = async (productId, client = pool) => {
const findProductCompositionsBySource = async (source, productId, client = pool) => {
const identity = normalizeText(productId);
const result = await client.query(`
@@ -774,11 +775,24 @@ const findProductCompositions = async (productId, client = pool) => {
OR composition.finished_product_sku = UPPER($2)
)
ORDER BY composition.finished_product_sku, composition.finished_product_description;
`, [TINY_OLIST_V3_COMPOSITION_SOURCE, identity]);
`, [source, identity]);
return result.rows.map(mapProductCompositionRow);
};
const findProductCompositions = async (productId, client = pool) => {
const [manualCompositions, syncedCompositions] = await Promise.all([
findProductCompositionsBySource(MANUAL_COMPOSITION_SOURCE, productId, client),
findProductCompositionsBySource(TINY_OLIST_V3_COMPOSITION_SOURCE, productId, client)
]);
const compositionsByProduct = new Map();
[...manualCompositions, ...syncedCompositions].forEach(composition => {
const identity = composition.finishedTinyProductId || composition.finishedProductSku;
if (identity && !compositionsByProduct.has(identity)) compositionsByProduct.set(identity, composition);
});
return [...compositionsByProduct.values()];
};
const getProductComposition = async (productId, client = pool) => (
(await findProductCompositions(productId, client))[0] || null
);
@@ -968,6 +982,104 @@ const upsertTinyProductComposition = async (order, components) => {
}
};
const saveManualProductComposition = async (productId, payload = {}) => {
const finishedProductIdentity = normalizeText(productId) || normalizeSku(payload.productSku);
const finishedProductSku = normalizeSku(payload.productSku);
const finishedProductDescription = normalizeText(payload.productDescription);
const finishedProductUnit = normalizeText(payload.productUnit) || 'UN';
const rawComponents = normalizeCompositionComponents(payload.components);
const components = rawComponents.map(component => ({
...component,
componentName: normalizeText(component.componentName),
unit: normalizeText(component.unit).toUpperCase(),
quantityPerUnit: Number(component.quantityPerUnit)
}));
if (!finishedProductIdentity || !finishedProductDescription) {
const error = new Error('Produto da composição inválido.');
error.statusCode = 400;
throw error;
}
if (!components.length || components.some(component => !component.componentName || !component.unit || !Number.isFinite(component.quantityPerUnit) || component.quantityPerUnit <= 0)) {
const error = new Error('Adicione pelo menos um insumo à composição.');
error.statusCode = 400;
throw error;
}
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;
`, [MANUAL_COMPOSITION_SOURCE, finishedProductIdentity]);
const values = [
`MANUAL-${finishedProductIdentity}`,
finishedProductIdentity,
finishedProductSku || null,
finishedProductDescription,
finishedProductUnit,
finishedProductIdentity,
JSON.stringify({ provider: 'Graphs', notes: 'Composição cadastrada manualmente.' })
];
let compositionId = existingResult.rows[0]?.id;
if (compositionId) {
await client.query(`
UPDATE product_compositions
SET external_source_id = $1, finished_product_identity = $2,
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;
`, [MANUAL_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 components) {
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 getProductComposition(finishedProductIdentity);
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
const deleteManualProductComposition = async (productId) => {
const identity = normalizeText(productId);
const result = await pool.query(`
DELETE FROM product_compositions
WHERE source = $1
AND (finished_tiny_product_id = $2 OR finished_product_sku = UPPER($2))
RETURNING id;
`, [MANUAL_COMPOSITION_SOURCE, identity]);
return { deleted: result.rows.length > 0 };
};
const importTinyProductCompositionExport = async (payload = {}) => {
const compositions = Array.isArray(payload)
? payload
@@ -1208,11 +1320,13 @@ const updateProductionOrderStatus = async (id, status) => {
module.exports = {
createProductionOrders,
deleteManualProductComposition,
getProductComposition,
importTinyProductCompositionExport,
listProductCompositions,
listProductionOrders,
normalizeStatus,
saveManualProductComposition,
upsertTinyProductComposition,
upsertTinyProductionOrderDetail,
updateProductionOrderStatus