Asesores
Ruta:
/owner/settings/advisorsInvita y gestiona el acceso de asesores fiscales y contables a tu negocio.
Vista general
Sección titulada «Vista general»El submódulo de Asesores permite a los propietarios de negocios compartir acceso controlado con profesionales externos (asesores fiscales, contables, gestorías) para que puedan consultar y gestionar información del negocio.
Características principales:
- Enviar invitaciones a asesores por email
- Flujo bidireccional: propietarios invitan asesores o asesores solicitan acceso
- Control granular de permisos por sección
- Gestión de asesores activos y revocación de acceso
- Seguimiento de invitaciones pendientes, aceptadas y expiradas
Pestañas de gestión
Sección titulada «Pestañas de gestión»La pantalla de Asesores está organizada en tres pestañas:
Invitaciones Enviadas
Sección titulada «Invitaciones Enviadas»Lista de invitaciones que el propietario ha enviado a asesores.
Estados posibles:
| Estado | Badge | Descripción |
|---|---|---|
| Pendiente | Amarillo | Invitación enviada, esperando aceptación |
| Aceptada | Verde | Asesor vinculado al negocio |
| Expirada | Gris | Invitación venció (7 días) |
| Rechazada | Rojo | Asesor rechazó la invitación |
Acciones disponibles:
- Reenviar invitación (solo pendientes)
- Configurar permisos (pendientes y aceptadas)
- Eliminar invitación o revocar acceso
Solicitudes Recibidas
Sección titulada «Solicitudes Recibidas»Lista de solicitudes de acceso enviadas por asesores al propietario.
Elementos mostrados:
- Nombre del asesor
- Nombre del negocio
- Fecha de solicitud
- Estado (pendiente, aceptada, rechazada)
Acciones disponibles:
- Revisar solicitud (abre página de revisión)
- Ver permisos solicitados
- Aceptar o rechazar
Asesores Activos
Sección titulada «Asesores Activos»Lista de asesores con acceso activo al negocio.
Información mostrada:
- Nombre del asesor
- Email (cuando disponible)
- Fecha de vinculación
- Estado: Activo (badge verde)
Acciones disponibles:
- Gestionar permisos
- Revocar acceso (elimina vínculo y permisos)
Sistema de permisos
Sección titulada «Sistema de permisos»Los permisos se configuran por sección del negocio con tres niveles de acceso:
Niveles de acceso
Sección titulada «Niveles de acceso»| Nivel | Descripción | Capabilities |
|---|---|---|
| Ninguno | Sin acceso | No ve la sección |
| Lectura | Solo consulta | Ver datos, exportar |
| Edición | Consulta y modificación | Ver, crear, editar, eliminar |
Secciones configurables
Sección titulada «Secciones configurables»- Facturas (invoices)- Gastos (expenses)- Tickets (tickets)- Presupuestos (quotes)- Productos (products)- Clientes (customers)- Reportes (reports)- Proyectos (projects)- Tareas (tasks)Permisos predeterminados: Todas las secciones con acceso de Edición.
Flujo de invitación
Sección titulada «Flujo de invitación»Propietario invita a asesor
Sección titulada «Propietario invita a asesor»1. Propietario ingresa email del asesor2. Configura permisos iniciales3. Sistema envía invitación por email4. Asesor recibe email con enlace único5. Asesor acepta invitación6. Sistema crea vínculo activo7. Permisos configurados se aplican automáticamenteAsesor solicita acceso
Sección titulada «Asesor solicita acceso»1. Asesor envía solicitud desde su panel2. Propietario recibe notificación3. Propietario revisa solicitud4. Propietario configura permisos5. Propietario acepta o rechaza6. Si acepta: sistema crea vínculo activo7. Asesor recibe notificación de confirmaciónFunciones
Sección titulada «Funciones»| Función | Descripción | Documentación |
|---|---|---|
| Invitar Asesor | Enviar invitación con permisos configurables | Ver |
| Reenviar Invitación | Regenerar enlace para invitaciones pendientes | Ver |
| Configurar Permisos | Ajustar acceso por sección | Ver |
| Revisar Solicitud | Aceptar/rechazar solicitud de asesor | Ver |
| Revocar Acceso | Eliminar acceso de asesor activo | Ver |
Diferencias entre usuarios existentes y nuevos
Sección titulada «Diferencias entre usuarios existentes y nuevos»El sistema detecta automáticamente si el email del asesor ya está registrado en Factux:
Usuario existente
Sección titulada «Usuario existente»- Email adaptado: “Ya tienes cuenta en Factux…”
- No requiere crear contraseña
- Acceso directo con credenciales actuales
- Negocio se agrega a su panel de asesor
Usuario nuevo
Sección titulada «Usuario nuevo»- Email adaptado: “Te han invitado como gestor…”
- Requiere crear contraseña
- Primer acceso a Factux
- Creación de perfil de asesor
Seguridad y validaciones
Sección titulada «Seguridad y validaciones»Expiración de invitaciones: 7 días desde el envío.
Validaciones de email:
- Formato válido (RFC 5322)
- Máximo 255 caracteres
- Normalizado a minúsculas
Protecciones:
- Una invitación pendiente por asesor/negocio
- Asesores ya vinculados no pueden recibir nuevas invitaciones
- Tokens únicos y criptográficamente seguros (UUID v4)
- Invitaciones canceladas se eliminan físicamente (permite reinvitación)
Referencia técnica
Sección titulada «Referencia técnica»Detalles técnicos
Tablas de base de datos
Sección titulada «Tablas de base de datos»Ver esquema de tablas
advisor_invitations
Sección titulada «advisor_invitations»Invitaciones enviadas por propietarios a asesores.
| Columna | Tipo | Descripción |
|---|---|---|
id | UUID | Identificador único |
owner_id | UUID | FK a auth.users (propietario) |
email | TEXT | Email del asesor invitado |
business_id | UUID | FK a businesses |
status | TEXT | pending, accepted, expired, cancelled |
token | TEXT | Token único de invitación |
expires_at | TIMESTAMPTZ | Fecha de expiración (7 días) |
permissions | JSONB | Permisos preconfigurados |
is_existing_user | BOOLEAN | Usuario ya registrado |
sent_at | TIMESTAMPTZ | Fecha de envío |
accepted_at | TIMESTAMPTZ | Fecha de aceptación |
advisor_to_owner_invitations
Sección titulada «advisor_to_owner_invitations»Solicitudes enviadas por asesores a propietarios (flujo inverso).
| Columna | Tipo | Descripción |
|---|---|---|
id | UUID | Identificador único |
advisor_id | UUID | FK a auth.users (asesor) |
owner_id | UUID | FK a auth.users (propietario) |
business_id | UUID | FK a businesses |
status | TEXT | pending, accepted, rejected, expired |
token | TEXT | Token único de invitación |
permissions | JSONB | Permisos solicitados |
final_permissions | JSONB | Permisos finales aceptados |
expires_at | TIMESTAMPTZ | Fecha de expiración |
advisor_name | TEXT | Nombre del asesor |
advisor_client_links
Sección titulada «advisor_client_links»Vínculos activos entre asesores y negocios cliente.
| Columna | Tipo | Descripción |
|---|---|---|
id | UUID | Identificador único |
advisor_id | UUID | FK a auth.users (asesor) |
client_business_id | UUID | FK a businesses |
invited_by | UUID | FK a auth.users (propietario) |
status | TEXT | active, inactive, suspended |
linked_at | TIMESTAMPTZ | Fecha de vinculación |
last_accessed_at | TIMESTAMPTZ | Último acceso del asesor |
last_export_at | TIMESTAMPTZ | Última exportación |
export_count | INTEGER | Contador de exportaciones |
unread_documents_count | INTEGER | Documentos no vistos |
advisor_section_permissions
Sección titulada «advisor_section_permissions»Permisos granulares por sección para cada asesor-negocio.
| Columna | Tipo | Descripción |
|---|---|---|
id | UUID | Identificador único |
advisor_id | UUID | FK a auth.users (asesor) |
client_business_id | UUID | FK a businesses |
section | TEXT | invoices, expenses, tickets, etc. |
access_level | TEXT | none, read, edit |
Constraint: section debe ser uno de: invoices, tickets, quotes, customers, products, expenses, projects, tasks, reports
advisor_profiles
Sección titulada «advisor_profiles»Perfiles extendidos de asesores.
| Columna | Tipo | Descripción |
|---|---|---|
id | UUID | Identificador único |
user_id | UUID | FK a auth.users |
business_name | TEXT | Nombre de gestoría/firma |
tax_id | TEXT | CIF/NIF de la gestoría |
phone | TEXT | Teléfono de contacto |
province | TEXT | Provincia |
country | TEXT | País (default: España) |
accounting_software | TEXT | Software contable usado |
client_count_range | TEXT | Rango de clientes |
onboarding_completed | BOOLEAN | Onboarding completado |
Políticas RLS
Sección titulada «Políticas RLS»Ver políticas de seguridad
advisor_invitations
Sección titulada «advisor_invitations»-- Propietarios ven invitaciones que enviaronCREATE POLICY "Owners can view their sent invitations"ON advisor_invitations FOR SELECTUSING (auth.uid() = owner_id);
-- Propietarios pueden crear invitacionesCREATE POLICY "Owners can insert invitations"ON advisor_invitations FOR INSERTWITH CHECK (auth.uid() = owner_id);
-- Propietarios pueden actualizar sus invitacionesCREATE POLICY "Owners can update their invitations"ON advisor_invitations FOR UPDATEUSING (auth.uid() = owner_id);advisor_to_owner_invitations
Sección titulada «advisor_to_owner_invitations»-- Asesores ven invitaciones que enviaronCREATE POLICY "Advisors can view their sent invitations"ON advisor_to_owner_invitations FOR SELECTUSING (auth.uid() = advisor_id);
-- Propietarios ven invitaciones recibidasCREATE POLICY "Owners can view invitations sent to them"ON advisor_to_owner_invitations FOR SELECTUSING (auth.uid() = owner_id);
-- Propietarios pueden responder (aceptar/rechazar)CREATE POLICY "Owners can respond to invitations"ON advisor_to_owner_invitations FOR UPDATEUSING (auth.uid() = owner_id AND status = 'pending');advisor_client_links
Sección titulada «advisor_client_links»-- Asesores ven sus vínculosCREATE POLICY "Advisors can view their client links"ON advisor_client_links FOR SELECTUSING (auth.uid() = advisor_id);
-- Propietarios ven asesores vinculados a sus negociosCREATE POLICY "Owners can view their advisors"ON advisor_client_links FOR SELECTUSING ( EXISTS ( SELECT 1 FROM user_businesses ub WHERE ub.business_id = advisor_client_links.client_business_id AND ub.user_id = auth.uid() AND ub.role = 'owner' ));advisor_section_permissions
Sección titulada «advisor_section_permissions»-- Asesores pueden ver sus permisosCREATE POLICY "Advisors can view their permissions"ON advisor_section_permissions FOR SELECTUSING (auth.uid() = advisor_id);
-- Propietarios pueden gestionar permisos de sus asesoresCREATE POLICY "Owners can manage advisor permissions"ON advisor_section_permissions FOR ALLUSING ( EXISTS ( SELECT 1 FROM user_businesses ub WHERE ub.business_id = advisor_section_permissions.client_business_id AND ub.user_id = auth.uid() AND ub.role = 'owner' ));Edge Functions
Sección titulada «Edge Functions»Ver funciones edge
| Función | Trigger | Descripción |
|---|---|---|
send-advisor-invitation | Envío de invitación | Crea invitación, detecta usuario existente, envía email personalizado via SendGrid |
accept-advisor-invitation | Aceptación de invitación | Valida token, crea vínculo activo, configura permisos, asigna rol de asesor |
send-advisor-to-owner-invitation | Solicitud de asesor | Crea solicitud de acceso, envía notificación al propietario |
respond-advisor-to-owner-invitation | Respuesta del propietario | Acepta/rechaza solicitud, crea vínculo si se acepta |
revoke-advisor-access | Revocación de acceso | Desactiva vínculo, elimina permisos, notifica al asesor |
list-advisors | Consulta de asesores | Lista asesores activos con metadatos |
send-advisor-invitation
Sección titulada «send-advisor-invitation»Input:
{ email: string; // Email del asesor (validado, normalizado) businessId: string; // UUID del negocio ownerName: string; // Nombre del propietario businessName: string; // Nombre del negocio isResend?: boolean; // Si es reenvío de invitación permissions?: Array<{ // Permisos preconfigurados section: string; access: 'none' | 'read' | 'edit'; }>;}Output:
{ success: true; invitationId: string; // UUID de la invitación creada}Lógica:
- Valida esquema de entrada con Zod
- Detecta si email pertenece a usuario existente
- Verifica invitaciones previas:
pending: error o regenera token siisResendaccepted: error (ya tiene acceso)expired/cancelled: reactiva con nuevo token
- Crea/actualiza registro en
advisor_invitations - Genera enlace de invitación:
{APP_URL}/accept-advisor-invitation/{token} - Envía email personalizado según tipo de usuario
- Token expira en 7 días
revoke-advisor-access
Sección titulada «revoke-advisor-access»Input:
{ invitationId: string; // UUID de la invitación}Output:
{ success: true; message: "Invitación eliminada y acceso revocado correctamente"}Lógica:
- Verifica que el usuario autenticado es el propietario
- Obtiene invitación y valida permisos
- Marca invitación como
cancelledy la elimina - Si la invitación estaba aceptada:
- Busca usuario asesor por email
- Desactiva vínculo en
advisor_client_links(status = ‘inactive’) - Verifica si tiene otros vínculos activos
- Si no tiene más vínculos: elimina rol de asesor y marca perfil como inactivo
- Crea notificación para el asesor
Schemas de validación
Sección titulada «Schemas de validación»Ver schemas Zod
Validación de invitación
Sección titulada «Validación de invitación»const permissionSchema = z.object({ section: z.string(), access: z.enum(['none', 'read', 'edit'])});
const advisorInvitationSchema = z.object({ email: z.string() .email({ message: "Email inválido" }) .max(255, { message: "Email demasiado largo" }) .transform(val => val.toLowerCase().trim()), businessId: z.string().uuid({ message: "ID de negocio inválido" }), ownerName: z.string() .min(1, { message: "Nombre del propietario requerido" }) .max(100, { message: "Nombre demasiado largo" }), businessName: z.string() .min(1, { message: "Nombre del negocio requerido" }) .max(200, { message: "Nombre demasiado largo" }), isResend: z.boolean().optional(), permissions: z.array(permissionSchema).optional(),});Componentes React
Sección titulada «Componentes React»Ver componentes principales
OwnerAdvisorInvitationsManager
Sección titulada «OwnerAdvisorInvitationsManager»Ubicación: src/panels/advisor/components/OwnerAdvisorInvitationsManager.tsx
Props: Ninguno (usa contexto de autenticación)
Estado:
{ email: string; // Email del asesor a invitar selectedBusinessId: string; // Negocio seleccionado sending: boolean; // Estado de envío resendingId: string | null; // ID de invitación reenviando deletingId: string | null; // ID de invitación eliminando invitationToDelete: SentInvitation | null; permissionsModalOpen: boolean; // Modal de permisos abierto selectedInvitation: SentInvitation | null; currentPermissions: Permission[]; // Permisos actuales}Queries:
owner-businesses: Lista de negocios del propietarioowner-sent-invitations: Invitaciones enviadasowner-received-invitations: Solicitudes recibidasowner-active-advisors: Asesores activos
Funciones:
sendInvitation(): Envía nueva invitaciónresendInvitation(id): Reenvía invitación pendientedeleteInvitation(id): Elimina invitación o revoca accesohandleReviewRequest(invitation): Navega a revisión de solicitudhandleSavePermissions(permissions): Guarda permisos configurados
Subscripciones realtime:
- Actualiza automáticamente en cambios a
advisor_invitations - Actualiza automáticamente en cambios a
advisor_to_owner_invitations - Actualiza automáticamente en cambios a
advisor_client_links
UserPermissionsModal
Sección titulada «UserPermissionsModal»Ubicación: src/panels/admin/components/UserPermissionsModal.tsx
Modal reutilizable para configurar permisos granulares.
Props:
{ open: boolean; // Estado del modal onOpenChange: (open: boolean) => void; userName: string; // Nombre/email del usuario permissions: Permission[]; // Permisos actuales onSave: (permissions: Permission[]) => Promise<void>; isNewInvitation: boolean; // Si es invitación nueva businessId: string; // ID del negocio}Queries de diagnóstico
Sección titulada «Queries de diagnóstico»Ver queries SQL
Invitaciones pendientes de un propietario:
SELECT id, email, business_id, status, sent_at, expires_at, permissionsFROM advisor_invitationsWHERE owner_id = '<owner_id>' AND status = 'pending' AND expires_at > NOW()ORDER BY sent_at DESC;Invitaciones expiradas (candidatas a limpieza):
SELECT id, email, business_id, sent_at, expires_atFROM advisor_invitationsWHERE status = 'pending' AND expires_at < NOW()ORDER BY expires_at;Asesores activos de un negocio con permisos:
SELECT acl.id, acl.advisor_id, acl.linked_at, acl.last_accessed_at, acl.export_count, COALESCE( json_agg( json_build_object( 'section', asp.section, 'access', asp.access_level ) ) FILTER (WHERE asp.id IS NOT NULL), '[]' ) as permissionsFROM advisor_client_links aclLEFT JOIN advisor_section_permissions asp ON asp.advisor_id = acl.advisor_id AND asp.client_business_id = acl.client_business_idWHERE acl.client_business_id = '<business_id>' AND acl.status = 'active'GROUP BY acl.id, acl.advisor_id, acl.linked_at, acl.last_accessed_at, acl.export_count;Verificar permisos de un asesor en un negocio:
SELECT section, access_levelFROM advisor_section_permissionsWHERE advisor_id = '<advisor_id>' AND client_business_id = '<business_id>'ORDER BY section;Invitaciones duplicadas (diagnóstico de inconsistencias):
SELECT owner_id, email, business_id, COUNT(*) as countFROM advisor_invitationsWHERE status = 'pending'GROUP BY owner_id, email, business_idHAVING COUNT(*) > 1;