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
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m38s
This commit is contained in:
187
src/scripts/backfill.ts
Normal file
187
src/scripts/backfill.ts
Normal 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();
|
||||
Reference in New Issue
Block a user