Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 108 additions & 15 deletions src/routes/v2/peruserver/trucky/live-jobs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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: [] });
}
Expand Down
33 changes: 24 additions & 9 deletions src/routes/v2/peruserver/trucky/top-km/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down
31 changes: 23 additions & 8 deletions src/routes/v2/peruserver/trucky/top-km/monthly.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down
Loading