fix(sat, payments, admin): multiple production fixes

- sat sweep-stale-jobs: increase initial/custom sync threshold 8h→24h to prevent watchdog killing long historical syncs
- sat-client: fix formatDateForSat same-day rejection by auto-adjusting fechaFin
- sat-sync job: check fiel_contribuyente in addition to fiel_credentials for cron eligibility
- database: extend pool idle cleanup from 5min to 12h to prevent pool closure during long syncs
- webhook controller: auto-extend currentPeriodEnd on recurring MercadoPago payments
- invoicing service: auto-send FacturAPI invoice by email after creation
- admin-clientes: fix no-renovaciones detection to include expired trials and deleted subscriptions
This commit is contained in:
Horux Dev
2026-06-10 18:11:47 +00:00
parent bd7e499ab7
commit b1eaf41681
7 changed files with 192 additions and 26 deletions

View File

@@ -10,6 +10,21 @@ import { despachoPlanTieneDualidadDb } from '../services/plan-catalogo.service.j
import { emailService } from '../services/email/email.service.js';
import { getTenantOwnerEmail } from '../utils/memberships.js';
/**
* Calcula la siguiente fecha de fin de período según la frecuencia.
* Usa el mismo algoritmo que Mercado Pago: mismo día del mes siguiente,
* ajustando al último día si el mes destino tiene menos días.
*/
function computeNextPeriodEnd(date: Date, frequency: string): Date {
const d = new Date(date);
if (frequency === 'monthly') {
d.setMonth(d.getMonth() + 1);
} else if (frequency === 'annual' || frequency === 'yearly') {
d.setFullYear(d.getFullYear() + 1);
}
return d;
}
export async function handleMercadoPagoWebhook(req: Request, res: Response, next: NextFunction) {
try {
const { type, data } = req.body;
@@ -187,9 +202,20 @@ async function handlePaymentNotification(paymentId: string) {
// precio de renewal. Se detecta comparando el monto cobrado contra lo que
// `getPlanPrice(phase='firstYear')` devolvería para este plan.
const esPrimerPago = subscription.status === 'pending';
const updateData: { status: string; currentPeriodEnd?: Date } = { status: 'authorized' };
// Extender currentPeriodEnd para renovaciones recurrentes.
// El primer pago ya tiene currentPeriodEnd establecido al crear la suscripción;
// solo extendemos en pagos subsecuentes para reflejar el nuevo período cobrado.
if (!esPrimerPago && subscription.currentPeriodEnd) {
const nextPeriodEnd = computeNextPeriodEnd(subscription.currentPeriodEnd, subscription.frequency);
updateData.currentPeriodEnd = nextPeriodEnd;
console.log(`[WEBHOOK] Subscription ${subscription.id} extended to ${nextPeriodEnd.toISOString()} (${subscription.frequency})`);
}
await prisma.subscription.update({
where: { id: subscription.id },
data: { status: 'authorized' },
data: updateData,
});
subscriptionService.invalidateSubscriptionCache(tenantId);