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

@@ -10,10 +10,10 @@ const signupSchema = z.object({
regimenFiscal: z.string().optional(),
codigoPostal: z.string().regex(/^\d{5}$/, 'Código postal inválido').optional(),
verticalProfile: z.enum(['CONTABLE', 'JURIDICO', 'ARQUITECTURA']),
plan: z.enum(['trial', 'mi_empresa', 'mi_empresa_plus', 'business_control', 'business_cloud']).optional().default('trial'),
// Solo aplica a mi_empresa y mi_empresa_plus (los otros pagados son
// anuales fijos). Default annual sesga el cash-flow del negocio.
frequency: z.enum(['monthly', 'annual']).optional().default('annual'),
// plan y frequency ya no se escogen en el registro — todos empiezan con trial genérico.
// Se mantienen opcionales para compatibilidad backward con clientes antiguos.
plan: z.enum(['trial', 'mi_empresa', 'mi_empresa_plus', 'business_control', 'business_cloud']).optional(),
frequency: z.enum(['monthly', 'annual']).optional(),
}),
owner: z.object({
nombre: z.string().min(2, 'Nombre del owner requerido'),
@@ -42,16 +42,10 @@ export async function getMyPlan(req: Request, res: Response, next: NextFunction)
// planes, dbMode también es MANAGED y reportar `business_cloud` daba
// mapeo equivocado. tenant.plan es la fuente de verdad post-migración
// 20260426073942 (que añadió mi_empresa y mi_empresa_plus al enum).
let currentPlan: string;
if (isTrialActive) {
currentPlan = 'trial';
} else {
currentPlan = String(tenant.plan);
}
// Estado de suscripción activa (si hay) — alimenta la UI con el monto
// recurrente actual, fecha de próxima renovación y si el primer pago
// (cuando aplica dualidad firstYear) ya fue completado.
//
// FIX: Si hay una subscription en trial con un plan específico (ej.
// business_control desde una TrialInvitation), respetamos ese plan
// para que el feature-gate y los límites funcionen correctamente.
const subscription = await prisma.subscription.findFirst({
where: { tenantId, status: { in: ['authorized', 'pending', 'paused', 'trial'] } },
orderBy: { createdAt: 'desc' },
@@ -61,6 +55,18 @@ export async function getMyPlan(req: Request, res: Response, next: NextFunction)
},
});
let currentPlan: string;
if (subscription?.status === 'trial' && subscription.plan && subscription.plan !== 'trial') {
currentPlan = subscription.plan;
} else if (isTrialActive) {
currentPlan = 'trial';
} else {
currentPlan = String(tenant.plan);
}
// Estado de suscripción activa (si hay) — alimenta la UI con el monto
// recurrente actual, fecha de próxima renovación y si el primer pago
// (cuando aplica dualidad firstYear) ya fue completado.
return res.json({
plan: currentPlan,
dbMode: tenant.dbMode,