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:
@@ -13,6 +13,13 @@ export interface SweepResult {
|
||||
}>;
|
||||
}
|
||||
|
||||
const DEFAULT_RUNNING_HOURS_BY_TYPE: Record<string, number> = {
|
||||
initial: 8,
|
||||
daily: 4,
|
||||
incremental: 2,
|
||||
custom: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* Watchdog para jobs `sat_sync_jobs` stale.
|
||||
*
|
||||
@@ -22,35 +29,45 @@ export interface SweepResult {
|
||||
* (dev, caída, reinicio largo) el job queda colgado y bloquea el
|
||||
* lock para nuevos syncs del mismo (tenant, contribuyente).
|
||||
*
|
||||
* 2. `running` con `startedAt` > runningHours atrás. Un sync inicial
|
||||
* típico termina en <2h; si lleva >runningHours es casi seguro
|
||||
* huérfano de un proceso que murió. La solicitud SAT ya expiró.
|
||||
* 2. `running` con `startedAt` > runningHours atrás. Thresholds difieren
|
||||
* por tipo: initial (8h) porque un bootstrap de 6 años puede tardar
|
||||
* varias horas; daily (4h); incremental (2h) porque es ventana corta.
|
||||
* Si lleva >threshold es casi seguro huérfano de un proceso que murió.
|
||||
*
|
||||
* Marca ambos como `failed` con `errorMessage` descriptivo. Idempotente
|
||||
* (volver a correrlo no reabre los ya-marcados-failed).
|
||||
*
|
||||
* - `apply=false` (default): dry-run, no toca BD.
|
||||
* - `pendingHours`/`runningHours`: thresholds (default 12h / 4h).
|
||||
* - `pendingHours`: threshold pending (default 12h).
|
||||
* - `runningHours`: fallback threshold running si no se usa por-tipo (default 4h).
|
||||
* - `runningHoursByType`: override por tipo de sync.
|
||||
*/
|
||||
export async function sweepStaleSatJobs(params: {
|
||||
apply: boolean;
|
||||
pendingHours?: number;
|
||||
runningHours?: number;
|
||||
runningHoursByType?: Record<string, number>;
|
||||
} = { apply: false }): Promise<SweepResult> {
|
||||
const pendingHours = params.pendingHours ?? 12;
|
||||
const runningHours = params.runningHours ?? 4;
|
||||
const runningHoursByType = { ...DEFAULT_RUNNING_HOURS_BY_TYPE, ...(params.runningHoursByType || {}) };
|
||||
const now = new Date();
|
||||
const pendingCutoff = new Date(now.getTime() - pendingHours * 3600 * 1000);
|
||||
const runningCutoff = new Date(now.getTime() - runningHours * 3600 * 1000);
|
||||
|
||||
const stalePending = await prisma.satSyncJob.findMany({
|
||||
where: { status: 'pending', nextRetryAt: { lt: pendingCutoff } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
const staleRunning = await prisma.satSyncJob.findMany({
|
||||
where: { status: 'running', startedAt: { lt: runningCutoff } },
|
||||
|
||||
// running: evaluar por tipo usando thresholds distintos
|
||||
const allRunning = await prisma.satSyncJob.findMany({
|
||||
where: { status: 'running' },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
const staleRunning = allRunning.filter(j => {
|
||||
const thresholdHours = runningHoursByType[j.type] ?? params.runningHours ?? 4;
|
||||
const cutoff = new Date(now.getTime() - thresholdHours * 3600 * 1000);
|
||||
return (j.startedAt ?? j.createdAt) < cutoff;
|
||||
});
|
||||
|
||||
const result: SweepResult = {
|
||||
pendingFound: stalePending.length,
|
||||
@@ -83,12 +100,13 @@ export async function sweepStaleSatJobs(params: {
|
||||
result.pendingMarked++;
|
||||
}
|
||||
for (const j of staleRunning) {
|
||||
const thresholdHours = runningHoursByType[j.type] ?? params.runningHours ?? 4;
|
||||
await prisma.satSyncJob.update({
|
||||
where: { id: j.id },
|
||||
data: {
|
||||
status: 'failed',
|
||||
completedAt: now,
|
||||
errorMessage: `Abandoned by watchdog: running with startedAt ${j.startedAt?.toISOString()} > ${runningHours}h (process crash / orphan). SAT request is lost; re-launch manually.`,
|
||||
errorMessage: `Abandoned by watchdog: running ${j.type} with startedAt ${j.startedAt?.toISOString()} > ${thresholdHours}h (process crash / orphan). SAT request is lost; re-launch manually.`,
|
||||
},
|
||||
});
|
||||
result.runningMarked++;
|
||||
|
||||
Reference in New Issue
Block a user