feat: add backfill script to push historical tiny orders to n8n
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m38s

This commit is contained in:
Cauê Faleiros
2026-05-07 17:09:54 -03:00
parent 8af4bc9885
commit 7dfb3d4a03

187
src/scripts/backfill.ts Normal file
View File

@@ -0,0 +1,187 @@
import axios from 'axios';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
dotenv.config();
const TINY_API_TOKEN = process.env.TINY_API_TOKEN;
const N8N_WEBHOOK_GRAPHS = process.env.N8N_WEBHOOK_GRAPHS;
const STATE_FILE = path.join(__dirname, 'backfill_state.json');
// Delay helper to prevent hitting rate limits
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const loadState = () => {
if (fs.existsSync(STATE_FILE)) {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
}
// Default starting date based on user input
return { lastProcessedDate: '13/02/2025', currentStatus: 'idle' };
};
const saveState = (state: any) => {
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
};
// Helper to increment date by 1 day in DD/MM/YYYY format
const getNextDate = (dateStr: string) => {
const [day, month, year] = dateStr.split('/');
const date = new Date(Number(year), Number(month) - 1, Number(day));
date.setDate(date.getDate() + 1);
// Stop if we reach tomorrow
const today = new Date();
if (date > today) return null;
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = date.getFullYear();
return `${d}/${m}/${y}`;
};
const fetchOrdersForDate = async (dateStr: string, page: number = 1): Promise<any[]> => {
console.log(`[Tiny API] Searching orders for ${dateStr} (Page ${page})...`);
const params = new URLSearchParams();
params.append('token', TINY_API_TOKEN!);
params.append('formato', 'JSON');
params.append('dataInicial', dateStr);
params.append('dataFinal', dateStr);
params.append('pagina', page.toString());
try {
const response = await axios.post('https://api.tiny.com.br/api2/pedidos.pesquisa.php', params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
await sleep(1000); // 1 second delay
const retorno = response.data?.retorno;
if (retorno?.status === 'OK') {
const orders = retorno.pedidos || [];
const numPages = Number(retorno.numero_paginas || 1);
let allOrders = orders.map((o: any) => o.pedido);
// If there are more pages for this specific day, fetch them
if (page < numPages) {
const nextOrders = await fetchOrdersForDate(dateStr, page + 1);
allOrders = allOrders.concat(nextOrders);
}
return allOrders;
}
if (retorno?.codigo_erro === '20') {
// Error 20 usually means no records found for this date. That's fine.
return [];
}
console.error(`[Tiny API] Error fetching summaries:`, retorno?.erros);
return [];
} catch (e: any) {
console.error(`[Tiny API] Request failed: ${e.message}`);
await sleep(5000); // Wait longer on failure
return []; // Skip to next day on hard crash to keep moving
}
};
const fetchOrderDetails = async (orderId: string) => {
const params = new URLSearchParams();
params.append('token', TINY_API_TOKEN!);
params.append('id', orderId);
params.append('formato', 'JSON');
try {
const response = await axios.post('https://api.tiny.com.br/api2/pedido.obter.php', params, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
await sleep(1500); // 1.5 second delay. This is the heavy part.
if (response.data?.retorno?.status === 'OK') {
return {
pedido: response.data.retorno.pedido,
status_processamento: response.data.retorno.status_processamento
};
}
console.error(`[Tiny API] Error fetching details for ${orderId}:`, response.data?.retorno?.erros);
return null;
} catch (e: any) {
console.error(`[Tiny API] Details request failed for ${orderId}: ${e.message}`);
return null;
}
};
const sendToN8n = async (payload: any) => {
try {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (process.env.N8N_AUTH_TOKEN) {
headers['Authorization'] = process.env.N8N_AUTH_TOKEN;
}
await axios.post(N8N_WEBHOOK_GRAPHS!, payload, { headers });
console.log(`[n8n] Successfully forwarded Order ID: ${payload.id}`);
} catch (e: any) {
console.error(`[n8n] Failed to send Order ID: ${payload.id}: ${e.message}`);
}
};
const runBackfill = async () => {
if (!TINY_API_TOKEN || !N8N_WEBHOOK_GRAPHS) {
console.error('Missing TINY_API_TOKEN or N8N_WEBHOOK_GRAPHS in environment.');
process.exit(1);
}
let state = loadState();
let currentDate = state.lastProcessedDate;
console.log(`Starting backfill from: ${currentDate}`);
while (currentDate) {
console.log(`\n--- Processing Date: ${currentDate} ---`);
const summaryOrders = await fetchOrdersForDate(currentDate);
console.log(`Found ${summaryOrders.length} orders for ${currentDate}.`);
for (const summary of summaryOrders) {
const details = await fetchOrderDetails(summary.id);
if (!details) continue;
const fullOrderDetails = details.pedido;
// Build the exact payload that the middleware uses
const finalPayload = {
id: fullOrderDetails?.id || "",
numero: fullOrderDetails?.numero || "",
numero_ecommerce: fullOrderDetails?.numero_ecommerce || fullOrderDetails?.ecommerce?.numeroPedidoEcommerce || "",
data_pedido: fullOrderDetails?.data_pedido || "",
data_prevista: fullOrderDetails?.data_prevista || "",
nome: fullOrderDetails?.cliente?.nome || "",
valor: parseFloat(fullOrderDetails?.total_pedido || fullOrderDetails?.valor_total || "0"),
id_vendedor: fullOrderDetails?.id_vendedor || "",
nome_vendedor: fullOrderDetails?.nome_vendedor || "",
whatsapp_vendedor: "", // Skipping seller lookup to save API limits
situacao: fullOrderDetails?.situacao || "",
fone: fullOrderDetails?.cliente?.celular || fullOrderDetails?.cliente?.telefone || fullOrderDetails?.cliente?.fone || "",
email: fullOrderDetails?.cliente?.email || "",
status_processamento: details.status_processamento || "",
forma_envio: fullOrderDetails?.forma_envio || "",
codigo_rastreamento: fullOrderDetails?.codigo_rastreamento || "",
url_rastreamento: fullOrderDetails?.url_rastreamento || "",
itens: fullOrderDetails?.itens || []
};
await sendToN8n(finalPayload);
}
// Save progress after each day is completed
currentDate = getNextDate(currentDate);
if (currentDate) {
state.lastProcessedDate = currentDate;
saveState(state);
}
}
console.log('\n✅ BACKFILL COMPLETE! Caught up to today.');
};
runBackfill();