Invitar Asesor
Exportar contenido
Envía una invitación por email a un asesor fiscal o contable para que acceda a tu negocio.
- Ve a Configuración → Asesores.
- Selecciona el negocio en el desplegable (si gestionas varios).
- Ingresa el email del gestor en el campo correspondiente.
- Clic en “Enviar Invitación” — se abre el modal de permisos.
- Configura los permisos por sección (ver tabla abajo).
- Clic en “Guardar y Enviar” — invitación enviada.
Configuración de permisos
Sección titulada «Configuración de permisos»El modal muestra todas las secciones disponibles con tres opciones de acceso:
| Nivel | Descripción | Ejemplo |
|---|---|---|
| Ninguno | El asesor no ve esta sección | No necesita acceso a Proyectos |
| Lectura | Puede ver y exportar, no modificar | Solo consulta de Facturas |
| Edición | Acceso completo: ver, crear, editar, eliminar | Gestión total de Gastos |
Secciones disponibles:
✓ Facturas (invoices)✓ Gastos (expenses)✓ Tickets (tickets)✓ Presupuestos (quotes)✓ Productos (products)✓ Clientes (customers)✓ Reportes (reports)✓ Proyectos (projects)✓ Tareas (tasks)Permisos predeterminados: Todas las secciones con Edición (puedes cambiarlos).
Campos del formulario
Sección titulada «Campos del formulario»| Campo | Obligatorio | Validación | Ejemplo |
|---|---|---|---|
| Negocio | Sí | Debe ser un negocio propio | Mi Empresa S.L. |
| Email del gestor | Sí | Email válido, max 255 caracteres | gestor@asesoria.com |
Cómo sé que lo hice bien
Sección titulada «Cómo sé que lo hice bien»- Toast de confirmación: “Invitación enviada a [email]”.
- Invitación aparece en la pestaña “Enviadas” con estado “Pendiente”.
- Asesor recibe email con enlace de invitación.
- Badge muestra contador en pestaña “Enviadas” si hay pendientes.
Email recibido por el asesor
Sección titulada «Email recibido por el asesor»El sistema envía dos tipos de email según si el usuario ya está registrado:
Usuario nuevo (primera vez en Factux)
Sección titulada «Usuario nuevo (primera vez en Factux)»Asunto: “Te han invitado como gestor a Factux”
Contenido:
- Mensaje de bienvenida
- Nombre del negocio destacado
- Botón: “Crear cuenta y unirme”
- Nota: Requiere crear contraseña
- Expiración: 7 días
Usuario existente (ya tiene cuenta)
Sección titulada «Usuario existente (ya tiene cuenta)»Asunto: “[Nombre del negocio] te ha agregado como gestor”
Contenido:
- Mensaje informativo
- Nombre del negocio destacado
- Botón: “Aceptar y agregar negocio”
- Nota: No requiere contraseña nueva
- Expiración: 7 días
Errores comunes
Sección titulada «Errores comunes»| Mensaje | Causa | Solución |
|---|---|---|
| ”Por favor, completa el email y selecciona un negocio” | Falta email o negocio | Completa ambos campos |
| ”Por favor, introduce un email válido” | Email con formato incorrecto | Verifica el email (debe contener @ y dominio) |
| “Ya existe una invitación pendiente…” | Invitación no aceptada previa | Espera a que la acepte, reenvíala o cancélala |
| ”Este asesor ya tiene acceso…” | Asesor vinculado actualmente | Ve a pestaña “Activos” para gestionar sus permisos |
| ”Error al enviar la invitación” | Error de servidor o email | Verifica el email y reintenta, o contacta soporte |
Detalles técnicos — Invitar Asesor
Sección titulada «Detalles técnicos — Invitar Asesor»Ver implementación técnica
Edge Function: send-advisor-invitation
Validaciones (Zod Schema):
| Código | Regla |
|---|---|
| VAL-01 | email válido según RFC 5322 |
| VAL-02 | email max 255 caracteres |
| VAL-03 | email normalizado (lowercase, trim) |
| VAL-04 | businessId UUID válido |
| VAL-05 | ownerName min 1, max 100 caracteres |
| VAL-06 | businessName min 1, max 200 caracteres |
| VAL-07 | permissions array de {section, access} |
| VAL-08 | access debe ser ‘none’, ‘read’ o ‘edit’ |
Lógica de detección de usuario:
// Verifica si email pertenece a usuario existenteconst { data: existingUsers } = await supabase.auth.admin.listUsers();const existingUser = existingUsers?.users.find(u => u.email === normalizedEmail);const isExistingUser = !!existingUser;
// Almacena flag en invitación{ ...invitationData, is_existing_user: isExistingUser}Manejo de invitaciones previas:
| Estado previo | Acción | Resultado |
|---|---|---|
pending (sin isResend) | Error | Usuario debe esperar o cancelar |
pending (con isResend) | Regenera token y fecha | Nueva invitación enviada |
accepted | Error | Usuario ya tiene acceso |
expired | Reactiva con nuevo token | Nueva invitación enviada |
cancelled | Reactiva con nuevo token | Nueva invitación enviada |
| No existe | Crea nueva | Nueva invitación enviada |
Generación de token:
const token = crypto.randomUUID(); // UUID v4const expiresAt = new Date();expiresAt.setDate(expiresAt.getDate() + 7); // +7 díasURL de invitación:
{APP_URL}/accept-advisor-invitation/{token}Tabla: advisor_invitations
{ id: UUID; // Auto-generado owner_id: UUID; // auth.uid() del propietario email: string; // Email normalizado business_id: UUID; // Negocio seleccionado token: string; // UUID único status: 'pending'; // Estado inicial expires_at: TIMESTAMPTZ; // Ahora + 7 días sent_at: TIMESTAMPTZ; // NOW() permissions: JSONB; // Array de permisos configurados is_existing_user: boolean; // Usuario ya registrado created_at: TIMESTAMPTZ; // NOW() updated_at: TIMESTAMPTZ; // NOW()}Email enviado vía SendGrid:
const emailResponse = await fetch('https://api.sendgrid.com/v3/mail/send', { method: 'POST', headers: { 'Authorization': `Bearer ${sendgridApiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ personalizations: [{ to: [{ email }] }], from: { email: 'noreply@facturik.com', name: 'Factux' }, subject: emailSubject, content: [{ type: 'text/html', value: emailHtml }] })});Query de diagnóstico:
-- Ver invitación con detallesSELECT ai.id, ai.email, ai.status, ai.sent_at, ai.expires_at, ai.is_existing_user, ai.permissions, b.name as business_name, p.first_name || ' ' || p.last_name as owner_nameFROM advisor_invitations aiJOIN businesses b ON ai.business_id = b.idJOIN profiles p ON ai.owner_id = p.user_idWHERE ai.id = '<invitation_id>';Troubleshooting:
-- Invitaciones duplicadas (no deberían existir)SELECT owner_id, email, business_id, COUNT(*) as countFROM advisor_invitationsWHERE status = 'pending'GROUP BY owner_id, email, business_idHAVING COUNT(*) > 1;
-- Invitaciones próximas a expirarSELECT id, email, business_id, expires_at, EXTRACT(EPOCH FROM (expires_at - NOW())) / 3600 as hours_until_expiryFROM advisor_invitationsWHERE status = 'pending' AND expires_at > NOW() AND expires_at < NOW() + INTERVAL '24 hours'ORDER BY expires_at;