feat: normalize order dates in database
This commit is contained in:
@@ -7,22 +7,46 @@ const pool = new Pool({
|
|||||||
|
|
||||||
const initDB = async () => {
|
const initDB = async () => {
|
||||||
try {
|
try {
|
||||||
|
await pool.query(`SET TIME ZONE 'America/Sao_Paulo';`);
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS orders (
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
cliente_nome VARCHAR(255),
|
cliente_nome VARCHAR(255),
|
||||||
data_pedido VARCHAR(50),
|
data_pedido VARCHAR(50),
|
||||||
|
data_pedido_date DATE,
|
||||||
valor_pedido NUMERIC(10, 2),
|
valor_pedido NUMERIC(10, 2),
|
||||||
produto_id VARCHAR(100),
|
produto_id VARCHAR(100),
|
||||||
produto_descricao TEXT,
|
produto_descricao TEXT,
|
||||||
quantidade INTEGER,
|
quantidade INTEGER,
|
||||||
valor_unitario NUMERIC(10, 5),
|
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 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 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 => {
|
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);
|
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,
|
nome TEXT,
|
||||||
saldo INTEGER DEFAULT 0,
|
saldo INTEGER DEFAULT 0,
|
||||||
delta_estoque 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(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS stock_campaign_queue (
|
CREATE TABLE IF NOT EXISTS stock_campaign_queue (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@@ -49,15 +79,25 @@ const initDB = async () => {
|
|||||||
status VARCHAR(20) DEFAULT 'pending',
|
status VARCHAR(20) DEFAULT 'pending',
|
||||||
attempts INTEGER DEFAULT 0,
|
attempts INTEGER DEFAULT 0,
|
||||||
last_error TEXT,
|
last_error TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
sent_at 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_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_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_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.');
|
console.log('Database initialized successfully.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -11,14 +11,40 @@ const formatOrderRow = (row) => ({
|
|||||||
Fone_Cliente: row.cliente_fone
|
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 normalizeOrderPayload = (item) => {
|
||||||
const fallbackId = `${item.Nome_Cliente}_${item.Data_Pedido}_${item.Valor_Pedido}`;
|
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 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 fone = item.Fone_Cliente || item.fone || item.celular || '';
|
||||||
|
const orderDate = item.Data_Pedido || '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
item.Nome_Cliente || 'Unknown',
|
item.Nome_Cliente || 'Unknown',
|
||||||
item.Data_Pedido || '',
|
orderDate,
|
||||||
|
normalizeOrderDate(orderDate),
|
||||||
parseFloat(item.Valor_Pedido) || 0,
|
parseFloat(item.Valor_Pedido) || 0,
|
||||||
item.ID_Produto || '',
|
item.ID_Produto || '',
|
||||||
item.Descricao_Produto || '',
|
item.Descricao_Produto || '',
|
||||||
@@ -31,5 +57,6 @@ const normalizeOrderPayload = (item) => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
formatOrderRow,
|
formatOrderRow,
|
||||||
|
normalizeOrderDate,
|
||||||
normalizeOrderPayload
|
normalizeOrderPayload
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ const upsertOrders = async (payload) => {
|
|||||||
|
|
||||||
const insertQuery = `
|
const insertQuery = `
|
||||||
INSERT INTO orders (
|
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
|
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
|
ON CONFLICT (pedido_id, produto_id) DO UPDATE SET
|
||||||
cliente_nome = EXCLUDED.cliente_nome,
|
cliente_nome = EXCLUDED.cliente_nome,
|
||||||
data_pedido = EXCLUDED.data_pedido,
|
data_pedido = EXCLUDED.data_pedido,
|
||||||
|
data_pedido_date = EXCLUDED.data_pedido_date,
|
||||||
valor_pedido = EXCLUDED.valor_pedido,
|
valor_pedido = EXCLUDED.valor_pedido,
|
||||||
produto_descricao = EXCLUDED.produto_descricao,
|
produto_descricao = EXCLUDED.produto_descricao,
|
||||||
quantidade = EXCLUDED.quantidade,
|
quantidade = EXCLUDED.quantidade,
|
||||||
|
|||||||
37
backend/test/orderMapper.test.js
Normal file
37
backend/test/orderMapper.test.js
Normal file
@@ -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');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user