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:
Horux Dev
2026-05-09 21:56:42 +00:00
parent b00b677c54
commit 9f11a0ba39
70 changed files with 2801 additions and 609 deletions

View File

@@ -6,16 +6,24 @@ import { isGlobalAdmin } from '../utils/global-admin.js';
import { isOwnerSomewhere } from '../utils/memberships.js';
async function requireGlobalAdmin(req: Request): Promise<void> {
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role))) {
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId))) {
throw new AppError(403, 'Solo el administrador global puede gestionar clientes');
}
}
export async function getAllTenants(req: Request, res: Response, next: NextFunction) {
try {
await requireGlobalAdmin(req);
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
if (!isAdmin) {
// Evita 403 en consola del frontend cuando componentes sin-gate hacen polling
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
return res.json([]);
}
const tenants = await tenantsService.getAllTenants();
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.json(tenants);
} catch (error) {
next(error);
@@ -24,7 +32,10 @@ export async function getAllTenants(req: Request, res: Response, next: NextFunct
export async function getTenant(req: Request, res: Response, next: NextFunction) {
try {
await requireGlobalAdmin(req);
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
if (!isAdmin) {
return res.status(404).json({ message: 'Cliente no encontrado' });
}
const tenant = await tenantsService.getTenantById(String(req.params.id));
if (!tenant) {
@@ -68,13 +79,15 @@ export async function updateTenant(req: Request, res: Response, next: NextFuncti
await requireGlobalAdmin(req);
const id = String(req.params.id);
const { nombre, rfc, plan, active } = req.body;
const { nombre, rfc, plan, active, amount, firstPaymentDueAt } = req.body;
const tenant = await tenantsService.updateTenant(id, {
nombre,
rfc,
plan,
active,
amount,
firstPaymentDueAt: firstPaymentDueAt || null,
});
res.json(tenant);