Ir al contenido

Asesores

Ruta: /owner/settings/advisors Invita y gestiona el acceso de asesores fiscales y contables a tu negocio.


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

La pantalla de Asesores está organizada en tres pestañas:

Lista de invitaciones que el propietario ha enviado a asesores.

Estados posibles:

EstadoBadgeDescripción
PendienteAmarilloInvitación enviada, esperando aceptación
AceptadaVerdeAsesor vinculado al negocio
ExpiradaGrisInvitación venció (7 días)
RechazadaRojoAsesor rechazó la invitación

Acciones disponibles:

  • Reenviar invitación (solo pendientes)
  • Configurar permisos (pendientes y aceptadas)
  • Eliminar invitación o revocar acceso

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

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)

Los permisos se configuran por sección del negocio con tres niveles de acceso:

NivelDescripciónCapabilities
NingunoSin accesoNo ve la sección
LecturaSolo consultaVer datos, exportar
EdiciónConsulta y modificaciónVer, crear, editar, eliminar
- 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.


1. Propietario ingresa email del asesor
2. Configura permisos iniciales
3. Sistema envía invitación por email
4. Asesor recibe email con enlace único
5. Asesor acepta invitación
6. Sistema crea vínculo activo
7. Permisos configurados se aplican automáticamente
1. Asesor envía solicitud desde su panel
2. Propietario recibe notificación
3. Propietario revisa solicitud
4. Propietario configura permisos
5. Propietario acepta o rechaza
6. Si acepta: sistema crea vínculo activo
7. Asesor recibe notificación de confirmación

FunciónDescripciónDocumentación
Invitar AsesorEnviar invitación con permisos configurablesVer
Reenviar InvitaciónRegenerar enlace para invitaciones pendientesVer
Configurar PermisosAjustar acceso por secciónVer
Revisar SolicitudAceptar/rechazar solicitud de asesorVer
Revocar AccesoEliminar acceso de asesor activoVer

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:

  • 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
  • Email adaptado: “Te han invitado como gestor…”
  • Requiere crear contraseña
  • Primer acceso a Factux
  • Creación de perfil de asesor

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)

Detalles técnicos
Ver esquema de tablas

Invitaciones enviadas por propietarios a asesores.

ColumnaTipoDescripción
idUUIDIdentificador único
owner_idUUIDFK a auth.users (propietario)
emailTEXTEmail del asesor invitado
business_idUUIDFK a businesses
statusTEXTpending, accepted, expired, cancelled
tokenTEXTToken único de invitación
expires_atTIMESTAMPTZFecha de expiración (7 días)
permissionsJSONBPermisos preconfigurados
is_existing_userBOOLEANUsuario ya registrado
sent_atTIMESTAMPTZFecha de envío
accepted_atTIMESTAMPTZFecha de aceptación

Solicitudes enviadas por asesores a propietarios (flujo inverso).

ColumnaTipoDescripción
idUUIDIdentificador único
advisor_idUUIDFK a auth.users (asesor)
owner_idUUIDFK a auth.users (propietario)
business_idUUIDFK a businesses
statusTEXTpending, accepted, rejected, expired
tokenTEXTToken único de invitación
permissionsJSONBPermisos solicitados
final_permissionsJSONBPermisos finales aceptados
expires_atTIMESTAMPTZFecha de expiración
advisor_nameTEXTNombre del asesor

Vínculos activos entre asesores y negocios cliente.

ColumnaTipoDescripción
idUUIDIdentificador único
advisor_idUUIDFK a auth.users (asesor)
client_business_idUUIDFK a businesses
invited_byUUIDFK a auth.users (propietario)
statusTEXTactive, inactive, suspended
linked_atTIMESTAMPTZFecha de vinculación
last_accessed_atTIMESTAMPTZÚltimo acceso del asesor
last_export_atTIMESTAMPTZÚltima exportación
export_countINTEGERContador de exportaciones
unread_documents_countINTEGERDocumentos no vistos

Permisos granulares por sección para cada asesor-negocio.

ColumnaTipoDescripción
idUUIDIdentificador único
advisor_idUUIDFK a auth.users (asesor)
client_business_idUUIDFK a businesses
sectionTEXTinvoices, expenses, tickets, etc.
access_levelTEXTnone, read, edit

Constraint: section debe ser uno de: invoices, tickets, quotes, customers, products, expenses, projects, tasks, reports

Perfiles extendidos de asesores.

ColumnaTipoDescripción
idUUIDIdentificador único
user_idUUIDFK a auth.users
business_nameTEXTNombre de gestoría/firma
tax_idTEXTCIF/NIF de la gestoría
phoneTEXTTeléfono de contacto
provinceTEXTProvincia
countryTEXTPaís (default: España)
accounting_softwareTEXTSoftware contable usado
client_count_rangeTEXTRango de clientes
onboarding_completedBOOLEANOnboarding completado
Ver políticas de seguridad
-- Propietarios ven invitaciones que enviaron
CREATE POLICY "Owners can view their sent invitations"
ON advisor_invitations FOR SELECT
USING (auth.uid() = owner_id);
-- Propietarios pueden crear invitaciones
CREATE POLICY "Owners can insert invitations"
ON advisor_invitations FOR INSERT
WITH CHECK (auth.uid() = owner_id);
-- Propietarios pueden actualizar sus invitaciones
CREATE POLICY "Owners can update their invitations"
ON advisor_invitations FOR UPDATE
USING (auth.uid() = owner_id);
-- Asesores ven invitaciones que enviaron
CREATE POLICY "Advisors can view their sent invitations"
ON advisor_to_owner_invitations FOR SELECT
USING (auth.uid() = advisor_id);
-- Propietarios ven invitaciones recibidas
CREATE POLICY "Owners can view invitations sent to them"
ON advisor_to_owner_invitations FOR SELECT
USING (auth.uid() = owner_id);
-- Propietarios pueden responder (aceptar/rechazar)
CREATE POLICY "Owners can respond to invitations"
ON advisor_to_owner_invitations FOR UPDATE
USING (auth.uid() = owner_id AND status = 'pending');
-- Asesores ven sus vínculos
CREATE POLICY "Advisors can view their client links"
ON advisor_client_links FOR SELECT
USING (auth.uid() = advisor_id);
-- Propietarios ven asesores vinculados a sus negocios
CREATE POLICY "Owners can view their advisors"
ON advisor_client_links FOR SELECT
USING (
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'
)
);
-- Asesores pueden ver sus permisos
CREATE POLICY "Advisors can view their permissions"
ON advisor_section_permissions FOR SELECT
USING (auth.uid() = advisor_id);
-- Propietarios pueden gestionar permisos de sus asesores
CREATE POLICY "Owners can manage advisor permissions"
ON advisor_section_permissions FOR ALL
USING (
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'
)
);
Ver funciones edge
FunciónTriggerDescripción
send-advisor-invitationEnvío de invitaciónCrea invitación, detecta usuario existente, envía email personalizado via SendGrid
accept-advisor-invitationAceptación de invitaciónValida token, crea vínculo activo, configura permisos, asigna rol de asesor
send-advisor-to-owner-invitationSolicitud de asesorCrea solicitud de acceso, envía notificación al propietario
respond-advisor-to-owner-invitationRespuesta del propietarioAcepta/rechaza solicitud, crea vínculo si se acepta
revoke-advisor-accessRevocación de accesoDesactiva vínculo, elimina permisos, notifica al asesor
list-advisorsConsulta de asesoresLista asesores activos con metadatos

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:

  1. Valida esquema de entrada con Zod
  2. Detecta si email pertenece a usuario existente
  3. Verifica invitaciones previas:
    • pending: error o regenera token si isResend
    • accepted: error (ya tiene acceso)
    • expired/cancelled: reactiva con nuevo token
  4. Crea/actualiza registro en advisor_invitations
  5. Genera enlace de invitación: {APP_URL}/accept-advisor-invitation/{token}
  6. Envía email personalizado según tipo de usuario
  7. Token expira en 7 días

Input:

{
invitationId: string; // UUID de la invitación
}

Output:

{
success: true;
message: "Invitación eliminada y acceso revocado correctamente"
}

Lógica:

  1. Verifica que el usuario autenticado es el propietario
  2. Obtiene invitación y valida permisos
  3. Marca invitación como cancelled y la elimina
  4. 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
Ver schemas Zod
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(),
});
Ver componentes principales

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 propietario
  • owner-sent-invitations: Invitaciones enviadas
  • owner-received-invitations: Solicitudes recibidas
  • owner-active-advisors: Asesores activos

Funciones:

  • sendInvitation(): Envía nueva invitación
  • resendInvitation(id): Reenvía invitación pendiente
  • deleteInvitation(id): Elimina invitación o revoca acceso
  • handleReviewRequest(invitation): Navega a revisión de solicitud
  • handleSavePermissions(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

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
}
Ver queries SQL

Invitaciones pendientes de un propietario:

SELECT id, email, business_id, status, sent_at, expires_at, permissions
FROM advisor_invitations
WHERE 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_at
FROM advisor_invitations
WHERE 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 permissions
FROM advisor_client_links acl
LEFT JOIN advisor_section_permissions asp
ON asp.advisor_id = acl.advisor_id
AND asp.client_business_id = acl.client_business_id
WHERE 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_level
FROM advisor_section_permissions
WHERE 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 count
FROM advisor_invitations
WHERE status = 'pending'
GROUP BY owner_id, email, business_id
HAVING COUNT(*) > 1;