FactuGES_ERP/docs/settings/frontend-erp-settings-system-design.md
david fbf26d4745 feat: implement account management features
- Added services and use cases for managing account details, including changing passwords and updating profiles.
- Introduced error handling for duplicate emails and password mismatches.
- Created controllers for handling account-related requests in the Express framework.
- Updated API routes to include new account management endpoints.
- Enhanced the account profile schema to support email updates.
- Implemented tests for account management functionalities to ensure reliability and correctness.
2026-08-17 23:30:41 +02:00

22 KiB
Raw Blame History

Frontend ERP – Sistema modular de Settings

1. Estado del documento

Estado: DISEÑO V1 AJUSTADO A LA ARQUITECTURA REAL DEL REPOSITORIO

Este documento sustituye la versión inicial del diseño de Settings y recoge las correcciones derivadas de la auditoría del monorepo realizada el 11 de agosto de 2026.

Decisiones ya consolidadas:

/settings
→ configuración de la empresa activa

/account
→ configuración personal del usuario
Settings shell != dominio != persistencia
El shell conoce la navegación de Settings.
Los módulos conocen sus páginas, rutas y lógica.

La arquitectura modular real del frontend ya compone rutas aportadas por módulos mediante IModuleClient, pero no dispone todavía de navegación modular, permisos frontend para navegación ni un subtree /settings/* compartido.


2. Propósito

Definir una infraestructura mínima y extensible para la configuración empresarial del ERP.

Objetivo principal:

Añadir una nueva opción de configuración desde un módulo no debe requerir modificar la Page ni la navegación interna del shell de Settings.

El sistema debe aprovechar la infraestructura modular existente y no crear un registry paralelo de rutas ni un framework genérico de persistencia.


3. Alcance

3.1 /settings

/settings representa exclusivamente configuración de la empresa activa.

Ejemplos previstos:

/settings/company/details
/settings/billing/verifactu
/settings/billing/document-series
/settings/billing/certificates

Una configuración situada bajo /settings debe operar sobre la empresa activa resuelta por la infraestructura global de identidad.

Caso ya implementado:

/settings/company/details
-> GET /companies/current
-> PATCH /companies/current

La pantalla no selecciona el target mediante companyId.

3.2 /account

/account queda reservado para configuración personal del usuario.

Ejemplos futuros:

/account/profile
/account/preferences
/account/security

No forma parte de este diseño y no debe mezclarse con /settings.

Backend ya preparado actualmente para el subtree de usuario:

GET  /identity/account
PATCH /identity/account
POST /identity/account/change-password

Regla de acceso:

/account
→ requiere usuario autenticado
→ no requiere empresa activa
→ no requiere X-Company-Id

No se introduce scope: "company" | "user" en V1.


4. Arquitectura real de referencia

La aplicación compone actualmente los módulos así:

App
└─ apps/web/src/app.tsx
   ├─ QueryClientProvider
   ├─ DataSourceProvider
   ├─ IdentityAuthSessionProvider
   └─ RouterProvider(getAppRouter)
      └─ apps/web/src/routes/app-routes.tsx
         ├─ modules = apps/web/src/register-modules.tsx
         ├─ buildModuleRoutes(modules, "auth")
         ├─ buildModuleRoutes(modules, "app-sidebar")
         ├─ buildModuleRoutes(modules, "app-fullscreen")
         └─ createBrowserRouter(...)
            ├─ AuthLayout
            ├─ AppFullscreenLayout
            └─ AppSidebarLayout

Piezas reales relevantes:

apps/web/src/register-modules.tsx
→ modules: IModuleClient[]

modules/core/src/web/lib/modules/module-client.interface.ts
→ IModuleClient
→ ModuleRouteObject
→ ModuleRouteHandle

apps/web/src/components/module-routes.tsx
→ buildModuleRoutes()
→ normalizeRoute()

apps/web/src/routes/app-routes.tsx
→ router raíz

V1 de Settings debe extender esta arquitectura, no sustituirla.


5. Principios de diseño

5.1 El shell no conoce dominio

apps/web puede conocer:

secciones de Settings
entries publicadas por módulos
paths
labels
layout
navegación

No debe conocer:

DTOs
Entities
formularios
controllers
endpoints
reglas VeriFactu
reglas de Series
reglas de certificados

5.2 Cada módulo conserva ownership

El módulo propietario de una configuración mantiene:

route
Page
Page Controller
Controllers
shared/api
shared/adapters
shared/entities
shared/hooks
validación
persistencia
reglas de negocio
UI específica

En company/details, los campos editables actuales son:

legal_name
trade_name
tin
email
phone
website
default_payment_term_id

La UI actual de esta pantalla se organiza en:

Datos generales
→ legal_name
→ trade_name
→ tin

Datos de contacto
→ email
→ phone
→ website

Condiciones comerciales
→ default_payment_term_id

Semántica de guardado esperada para esta pantalla:

PATCH parcial
→ solo viajan los campos dirty

campo omitido
→ no modificar

campo nullable enviado como null
→ limpiar valor

La pantalla sigue operando exclusivamente contra:

GET /companies/current
PATCH /companies/current

sin usar companyId como selector del target.

Tras un guardado correcto, la respuesta del servidor debe seguir siendo la fuente única de verdad:

response
→ set/reset del formulario
→ sincronización de active company visible

Política de errores consolidada para esta pantalla:

error inicial sin datos requeridos
→ PageErrorState

error de refetch con datos previos
→ mantener contenido
→ ErrorAlert inline

cancelled
→ no elevar a pantalla bloqueante

422 de backend con path mapeable
→ error de campo en RHF

error general de mutation
→ feedback local

Fuera de alcance del formulario actual:

slug
status
fiscalidad avanzada
VeriFactu
document series
payment methods
branding
bank accounts
defaultPaymentMethodId

El hecho de que una Page aparezca dentro de /settings no cambia su arquitectura interna.

Flujo obligatorio:

API
→ DTO
→ Adapter
→ Entity
→ Hook
→ Controller
→ Page Controller
→ UI

5.3 Settings no es persistencia

No crear:

GenericSettingsRepository
settings(key, value)
useSettings()
tabla genérica de settings

Cada dominio conserva su propio modelo de persistencia.

5.4 Settings no es un router alternativo

La navegación de Settings referencia rutas reales ya registradas en el router modular.

No registrar componentes React dentro de metadata de Settings.


6. Arquitectura objetivo V1

apps/web
│
├── register-modules.tsx
│      └── IModuleClient[]
│
├── routes/app-routes.tsx
│      └── nueva rama /settings/*
│             └── SettingsLayout
│                    └── rutas de módulos bajo namespace /settings/...
│
└── settings/
       ├── settings-section-catalog.ts
       ├── build-settings-navigation.ts
       ├── build-settings-routes.tsx
       ├── settings-layout.tsx
       ├── settings-navigation.tsx
       └── settings-index-page.tsx
                ▲
                │
          metadata ligera
                │
      ┌─────────┴──────────────────┐
      │                            │
customer-invoices            document-series
manifest                     manifest
settings entries             settings entries

No se introduce un SettingsRegistry autónomo con ciclo de vida propio.

La pieza de agregación debe ser una función/estructura ligera sobre los IModuleClient[] ya registrados en apps/web.


7. IModuleClient como extension point

El manifest.ts real del módulo utiliza IModuleClient.

Actualmente IModuleClient contiene, entre otros conceptos:

name
version
description?
dependencies?
protected?
layout?
icon?
routes?

No contiene Settings.

V1 puede ampliar el contrato compartido con metadata ligera de Settings.

Dirección conceptual:

export interface ModuleSettingsEntryContribution {
  id: string;
  sectionId: string;
  labelKey: string;
  path: string;
}

export interface ModuleSettingsContribution {
  entries: ModuleSettingsEntryContribution[];
}

export interface IModuleClient {
  // contrato existente
  settings?: ModuleSettingsContribution;
}

El shape definitivo debe adaptarse al tipo real sin duplicar contratos existentes.

No crear un segundo manifest específico para Settings salvo que aparezca una razón técnica durante la implementación.


8. Secciones: catálogo central en V1

En V1, las secciones se definen en el shell, no en los módulos.

Motivo:

  • la navegación actual es responsabilidad de apps/web;
  • las secciones son conceptos de información global;
  • evita conflictos de namespace i18n;
  • evita implementar prematuramente conflict detection;
  • permite que varios módulos contribuyan a una misma sección sin conocerse.

Ejemplo:

export interface SettingsSectionDefinition {
  id: string;
  labelKey: string;
}

export const SETTINGS_SECTIONS: SettingsSectionDefinition[] = [
  {
    id: "company",
    labelKey: "settings.sections.company",
  },
  {
    id: "billing",
    labelKey: "settings.sections.billing",
  },
];

Esto no implica que el shell conozca las entries concretas.

El shell conoce:

billing = Facturación

pero no:

billing contiene VeriFactu
billing contiene Series documentales

Estas entries proceden de módulos.


9. Entries modulares

Contrato mínimo V1:

export interface ModuleSettingsEntryContribution {
  id: string;
  sectionId: string;
  labelKey: string;
  path: string;
}

Semántica:

id

Identificador estable dentro del módulo.

Ejemplos:

verifactu
document-series
certificates
company-details

sectionId

Referencia a una sección conocida por el shell.

Ejemplos:

company
billing

labelKey

Clave i18n del módulo.

El namespace se resuelve con manifest.name, siguiendo el sistema de traducciones existente.

path

Ruta absoluta real de la aplicación.

Ejemplo:

/settings/billing/verifactu

La metadata no registra ni importa la Page.


10. Campos diferidos

No forman parte del contrato mínimo obligatorio V1:

descriptionKey
order
permission
icon
badge
status
feature flags
keywords
children anidados

descriptionKey

Se elimina de V1 porque no existe un patrón real de navegación que lo requiera.

order

Se difiere hasta comprobar que el orden central/default no sea suficiente.

Si se introduce posteriormente, deberá tener semántica explícita y determinista.

permission

Existe PermissionCode = string en el proyecto, pero el shell no dispone todavía de un mecanismo frontend operativo para filtrar navegación o proteger routes por permiso.

Por tanto, no se promete enforcement en V1.

Cuando exista infraestructura frontend real, el contrato podrá evolucionar a:

permission?: PermissionCode;

sin crear un sistema paralelo de autorización.


11. i18n

El sistema real usa namespaces por módulo mediante:

registerTranslations(moduleName, locale, resources)

Regla V1:

sección
→ traducción del shell/apps-web

entry
→ traducción del módulo propietario

Ejemplo conceptual:

settings: {
  entries: [
    {
      id: "verifactu",
      sectionId: "billing",
      labelKey: "settings.verifactu.title",
      path: "/settings/billing/verifactu",
    },
  ],
}

El shell resolverá esa key usando el namespace del manifest.name.

No introducir namespace + key explícitos salvo que aparezca una necesidad real.


12. Routing

12.1 Estado actual

El router ya permite rutas modulares por layout, pero no existe todavía una rama compartida:

/settings/*

con SettingsLayout.

Existe /settings como placeholder, no como feature implementada.

12.2 Cambio requerido

V1 debe añadir una rama explícita en el router shell para Settings.

Objetivo conceptual:

AppSidebarLayout
└── /settings
    └── SettingsLayout
        ├── index
        └── rutas de módulos bajo /settings/...

El detalle exacto debe integrarse con buildModuleRoutes() y ModuleRouteObject.

No crear un router secundario.

12.3 Regla de acoplamiento

SettingsNavigation
→ path

Router modular
→ route correspondiente

Módulo
→ Page

Nunca:

SettingsNavigation
→ React component

13. Ruta raíz /settings

Decisión cerrada en V1:

si existe al menos una entry
→ redirección determinista a la primera entry agregada

si no existe ninguna entry
→ estado vacío útil del shell

La elección sigue sin afectar al contrato de los módulos.


14. Agregación de entries

No crear un objeto SettingsRegistry con estado propio.

Usar una transformación determinista sobre los módulos registrados:

IModuleClient[]
→ flatMap(settings.entries)
→ validar sectionId conocido
→ agrupar por sectionId
→ resolver labels
→ SettingsNavigation

La agregación no debe:

consultar APIs
mantener estado
crear context global
resolver permisos inexistentes
registrar routes

Debe ser una transformación pequeña y testeable.


15. Navegación principal

La navegación actual está hardcodeada en apps/web.

Existe ya un menú de Configuración con links placeholder.

V1 debe:

  1. conservar una única entrada principal Configuración;
  2. eliminar/reemplazar los links placeholder que dupliquen el nuevo sistema;
  3. dirigir a /settings;
  4. construir la navegación interna de Settings a partir de las secciones centrales + entries modulares.

No crear un segundo sistema general de navegación para toda la aplicación.

La modularización se limita inicialmente a las entries internas de Settings.


16. Layout de Settings

Responsabilidad de apps/web.

Modelo recomendado:

┌───────────────────────────────────────────────────────────┐
│ Configuración                                             │
│ Configura el comportamiento de la empresa activa          │
├─────────────────┬─────────────────────────────────────────┤
│ Empresa         │                                         │
│   ...           │                                         │
│                 │       <Outlet />                        │
│ Facturación     │                                         │
│   VeriFactu     │                                         │
│   Series        │                                         │
│                 │                                         │
└─────────────────┴─────────────────────────────────────────┘

Responsabilidades:

SettingsLayout
→ estructura + Outlet

SettingsNavigation
→ secciones + links

route del módulo
→ contenido

El estado activo debe derivarse de la URL, no de useState.


17. Sistema visual

Settings debe reutilizar los componentes Rdx existentes cuando sean adecuados.

Ejemplos:

RdxPageContainer
RdxPageHeader
RdxPageStack

No mover a packages/rdx-ui:

routing
SettingsLayout dependiente del router
agregación de manifests
active company
permissions de aplicación

rdx-ui solo puede albergar componentes visuales genéricos y desacoplados.


18. Empresa activa

Fuente real:

IdentityAuthSessionProvider

Persistencia:

active-company-storage.ts
→ react-secure-storage

HTTP:

setup-interceptors.ts
→ X-Company-Id

Backend:

require-company-context.middleware.ts
→ valida company/membership
→ req.user.companyId

Regla consolidada:

/settings NO incluye companyId en la URL

pero esta decisión depende de mantener:

X-Company-Id
RequireCompany guard
reset global de React Query al cambiar empresa

No introducir un segundo selector o contexto de company dentro de Settings.


19. React Query y cambio de empresa

Las query keys actuales no incorporan companyId.

La coherencia entre empresas se mantiene globalmente mediante el cambio de empresa:

cancelQueries()
removeQueries({ type: "inactive" })
resetQueries({ type: "active" })

Settings debe reutilizar este mecanismo.

No crear query keys especiales con companyId solo para Settings si el resto del ERP no sigue esa estrategia.

Debe considerarse una invariante del shell:

Cambiar la empresa activa debe resetear los datos visibles antes de reutilizar cualquier Page de Settings.


20. Persistencia

Regla consolidada:

Settings framework != Settings persistence

Ejemplos:

VeriFactu
→ configuración propia del dominio

DocumentSeries
→ document-series

Company
→ identity/company

Certificados
→ módulo propietario

La coexistencia en una misma UI no implica modelo de datos compartido.


21. Page / Controller / Shared

Cada setting conserva las reglas generales del ERP.

Page Controller

integra contexto/routing
compone controllers

Controller

gestiona caso de uso/formulario
construye Params
maneja side effects

No usa directamente:

useParams
useNavigate
DTOs
Settings shell

shared

shared/api
→ transporte

shared/adapters
→ DTO → Entity

shared/entities
→ contratos frontend

shared/hooks
→ React Query

No conoce routing ni Settings.


22. Formularios y guardado

Cada setting mantiene su propio formulario y transacción.

No existe:

Guardar toda la configuración

Ejemplo:

/settings/billing/verifactu
→ Guardar configuración VeriFactu

Una Page de configuración puede usar el footer/editor pattern existente del ERP si corresponde, pero el shell no impone un GenericSettingsForm.


23. Loading y errores

El shell solo gestiona errores estructurales de routing/render.

Cada módulo gestiona:

loading inicial
error de query
retry
submit
validation errors
refetch errors

mediante los patrones vigentes:

PageErrorState
ErrorAlert
ErrorBoundary / route error cuando corresponda

El shell no interpreta ApiClientError de dominios concretos.


Cada entry debe ser una ruta real y refrescable:

/settings/billing/verifactu

Debe funcionar con:

refresh
bookmark
deep link
navegación interna

No depender de estado previo del shell.


25. Unsaved changes

Cada módulo es responsable de su política de cambios sin guardar.

No crear un dirty global dentro del Settings shell.


26. Accesibilidad

La navegación debe usar semántica real:

<nav>
<a>/<Link>
aria-current="page"
focus-visible
jerarquía correcta de headings

No utilizar div clicables.

En responsive debe conservarse el mismo modelo de rutas y metadata.


27. Seguridad

La navegación frontend no es una barrera de seguridad.

Aunque en el futuro una entry declare permission, el backend debe validar siempre autorización y company context.

No duplicar autorización backend dentro de Settings.


28. Testing V1

Agregador

Probar:

recoge entries de varios módulos
agrupa por sectionId
ignora módulos sin settings
rechaza sectionId desconocido
resultado determinista

No implementar todavía conflict detection compleja.

Routing

Probar:

/settings existe
deep link a una route modular
refresh
ruta inexistente bajo settings

UI

Testing Library:

nav semántica
aria-current
navegación entre entries
render de Outlet

29. Caso piloto: VeriFactu

customer-invoices es un primer consumidor viable.

El módulo ya dispone de:

manifest
routing modular
lazy routes
i18n por namespace
public exports

El trabajo previo necesario está en el shell:

rama /settings/*
SettingsLayout
agregación de entries
navegación interna

La implementación funcional de VeriFactu se documentará por separado.

Settings no debe incorporar conocimiento de:

obligada/exenta
certificados
AEAT
régimen
calificación de operación
causa de exención

30. No objetivos V1

No implementar todavía:

settings sections dinámicas por módulo
registro modular de navegación global
permission filtering real
order configurable por módulo
descriptionKey
search de settings
badges
feature flags
favoritos
recent settings
nested sections
GenericSettingsRepository
SettingsProvider global
SettingsRegistry con estado

31. Criterios de aceptación

La V1 es correcta si:

  1. /settings representa únicamente la empresa activa.
  2. /account permanece separado.
  3. Existe una única entrada Configuración en la navegación principal.
  4. Las secciones son definidas por el shell.
  5. Varios módulos pueden publicar entries en la misma sección.
  6. Las entries se descubren desde IModuleClient[].
  7. El shell no importa Pages de dominio.
  8. Cada entry apunta a una route real del módulo.
  9. El router sigue siendo la única fuente de verdad de navegación.
  10. No se introduce persistencia genérica.
  11. No se crea un segundo contexto de company.
  12. rdx-ui no conoce routing ni manifests.
  13. Añadir una nueva entry no requiere modificar SettingsLayout ni SettingsNavigation.

32. Decisiones abiertas

Pendientes de cerrar durante la implementación del shell:

  1. Si order resulta necesario en el primer segundo consumidor.
  2. Cuándo introducir permission?: PermissionCode.
  3. Si en el futuro se necesita namespace i18n explícito.

Estas decisiones no deben bloquear la V1 mínima.


33. Regla mental final

App shell
→ define secciones y layout

IModuleClient
→ declara entries

Router
→ resuelve paths

Módulo
→ implementa dominio

Settings
→ compone navegación, no negocio

Criterio principal:

Añadir una opción de configuración desde un módulo no debe requerir modificar SettingsLayout, SettingsNavigation ni un switch central de Pages.