ums

Tenant — Arquitectura del Agregado

Idioma: English Español

Bounded Context: Identity Aggregate Root: Tenant Modulo: Ums.Domain.Identity.Tenant Estado: Produccion


1. Descripcion del Agregado

Proposito

Tenant es la unidad organizativa raiz del sistema. Representa a una empresa o division que usa UMS como plataforma de gestion de identidades. Agrupa a todos los usuarios, ramas (Branch), configuraciones de branding (Branding) y proveedores de identidad (IdentityProvider) bajo un espacio de nombres unico y aislado.

Responsabilidad de Negocio

Branch: Representa una unidad de ubicacion fisica o logica. Provee un ambito geográfico u organizacional. Aplica reglas de geocercado y habilita delegación de administración. Branding: Contiene la configuración de identidad visual (logo, colores, textos) y dominio personalizado con verificación DNS. Controla cómo se renderiza el portal de login. IdentityProvider: Representa un proveedor de autenticación externo (OIDC, SAML2, WS_FED). Registra la intención estratégica y el contrato a nivel de negocio para el tenant.

Aggregate Root

Tenant es su propio aggregate root. Todas las mutaciones de Branch, Branding e IdentityProvider pasan por comandos de Tenant.

Invariantes y Reglas de Consistencia

  1. Tenant: Code debe ser globalmente unico en todo el sistema.
  2. Tenant: Un Tenant con TenantStatus = Suspended bloquea todos los flujos de autenticacion de sus usuarios.
  3. Tenant: IdpStrategy debe ser coherente: si es FEDERATED, debe existir al menos un IdentityProvider activo.
  4. Tenant: Un tenant hijo (ParentTenantId != null) hereda politicas del tenant padre.
  5. Branch: Code debe ser unico dentro del Tenant propietario.
  6. Branch: Una Branch no puede ser eliminada si existen registros activos de UserAccount o Profile asociados.
  7. Branch: GeofencingMetadata debe ser JSON valido cuando se proporciona.
  8. Branch: La desactivacion no elimina; los registros se conservan para trazabilidad historica.
  9. Branding: Solo puede existir un registro Branding por Tenant (relacion 1:1).
  10. Branding: CustomDomain debe ser un hostname valido cuando se proporciona.
  11. Branding: DnsVerificationStatus comienza en PENDING cuando se establece CustomDomain y no puede establecerse manualmente a VERIFIED (solo por el servicio de verificacion DNS).
  12. Branding: LogoFormat debe coincidir con el formato real del URI de Logo subido.
  13. IdentityProvider: Code debe ser unico dentro del Tenant propietario.
  14. IdentityProvider: Un IdentityProvider debe ser desactivado antes de ser eliminado.
  15. IdentityProvider: Desactivar un IdentityProvider que es el unico IdP activo para un tenant Federado no esta permitido a menos que se cambie primero el IdpStrategy.
  16. IdentityProvider: Strategy no puede cambiarse despues del registro — es inmutable una vez establecida.

Entidades Relacionadas / Value Objects

| Entidad / VO | Tipo | Notas | |—|—|—| | Code | Value Object | Identificador unico global del tenant | | Name | Value Object | Nombre para mostrar | | OrganizationType | Enum | COMPANY · DIVISION · BRANCH_OFFICE | | IdpStrategy | Enum | LOCAL · FEDERATED · HYBRID | | CompanyReference | Value Object | Referencia al sistema ERP (nullable) | | TenantStatus | Enum | Active · Suspended · Inactive | | AuditValueObject | Value Object | CreatedAt/By, UpdatedAt/By |

Eventos de Dominio

| Evento | Disparador | |—|—| | TenantCreatedEvent | Nuevo tenant registrado | | TenantSuspendedEvent | Tenant suspendido por admin de plataforma | | TenantActivatedEvent | Tenant reactivado | | BranchCreatedEvent | Nueva rama agregada al tenant | | BranchDeactivatedEvent | Rama desactivada | | BranchReactivatedEvent | Rama reactivada | | BranchRemovedEvent | Rama eliminada definitivamente | | BrandingCreatedEvent | Branding configurado por primera vez | | BrandingUpdatedEvent | Atributos de branding actualizados | | BrandingRemovedEvent | Configuracion de branding eliminada | | BrandingDnsVerifiedEvent | Dominio personalizado verificado por DNS | | BrandingDnsFailedEvent | Intento de verificacion DNS fallido | | IdentityProviderRegisteredEvent | Nuevo IdP registrado | | IdentityProviderActivatedEvent | IdP activado | | IdentityProviderDeactivatedEvent | IdP desactivado | | IdentityProviderRemovedEvent | IdP eliminado definitivamente |

Comandos / Casos de Uso

| Comando | Descripcion | |—|—| | RegisterTenantCommand | Crear un nuevo tenant | | SuspendTenantCommand | Suspender un tenant activo | | ActivateTenantCommand | Reactivar un tenant suspendido | | AddBranchCommand | Agregar una rama al tenant | | UpdateBranchCommand | Actualizar nombre o metadatos de geocercado | | DeactivateBranchCommand | Desactivar una rama | | ReactivateBranchCommand | Reactivar una rama | | RemoveBranchCommand | Eliminar una rama sin dependientes | | ConfigureBrandingCommand | Configurar branding por primera vez | | UpdateBrandingCommand | Actualizar atributos de branding | | SetCustomDomainCommand | Agregar o reemplazar el dominio personalizado | | RemoveBrandingCommand | Eliminar la configuracion de branding | | MarkDnsVerifiedCommand | Interno — llamado por el servicio de verificacion DNS | | MarkDnsFailedCommand | Interno — llamado por el servicio de verificacion DNS | | RegisterIdentityProviderCommand | Registrar un IdP externo | | ActivateIdentityProviderCommand | Activar un IdP | | DeactivateIdentityProviderCommand | Desactivar un IdP | | RemoveIdentityProviderCommand | Eliminar definitivamente un IdP inactivo |

Limites de Repositorio / Servicio


2. Modelo de Objetos

Tenant (Aggregate Root)
├── Props: TenantProps
│   ├── Id: IdValueObject
│   ├── Code: Code
│   ├── Name: Name
│   ├── OrganizationType: OrganizationType
│   ├── IdpStrategy: IdpStrategy
│   ├── CompanyReference?: CompanyReference
│   ├── ParentTenantId?: TenantId
│   ├── Status: TenantStatus
│   └── Audit: AuditValueObject
├── Branch (Entidad Propia, 0..N)
│   └── Props: BranchProps
│       ├── Id: IdValueObject
│       ├── TenantId: TenantId
│       ├── Code: Code
│       ├── Name: Name
│       ├── GeofencingMetadata?: Value (JSON)
│       ├── IsActive: bool
│       └── Audit: AuditValueObject
├── Branding (Entidad Propia, 0..1)
│   └── Props: BrandingProps
│       ├── Id: IdValueObject
│       ├── TenantId: TenantId
│       ├── Logo: Logo
│       ├── LogoFormat: LogoFormat
│       ├── PrimaryColor: HexColor
│       ├── BackgroundStyle: BackgroundStyle
│       ├── HeadlineText: LoginText
│       ├── SecondaryText: LoginText
│       ├── PrimaryButtonLabel: LoginText
│       ├── FooterText: LoginText
│       ├── CustomDomain?: CustomDomain
│       ├── DnsVerificationStatus: DnsVerificationStatus
│       ├── DnsCnameTarget: DnsCnameTarget
│       ├── MagicLinkFallbackEnabled: bool
│       └── Audit: AuditValueObject
└── IdentityProvider (Entidad Propia, 0..N)
    └── Props: IdentityProviderProps
        ├── Id: IdValueObject
        ├── TenantId: TenantId
        ├── Code: Code
        ├── Name: Name
        ├── Description: Description
        ├── Strategy: IdpStrategy
        ├── IsActive: bool
        └── Audit: AuditValueObject

Ciclo de Vida

Tenant:

Active ──► Suspended ──► Active
Active ──► Inactive (terminal)

Branch:

Activo (IsActive = true) ──► Desactivado (IsActive = false) ──► Activo
                                     └──► Eliminado (si no tiene dependientes)

Branding (DNS):

(CustomDomain establecido) -> DnsVerificationStatus = Pending
                                 ├──► Verified  (CNAME DNS coincide)
                                 └──► Failed    (CNAME faltante o incorrecto)
                                         └──► Pending (en reintento)

IdentityProvider:

Registrado (IsActive = false) ──► Activado (IsActive = true) ──► Desactivado ──► Eliminado

3. Diagramas de Secuencia

Flujo: Registrar Tenant

sequenceDiagram
    participant C as Cliente
    participant H as RegisterTenantHandler
    participant R as ITenantRepository

    C->>H: RegisterTenantCommand(code, name, orgType, idpStrategy, createdBy)
    H->>R: ExistsByCode(code)
    R-->>H: false
    H->>H: Crear Tenant (Status = Active)
    H->>H: Emitir TenantCreatedEvent
    H->>R: Add(tenant)
    H-->>C: tenantId

Flujo: Suspender Tenant

sequenceDiagram
    participant C as Cliente
    participant H as SuspendTenantHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)

    C->>H: SuspendTenantCommand(tenantId, actorId)
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.Suspend(actorId)
    T->>T: Guardia: Status debe ser Active
    T->>T: Status = Suspended
    T->>T: Emitir TenantSuspendedEvent
    H->>R: Update(tenant)
    H-->>C: void

Flujo: Agregar Rama

sequenceDiagram
    participant C as Cliente
    participant H as AddBranchHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)

    C->>H: AddBranchCommand(tenantId, code, name, geofencing?)
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.AddBranch(branchId, code, name, geofencing, createdBy)
    T->>T: Guardia: code unico dentro del tenant
    T->>T: Guardia: tenant activo
    T->>T: Crear Branch (IsActive = true)
    T->>T: Emitir BranchCreatedEvent
    H->>R: Update(tenant)
    H-->>C: BranchId

Flujo: Eliminar Rama

sequenceDiagram
    participant C as Cliente
    participant H as RemoveBranchHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)
    participant D as IBranchDependencyChecker

    C->>H: RemoveBranchCommand(tenantId, branchId, actorId)
    H->>D: HasDependents(branchId)
    D-->>H: false
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.RemoveBranch(branchId, actorId)
    T->>T: Eliminar Branch de coleccion
    T->>T: Emitir BranchRemovedEvent
    H->>R: Update(tenant)
    H-->>C: void

Flujo: Configurar Branding

sequenceDiagram
    participant C as Cliente
    participant H as ConfigureBrandingHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)
    participant DNS as IDnsVerificationService

    C->>H: ConfigureBrandingCommand(tenantId, logo, colores, textos, customDomain?)
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.ConfigureBranding(props, createdBy)
    T->>T: Guardia: Branding no debe existir
    T->>T: Crear Branding (DnsVerificationStatus = Pending si customDomain)
    T->>T: Emitir BrandingCreatedEvent
    H->>R: Update(tenant)
    alt customDomain proporcionado
        H->>DNS: ScheduleVerification(tenantId, cnameTarget, customDomain)
    end
    H-->>C: BrandingId

Flujo: Verificacion DNS (Branding)

sequenceDiagram
    participant DNS as IDnsVerificationService
    participant H as MarkDnsVerifiedHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)

    DNS->>H: MarkDnsVerifiedCommand(tenantId, brandingId)
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.MarkDnsVerified(brandingId)
    T->>T: Branding.DnsVerificationStatus = Verified
    T->>T: Emitir BrandingDnsVerifiedEvent
    H->>R: Update(tenant)
    DNS-->>H: ok

Flujo: Registrar IdP

sequenceDiagram
    participant C as Cliente
    participant H as RegisterIdpHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)

    C->>H: RegisterIdentityProviderCommand(tenantId, code, name, description, strategy, createdBy)
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.RegisterIdentityProvider(idpId, code, name, description, strategy, createdBy)
    T->>T: Guardia: code unico dentro del tenant
    T->>T: Crear IdentityProvider (IsActive = false)
    T->>T: Emitir IdentityProviderRegisteredEvent
    H->>R: Update(tenant)
    H-->>C: IdpId

Flujo: Activar IdP

sequenceDiagram
    participant C as Cliente
    participant H as ActivateIdpHandler
    participant R as ITenantRepository
    participant T as Tenant (AR)
    participant SVC as IIdpStrategyConsistencyService

    C->>H: ActivateIdentityProviderCommand(tenantId, idpId, actorId)
    H->>SVC: ValidateActivation(tenantId, idpId)
    SVC-->>H: valido
    H->>R: GetById(tenantId)
    R-->>H: Tenant
    H->>T: tenant.ActivateIdentityProvider(idpId, actorId)
    T->>T: Buscar IdP en coleccion
    T->>T: IdP.IsActive = true
    T->>T: Emitir IdentityProviderActivatedEvent
    H->>R: Update(tenant)
    H-->>C: void

4. Modelo Entidad-Relacion

erDiagram
    TENANT ||--o{ BRANCH : "opera"
    TENANT ||--o| BRANDING : "configura"
    TENANT ||--o{ IDENTITY_PROVIDER : "registra"
    TENANT ||--o{ USER_ACCOUNT : "tiene"
    TENANT |o--o{ TENANT : "es_padre_de"
    BRANCH ||--o{ USER_ACCOUNT : "alcance"
    BRANCH ||--o{ PROFILE : "contexto_para"
    IDENTITY_PROVIDER ||--o{ IDP_CONFIGURATION : "configured_by"

    TENANT {
        uniqueidentifier TenantId PK
        nvarchar Code "Unico global"
        nvarchar Name
        nvarchar OrganizationType "COMPANY-DIVISION-BRANCH_OFFICE"
        nvarchar IdpStrategy "LOCAL-FEDERATED-HYBRID"
        nvarchar CompanyReference "Nullable"
        uniqueidentifier ParentTenantId "Nullable FK"
        nvarchar Status "ACTIVE-SUSPENDED-INACTIVE"
        datetime2 CreatedAt
        uniqueidentifier CreatedBy
        datetime2 UpdatedAt
        uniqueidentifier UpdatedBy
    }

    BRANCH {
        uniqueidentifier BranchId PK
        uniqueidentifier TenantId FK
        nvarchar Code "Unico por TenantId"
        nvarchar Name
        nvarchar GeofencingMetadata "JSON Nullable"
        bit IsActive
        datetime2 CreatedAt
        uniqueidentifier CreatedBy
        datetime2 UpdatedAt
        uniqueidentifier UpdatedBy
    }

    BRANDING {
        uniqueidentifier BrandingId PK
        uniqueidentifier TenantId FK "Unico - 1:1"
        nvarchar Logo "URI Storage Path"
        nvarchar LogoFormat "PNG-SVG-JPEG"
        nvarchar PrimaryColor "Hex Color"
        nvarchar BackgroundStyle "Glassmorphism-SleekDark"
        nvarchar HeadlineText
        nvarchar SecondaryText
        nvarchar PrimaryButtonLabel
        nvarchar FooterText
        nvarchar CustomDomain "FQDN Nullable"
        nvarchar DnsVerificationStatus "PENDING-VERIFIED-FAILED"
        nvarchar DnsCnameTarget "CNAME de Plataforma"
        bit MagicLinkFallbackEnabled
        datetime2 CreatedAt
        uniqueidentifier CreatedBy
        datetime2 UpdatedAt
        uniqueidentifier UpdatedBy
    }

    IDENTITY_PROVIDER {
        uniqueidentifier IdpId PK
        uniqueidentifier TenantId FK
        nvarchar Code "Unico por TenantId"
        nvarchar Name
        nvarchar Description
        nvarchar Strategy "OIDC-SAML2-WS_FED"
        bit IsActive
        datetime2 CreatedAt
        uniqueidentifier CreatedBy
        datetime2 UpdatedAt
        uniqueidentifier UpdatedBy
    }

5. Modelo de Bounded Context

flowchart TD
    subgraph Identity["Identity BC"]
        T[Tenant AR]
        B[Branch Entity]
        BR[Branding Entity]
        IDP[IdentityProvider Entity]
        UA[UserAccount AR]
        T --> B
        T --> BR
        T --> IDP
        UA -->|TenantId| T
        UA -->|BranchId opcional| B
    end

    subgraph Authorization["Authorization BC"]
        PROF[Profile AR]
        PROF -->|BranchId alcance opcional| B
    end

    subgraph Configuration["Configuration BC"]
        IDPC[IdpConfiguration AR]
    end

    subgraph Infrastructure["Infrastructure"]
        DNS[DNS Verification Service]
        STORE[File Storage - Logo URI]
        EXTIDP[IdP Externo - Azure AD, Okta]
    end

    subgraph Audit["Audit BC"]
        AUD[AuditRecord]
    end

    IDP -->|IdentityProviderRegisteredEvent| IDPC
    IDP -->|IdentityProviderDeactivatedEvent| IDPC
    EXTIDP -->|Contrato de Protocolo| IDP
    DNS -->|MarkDnsVerifiedCommand| BR
    DNS -->|MarkDnsFailedCommand| BR
    STORE -->|Logo URI almacenado| BR
    T -->|eventos de dominio| AUD
    B -->|eventos de dominio| AUD
    BR -->|eventos de dominio| AUD
    IDP -->|eventos de dominio| AUD

6. Contrato de Capa de Aplicacion

Comandos

| Comando | Entrada | Salida | |—|—|—| | RegisterTenantCommand | code, name, orgType, idpStrategy, createdBy | Guid tenantId | | SuspendTenantCommand | tenantId, actorId | void | | ActivateTenantCommand | tenantId, actorId | void | | AddBranchCommand | tenantId, code, name, geofencingMetadata?, createdBy | Guid branchId | | UpdateBranchCommand | tenantId, branchId, name?, geofencingMetadata?, updatedBy | void | | DeactivateBranchCommand | tenantId, branchId, actorId | void | | ReactivateBranchCommand | tenantId, branchId, actorId | void | | RemoveBranchCommand | tenantId, branchId, actorId | void | | ConfigureBrandingCommand | tenantId, logo, logoFormat, primaryColor, backgroundStyle, headlineText, secondaryText, primaryButtonLabel, footerText, customDomain?, cnameTarget, magicLinkFallback, createdBy | Guid brandingId | | UpdateBrandingCommand | tenantId, brandingId, campos..., updatedBy | void | | SetCustomDomainCommand | tenantId, brandingId, customDomain, updatedBy | void | | RemoveBrandingCommand | tenantId, brandingId, actorId | void | | MarkDnsVerifiedCommand | tenantId, brandingId | void | | MarkDnsFailedCommand | tenantId, brandingId, reason | void | | RegisterIdentityProviderCommand | tenantId, code, name, description, strategy, createdBy | Guid idpId | | ActivateIdentityProviderCommand | tenantId, idpId, actorId | void | | DeactivateIdentityProviderCommand | tenantId, idpId, actorId | void | | RemoveIdentityProviderCommand | tenantId, idpId, actorId | void |

Consultas

| Consulta | Retorna | |—|—| | GetTenantByIdQuery(tenantId) | TenantDto? | | GetTenantByCodeQuery(code) | TenantDto? |

Casos de Error

| Codigo | Condicion | |—|—| | TENANT_CODE_DUPLICATE | Code ya existe globalmente | | TENANT_NOT_FOUND | tenantId desconocido | | TENANT_NOT_ACTIVE | Operacion requiere tenant activo | | TENANT_SUSPENDED | Tenant actualmente suspendido | | BRANCH_CODE_DUPLICATE | Code ya existe en el tenant | | BRANCH_NOT_FOUND | branchId desconocido en el tenant | | BRANCH_HAS_DEPENDENTS | Eliminacion bloqueada por usuarios o perfiles activos | | BRANCH_ALREADY_INACTIVE | Desactivar una rama ya inactiva | | BRANDING_ALREADY_EXISTS | ConfigureBranding llamado dos veces | | BRANDING_NOT_FOUND | Sin branding configurado para el tenant | | DNS_ALREADY_VERIFIED | Intento de re-verificar un dominio ya verificado | | INVALID_CUSTOM_DOMAIN | No es un formato FQDN valido | | IDP_CODE_DUPLICATE | Code existe en el tenant | | IDP_NOT_FOUND | idpId desconocido en el tenant | | IDP_STRATEGY_IMMUTABLE | Intento de cambiar Strategy | | IDP_SOLE_ACTIVE_PROVIDER | Desactivacion dejaria al tenant sin autenticacion | | IDP_NOT_INACTIVE | Eliminacion intentada en IdP activo |


7. Notas de Persistencia

Indices

| Indice | Columnas | Tipo | |—|—|—| | IX_Tenant_Code | Code | Unico | | IX_Tenant_ParentTenantId | ParentTenantId | No unico | | IX_Branch_TenantId | TenantId | No unico | | IX_Branch_TenantId_Code | TenantId, Code | Unico | | IX_Branch_IsActive | IsActive | No unico | | IX_Branding_TenantId | TenantId | Unico (impone 1:1) | | IX_Branding_CustomDomain | CustomDomain | Unico (parcial - no nulo) | | IX_IdentityProvider_TenantId_Code | TenantId, Code | Unico | | IX_IdentityProvider_TenantId_IsActive | TenantId, IsActive | No unico |

Consideraciones Multi-Tenant


8. Seguridad y Auditoria

Reglas de Autorizacion

| Operacion | Rol Requerido | |—|—| | Registrar Tenant | Platform:Admin | | Suspender / Activar Tenant | Platform:Admin | | Agregar / Eliminar Branch | Tenant:Admin | | Desactivar / Reactivar Branch | Tenant:Admin | | Listar Branches | Tenant:Admin · Tenant:UserManager | | Configurar / Actualizar Branding | Tenant:Admin | | Establecer Dominio Personalizado | Tenant:Admin | | Marcar DNS Verificado/Fallido | Solo servicio interno | | Registrar / Eliminar IdP | Tenant:Admin | | Activar / Desactivar IdP | Tenant:Admin |

Eventos de Auditoria

Datos Sensibles