From e33264d7f04088174711693c9dc11f2be974c550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ortiz?= Date: Mon, 16 Mar 2026 18:33:38 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20simplificar=20la=20gesti=C3=B3n=20de=20?= =?UTF-8?q?claves=20de=20Supabase=20y=20mejorar=20la=20recuperaci=C3=B3n?= =?UTF-8?q?=20de=20trabajos=20en=20tiempo=20real=20y=20mensuales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/peruserver/trucky/live-jobs/index.js | 123 +++++++++++++++--- .../v2/peruserver/trucky/top-km/index.js | 33 +++-- .../v2/peruserver/trucky/top-km/monthly.js | 31 +++-- 3 files changed, 155 insertions(+), 32 deletions(-) diff --git a/src/routes/v2/peruserver/trucky/live-jobs/index.js b/src/routes/v2/peruserver/trucky/live-jobs/index.js index 44ab725..cf7a5a9 100644 --- a/src/routes/v2/peruserver/trucky/live-jobs/index.js +++ b/src/routes/v2/peruserver/trucky/live-jobs/index.js @@ -44,11 +44,7 @@ const routeFullRedisCache = process.env.REDIS_URL : null; const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || ''; -const SUPABASE_ANON_KEY = - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || - process.env.SUPABASE_ANON_KEY || - process.env.SUPABASE_SERVICE_ROLE_KEY || - ''; +const SUPABASE_ANON_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; startPeriodicProxyUpdate(); @@ -792,6 +788,95 @@ const fetchRegisteredCompanies = async (companiesSource = 'auto') => { }; }; +const parseWebhookRawPayload = (raw) => { + if (!raw) return null; + if (typeof raw === 'object') return raw; + if (typeof raw !== 'string') return null; + + try { + return JSON.parse(raw); + } catch (error) { + return null; + } +}; + +const mapWebhookJob = (row, company) => { + const rawPayload = parseWebhookRawPayload(row.raw); + const rawData = rawPayload && typeof rawPayload === 'object' ? rawPayload.data || {} : {}; + const sourceName = String(rawData.source_city_name || row.source_city_id || 'Origen no identificado').trim(); + const destinationName = String(rawData.destination_city_name || row.destination_city_id || 'Destino no identificado').trim(); + + return { + id: Number(row.job_id || row.id || 0), + companyId: company.id, + companyName: company.name, + updatedAt: row.updated_at || new Date(0).toISOString(), + startedAt: rawData.started_at || row.created_at || null, + status: row.status || 'unknown', + source: { + key: normalizePointKey(row.source_city_id, sourceName), + cityId: row.source_city_id || null, + cityName: sourceName, + }, + destination: { + key: normalizePointKey(row.destination_city_id, destinationName), + cityId: row.destination_city_id || null, + cityName: destinationName, + }, + driverName: (rawData.driver && rawData.driver.name && String(rawData.driver.name).trim()) || 'Sin conductor', + driverAvatarUrl: rawData.driver ? rawData.driver.avatar_url || null : null, + driverProfileUrl: rawData.driver ? rawData.driver.public_url || null : null, + cargoName: (rawData.cargo_name && String(rawData.cargo_name).trim()) || (row.cargo_id ? String(row.cargo_id).trim() : 'Carga no especificada'), + plannedDistanceKm: row.planned_distance_km != null ? Number(row.planned_distance_km) : null, + publicUrl: rawData.public_url || `https://hub.truckyapp.com/job/${row.job_id || row.id || ''}`, + }; +}; + +const fetchLiveJobsFromSupabase = async ({ cutoffMs, companyMap }) => { + if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { + throw new Error('Falta configurar SUPABASE_URL o SUPABASE_ANON_KEY'); + } + + const jobs = []; + const pageSize = 1000; + let offset = 0; + + while (true) { + const response = await axios.get( + `${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/jobs_webhooks?status=eq.in_progress&select=job_id,company_id,status,source_city_id,destination_city_id,cargo_id,planned_distance_km,created_at,updated_at,raw&order=updated_at.desc&limit=${pageSize}&offset=${offset}`, + { + headers: { + apikey: SUPABASE_ANON_KEY, + Authorization: `Bearer ${SUPABASE_ANON_KEY}`, + }, + timeout: 20000, + } + ); + + const rows = Array.isArray(response.data) ? response.data : []; + + for (const row of rows) { + if (!isWithinLastDays(row, cutoffMs)) continue; + + const companyId = Number(row.company_id); + const company = companyMap.get(companyId) || { + id: companyId, + name: `Empresa ${Number.isFinite(companyId) && companyId > 0 ? companyId : 'N/D'}`, + }; + const job = mapWebhookJob(row, company); + if (job.id > 0) jobs.push(job); + } + + if (rows.length < pageSize) { + break; + } + + offset += pageSize; + } + + return jobs; +}; + const fetchCompanyJobs = async (company, cutoffMs, proxyCandidates, useProxyPool) => { const response = await truckyRequestWithRetry({ params: { @@ -874,6 +959,13 @@ const buildCoreSnapshot = async ({ days, companiesSource, companyBatchSize, prox payload.companiesSourceUsed = sourceUsed; payload.companiesProcessed = companies.length; + const companyMap = new Map(companies.map((company) => [company.id, company])); + payload.jobs = (await fetchLiveJobsFromSupabase({ cutoffMs, companyMap })) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, MAX_JOBS); + payload.errors = []; + return payload; + if (!companies.length) { return payload; } @@ -1131,16 +1223,17 @@ const parseRequestOptions = (query) => { router.get('/', async (req, res) => { try { - // Mostrar trabajos activos (status: in_progress) desde jobs_webhooks - const url = `${SUPABASE_URL}/rest/v1/jobs_webhooks?status=eq.in_progress&select=*`; - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - apikey: SUPABASE_ANON_KEY, - }, - }); - const jobs = await response.json(); - return res.json({ ok: true, jobs }); + const options = parseRequestOptions(req.query); + const snapshot = await getCoreSnapshot({ + days: options.days, + useDbCache: options.useDbCache, + companiesSource: options.companiesSource, + companyBatchSize: options.companyBatchSize, + proxyCandidates: [], + useProxyPool: false, + }); + + return res.json({ ok: true, jobs: snapshot.jobs }); } catch (error) { return res.status(500).json({ ok: false, error: error.message || 'Error interno', jobs: [] }); } diff --git a/src/routes/v2/peruserver/trucky/top-km/index.js b/src/routes/v2/peruserver/trucky/top-km/index.js index bf65fba..65868dc 100644 --- a/src/routes/v2/peruserver/trucky/top-km/index.js +++ b/src/routes/v2/peruserver/trucky/top-km/index.js @@ -4,7 +4,7 @@ const axios = require('axios'); const router = Router(); const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || ''; -const SUPABASE_ANON_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || ''; +const SUPABASE_ANON_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; const DEFAULT_LIMIT = 50; const MAX_LIMIT = 200; const CACHE_TTL_CURRENT_MONTH_MS = 30 * 60 * 1000; @@ -123,7 +123,7 @@ const fetchAllJobsFromStart = async (startDateIso) => { while (true) { const response = await axios.get( - `${supabaseUrl}/rest/v1/jobs_webhooks?created_at=gte.${startDateIso}&select=company_id,driven_distance_km,real_driven_distance_km,job_id,created_at&order=created_at.asc&limit=${pageSize}&offset=${offset}`, + `${supabaseUrl}/rest/v1/jobs_webhooks?created_at=gte.${startDateIso}&select=company_id,driven_distance_km,real_driven_distance_km,job_id,created_at,status,event_type&order=created_at.asc&limit=${pageSize}&offset=${offset}`, { headers, timeout: 20000, @@ -143,14 +143,18 @@ const fetchAllJobsFromStart = async (startDateIso) => { return jobs; }; -const fetchCompaniesMap = async (companyIds) => { +const fetchCompaniesMap = async (companyIds = null) => { const supabaseUrl = SUPABASE_URL.replace(/\/+$/, ''); const headers = getSupabaseHeaders(); const companiesMap = new Map(); - for (const batch of chunkArray(companyIds, 150)) { + const batches = companyIds == null ? [null] : chunkArray(companyIds, 150); + + for (const batch of batches) { const response = await axios.get( - `${supabaseUrl}/rest/v1/trucky_companies?company_id=in.(${batch.join(',')})&select=company_id,name,tag,members_count`, + batch == null + ? `${supabaseUrl}/rest/v1/trucky_companies?select=company_id,name,tag,members_count&order=company_id.asc` + : `${supabaseUrl}/rest/v1/trucky_companies?company_id=in.(${batch.join(',')})&select=company_id,name,tag,members_count`, { headers, timeout: 15000, @@ -172,8 +176,17 @@ const buildAccumulatedResponse = async ({ startMonth, startYear, limit }) => { const currentYear = currentDate.getUTCFullYear(); const monthRanges = generateMonthRanges(startMonth, startYear, currentMonth, currentYear); const startDate = new Date(Date.UTC(startYear, startMonth - 1, 1)); + const companiesMap = await fetchCompaniesMap(); const jobs = await fetchAllJobsFromStart(startDate.toISOString()); - const rankingMap = new Map(); + const rankingMap = new Map( + [...companiesMap.values()].map((company) => [Number(company.company_id), { + id: Number(company.company_id), + total_distance: 0, + total_jobs: 0, + months_processed: monthRanges.length, + months_with_errors: 0, + }]) + ); for (const job of jobs) { const companyId = Number(job.company_id); @@ -192,16 +205,18 @@ const buildAccumulatedResponse = async ({ startMonth, startYear, limit }) => { const item = rankingMap.get(companyId); const distance = Number(job.driven_distance_km ?? job.real_driven_distance_km) || 0; item.total_distance += distance; - item.total_jobs += 1; + const eventType = String(job.event_type || '').trim().toLowerCase(); + if (eventType === 'job_completed') { + item.total_jobs += 1; + } } - const companyIds = [...rankingMap.keys()]; - const companiesMap = companyIds.length > 0 ? await fetchCompaniesMap(companyIds) : new Map(); const items = [...rankingMap.values()] .map((item) => { const company = companiesMap.get(item.id) || {}; return { ...item, + total_distance: Math.floor(Number(item.total_distance) || 0), name: company.name || `Empresa ${item.id}`, tag: company.tag || '', members: Number.isFinite(Number(company.members_count)) ? Number(company.members_count) : null, diff --git a/src/routes/v2/peruserver/trucky/top-km/monthly.js b/src/routes/v2/peruserver/trucky/top-km/monthly.js index 54b3f2a..d8ab1e1 100644 --- a/src/routes/v2/peruserver/trucky/top-km/monthly.js +++ b/src/routes/v2/peruserver/trucky/top-km/monthly.js @@ -4,7 +4,7 @@ const axios = require('axios'); const router = Router(); const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || ''; -const SUPABASE_ANON_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || ''; +const SUPABASE_ANON_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || ''; const DEFAULT_LIMIT = 50; const MAX_LIMIT = 200; const CACHE_TTL_MS = 30 * 60 * 1000; @@ -100,7 +100,7 @@ const fetchMonthlyJobs = async (month, year) => { while (true) { const response = await axios.get( - `${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/jobs_webhooks?created_at=gte.${startDate.toISOString()}&created_at=lt.${endDate.toISOString()}&select=company_id,driven_distance_km,real_driven_distance_km,job_id,created_at&order=created_at.asc&limit=${pageSize}&offset=${offset}`, + `${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/jobs_webhooks?created_at=gte.${startDate.toISOString()}&created_at=lt.${endDate.toISOString()}&select=company_id,driven_distance_km,real_driven_distance_km,job_id,created_at,status,event_type&order=created_at.asc&limit=${pageSize}&offset=${offset}`, { headers: getSupabaseHeaders(), timeout: 20000, @@ -120,12 +120,15 @@ const fetchMonthlyJobs = async (month, year) => { return jobs; }; -const fetchCompaniesMap = async (companyIds) => { +const fetchCompaniesMap = async (companyIds = null) => { const companiesMap = new Map(); + const batches = companyIds == null ? [null] : chunkArray(companyIds, 150); - for (const batch of chunkArray(companyIds, 150)) { + for (const batch of batches) { const response = await axios.get( - `${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/trucky_companies?company_id=in.(${batch.join(',')})&select=company_id,name,tag,members_count`, + batch == null + ? `${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/trucky_companies?select=company_id,name,tag,members_count&order=company_id.asc` + : `${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/trucky_companies?company_id=in.(${batch.join(',')})&select=company_id,name,tag,members_count`, { headers: getSupabaseHeaders(), timeout: 15000, @@ -142,8 +145,17 @@ const fetchCompaniesMap = async (companyIds) => { }; const buildMonthlyResponse = async ({ month, year, limit }) => { + const companiesMap = await fetchCompaniesMap(); const jobs = await fetchMonthlyJobs(month, year); - const rankingMap = new Map(); + const rankingMap = new Map( + [...companiesMap.values()].map((company) => [Number(company.company_id), { + id: Number(company.company_id), + total_distance: 0, + total_jobs: 0, + months_processed: 1, + months_with_errors: 0, + }]) + ); for (const job of jobs) { const companyId = Number(job.company_id); @@ -162,15 +174,18 @@ const buildMonthlyResponse = async ({ month, year, limit }) => { const item = rankingMap.get(companyId); const distance = Number(job.driven_distance_km ?? job.real_driven_distance_km) || 0; item.total_distance += distance; - item.total_jobs += 1; + const eventType = String(job.event_type || '').trim().toLowerCase(); + if (eventType === 'job_completed') { + item.total_jobs += 1; + } } - const companiesMap = await fetchCompaniesMap([...rankingMap.keys()]); const items = [...rankingMap.values()] .map((item) => { const company = companiesMap.get(item.id) || {}; return { ...item, + total_distance: Math.floor(Number(item.total_distance) || 0), name: company.name || `Empresa ${item.id}`, tag: company.tag || '', members: Number.isFinite(Number(company.members_count)) ? Number(company.members_count) : null,