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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user