feat: facturación primer pago, fixes SAT/MP, autocompletado RFCs/conceptos
Backend: - Notificación email al admin cuando llega primer pago aprobado (sin factura auto) - Endpoints GET /pagos-sin-factura y POST /emitir-factura-pago para admin global - Fix vinculación org Facturapi Horux 360 (69f23a5a242e0af47a41fa0d) - Fix webhook MP: validación defensiva de x-signature header - Fix autocompleto RFCs: eliminado filtro por contribuyenteId - Fix autocompleto conceptos: eliminado filtro por contribuyenteId - SAT fixes: anti-bot CSF scraper, request reuse, date range fix, stale job thresholds - SAT sync request reuse across jobs para evitar agotar cuota diaria - Typo fix MP_ACCESS_TOKEN en .env - Trial invitations system backend Frontend: - Nueva página /admin/facturas-pendientes con tabla y emisión manual - Métrica 'Facturas pendientes' en /clientes (clickable) - Navegación onboarding FIEL/CSD corregida - Sidebar themes sincronizados - Fix SAT portal migration scraper (NetIQ) - Trial invitation acceptance pages
This commit is contained in:
@@ -547,9 +547,35 @@ async function requestAndDownload(
|
||||
// Intentar reusar requestId previo del mismo job/kindKey (caso retry)
|
||||
const jobRow = await prisma.satSyncJob.findUnique({
|
||||
where: { id: jobId },
|
||||
select: { satRequestIds: true },
|
||||
select: { satRequestIds: true, tenantId: true, contribuyenteId: true, dateFrom: true, dateTo: true },
|
||||
});
|
||||
const existingMap = (jobRow?.satRequestIds as Record<string, string> | null) || {};
|
||||
let existingMap = (jobRow?.satRequestIds as Record<string, string> | null) || {};
|
||||
|
||||
// Si no existe en el job actual, buscar en el job más reciente del mismo tenant/contribuyente
|
||||
// SOLO si el rango de fechas es idéntico (mismo dateFrom/dateTo).
|
||||
if (!existingMap[kindKey]) {
|
||||
const previousJob = await prisma.satSyncJob.findFirst({
|
||||
where: {
|
||||
tenantId: jobRow?.tenantId,
|
||||
contribuyenteId: jobRow?.contribuyenteId ?? null,
|
||||
id: { not: jobId },
|
||||
dateFrom: jobRow?.dateFrom,
|
||||
dateTo: jobRow?.dateTo,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { satRequestIds: true },
|
||||
});
|
||||
if (previousJob?.satRequestIds) {
|
||||
const prevMap = previousJob.satRequestIds as Record<string, string>;
|
||||
if (prevMap[kindKey]) {
|
||||
console.log(`[SAT] Reutilizando requestId de job previo (${label}): ${prevMap[kindKey]}`);
|
||||
// Copiar al job actual para futuros usos
|
||||
await persistSatRequestId(jobId, kindKey, prevMap[kindKey]);
|
||||
existingMap = { ...existingMap, [kindKey]: prevMap[kindKey] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let requestId: string | null = existingMap[kindKey] || null;
|
||||
let verifyResult: Awaited<ReturnType<typeof verifySatRequest>> | undefined;
|
||||
|
||||
@@ -651,7 +677,8 @@ async function processDateRange(
|
||||
jobId: string,
|
||||
fechaInicio: Date,
|
||||
fechaFin: Date,
|
||||
tipoCfdi: CfdiSyncType
|
||||
tipoCfdi: CfdiSyncType,
|
||||
skipJobUpdate = false
|
||||
): Promise<{ found: number; downloaded: number; inserted: number; updated: number }> {
|
||||
let totalFound = 0;
|
||||
let totalDownloaded = 0;
|
||||
@@ -678,12 +705,14 @@ async function processDateRange(
|
||||
console.error(`[SAT] Error en XMLs ${tipoCfdi}: ${error.message}`);
|
||||
}
|
||||
|
||||
await updateJobProgress(jobId, {
|
||||
cfdisFound: totalFound,
|
||||
cfdisDownloaded: totalDownloaded,
|
||||
cfdisInserted: totalInserted,
|
||||
cfdisUpdated: totalUpdated,
|
||||
});
|
||||
if (!skipJobUpdate) {
|
||||
await updateJobProgress(jobId, {
|
||||
cfdisFound: totalFound,
|
||||
cfdisDownloaded: totalDownloaded,
|
||||
cfdisInserted: totalInserted,
|
||||
cfdisUpdated: totalUpdated,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
found: totalFound,
|
||||
@@ -787,7 +816,9 @@ async function processInitialSync(
|
||||
customDateTo?: Date
|
||||
): Promise<void> {
|
||||
const ahora = new Date();
|
||||
const inicioHistorico = customDateFrom || new Date(ahora.getFullYear() - YEARS_TO_SYNC, ahora.getMonth(), 1);
|
||||
// Exactamente 6 años atrás desde hoy (mismo día del mes), no inicio de mes.
|
||||
// El SAT rechaza "mayor a 6 años" si usamos el día 1 del mes hace 6 años.
|
||||
const inicioHistorico = customDateFrom || new Date(ahora.getFullYear() - YEARS_TO_SYNC, ahora.getMonth(), ahora.getDate());
|
||||
const fechaFin = customDateTo || ahora;
|
||||
|
||||
// Paso 1: Sondeo — determinar tamaño de bloque para XMLs
|
||||
@@ -802,13 +833,29 @@ async function processInitialSync(
|
||||
let totalInserted = 0;
|
||||
let totalUpdated = 0;
|
||||
|
||||
const totalSteps = xmlChunks.length * 2 + metaChunks.length * 2; // emitidos + recibidos por cada chunk
|
||||
let completedSteps = 0;
|
||||
|
||||
// Helper para actualizar progreso acumulado
|
||||
async function reportProgress() {
|
||||
completedSteps++;
|
||||
const progressPercent = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
||||
await updateJobProgress(jobId, {
|
||||
cfdisFound: totalFound,
|
||||
cfdisDownloaded: totalDownloaded,
|
||||
cfdisInserted: totalInserted,
|
||||
cfdisUpdated: totalUpdated,
|
||||
progressPercent,
|
||||
});
|
||||
}
|
||||
|
||||
// Paso 2: Descargar XMLs de vigentes (bloques de 3/6 meses)
|
||||
for (let i = 0; i < xmlChunks.length; i++) {
|
||||
const { start, end } = xmlChunks[i];
|
||||
console.log(`[SAT] XML bloque ${i + 1}/${xmlChunks.length}: ${start.toISOString().slice(0, 10)} → ${end.toISOString().slice(0, 10)}`);
|
||||
|
||||
try {
|
||||
const emitidos = await processDateRange(ctx, jobId, start, end, 'emitidos');
|
||||
const emitidos = await processDateRange(ctx, jobId, start, end, 'emitidos', true);
|
||||
totalFound += emitidos.found;
|
||||
totalDownloaded += emitidos.downloaded;
|
||||
totalInserted += emitidos.inserted;
|
||||
@@ -816,9 +863,10 @@ async function processInitialSync(
|
||||
} catch (error: any) {
|
||||
console.error(`[SAT] Error emitidos XML bloque ${i + 1}:`, error.message);
|
||||
}
|
||||
await reportProgress();
|
||||
|
||||
try {
|
||||
const recibidos = await processDateRange(ctx, jobId, start, end, 'recibidos');
|
||||
const recibidos = await processDateRange(ctx, jobId, start, end, 'recibidos', true);
|
||||
totalFound += recibidos.found;
|
||||
totalDownloaded += recibidos.downloaded;
|
||||
totalInserted += recibidos.inserted;
|
||||
@@ -826,6 +874,7 @@ async function processInitialSync(
|
||||
} catch (error: any) {
|
||||
console.error(`[SAT] Error recibidos XML bloque ${i + 1}:`, error.message);
|
||||
}
|
||||
await reportProgress();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
@@ -842,6 +891,7 @@ async function processInitialSync(
|
||||
} catch (error: any) {
|
||||
console.error(`[SAT] Error metadata emitidos bloque ${i + 1}:`, error.message);
|
||||
}
|
||||
await reportProgress();
|
||||
|
||||
try {
|
||||
const { inserted, updated } = await processMetadataRange(ctx, jobId, start, end, 'recibidos');
|
||||
@@ -850,6 +900,7 @@ async function processInitialSync(
|
||||
} catch (error: any) {
|
||||
console.error(`[SAT] Error metadata recibidos bloque ${i + 1}:`, error.message);
|
||||
}
|
||||
await reportProgress();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
@@ -859,6 +910,7 @@ async function processInitialSync(
|
||||
cfdisDownloaded: totalDownloaded,
|
||||
cfdisInserted: totalInserted,
|
||||
cfdisUpdated: totalUpdated,
|
||||
progressPercent: 100,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user