Files
HoruxDespachosNuevo/apps/api/src/services/admin-dashboard.service.ts
Horux Dev 9f11a0ba39 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
2026-05-09 21:56:42 +00:00

90 lines
3.2 KiB
TypeScript

import { prisma } from '../config/database.js';
export async function getDashboardMetrics() {
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const [
totalTenants,
activeTenants,
trialTenants,
cancelledSubs,
recentSignups,
connectorStatuses,
payments,
] = await Promise.all([
prisma.tenant.count(),
prisma.tenant.count({ where: { active: true } }),
prisma.tenant.count({ where: { trialEndsAt: { gt: now } } }),
prisma.subscription.count({ where: { status: 'cancelled' } }),
prisma.tenant.count({ where: { createdAt: { gte: thirtyDaysAgo } } }),
prisma.tenant.findMany({
where: { connectorTunnelHostname: { not: null } },
select: { id: true, nombre: true, rfc: true, connectorLastSeen: true, connectorVersion: true },
}),
prisma.payment.aggregate({
where: { status: 'approved', createdAt: { gte: thirtyDaysAgo } },
_sum: { amount: true },
_count: true,
}),
]);
const connectors = connectorStatuses.map(t => {
let status: 'connected' | 'degraded' | 'disconnected' = 'disconnected';
if (t.connectorLastSeen) {
const diff = now.getTime() - t.connectorLastSeen.getTime();
if (diff < 60_000) status = 'connected';
else if (diff < 300_000) status = 'degraded';
}
return { id: t.id, nombre: t.nombre, rfc: t.rfc, status, lastSeen: t.connectorLastSeen?.toISOString(), version: t.connectorVersion };
});
const connectorsDown = connectors.filter(c => c.status === 'disconnected').length;
return {
tenants: { total: totalTenants, active: activeTenants, trial: trialTenants, cancelled: cancelledSubs },
signupsLast30Days: recentSignups,
revenue: { last30Days: Number(payments._sum.amount || 0), paymentsCount: payments._count },
connectors: { total: connectors.length, down: connectorsDown, list: connectors },
};
}
export async function listAllDespachos(filters?: { vertical?: string; status?: string; search?: string }) {
const where: any = {};
if (filters?.vertical) where.verticalProfile = filters.vertical;
if (filters?.status === 'active') where.active = true;
if (filters?.status === 'inactive') where.active = false;
if (filters?.search) {
where.OR = [
{ nombre: { contains: filters.search, mode: 'insensitive' } },
{ rfc: { contains: filters.search, mode: 'insensitive' } },
];
}
const tenants = await prisma.tenant.findMany({
where,
select: {
id: true, nombre: true, rfc: true, plan: true, active: true, verticalProfile: true,
dbMode: true, connectorLastSeen: true, createdAt: true, trialEndsAt: true,
},
orderBy: { createdAt: 'desc' },
take: 100,
});
return tenants.map(t => ({
...t,
connectorLastSeen: t.connectorLastSeen?.toISOString(),
createdAt: t.createdAt.toISOString(),
trialEndsAt: t.trialEndsAt?.toISOString(),
}));
}
export async function getRecentActivity(limit = 20) {
const logs = await prisma.auditLog.findMany({
where: { action: { in: ['user.register', 'user.login', 'subscription.created', 'subscription.cancelled', 'subscription.upgraded', 'price.updated'] } },
orderBy: { createdAt: 'desc' },
take: limit,
});
return logs;
}