diff --git a/backend/db.js b/backend/db.js index b671089..eb2d604 100644 --- a/backend/db.js +++ b/backend/db.js @@ -7,22 +7,46 @@ const pool = new Pool({ const initDB = async () => { try { + await pool.query(`SET TIME ZONE 'America/Sao_Paulo';`); + await pool.query(` CREATE TABLE IF NOT EXISTS orders ( id SERIAL PRIMARY KEY, cliente_nome VARCHAR(255), data_pedido VARCHAR(50), + data_pedido_date DATE, valor_pedido NUMERIC(10, 2), produto_id VARCHAR(100), produto_descricao TEXT, quantidade INTEGER, valor_unitario NUMERIC(10, 5), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); `); 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 + ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo', + ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP; + `).catch(() => {}); + + await pool.query(` + UPDATE orders + SET data_pedido_date = CASE + WHEN data_pedido ~ '^\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}' THEN to_date(replace(left(data_pedido, 10), '/', '-'), 'YYYY-MM-DD') + WHEN data_pedido ~ '^\\d{1,2}[-/]\\d{1,2}[-/]\\d{4}' THEN to_date(replace(left(data_pedido, 10), '/', '-'), 'DD-MM-YYYY') + ELSE NULL + END + WHERE data_pedido_date IS NULL + AND data_pedido IS NOT NULL + AND data_pedido != ''; + `).catch(err => { + console.error('Notice: Could not backfill normalized order dates:', err.message); + }); await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS unique_order_product ON orders (pedido_id, produto_id);`).catch(err => { console.error('Notice: Could not create unique index (might already exist or there are duplicates):', err.message); @@ -34,10 +58,16 @@ const initDB = async () => { nome TEXT, saldo INTEGER DEFAULT 0, delta_estoque INTEGER DEFAULT 0, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); `); + await pool.query(` + ALTER TABLE stock + ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo', + ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP; + `).catch(() => {}); + await pool.query(` CREATE TABLE IF NOT EXISTS stock_campaign_queue ( id SERIAL PRIMARY KEY, @@ -49,15 +79,25 @@ const initDB = async () => { status VARCHAR(20) DEFAULT 'pending', attempts INTEGER DEFAULT 0, last_error TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - sent_at TIMESTAMP + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + sent_at TIMESTAMPTZ ); `); + await pool.query(` + ALTER TABLE stock_campaign_queue + ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo', + ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP, + ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'America/Sao_Paulo', + ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP, + ALTER COLUMN sent_at TYPE TIMESTAMPTZ USING sent_at AT TIME ZONE 'America/Sao_Paulo'; + `).catch(() => {}); + await pool.query(`CREATE INDEX IF NOT EXISTS idx_stock_campaign_queue_status ON stock_campaign_queue (status);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_cliente_fone ON orders (cliente_fone);`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_produto_id ON orders (produto_id);`); + await pool.query(`CREATE INDEX IF NOT EXISTS idx_orders_data_pedido_date ON orders (data_pedido_date);`); console.log('Database initialized successfully.'); } catch (err) { diff --git a/backend/mappers/orderMapper.js b/backend/mappers/orderMapper.js index 43bc32d..517554c 100644 --- a/backend/mappers/orderMapper.js +++ b/backend/mappers/orderMapper.js @@ -11,14 +11,40 @@ const formatOrderRow = (row) => ({ Fone_Cliente: row.cliente_fone }); +const normalizeOrderDate = (dateValue) => { + if (!dateValue) return null; + + const value = String(dateValue).trim(); + const match = value.match(/^(\d{1,4})[-/](\d{1,2})[-/](\d{1,4})/); + if (!match) return null; + + const [, first, second, third] = match; + const year = first.length === 4 ? Number(first) : Number(third); + const month = Number(second); + const day = first.length === 4 ? Number(third) : Number(first); + const date = new Date(Date.UTC(year, month - 1, day)); + + if ( + date.getUTCFullYear() !== year || + date.getUTCMonth() !== month - 1 || + date.getUTCDate() !== day + ) { + return null; + } + + return date.toISOString().slice(0, 10); +}; + 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; const fone = item.Fone_Cliente || item.fone || item.celular || ''; + const orderDate = item.Data_Pedido || ''; return [ item.Nome_Cliente || 'Unknown', - item.Data_Pedido || '', + orderDate, + normalizeOrderDate(orderDate), parseFloat(item.Valor_Pedido) || 0, item.ID_Produto || '', item.Descricao_Produto || '', @@ -31,5 +57,6 @@ const normalizeOrderPayload = (item) => { module.exports = { formatOrderRow, + normalizeOrderDate, normalizeOrderPayload }; diff --git a/backend/services/ordersService.js b/backend/services/ordersService.js index 290ba56..3839164 100644 --- a/backend/services/ordersService.js +++ b/backend/services/ordersService.js @@ -14,12 +14,13 @@ const upsertOrders = async (payload) => { const insertQuery = ` INSERT INTO orders ( - cliente_nome, data_pedido, valor_pedido, + 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) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (pedido_id, produto_id) DO UPDATE SET cliente_nome = EXCLUDED.cliente_nome, data_pedido = EXCLUDED.data_pedido, + data_pedido_date = EXCLUDED.data_pedido_date, valor_pedido = EXCLUDED.valor_pedido, produto_descricao = EXCLUDED.produto_descricao, quantidade = EXCLUDED.quantidade, diff --git a/backend/test/orderMapper.test.js b/backend/test/orderMapper.test.js new file mode 100644 index 0000000..9dd2283 --- /dev/null +++ b/backend/test/orderMapper.test.js @@ -0,0 +1,37 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const { normalizeOrderDate, normalizeOrderPayload } = require('../mappers/orderMapper'); + +test('normalizeOrderDate accepts Brazilian display dates', () => { + assert.equal(normalizeOrderDate('28/05/2026'), '2026-05-28'); + assert.equal(normalizeOrderDate('28-05-2026'), '2026-05-28'); +}); + +test('normalizeOrderDate accepts ISO-like dates', () => { + assert.equal(normalizeOrderDate('2026-05-28'), '2026-05-28'); + assert.equal(normalizeOrderDate('2026/05/28 10:30:00'), '2026-05-28'); +}); + +test('normalizeOrderDate rejects invalid dates', () => { + assert.equal(normalizeOrderDate(''), null); + assert.equal(normalizeOrderDate('not a date'), null); + assert.equal(normalizeOrderDate('31/02/2026'), null); +}); + +test('normalizeOrderPayload includes normalized date without changing display date', () => { + 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', + Fone_Cliente: '(16) 99999-9999' + }); + + assert.equal(payload[1], '28/05/2026'); + assert.equal(payload[2], '2026-05-28'); +});