Licensing Domain (licensing_v1)
The Licensing domain (licensing_v1) is a bounded context responsible for managing software licenses and controlling feature access through license types and seat allocations. It implements a license lifecycle model where licenses can be created with specific seat capacities, seats can be allocated to users, and license status can transition through various states (active, suspended, expired, terminated). The domain enforces business rules around seat capacity constraints and license validity windows.
Overview
Section titled “Overview”The Licensing domain implements a comprehensive license management system where:
- Licenses define software entitlements with a specific seat capacity and validity window
- License Types categorize licenses (personal, organization, trial) to enable different feature access levels
- Seat Allocations map individual users to licenses, tracking who has access and what seat type they hold
- License Status controls whether a license is active, suspended, expired, or terminated
- Business Rules enforce seat capacity limits and prevent over-allocation
- Audit Trails capture license creation, seat assignments, and status changes through domain events
This domain enables the system to implement feature access control by:
- Determining which users can access premium features based on license type
- Limiting concurrent users via seat allocations
- Tracking feature entitlements and their validity periods
- Maintaining compliance with license terms
License Model
Section titled “License Model”License Types
Section titled “License Types”Licenses are categorized by type to support different feature access levels:
| License Type | Purpose | Typical Use Case |
|---|---|---|
| personal | Individual user license | Single user with full feature access |
| organization | Team or enterprise license | Multiple users in an organization |
| trial | Time-limited evaluation license | Onboarding and feature trial |
License type is a string value determined at creation time and used by consuming services to determine available features.
License Owner Types
Section titled “License Owner Types”Each license has an owner, which can be either a user or organization:
enum LicenceOwnerType { user, // Owned by an individual user organization; // Owned by an organization}User-owned licenses typically represent personal or individual team subscriptions.
Organization-owned licenses represent enterprise subscriptions serving all members of an organization.
License Status
Section titled “License Status”A license has a lifecycle with four possible states:
class LicenceStatus { static const active = 'active'; // Currently effective and in use static const suspended = 'suspended'; // Temporarily disabled (e.g., payment issue) static const expired = 'expired'; // Validity window has passed static const terminated = 'terminated'; // Explicitly ended}License status transitions are immutable records—only directives can change status by emitting events.
Seat Types
Section titled “Seat Types”Seat allocations specify the user’s role within a licensed feature:
// Common seat types by business context'admin' // Full administrative access to feature'editor' // Can create and modify content'viewer' // Read-only accessSeat types are domain-specific strings determined by each consuming service.
License Validity Window
Section titled “License Validity Window”Each license has an effective period defined by two dates:
- effectiveFrom: Date when the license becomes valid (inclusive)
- effectiveUntil: Optional expiration date (inclusive); unlimited if null
// Example: License valid Jan 1 - Dec 31, 2024final license = LicenceAggregate( effectiveFrom: DateTime(2024, 1, 1), effectiveUntil: DateTime(2024, 12, 31), // ... other fields);A license is considered currently valid only if:
- Status is ‘active’
- Current date is
>=effectiveFrom - Current date is
<=effectiveUntil (if effectiveUntil is set)
Key Aggregates
Section titled “Key Aggregates”LicenceAggregate
Section titled “LicenceAggregate”The root aggregate managing a single software license and its seat allocations.
Identity:
AggregateId: System-generated ID (mirrors licenceId)licenceId: LicenceId: Strongly-typed license identifier
Core Attributes:
class LicenceAggregate extends Aggregate<LicenceAggregate> { final LicenceId licenceId; // Unique license identifier final String licenceType; // 'personal', 'organization', 'trial' final String ownerId; // UserId or OrganizationId as string final String ownerType; // 'user' or 'organization' final int totalSeatCapacity; // Maximum concurrent users allowed final DateTime effectiveFrom; // License becomes valid final DateTime? effectiveUntil; // Optional expiration date final UserId createdBy; // Admin who created the license final DateTime createdAt; // License creation timestamp final DateTime updatedAt; // Last modification timestamp final String status; // 'active', 'suspended', 'expired', 'terminated' final Map<String, SeatAssignment> _seatAssignments; // Internal seat tracking}SeatAssignment Value Object
Section titled “SeatAssignment Value Object”Represents the allocation of a seat to a specific user.
class SeatAssignment { final SeatId seatId; // Unique seat identifier final UserId userId; // User occupying the seat final String seatType; // 'admin', 'editor', 'viewer', etc. final UserId allocatedBy; // Admin who allocated the seat final DateTime allocatedAt; // Allocation timestamp final DomainText? notes; // Optional notes about allocation final String status; // 'active' or 'released'
// Release tracking final UserId? releasedBy; // Admin who released the seat final DateTime? releasedAt; // Release timestamp final DomainText? releaseReason; // Why the seat was released
// Convenience properties bool get isActive => status == 'active'; bool get isReleased => status == 'released';}Key Methods
Section titled “Key Methods”Status Checkers:
bool get isActive => status == 'active';bool get isSuspended => status == 'suspended';bool get isExpired => status == 'expired';bool get isTerminated => status == 'terminated';Seat Queries:
// Get all currently active seat assignmentsList<SeatAssignment> get activeSeats
// Count available seatsint get availableSeats => totalSeatCapacity - activeSeats.length;
// Check if license can accept new seatsbool canAllocateSeat() => isActive && availableSeats > 0;
// Check if user has an active seatbool hasActiveSeat(UserId userId)
// Retrieve seat assignment for a userSeatAssignment? getSeatForUser(UserId userId)License Validity:
// Is license currently valid (active AND within effective dates)?bool get isCurrentlyValid
// How many of available seats are in use?double get utilizationPercentage
// Is license expiring soon (within 30 days)?bool get isNearExpiryBusiness Rules
Section titled “Business Rules”The aggregate enforces critical invariants via validate():
- License ID Required:
licenceIdcannot be unresolved or empty - Owner ID Required:
ownerIdcannot be unresolved or empty - Positive Capacity:
totalSeatCapacitymust be > 0 - No Overallocation: Cannot have more active seats than
totalSeatCapacity - Valid Status: Must be one of the four valid status values
- Date Logic:
effectiveUntilcannot be beforeeffectiveFrom
These rules are enforced when applying events and before persisting aggregates.
Domain Events
Section titled “Domain Events”The licensing domain emits four event types capturing all license state changes:
LicenceCreatedEvent
Section titled “LicenceCreatedEvent”Fired when a new software license is established.
class LicenceCreatedEvent implements Event { final LicenceId licenceId; final String licenceType; // 'personal', 'organization', 'trial' final String ownerId; // UserId or OrganizationId final String ownerType; // 'user' or 'organization' final int totalSeatCapacity; // Maximum seats for this license final DateTime effectiveFrom; // When license becomes valid final DateTime? effectiveUntil; // When license expires (optional) final UserId createdBy; // Admin who created the license final DateTime createdAt; // Creation timestamp final int? globalSequenceNumber; // Event ordering}Generated by: CreateLicenceDirective
Example:
LicenceCreatedEvent( licenceId: LicenceId('lic-org-acme-2024'), licenceType: 'organization', ownerId: 'org-acme-corp', ownerType: 'organization', totalSeatCapacity: 50, effectiveFrom: DateTime(2024, 1, 1), effectiveUntil: DateTime(2024, 12, 31), createdBy: UserId('admin-system'), createdAt: DateTime.now(),)SeatAllocatedEvent
Section titled “SeatAllocatedEvent”Fired when a license seat is assigned to a user.
class SeatAllocatedEvent implements Event { final LicenceId licenceId; final SeatId seatId; // Unique seat identifier final UserId userId; // User receiving the seat final String seatType; // 'admin', 'editor', 'viewer' final UserId allocatedBy; // Admin who allocated the seat final DateTime allocatedAt; // Allocation timestamp final DomainText? notes; // Optional allocation notes final int? globalSequenceNumber;}Generated by: AllocateSeatDirective
Example:
SeatAllocatedEvent( licenceId: LicenceId('lic-org-acme-2024'), seatId: SeatId('seat-alice-2024'), userId: UserId('alice-123'), seatType: 'editor', allocatedBy: UserId('admin-system'), allocatedAt: DateTime.now(), notes: DomainText('Allocated to project lead'),)SeatReleasedEvent
Section titled “SeatReleasedEvent”Fired when a license seat is freed up.
class SeatReleasedEvent implements Event { final LicenceId licenceId; final SeatId seatId; final UserId userId; final UserId releasedBy; // Admin who released the seat final DateTime releasedAt; // Release timestamp final DomainText? reason; // Why the seat was released final int? globalSequenceNumber;}Generated by: ReleaseSeatDirective
Example:
SeatReleasedEvent( licenceId: LicenceId('lic-org-acme-2024'), seatId: SeatId('seat-bob-2024'), userId: UserId('bob-456'), releasedBy: UserId('admin-system'), releasedAt: DateTime.now(), reason: DomainText('User left project'),)LicenceStatusChangedEvent
Section titled “LicenceStatusChangedEvent”Fired when license status transitions between states.
class LicenceStatusChangedEvent implements Event { final LicenceId licenceId; final String previousStatus; // Former status final String newStatus; // New status final UserId changedBy; // Admin who made the change final DateTime changedAt; // Transition timestamp final DomainText? reason; // Why status changed final int? globalSequenceNumber;}Generated by: UpdateLicenceStatusDirective
Example:
LicenceStatusChangedEvent( licenceId: LicenceId('lic-org-acme-2024'), previousStatus: 'active', newStatus: 'suspended', changedBy: UserId('admin-system'), changedAt: DateTime.now(), reason: DomainText('Payment issue detected'),)Directives (Command Handlers)
Section titled “Directives (Command Handlers)”Directives are command handlers that validate business rules and emit events. All directives target the LicenceAggregate.
CreateLicenceDirective
Section titled “CreateLicenceDirective”Creates a new software license.
Input Payload:
class CreateLicencePayload { final LicenceId licenceId; final String licenceType; // 'personal', 'organization', 'trial' final UserId? userOwnerId; // For user-owned licenses final OrganizationId? orgOwnerId; // For organization-owned licenses final LicenceOwnerType ownerType; // user or organization final int totalSeatCapacity; // Number of concurrent seats final DateTime effectiveFrom; // License start date final DateTime? effectiveUntil; // License end date (optional) final UserId createdBy; // Admin creating the license}Validation:
- Exactly one of
userOwnerIdororgOwnerIdmust be provided based onownerType totalSeatCapacitymust be positivecreatedBymust be a valid user
Factory Constructors for Convenience:
// Create a user-owned licenseCreateLicencePayload.forUser( licenceId: LicenceId('lic-personal-alice'), licenceType: 'personal', userOwnerId: UserId('alice-123'), totalSeatCapacity: 1, effectiveFrom: DateTime.now(), createdBy: UserId('admin-system'),)
// Create an organization-owned licenseCreateLicencePayload.forOrganization( licenceId: LicenceId('lic-org-acme-2024'), licenceType: 'organization', orgOwnerId: OrganizationId('org-acme'), totalSeatCapacity: 50, effectiveFrom: DateTime.now(), effectiveUntil: DateTime.now().add(Duration(days: 365)), createdBy: UserId('admin-system'),)Example:
final directive = CreateLicenceDirective( payload: CreateLicencePayload.forOrganization( licenceId: LicenceId('lic-org-acme-2024'), licenceType: 'organization', orgOwnerId: OrganizationId('org-acme'), totalSeatCapacity: 50, effectiveFrom: DateTime(2024, 1, 1), effectiveUntil: DateTime(2024, 12, 31), createdBy: UserId('admin-system'), ),);
final result = await engine.execute<LicenceAggregate, CreateLicencePayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
// result.updatedAggregate contains the new license// result.events contains LicenceCreatedEventAllocateSeatDirective
Section titled “AllocateSeatDirective”Assigns a license seat to a user.
Input Payload:
class AllocateSeatPayload { final LicenceId licenceId; final SeatId seatId; final UserId userId; final String seatType; // 'admin', 'editor', 'viewer', etc. final UserId allocatedBy; final DomainText? notes;}Business Rules:
- License must exist and be active
- License must have available seats (activeSeats < totalSeatCapacity)
- User cannot already have an active seat on this license (single seat per user)
Example:
final directive = AllocateSeatDirective( payload: AllocateSeatPayload( licenceId: LicenceId('lic-org-acme-2024'), seatId: SeatId('seat-alice-2024'), userId: UserId('alice-123'), seatType: 'editor', allocatedBy: UserId('admin-system'), notes: DomainText('Project lead for Q1'), ),);
final result = await engine.execute<LicenceAggregate, AllocateSeatPayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
// License now has one more active seatassert(result.updatedAggregate.activeSeats.length == 1);ReleaseSeatDirective
Section titled “ReleaseSeatDirective”Frees up a license seat previously allocated to a user.
Input Payload:
class ReleaseSeatPayload { final LicenceId licenceId; final SeatId seatId; final UserId userId; final UserId releasedBy; final DomainText? reason;}Business Rules:
- Seat must exist and be active
- Seat user must match the provided
userId
Example:
final directive = ReleaseSeatDirective( payload: ReleaseSeatPayload( licenceId: LicenceId('lic-org-acme-2024'), seatId: SeatId('seat-bob-2024'), userId: UserId('bob-456'), releasedBy: UserId('admin-system'), reason: DomainText('User transferred to different project'), ),);
final result = await engine.execute<LicenceAggregate, ReleaseSeatPayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
// Seat is now marked as 'released'final seat = result.updatedAggregate._seatAssignments[SeatId('seat-bob-2024').value];assert(seat?.isReleased == true);UpdateLicenceStatusDirective
Section titled “UpdateLicenceStatusDirective”Changes a license’s status (active → suspended, expired, terminated).
Input Payload:
class UpdateLicenceStatusPayload { final LicenceId licenceId; final String previousStatus; // Expected current status final String newStatus; // Target status final UserId changedBy; final DomainText? reason;}Business Rules:
- Only transitions to valid status values
- Status must differ from current
Example:
final directive = UpdateLicenceStatusDirective( payload: UpdateLicenceStatusPayload( licenceId: LicenceId('lic-org-acme-2024'), previousStatus: 'active', newStatus: 'suspended', changedBy: UserId('admin-system'), reason: DomainText('Payment processing failed'), ),);
final result = await engine.execute<LicenceAggregate, UpdateLicenceStatusPayload>( directive: directive, timelineId: timelineId, workspaceId: workspace,);
assert(result.updatedAggregate.isSuspended == true);License Checking Across the System
Section titled “License Checking Across the System”Within Aggregates
Section titled “Within Aggregates”The aggregate provides specialized query methods:
final license = licenceAggregate;
// Status checksif (license.isActive) { // License is currently in use}
if (license.isCurrentlyValid) { // License is active AND within validity window}
// Seat availabilityif (license.canAllocateSeat()) { // Safe to allocate another seat}
// User seat lookupsif (license.hasActiveSeat(userId)) { final seat = license.getSeatForUser(userId); print('${userId} has seat: ${seat?.seatType}');}
// Capacity metricsprint('${license.activeSeats.length} / ${license.totalSeatCapacity} seats in use');print('${license.utilizationPercentage}% utilization');Pre-Allocation Validation
Section titled “Pre-Allocation Validation”Before allocating a seat, verify capacity:
if (license.canAllocateSeat()) { // Safe to proceed with AllocateSeatDirective} else { throw StateError('No available seats on this license');}License Validity Checks
Section titled “License Validity Checks”For feature access control:
// Is feature unlocked by this license?if (license.isCurrentlyValid && license.licenceType == 'organization') { // User has access to premium features}
// Check user has a seatif (license.hasActiveSeat(userId)) { // User is entitled to use the feature}
// Warn if expiring soonif (license.isNearExpiry && license.status == 'active') { // Show renewal notice to admin}Value Objects
Section titled “Value Objects”LicenceId
Section titled “LicenceId”Strongly-typed identifier for licenses.
class LicenceId extends DomainText { static const unresolved = LicenceId._('unresolved_licence_id');
const LicenceId(String value) : assert(value.length > 0, 'LicenceId cannot be empty');
factory LicenceId.fromString(String v) { if (v == 'unresolved_licence_id') return unresolved; return LicenceId(v); }}
// Usagefinal id = LicenceId('lic-org-acme-2024');final fromString = LicenceId.fromString('lic-org-acme-2024');SeatId
Section titled “SeatId”Strongly-typed identifier for seat allocations.
class SeatId extends DomainText { static const unresolved = SeatId._('unresolved_seat_id');
const SeatId(String value) : assert(value.length > 0, 'SeatId cannot be empty');
factory SeatId.fromString(String v) { if (v == 'unresolved_seat_id') return unresolved; return SeatId(v); }}
// Usagefinal seatId = SeatId('seat-alice-2024');Domain Registration
Section titled “Domain Registration”Licensing types must be registered with Nomos before use:
import 'package:licensing_v1/licensing_v1.dart';
void main() { // Register all licensing domain types registerLicensingV1();
// Or use the DomainModule pattern final module = LicensingV1(); module.registerDomainTypes();}This registers:
LicenceAggregate(empty and fromJson factories)- All four event types and their JSON deserializers
- All four directive types and payload deserializers
Serialization and JSON
Section titled “Serialization and JSON”Aggregate Serialization
Section titled “Aggregate Serialization”The aggregate and its seat assignments serialize to JSON for persistence:
final license = licenceAggregate;final json = license.toJson();
// Result structure:// {// "id": "lic-org-acme-2024",// "licenceId": "lic-org-acme-2024",// "licenceType": "organization",// "ownerId": "org-acme",// "ownerType": "organization",// "totalSeatCapacity": 50,// "effectiveFrom": "2024-01-01T00:00:00.000Z",// "effectiveUntil": "2024-12-31T00:00:00.000Z",// "createdBy": "admin-system",// "createdAt": "2024-01-01T10:00:00.000Z",// "updatedAt": "2024-01-15T14:30:00.000Z",// "status": "active",// "seatAssignments": {// "seat-alice-2024": {// "seatId": "seat-alice-2024",// "userId": "alice-123",// "seatType": "editor",// "allocatedBy": "admin-system",// "allocatedAt": "2024-01-05T09:00:00.000Z",// "notes": "Project lead",// "status": "active",// "releasedBy": null,// "releasedAt": null,// "releaseReason": null// }// }// }
// Deserializefinal restored = LicenceAggregate.fromJson(json, AggregateId('lic-org-acme-2024'));assert(restored.isActive);assert(restored.activeSeats.length == 1);Usage Examples
Section titled “Usage Examples”Creating a License and Allocating Seats
Section titled “Creating a License and Allocating Seats”import 'package:licensing_v1/licensing_v1.dart';import 'package:contracts_v1/contracts_v1.dart';import 'package:nomos_core/nomos_core.dart';
// Initialize the domainregisterLicensingV1();
// Step 1: Create a new organization licensefinal createDirective = CreateLicenceDirective( payload: CreateLicencePayload.forOrganization( licenceId: LicenceId('lic-org-acme-2024'), licenceType: 'organization', orgOwnerId: OrganizationId('org-acme'), totalSeatCapacity: 50, effectiveFrom: DateTime(2024, 1, 1), effectiveUntil: DateTime(2024, 12, 31), createdBy: UserId('admin-system'), ),);
final createResult = await engine.execute<LicenceAggregate, CreateLicencePayload>( directive: createDirective, timelineId: timelineId, workspaceId: workspace,);
final license = createResult.updatedAggregate;assert(license.isActive);assert(license.availableSeats == 50);
// Step 2: Allocate seats to usersfinal userIds = [UserId('alice-123'), UserId('bob-456'), UserId('carol-789')];
for (final userId in userIds) { final allocateDirective = AllocateSeatDirective( payload: AllocateSeatPayload( licenceId: license.licenceId, seatId: SeatId('seat-${userId.value}-2024'), userId: userId, seatType: 'editor', allocatedBy: UserId('admin-system'), ), );
final allocResult = await engine.execute<LicenceAggregate, AllocateSeatPayload>( directive: allocateDirective, timelineId: timelineId, workspaceId: workspace, );
print('Allocated seat to $userId');}
// Step 3: Verify statefinal finalLicense = license; // Would reload from store in real applicationassert(finalLicense.activeSeats.length == 3);assert(finalLicense.availableSeats == 47);assert(finalLicense.utilizationPercentage == 6.0);Checking Feature Access
Section titled “Checking Feature Access”// Determine if user can access a premium featurebool canAccessPremiumFeature( LicenceAggregate license, UserId userId,) { // Must have an active organization or personal license if (!license.isCurrentlyValid) return false; if (license.licenceType == 'trial') return false;
// User must have an active seat allocation return license.hasActiveSeat(userId);}
// Usage in application logicfinal license = await loadLicense(licenceId);if (canAccessPremiumFeature(license, currentUser)) { // Enable premium UI features showPremiumDashboard();} else { // Prompt user to upgrade showUpgradePrompt();}Releasing Seats and Suspending Licenses
Section titled “Releasing Seats and Suspending Licenses”// Release a specific user's seatfinal releaseDirective = ReleaseSeatDirective( payload: ReleaseSeatPayload( licenceId: license.licenceId, seatId: SeatId('seat-alice-2024'), userId: UserId('alice-123'), releasedBy: UserId('admin-system'), reason: DomainText('User left organization'), ),);
final releaseResult = await engine.execute<LicenceAggregate, ReleaseSeatPayload>( directive: releaseDirective, timelineId: timelineId, workspaceId: workspace,);
print('Seat released. Available seats: ${releaseResult.updatedAggregate.availableSeats}');
// Suspend license due to payment issuefinal suspendDirective = UpdateLicenceStatusDirective( payload: UpdateLicenceStatusPayload( licenceId: license.licenceId, previousStatus: 'active', newStatus: 'suspended', changedBy: UserId('admin-system'), reason: DomainText('Payment processing failed'), ),);
final suspendResult = await engine.execute<LicenceAggregate, UpdateLicenceStatusPayload>( directive: suspendDirective, timelineId: timelineId, workspaceId: workspace,);
// All feature access is now blocked for users on this licenseassert(!suspendResult.updatedAggregate.isCurrentlyValid);Dependencies
Section titled “Dependencies”The licensing domain depends on:
- nomos_core: Event sourcing framework (aggregates, events, directives)
- contracts_v1: Shared value objects (UserId, OrganizationId, LicenceId, SeatId, SeatAssignment, etc.)
It has no dependencies on other domains, enabling it to be used across the system for feature access control without circular dependency risk.
Package Location
Section titled “Package Location”dart_packages/co2/domains/licensing_v1/├── lib/│ ├── licensing_v1.dart # Main export file and registration│ └── src/│ ├── aggregates/│ │ └── licence_aggregate.dart # Core aggregate and SeatAssignment│ ├── events/│ │ └── licensing_events.dart # Four event types│ └── directives/│ └── licensing_directives.dart # Four directive types + payloads├── pubspec.yaml└── test/ └── [tests]Key Concepts Summary
Section titled “Key Concepts Summary”| Concept | Purpose | Example |
|---|---|---|
| LicenceAggregate | Root aggregate managing one license | License for ACME Corp with 50 seats |
| SeatAssignment | Individual seat allocation to a user | Alice has an ‘editor’ seat |
| LicenceId | Strongly-typed license identifier | lic-org-acme-2024 |
| SeatId | Strongly-typed seat identifier | seat-alice-2024 |
| License Type | Feature tier (personal, organization, trial) | organization |
| Seat Type | User role within licensed feature | editor, admin |
| License Status | Lifecycle state | active, suspended, expired, terminated |
| Event | Immutable record of state change | LicenceCreatedEvent, SeatAllocatedEvent |
| Directive | Command handler enforcing rules | CreateLicenceDirective |
Common Patterns
Section titled “Common Patterns”Pattern 1: Bulk Seat Allocation
Section titled “Pattern 1: Bulk Seat Allocation”Allocate seats to multiple users when a license is created:
final licenceId = LicenceId('lic-org-new-2024');final userIds = [ UserId('alice-123'), UserId('bob-456'), UserId('carol-789'),];
// First, create the licensefinal createResult = await engine.execute<LicenceAggregate, CreateLicencePayload>( directive: CreateLicenceDirective( payload: CreateLicencePayload.forOrganization( licenceId: licenceId, licenceType: 'organization', orgOwnerId: OrganizationId('org-new'), totalSeatCapacity: userIds.length, effectiveFrom: DateTime.now(), createdBy: UserId('admin-system'), ), ), timelineId: timelineId, workspaceId: workspace,);
// Then allocate seatsfor (int i = 0; i < userIds.length; i++) { await engine.execute<LicenceAggregate, AllocateSeatPayload>( directive: AllocateSeatDirective( payload: AllocateSeatPayload( licenceId: licenceId, seatId: SeatId('seat-user-$i'), userId: userIds[i], seatType: 'editor', allocatedBy: UserId('admin-system'), ), ), timelineId: timelineId, workspaceId: workspace, );}Pattern 2: License Renewal Workflow
Section titled “Pattern 2: License Renewal Workflow”Track when licenses need renewal and handle expiration:
// Query licenses approaching expirationfinal expiringLicenses = allLicenses.where((lic) { return lic.isActive && lic.isNearExpiry;}).toList();
// Send renewal remindersfor (final license in expiringLicenses) { sendRenewalReminder(license.ownerId, license.licenceId);}
// On renewal date: create new license with same ownerfinal oldLicense = /* get old license */;final newLicense = await engine.execute<LicenceAggregate, CreateLicencePayload>( directive: CreateLicenceDirective( payload: oldLicense.ownerType == 'user' ? CreateLicencePayload.forUser( licenceId: LicenceId('lic-personal-${oldLicense.ownerId}-2025'), licenceType: oldLicense.licenceType, userOwnerId: UserId(oldLicense.ownerId), totalSeatCapacity: oldLicense.totalSeatCapacity, effectiveFrom: oldLicense.effectiveUntil?.add(Duration(days: 1)) ?? DateTime.now(), effectiveUntil: oldLicense.effectiveUntil?.add(Duration(days: 365)), createdBy: UserId('admin-system'), ) : CreateLicencePayload.forOrganization( licenceId: LicenceId('lic-org-${oldLicense.ownerId}-2025'), licenceType: oldLicense.licenceType, orgOwnerId: OrganizationId(oldLicense.ownerId), totalSeatCapacity: oldLicense.totalSeatCapacity, effectiveFrom: oldLicense.effectiveUntil?.add(Duration(days: 1)) ?? DateTime.now(), effectiveUntil: oldLicense.effectiveUntil?.add(Duration(days: 365)), createdBy: UserId('admin-system'), ), ), timelineId: timelineId, workspaceId: workspace,);Pattern 3: Audit Trail for License Changes
Section titled “Pattern 3: Audit Trail for License Changes”Track all license modifications through domain events:
// Listen to license events to build audit logfinal events = /* get events for a license */;
for (final event in events) { if (event is LicenceCreatedEvent) { auditLog.add(''' License created: ${event.licenceId} Type: ${event.licenceType} Owner: ${event.ownerId} Capacity: ${event.totalSeatCapacity} By: ${event.createdBy} At: ${event.createdAt} '''); } else if (event is SeatAllocatedEvent) { auditLog.add(''' Seat allocated: ${event.seatId} To: ${event.userId} Type: ${event.seatType} By: ${event.allocatedBy} At: ${event.allocatedAt} '''); } else if (event is LicenceStatusChangedEvent) { auditLog.add(''' Status changed: ${event.previousStatus} → ${event.newStatus} By: ${event.changedBy} Reason: ${event.reason?.value ?? 'N/A'} At: ${event.changedAt} '''); }}