Ir al contenido

Invitar Asesor

Envía una invitación por email a un asesor fiscal o contable para que acceda a tu negocio.

  1. Ve a Configuración → Asesores.
  2. Selecciona el negocio en el desplegable (si gestionas varios).
  3. Ingresa el email del gestor en el campo correspondiente.
  4. Clic en “Enviar Invitación” — se abre el modal de permisos.
  5. Configura los permisos por sección (ver tabla abajo).
  6. Clic en “Guardar y Enviar” — invitación enviada.

El modal muestra todas las secciones disponibles con tres opciones de acceso:

NivelDescripciónEjemplo
NingunoEl asesor no ve esta secciónNo necesita acceso a Proyectos
LecturaPuede ver y exportar, no modificarSolo consulta de Facturas
EdiciónAcceso completo: ver, crear, editar, eliminarGestió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).

CampoObligatorioValidaciónEjemplo
NegocioDebe ser un negocio propioMi Empresa S.L.
Email del gestorEmail válido, max 255 caracteresgestor@asesoria.com
  • 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.

El sistema envía dos tipos de email según si el usuario ya está registrado:

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

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
MensajeCausaSolución
”Por favor, completa el email y selecciona un negocio”Falta email o negocioCompleta ambos campos
”Por favor, introduce un email válido”Email con formato incorrectoVerifica el email (debe contener @ y dominio)
“Ya existe una invitación pendiente…”Invitación no aceptada previaEspera a que la acepte, reenvíala o cancélala
”Este asesor ya tiene acceso…”Asesor vinculado actualmenteVe a pestaña “Activos” para gestionar sus permisos
”Error al enviar la invitación”Error de servidor o emailVerifica el email y reintenta, o contacta soporte
Ver implementación técnica

Edge Function: send-advisor-invitation

Validaciones (Zod Schema):

CódigoRegla
VAL-01email válido según RFC 5322
VAL-02email max 255 caracteres
VAL-03email normalizado (lowercase, trim)
VAL-04businessId UUID válido
VAL-05ownerName min 1, max 100 caracteres
VAL-06businessName min 1, max 200 caracteres
VAL-07permissions array de {section, access}
VAL-08access debe ser ‘none’, ‘read’ o ‘edit’

Lógica de detección de usuario:

// Verifica si email pertenece a usuario existente
const { 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 previoAcciónResultado
pending (sin isResend)ErrorUsuario debe esperar o cancelar
pending (con isResend)Regenera token y fechaNueva invitación enviada
acceptedErrorUsuario ya tiene acceso
expiredReactiva con nuevo tokenNueva invitación enviada
cancelledReactiva con nuevo tokenNueva invitación enviada
No existeCrea nuevaNueva invitación enviada

Generación de token:

const token = crypto.randomUUID(); // UUID v4
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7); // +7 días

URL 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 detalles
SELECT
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_name
FROM advisor_invitations ai
JOIN businesses b ON ai.business_id = b.id
JOIN profiles p ON ai.owner_id = p.user_id
WHERE ai.id = '<invitation_id>';

Troubleshooting:

-- Invitaciones duplicadas (no deberían existir)
SELECT owner_id, email, business_id, COUNT(*) as count
FROM advisor_invitations
WHERE status = 'pending'
GROUP BY owner_id, email, business_id
HAVING COUNT(*) > 1;
-- Invitaciones próximas a expirar
SELECT id, email, business_id, expires_at,
EXTRACT(EPOCH FROM (expires_at - NOW())) / 3600 as hours_until_expiry
FROM advisor_invitations
WHERE status = 'pending'
AND expires_at > NOW()
AND expires_at < NOW() + INTERVAL '24 hours'
ORDER BY expires_at;