Bounded Context: Approvals
Aggregate Root: Yes
Module: Ums.Domain.Approvals.UserDocument
Status: Production
The UserDocument aggregate represents a digital credential or compliance document uploaded by a user (e.g., identity verification, certifications). It manages the document’s verification lifecycle, validity state, compliance status, and history of expiration notifications sent to the user (AccessNotification). AccessNotification records individual notification transmissions sent regarding upcoming expiration.
AccessNotification) as owned entities.UserDocument is a sovereign aggregate root within the Approvals context. It controls its internal state and guarantees that all children (such as AccessNotification) are modified exclusively through root domain methods. AccessNotification is strictly coordinated under its lifecycle.
ExpirationDate must be chronologically greater than its IssueDate.PendingReview.PendingReview can transition to Valid (via Validate) or Rejected (via Reject).Valid can transition to Expired (via Expire) when the calendar date passes ExpirationDate.Expired and Rejected documents can trigger ReUpload, which resets the status to PendingReview and resets the notification step counter.Rejected cannot transition directly to Valid without undergoing a new upload/verification cycle.FileChecksum) and reference an existing DocumentTypeId structure.AccessNotification is recorded, its properties cannot be modified.DaysRemaining must be a positive integer or zero, representing the remaining validity span.Step index must correspond to an active warning phase configured in the document type rules.| Entity / VO | Type | Description |
|—|—|—|
| UserDocumentId | Value Object | Unique aggregate identifier |
| UserId | Value Object | Owner reference, linking to the Identity Context |
| DocumentTypeId | Value Object | Reference to the definition template aggregate |
| DocumentStatus | Enum | PendingReview · Valid · Rejected · Expired |
| DocumentCriticity | Value Object | Compliance severity classification |
| TextValueObject | Value Object | Validated file system storage path |
| AccessNotification | Entity | Owned child entity logging alert history |
| AccessNotificationId | Value Object | Unique entity identifier |
| NotificationChannel | Enum | EMAIL · SMS · IN_APP · WEB_PUSH |
| Step | Primitive | Step index counter |
UserDocument (Aggregate Root)
├── Props: UserDocumentProps
│ ├── Id: UserDocumentId
│ ├── UserId: UserId (External Ref)
│ ├── DocumentTypeId: DocumentTypeId (External Ref)
│ ├── IssueDate: DateTime
│ ├── ExpirationDate: DateTime
│ ├── Status: DocumentStatus
│ ├── Criticity: DocumentCriticity
│ ├── FileStoragePath: TextValueObject
│ ├── FileChecksum: string
│ ├── NotificationStep: int
│ └── Audit: AuditValueObject
└── Notifications: AccessNotification[] (Child Collection)
└── Props: AccessNotificationProps
├── Id: IdValueObject
├── Step: int
├── Channel: NotificationChannel
├── DaysRemaining: int
└── SentAt: DateTime
classDiagram
direction TB
class UserDocument {
+UserDocumentProps Props
+IReadOnlyCollection~AccessNotification~ Notifications
+Upload(UserId, DocumentTypeId, IssueDate, ExpirationDate, DocumentCriticity, TextValueObject, string, ActorId) Result~UserDocument~
+Validate(ActorId) Result
+Reject(string, ActorId) Result
+Expire(ActorId) Result
+ReUpload(DateTime, DateTime, TextValueObject, string, ActorId) Result
+RecordNotificationSent(int, NotificationChannel, int, ActorId) Result
+RecordEnforcementExecuted(string, ActorId) Result
}
class UserDocumentProps {
+IdValueObject Id
+UserId UserId
+DocumentTypeId DocumentTypeId
+DateTime IssueDate
+DateTime ExpirationDate
+DocumentStatus Status
+DocumentCriticity Criticity
+TextValueObject FileStoragePath
+string FileChecksum
+int NotificationStep
+AuditValueObject Audit
}
class AccessNotification {
+Guid Id
+int Step
+NotificationChannel Channel
+int DaysRemaining
+DateTime SentAt
+Record(step, channel, daysRemaining) AccessNotification
}
class DocumentStatus {
<<enumeration>>
PendingReview
Valid
Rejected
Expired
}
class DocumentCriticity {
+string Name
+int SeverityLevel
}
class NotificationChannel {
<<enumeration>>
EMAIL
SMS
IN_APP
WEB_PUSH
}
UserDocument *-- UserDocumentProps
UserDocument "1" *-- "0..*" AccessNotification : owns
UserDocumentProps --> DocumentStatus
UserDocumentProps --> DocumentCriticity
AccessNotification --> NotificationChannel
sequenceDiagram
autonumber
actor Reviewer
participant Portal as Web Client
participant App as Application Service
participant Doc as UserDocument [Aggregate]
participant Repo as UserDocumentRepository
participant DB as SQL Server
Reviewer->>Portal: Review Document details
Portal->>App: ValidateUserDocumentCommand(DocId)
App->>Repo: GetByIdAsync(DocId)
Repo-->>App: UserDocument
App->>Doc: Validate(ReviewerId)
note over Doc: Verify INV-UD2<br/>(Status == PendingReview)
Doc-->>App: Success
App->>Repo: SaveAsync(UserDocument)
Repo->>DB: UPDATE USER_DOCUMENT SET Status = 'Valid'
DB-->>Repo: Acknowledge
Repo-->>App: Done
App-->>Portal: ValidateUserDocumentResponse(Success)
erDiagram
USER_DOCUMENT ||--o{ ACCESS_NOTIFICATION : "records"
USER_DOCUMENT }o--|| DOCUMENT_TYPE : "instantiates"
USER_DOCUMENT {
uniqueidentifier UserDocumentId PK
uniqueidentifier UserId FK "Identity Context"
uniqueidentifier DocumentTypeId FK
datetime2 IssueDate
datetime2 ExpirationDate
nvarchar Status
nvarchar Criticity
nvarchar FileStoragePath
nvarchar FileChecksum
int NotificationStep
nvarchar CreatedBy
datetime2 CreatedAt
nvarchar UpdatedBy
datetime2 UpdatedAt
}
ACCESS_NOTIFICATION {
uniqueidentifier NotificationId PK
uniqueidentifier UserDocumentId FK
int Step
nvarchar Channel
int DaysRemaining
datetime2 SentAt
}
UserId.ACCESS_NOTIFICATION is scoped via its parent aggregate UserDocument. Multi-tenant safety is guaranteed implicitly.flowchart TD
subgraph IdentityContext [Identity Context]
U[UserAccount]
end
subgraph ApprovalsContext [Approvals Context]
DT[DocumentType]
UD[UserDocument]
AN[AccessNotification]
end
UD -.->|references UserId| U
UD -->|instantiates| DT
UD *--|owns| AN
AccessNotification are read by the security compliance engine to verify notice protocols.Valid.Rejected, including reasons for revision.PendingReview.UserDocument to add AccessNotification.public class UserDocumentConfiguration : IEntityTypeConfiguration<UserDocument>
{
public void Configure(EntityTypeBuilder<UserDocument> builder)
{
builder.ToTable("USER_DOCUMENT");
builder.HasKey(e => e.Id);
builder.OwnsOne(e => e.Props, props =>
{
props.Property(p => p.Id).HasColumnName("UserDocumentId");
props.Property(p => p.UserId).HasColumnName("UserId");
props.Property(p => p.DocumentTypeId).HasColumnName("DocumentTypeId");
props.Property(p => p.IssueDate).HasColumnName("IssueDate");
props.Property(p => p.ExpirationDate).HasColumnName("ExpirationDate");
props.Property(p => p.Status).HasConversion<string>().HasColumnName("Status");
props.Property(p => p.Criticity).HasConversion(c => c.Name, n => DocumentCriticity.FromName(n)).HasColumnName("Criticity");
props.Property(p => p.FileStoragePath).HasConversion(p => p.GetValue(), s => TextValueObject.Create(s).Value).HasColumnName("FileStoragePath");
props.Property(p => p.FileChecksum).HasColumnName("FileChecksum");
props.Property(p => p.NotificationStep).HasColumnName("NotificationStep");
props.OwnsOne(p => p.Audit);
});
builder.HasMany(e => e.Notifications)
.WithOne()
.HasForeignKey("UserDocumentId")
.OnDelete(DeleteBehavior.Cascade);
}
}
AccessNotification is persisted as a dependent table mapped by EF Core with a cascade delete rule referencing its parent UserDocument.Role.User can upload or re-upload documents. Only Role.Reviewer can validate or reject.FileStoragePath) should be stored within protected directories. Cryptographic verification of FileChecksum protects against underlying file tampering.AccessNotification) are strictly read-only after creation to prevent tampering with security audit paths.AccessNotification as a nested collection guarantees chronological audit traces. Storing history alongside the parent document provides frictionless historical verification without querying generic communication logs. Persisting notification logs as owned entities rather than dispatching them to an external audit engine ensures aggregate self-sufficiency and high performance during compliance checks.