diff --git a/src/scripts/backfill.ts b/src/scripts/backfill.ts new file mode 100644 index 0000000..aaf0147 --- /dev/null +++ b/src/scripts/backfill.ts @@ -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 => { + 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 = { '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(); \ No newline at end of file