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