Add Tiny ERP order metadata ingestion

This commit is contained in:
Cauê Faleiros
2026-06-22 15:02:38 -03:00
parent 0874238fc6
commit 3568e48ce9
8 changed files with 283 additions and 38 deletions

View File

@@ -27,6 +27,12 @@ const initDB = async () => {
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS pedido_id VARCHAR(100);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS cliente_fone VARCHAR(50);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS data_pedido_date DATE;`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS cliente_nome_fantasia VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS id_vendedor VARCHAR(100);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS nome_vendedor VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS marketplace VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS canal_venda VARCHAR(255);`).catch(() => {});
await pool.query(`ALTER TABLE orders ADD COLUMN IF NOT EXISTS numero_ecommerce VARCHAR(100);`).catch(() => {});
await pool.query(`
ALTER TABLE orders

View File

@@ -8,7 +8,13 @@ const formatOrderRow = (row) => ({
Valor_Unitario: parseFloat(row.valor_unitario),
Recebido_Em: row.created_at,
ID_Pedido: row.pedido_id,
Fone_Cliente: row.cliente_fone
Fone_Cliente: row.cliente_fone,
cliente_nome_fantasia: row.cliente_nome_fantasia || '',
id_vendedor: row.id_vendedor || '',
nome_vendedor: row.nome_vendedor || '',
marketplace: row.marketplace || '',
canal_venda: row.canal_venda || '',
numero_ecommerce: row.numero_ecommerce || ''
});
const normalizeOrderDate = (dateValue) => {
@@ -35,6 +41,17 @@ const normalizeOrderDate = (dateValue) => {
return date.toISOString().slice(0, 10);
};
const pickFirstValue = (item, fieldNames) => {
for (const fieldName of fieldNames) {
const value = item[fieldName];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return '';
};
const normalizeOrderPayload = (item) => {
const fallbackId = `${item.Nome_Cliente}_${item.Data_Pedido}_${item.Valor_Pedido}`;
const orderId = item.id || item.ID_Pedido || (item.json && item.json.body && item.json.body.id) || fallbackId;
@@ -51,7 +68,13 @@ const normalizeOrderPayload = (item) => {
parseInt(item.Quantidade, 10) || 0,
parseFloat(item.Valor_Unitario) || 0,
String(orderId),
String(fone)
String(fone),
String(pickFirstValue(item, ['nome_fantasia', 'Nome_Fantasia', 'Cliente_Nome_Fantasia', 'cliente_nome_fantasia'])),
String(pickFirstValue(item, ['id_vendedor', 'ID_Vendedor'])),
String(pickFirstValue(item, ['nome_vendedor', 'Nome_Vendedor'])),
String(pickFirstValue(item, ['marketplace', 'Marketplace', 'nome_ecommerce', 'Nome_Ecommerce'])),
String(pickFirstValue(item, ['canal_venda', 'Canal_Venda'])),
String(pickFirstValue(item, ['numero_ecommerce', 'Numero_Ecommerce']))
];
};

View File

@@ -507,7 +507,13 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
produto_descricao,
quantidade,
valor_unitario,
pedido_id
pedido_id,
cliente_nome_fantasia,
id_vendedor,
nome_vendedor,
marketplace,
canal_venda,
numero_ecommerce
FROM orders
WHERE ${periodFilters.join(' AND ')}
ORDER BY data_pedido_date DESC, COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text) DESC;
@@ -559,7 +565,13 @@ const getClientDetailsAnalytics = async (clientToken, range = {}) => {
Quantidade: toNumber(row.quantidade),
Valor_Unitario: toNumber(row.valor_unitario),
ID_Pedido: row.pedido_id || '',
Fone_Cliente: row.cliente_fone || ''
Fone_Cliente: row.cliente_fone || '',
cliente_nome_fantasia: row.cliente_nome_fantasia || '',
id_vendedor: row.id_vendedor || '',
nome_vendedor: row.nome_vendedor || '',
marketplace: row.marketplace || '',
canal_venda: row.canal_venda || '',
numero_ecommerce: row.numero_ecommerce || ''
});
});

View File

@@ -15,8 +15,9 @@ const upsertOrders = async (payload) => {
const insertQuery = `
INSERT INTO orders (
cliente_nome, data_pedido, data_pedido_date, valor_pedido,
produto_id, produto_descricao, quantidade, valor_unitario, pedido_id, cliente_fone
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
produto_id, produto_descricao, quantidade, valor_unitario, pedido_id, cliente_fone,
cliente_nome_fantasia, id_vendedor, nome_vendedor, marketplace, canal_venda, numero_ecommerce
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (pedido_id, produto_id) DO UPDATE SET
cliente_nome = EXCLUDED.cliente_nome,
data_pedido = EXCLUDED.data_pedido,
@@ -26,6 +27,12 @@ const upsertOrders = async (payload) => {
quantidade = EXCLUDED.quantidade,
valor_unitario = EXCLUDED.valor_unitario,
cliente_fone = EXCLUDED.cliente_fone,
cliente_nome_fantasia = EXCLUDED.cliente_nome_fantasia,
id_vendedor = EXCLUDED.id_vendedor,
nome_vendedor = EXCLUDED.nome_vendedor,
marketplace = EXCLUDED.marketplace,
canal_venda = EXCLUDED.canal_venda,
numero_ecommerce = EXCLUDED.numero_ecommerce,
created_at = CURRENT_TIMESTAMP
`;

View File

@@ -1,7 +1,7 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const { normalizeOrderDate, normalizeOrderPayload } = require('../mappers/orderMapper');
const { formatOrderRow, normalizeOrderDate, normalizeOrderPayload } = require('../mappers/orderMapper');
test('normalizeOrderDate accepts Brazilian display dates', () => {
assert.equal(normalizeOrderDate('28/05/2026'), '2026-05-28');
@@ -35,3 +35,83 @@ test('normalizeOrderPayload includes normalized date without changing display da
assert.equal(payload[1], '28/05/2026');
assert.equal(payload[2], '2026-05-28');
});
test('normalizeOrderPayload maps Tiny ERP metadata aliases', () => {
const payload = normalizeOrderPayload({
Nome_Cliente: 'Cliente Teste',
Data_Pedido: '28/05/2026',
Valor_Pedido: '120.50',
ID_Produto: 'SKU-1',
Descricao_Produto: 'Produto',
Quantidade: '2',
Valor_Unitario: '60.25',
ID_Pedido: 'ORDER-1',
Nome_Fantasia: 'Cliente Fantasia',
ID_Vendedor: 123,
Nome_Vendedor: 'Maria',
Nome_Ecommerce: 'Mercado Livre',
Canal_Venda: 'Online',
Numero_Ecommerce: 'EC-987'
});
assert.equal(payload[10], 'Cliente Fantasia');
assert.equal(payload[11], '123');
assert.equal(payload[12], 'Maria');
assert.equal(payload[13], 'Mercado Livre');
assert.equal(payload[14], 'Online');
assert.equal(payload[15], 'EC-987');
});
test('normalizeOrderPayload maps lower-case metadata names', () => {
const payload = normalizeOrderPayload({
Nome_Cliente: 'Cliente Teste',
Data_Pedido: '28/05/2026',
Valor_Pedido: '120.50',
ID_Produto: 'SKU-1',
Descricao_Produto: 'Produto',
Quantidade: '2',
Valor_Unitario: '60.25',
ID_Pedido: 'ORDER-1',
cliente_nome_fantasia: 'Fantasia Lower',
id_vendedor: 'VEN-7',
nome_vendedor: 'Joao',
marketplace: 'Shopee',
canal_venda: 'Marketplace',
numero_ecommerce: '100200'
});
assert.equal(payload[10], 'Fantasia Lower');
assert.equal(payload[11], 'VEN-7');
assert.equal(payload[12], 'Joao');
assert.equal(payload[13], 'Shopee');
assert.equal(payload[14], 'Marketplace');
assert.equal(payload[15], '100200');
});
test('formatOrderRow returns Tiny ERP metadata fields', () => {
const row = formatOrderRow({
cliente_nome: 'Cliente Teste',
data_pedido: '28/05/2026',
valor_pedido: '120.50',
produto_id: 'SKU-1',
produto_descricao: 'Produto',
quantidade: 2,
valor_unitario: '60.25',
created_at: '2026-05-28T12:00:00.000Z',
pedido_id: 'ORDER-1',
cliente_fone: '(16) 99999-9999',
cliente_nome_fantasia: 'Cliente Fantasia',
id_vendedor: '123',
nome_vendedor: 'Maria',
marketplace: 'Mercado Livre',
canal_venda: 'Online',
numero_ecommerce: 'EC-987'
});
assert.equal(row.cliente_nome_fantasia, 'Cliente Fantasia');
assert.equal(row.id_vendedor, '123');
assert.equal(row.nome_vendedor, 'Maria');
assert.equal(row.marketplace, 'Mercado Livre');
assert.equal(row.canal_venda, 'Online');
assert.equal(row.numero_ecommerce, 'EC-987');
});

View File

@@ -0,0 +1,83 @@
const assert = require('node:assert/strict');
const test = require('node:test');
test('upsertOrders persists Tiny ERP metadata columns', async () => {
const queries = [];
const client = {
query: async (sql, params) => {
queries.push({ sql, params });
return { rows: [] };
},
release: () => {
queries.push({ sql: 'RELEASE' });
}
};
const dbPath = require.resolve('../db');
const servicePath = require.resolve('../services/ordersService');
const originalDbCache = require.cache[dbPath];
const originalServiceCache = require.cache[servicePath];
delete require.cache[servicePath];
require.cache[dbPath] = {
id: dbPath,
filename: dbPath,
loaded: true,
exports: {
pool: {
connect: async () => client
}
}
};
try {
const { upsertOrders } = require('../services/ordersService');
await upsertOrders([{
Nome_Cliente: 'Cliente Teste',
Data_Pedido: '28/05/2026',
Valor_Pedido: '120.50',
ID_Produto: 'SKU-1',
Descricao_Produto: 'Produto',
Quantidade: '2',
Valor_Unitario: '60.25',
ID_Pedido: 'ORDER-1',
Fone_Cliente: '(16) 99999-9999',
cliente_nome_fantasia: 'Cliente Fantasia',
id_vendedor: 'VEN-1',
nome_vendedor: 'Maria',
marketplace: 'Mercado Livre',
canal_venda: 'Online',
numero_ecommerce: 'EC-987'
}]);
} finally {
delete require.cache[servicePath];
if (originalServiceCache) {
require.cache[servicePath] = originalServiceCache;
}
if (originalDbCache) {
require.cache[dbPath] = originalDbCache;
} else {
delete require.cache[dbPath];
}
}
const insert = queries.find(query => query.sql.includes('INSERT INTO orders'));
assert.ok(insert);
assert.match(insert.sql, /cliente_nome_fantasia/);
assert.match(insert.sql, /id_vendedor/);
assert.match(insert.sql, /nome_vendedor/);
assert.match(insert.sql, /marketplace/);
assert.match(insert.sql, /canal_venda/);
assert.match(insert.sql, /numero_ecommerce/);
assert.match(insert.sql, /cliente_nome_fantasia = EXCLUDED\.cliente_nome_fantasia/);
assert.equal(insert.params.length, 16);
assert.deepEqual(insert.params.slice(10), [
'Cliente Fantasia',
'VEN-1',
'Maria',
'Mercado Livre',
'Online',
'EC-987'
]);
});