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:
@@ -3,7 +3,7 @@ import { prisma } from '../config/database.js';
|
||||
import { isGlobalAdmin } from '../utils/global-admin.js';
|
||||
|
||||
async function requireGlobalAdmin(req: Request, res: Response): Promise<boolean> {
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role);
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({ message: 'Solo el administrador global puede consultar el audit log' });
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ function effectiveTenantId(req: Request): string {
|
||||
const ROLES_OWNER = new Set(['owner', 'cfo']);
|
||||
const ROLES_SUPERVISORY = new Set(['owner', 'cfo', 'supervisor']);
|
||||
const ROLES_ASIGNADOS = new Set(['owner', 'cfo', 'supervisor', 'auxiliar']);
|
||||
const PLATFORM_SUPERSET = new Set(['platform_admin', 'platform_ti']);
|
||||
|
||||
function isPlatformStaff(user: Request['user']): boolean {
|
||||
return (user?.platformRoles || []).some(r => PLATFORM_SUPERSET.has(r));
|
||||
}
|
||||
|
||||
export async function getContribuyentesStats(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!ROLES_OWNER.has(req.user!.role)) {
|
||||
if (!isPlatformStaff(req.user) && !ROLES_OWNER.has(req.user!.role)) {
|
||||
throw new AppError(403, 'Solo owner puede ver estas métricas');
|
||||
}
|
||||
const tenantId = effectiveTenantId(req);
|
||||
@@ -27,7 +32,7 @@ export async function getContribuyentesStats(req: Request, res: Response, next:
|
||||
|
||||
export async function getMisAsignados(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!ROLES_ASIGNADOS.has(req.user!.role)) {
|
||||
if (!isPlatformStaff(req.user) && !ROLES_ASIGNADOS.has(req.user!.role)) {
|
||||
throw new AppError(403, 'No tienes contribuyentes asignados');
|
||||
}
|
||||
const año = req.query.año ? parseInt(String(req.query.año), 10) : undefined;
|
||||
@@ -47,7 +52,7 @@ export async function getMisAsignados(req: Request, res: Response, next: NextFun
|
||||
|
||||
export async function getEquipoStats(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!ROLES_SUPERVISORY.has(req.user!.role)) {
|
||||
if (!isPlatformStaff(req.user) && !ROLES_SUPERVISORY.has(req.user!.role)) {
|
||||
throw new AppError(403, 'Solo owner y supervisor pueden ver al equipo');
|
||||
}
|
||||
const año = req.query.año ? parseInt(String(req.query.año), 10) : undefined;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -518,8 +518,6 @@ export async function searchConceptos(req: Request, res: Response, next: NextFun
|
||||
whereType = `AND (c.type = 'EMITIDO' OR (c.type = 'RECIBIDO' AND c.uso_cfdi = 'G01'))`;
|
||||
}
|
||||
|
||||
const whereContrib = contribuyenteId ? `AND c.contribuyente_id = '${contribuyenteId}'` : '';
|
||||
|
||||
let whereSearch = '';
|
||||
const params: any[] = [];
|
||||
if (q.length >= 2) {
|
||||
@@ -548,7 +546,6 @@ export async function searchConceptos(req: Request, res: Response, next: NextFun
|
||||
JOIN cfdis c ON cc.cfdi_id = c.id
|
||||
WHERE c.status NOT IN ('Cancelado', '0')
|
||||
${whereType}
|
||||
${whereContrib}
|
||||
${whereSearch}
|
||||
ORDER BY cc.clave_prod_serv, cc.descripcion, c.fecha_emision DESC
|
||||
LIMIT 30
|
||||
@@ -664,40 +661,20 @@ export async function searchRfcs(req: Request, res: Response, next: NextFunction
|
||||
});
|
||||
const tenantRfc = tenant?.rfc || '';
|
||||
|
||||
// En multi-RFC con contribuyente activo, filtrar a contrapartes con las
|
||||
// que ese contribuyente ha tenido CFDIs (emisor o receptor). Sin
|
||||
// contribuyenteId, retornar el catálogo completo (compat con flujos
|
||||
// legacy / admin global sin contribuyente seleccionado).
|
||||
let rows;
|
||||
if (contribuyenteId) {
|
||||
({ rows } = await pool.query(`
|
||||
SELECT DISTINCT r.id, r.rfc,
|
||||
r.razon_social as "razonSocial",
|
||||
r.regimen_fiscal as "regimenFiscal",
|
||||
r.codigo_postal as "codigoPostal"
|
||||
FROM rfcs r
|
||||
WHERE r.rfc != $1
|
||||
AND (r.rfc ILIKE $2 OR r.razon_social ILIKE $2)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM cfdis c
|
||||
WHERE c.contribuyente_id = $3
|
||||
AND (c.rfc_emisor_id = r.id OR c.rfc_receptor_id = r.id)
|
||||
)
|
||||
ORDER BY r.razon_social
|
||||
LIMIT 10
|
||||
`, [tenantRfc, `%${q}%`, contribuyenteId]));
|
||||
} else {
|
||||
({ rows } = await pool.query(`
|
||||
SELECT id, rfc, razon_social as "razonSocial",
|
||||
regimen_fiscal as "regimenFiscal",
|
||||
codigo_postal as "codigoPostal"
|
||||
FROM rfcs
|
||||
WHERE rfc != $1
|
||||
AND (rfc ILIKE $2 OR razon_social ILIKE $2)
|
||||
ORDER BY razon_social
|
||||
LIMIT 10
|
||||
`, [tenantRfc, `%${q}%`]));
|
||||
}
|
||||
// Búsqueda en el catálogo completo de RFCs. El contribuyente activo solo
|
||||
// filtra CFDIs relacionados / PPD, no el autocompleto de RFCs — de lo
|
||||
// contrario no se podría facturar a un cliente nuevo que nunca haya
|
||||
// aparecido en un CFDI previo.
|
||||
const { rows } = await pool.query(`
|
||||
SELECT id, rfc, razon_social as "razonSocial",
|
||||
regimen_fiscal as "regimenFiscal",
|
||||
codigo_postal as "codigoPostal"
|
||||
FROM rfcs
|
||||
WHERE rfc != $1
|
||||
AND (rfc ILIKE $2 OR razon_social ILIKE $2)
|
||||
ORDER BY razon_social
|
||||
LIMIT 10
|
||||
`, [tenantRfc, `%${q}%`]);
|
||||
|
||||
res.json(rows);
|
||||
} catch (error) { next(error); }
|
||||
@@ -787,3 +764,123 @@ export async function comprarPaquete(req: Request, res: Response, next: NextFunc
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin global: pagos de suscripción sin factura ──
|
||||
|
||||
export async function getPagosSinFactura(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!(await hasPlatformRole(req.user!.userId, 'platform_admin'))) {
|
||||
return res.status(403).json({ message: 'Solo admin global puede consultar pagos sin factura' });
|
||||
}
|
||||
|
||||
const payments = await prisma.payment.findMany({
|
||||
where: {
|
||||
status: 'approved',
|
||||
facturapiInvoiceId: null,
|
||||
kind: 'subscription',
|
||||
amount: { gt: 0 },
|
||||
},
|
||||
include: {
|
||||
subscription: { select: { plan: true, frequency: true } },
|
||||
tenant: { select: { nombre: true, rfc: true } },
|
||||
},
|
||||
orderBy: { paidAt: 'desc' },
|
||||
});
|
||||
|
||||
res.json(payments);
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
export async function emitirFacturaPago(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!(await hasPlatformRole(req.user!.userId, 'platform_admin'))) {
|
||||
return res.status(403).json({ message: 'Solo admin global puede emitir facturas de pago' });
|
||||
}
|
||||
|
||||
const paymentId = String(req.params.paymentId);
|
||||
const payment = await prisma.payment.findUnique({
|
||||
where: { id: paymentId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
|
||||
if (!payment) {
|
||||
return next(new AppError(404, 'Pago no encontrado'));
|
||||
}
|
||||
if (payment.status !== 'approved') {
|
||||
return next(new AppError(400, 'Solo pagos aprobados pueden facturarse'));
|
||||
}
|
||||
if (payment.facturapiInvoiceId) {
|
||||
return next(new AppError(400, 'Este pago ya tiene una factura emitida'));
|
||||
}
|
||||
|
||||
// Reutilizar helpers del servicio de facturación
|
||||
const { getEmitterTenant, getCustomerFromTenant } = await import('../services/payment/invoicing.service.js');
|
||||
|
||||
const emitter = await getEmitterTenant();
|
||||
const amount = Number(payment.amount);
|
||||
const plan = (payment as any).subscription?.plan || 'custom';
|
||||
const frequency = (payment as any).subscription?.frequency || 'monthly';
|
||||
const descFrecuencia = frequency === 'annual' ? 'anual' : 'mensual';
|
||||
const description = `Suscripción ${plan} ${descFrecuencia} a Horux Despachos`;
|
||||
|
||||
const customer = await getCustomerFromTenant(payment.tenantId);
|
||||
if (!customer) {
|
||||
return next(new AppError(400, 'El tenant no tiene datos fiscales completos. No se puede facturar.'));
|
||||
}
|
||||
|
||||
const tenantPref = await prisma.tenant.findUnique({
|
||||
where: { id: payment.tenantId },
|
||||
select: { factUsoCfdi: true },
|
||||
});
|
||||
const usoCfdi = customer ? (tenantPref?.factUsoCfdi || 'G03') : 'S01';
|
||||
|
||||
const formaPagoMap: Record<string, string> = {
|
||||
master: '04', visa: '04', amex: '04',
|
||||
debmaster: '28', debvisa: '28',
|
||||
account_money: '03', bank_transfer: '03',
|
||||
};
|
||||
const normalizedMethod = (payment.paymentMethod || '').toLowerCase().replace(/^proration-/, '');
|
||||
const formaPago = formaPagoMap[normalizedMethod] || '03';
|
||||
|
||||
const payload = {
|
||||
customer: {
|
||||
legalName: customer.legalName,
|
||||
taxId: customer.taxId,
|
||||
taxSystem: customer.taxSystem,
|
||||
email: customer.email,
|
||||
zip: customer.zip,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
description,
|
||||
productKey: '81112502',
|
||||
unitKey: 'E48',
|
||||
unitName: 'Servicio',
|
||||
quantity: 1,
|
||||
price: amount,
|
||||
taxIncluded: true,
|
||||
taxes: [{ type: 'IVA', rate: 0.16, factor: 'Tasa' }],
|
||||
},
|
||||
],
|
||||
use: usoCfdi,
|
||||
paymentForm: formaPago,
|
||||
paymentMethod: 'PUE',
|
||||
currency: 'MXN',
|
||||
};
|
||||
|
||||
const invoice = await facturapiService.createInvoice(emitter.id, payload as any);
|
||||
|
||||
await prisma.payment.update({
|
||||
where: { id: payment.id },
|
||||
data: { facturapiInvoiceId: invoice.id },
|
||||
});
|
||||
|
||||
auditFromReq(req, 'invoice.emitted_manual', {
|
||||
entityType: 'Payment',
|
||||
entityId: payment.id,
|
||||
metadata: { facturapiInvoiceId: invoice.id, amount, plan, frequency },
|
||||
});
|
||||
|
||||
res.json({ success: true, invoiceId: invoice.id, paymentId: payment.id });
|
||||
} catch (error) { next(error); }
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export async function retry(req: Request, res: Response): Promise<void> {
|
||||
*/
|
||||
export async function cronInfo(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role))) {
|
||||
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId))) {
|
||||
res.status(403).json({ error: 'Solo el administrador global puede ver info del cron' });
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export async function cronInfo(req: Request, res: Response): Promise<void> {
|
||||
*/
|
||||
export async function runCron(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role))) {
|
||||
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId))) {
|
||||
res.status(403).json({ error: 'Solo el administrador global puede ejecutar el cron' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isGlobalAdmin } from '../utils/global-admin.js';
|
||||
import { auditFromReq } from '../utils/audit.js';
|
||||
|
||||
async function requireGlobalAdmin(req: Request, res: Response): Promise<boolean> {
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role);
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({ message: 'Solo el administrador global puede gestionar suscripciones' });
|
||||
}
|
||||
@@ -19,7 +19,7 @@ async function requireGlobalAdmin(req: Request, res: Response): Promise<boolean>
|
||||
*/
|
||||
async function requireOwnTenantOrGlobalAdmin(req: Request, res: Response, targetTenantId: string): Promise<boolean> {
|
||||
if (targetTenantId === req.user!.tenantId) return true;
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role);
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({ message: 'Solo puedes gestionar la suscripción de tu propio tenant' });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
130
apps/api/src/controllers/trial-invitations.controller.ts
Normal file
130
apps/api/src/controllers/trial-invitations.controller.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import * as trialInvitationService from '../services/trial-invitations.service.js';
|
||||
import { isGlobalAdmin } from '../utils/global-admin.js';
|
||||
import { prisma } from '../config/database.js';
|
||||
|
||||
async function requireGlobalAdmin(req: Request, res: Response): Promise<boolean> {
|
||||
const isAdmin = await isGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({ message: 'Solo el administrador global puede gestionar invitaciones de trial' });
|
||||
}
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
export async function createInvitation(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!(await requireGlobalAdmin(req, res))) return;
|
||||
|
||||
const { tenantId, plan, durationDays } = req.body;
|
||||
if (!tenantId || !durationDays || durationDays < 1 || durationDays > 365) {
|
||||
return res.status(400).json({ message: 'tenantId y durationDays (1-365) son requeridos' });
|
||||
}
|
||||
|
||||
const invitation = await trialInvitationService.createInvitation({
|
||||
tenantId,
|
||||
invitedByUserId: req.user!.userId,
|
||||
plan: plan || 'business_control',
|
||||
durationDays: parseInt(durationDays, 10),
|
||||
});
|
||||
|
||||
res.status(201).json(invitation);
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('ya tiene') || error.message?.includes('no encontrado')) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllInvitations(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!(await requireGlobalAdmin(req, res))) return;
|
||||
|
||||
const { tenantId, status } = req.query;
|
||||
const invitations = await trialInvitationService.getInvitations({
|
||||
tenantId: typeof tenantId === 'string' ? tenantId : undefined,
|
||||
status: typeof status === 'string' ? status : undefined,
|
||||
});
|
||||
|
||||
res.json(invitations);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMyPendingInvitation(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const invitation = await trialInvitationService.getPendingInvitationForTenant(req.user!.tenantId);
|
||||
res.json(invitation);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function acceptInvitation(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const token = typeof req.params.token === 'string' ? req.params.token : '';
|
||||
if (!token) {
|
||||
return res.status(400).json({ message: 'Token requerido' });
|
||||
}
|
||||
|
||||
const result = await trialInvitationService.acceptInvitation(token, req.user!.userId);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error.message?.includes('no encontrada') ||
|
||||
error.message?.includes('ya ') ||
|
||||
error.message?.includes('expirado') ||
|
||||
error.message?.includes('Solo el dueño')
|
||||
) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelInvitation(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
if (!(await requireGlobalAdmin(req, res))) return;
|
||||
|
||||
const id = typeof req.params.id === 'string' ? req.params.id : '';
|
||||
const result = await trialInvitationService.cancelInvitation(id);
|
||||
res.json(result);
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('no encontrada') || error.message?.includes('Solo se pueden')) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInvitationByToken(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const token = typeof req.params.token === 'string' ? req.params.token : '';
|
||||
const invitation = await prisma.trialInvitation.findUnique({
|
||||
where: { token },
|
||||
});
|
||||
|
||||
if (!invitation) {
|
||||
return res.status(404).json({ message: 'Invitación no encontrada' });
|
||||
}
|
||||
|
||||
const tenant = await prisma.tenant.findUnique({
|
||||
where: { id: invitation.tenantId },
|
||||
select: { nombre: true, rfc: true },
|
||||
});
|
||||
|
||||
// No exponer datos sensibles
|
||||
res.json({
|
||||
id: invitation.id,
|
||||
tenantId: invitation.tenantId,
|
||||
plan: invitation.plan,
|
||||
durationDays: invitation.durationDays,
|
||||
status: invitation.status,
|
||||
expiresAt: invitation.expiresAt,
|
||||
tenant,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const updateGlobalSchema = z.object({
|
||||
});
|
||||
|
||||
async function isGlobalAdmin(req: Request): Promise<boolean> {
|
||||
return checkGlobalAdmin(req.user!.tenantId, req.user!.role);
|
||||
return checkGlobalAdmin(req.user!.tenantId, req.user!.role, req.user!.userId);
|
||||
}
|
||||
|
||||
export async function getUsuarios(req: Request, res: Response, next: NextFunction) {
|
||||
|
||||
Reference in New Issue
Block a user