Idioma: English Español
Bounded Context: Identity
Aggregate Root: Tenant
Modulo: Ums.Domain.Identity.Tenant
Estado: Produccion
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.
Branch, Branding e IdentityProvider como entidades propias.IdpStrategy) a nivel de dominio.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.
Tenant es su propio aggregate root. Todas las mutaciones de Branch, Branding e IdentityProvider pasan por comandos de Tenant.
Code debe ser globalmente unico en todo el sistema.Tenant con TenantStatus = Suspended bloquea todos los flujos de autenticacion de sus usuarios.IdpStrategy debe ser coherente: si es FEDERATED, debe existir al menos un IdentityProvider activo.ParentTenantId != null) hereda politicas del tenant padre.Code debe ser unico dentro del Tenant propietario.Branch no puede ser eliminada si existen registros activos de UserAccount o Profile asociados.GeofencingMetadata debe ser JSON valido cuando se proporciona.Branding por Tenant (relacion 1:1).CustomDomain debe ser un hostname valido cuando se proporciona.DnsVerificationStatus comienza en PENDING cuando se establece CustomDomain y no puede establecerse manualmente a VERIFIED (solo por el servicio de verificacion DNS).LogoFormat debe coincidir con el formato real del URI de Logo subido.Code debe ser unico dentro del Tenant propietario.IdentityProvider debe ser desactivado antes de ser eliminado.IdentityProvider que es el unico IdP activo para un tenant Federado no esta permitido a menos que se cambie primero el IdpStrategy.Strategy no puede cambiarse despues del registro — es inmutable una vez establecida.| 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 |
| 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 |
| 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 |
ITenantRepository.IBranchDependencyChecker — servicio de dominio para validar dependencias antes de eliminar rama.IIdpStrategyConsistencyService — valida que la desactivacion de un IdP no deje al tenant sin ruta de autenticacion.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
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
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
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
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
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
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
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
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
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
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
}
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
| 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 |
| Consulta | Retorna |
|—|—|
| GetTenantByIdQuery(tenantId) | TenantDto? |
| GetTenantByCodeQuery(code) | TenantDto? |
| 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 |
| 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 |
TenantId.Code en Tenant es clave unica global — no por tenant.CustomDomain unico entre todos los tenants (un dominio no puede ser reclamado por dos tenants).| 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 |
TENANT_CREATED, TENANT_SUSPENDED, TENANT_ACTIVATEDBRANCH_CREATED, BRANCH_DEACTIVATED, BRANCH_REACTIVATED, BRANCH_REMOVEDBRANDING_CONFIGURED, BRANDING_UPDATED, BRANDING_REMOVED, DNS_VERIFIED, DNS_FAILEDIDP_REGISTERED, IDP_ACTIVATED, IDP_DEACTIVATED, IDP_REMOVEDIdentityProvider en si no almacena credenciales. Los secretos viven en IDP_CONFIGURATION.SecretRef (ruta al vault).