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
90 lines
3.2 KiB
TypeScript
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;
|
|
}
|