Skip to content

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.

The Licensing domain implements a comprehensive license management system where:

  1. Licenses define software entitlements with a specific seat capacity and validity window
  2. License Types categorize licenses (personal, organization, trial) to enable different feature access levels
  3. Seat Allocations map individual users to licenses, tracking who has access and what seat type they hold
  4. License Status controls whether a license is active, suspended, expired, or terminated
  5. Business Rules enforce seat capacity limits and prevent over-allocation
  6. 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

Licenses are categorized by type to support different feature access levels:

License TypePurposeTypical Use Case
personalIndividual user licenseSingle user with full feature access
organizationTeam or enterprise licenseMultiple users in an organization
trialTime-limited evaluation licenseOnboarding and feature trial

License type is a string value determined at creation time and used by consuming services to determine available features.

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.

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 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 access

Seat types are domain-specific strings determined by each consuming service.

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, 2024
final 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)

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
}

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';
}

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 assignments
List<SeatAssignment> get activeSeats
// Count available seats
int get availableSeats => totalSeatCapacity - activeSeats.length;
// Check if license can accept new seats
bool canAllocateSeat() => isActive && availableSeats > 0;
// Check if user has an active seat
bool hasActiveSeat(UserId userId)
// Retrieve seat assignment for a user
SeatAssignment? 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 isNearExpiry

The aggregate enforces critical invariants via validate():

  1. License ID Required: licenceId cannot be unresolved or empty
  2. Owner ID Required: ownerId cannot be unresolved or empty
  3. Positive Capacity: totalSeatCapacity must be > 0
  4. No Overallocation: Cannot have more active seats than totalSeatCapacity
  5. Valid Status: Must be one of the four valid status values
  6. Date Logic: effectiveUntil cannot be before effectiveFrom

These rules are enforced when applying events and before persisting aggregates.

The licensing domain emits four event types capturing all license state changes:

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(),
)

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'),
)

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'),
)

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 are command handlers that validate business rules and emit events. All directives target the LicenceAggregate.

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 userOwnerId or orgOwnerId must be provided based on ownerType
  • totalSeatCapacity must be positive
  • createdBy must be a valid user

Factory Constructors for Convenience:

// Create a user-owned license
CreateLicencePayload.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 license
CreateLicencePayload.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 LicenceCreatedEvent

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 seat
assert(result.updatedAggregate.activeSeats.length == 1);

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);

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);

The aggregate provides specialized query methods:

final license = licenceAggregate;
// Status checks
if (license.isActive) {
// License is currently in use
}
if (license.isCurrentlyValid) {
// License is active AND within validity window
}
// Seat availability
if (license.canAllocateSeat()) {
// Safe to allocate another seat
}
// User seat lookups
if (license.hasActiveSeat(userId)) {
final seat = license.getSeatForUser(userId);
print('${userId} has seat: ${seat?.seatType}');
}
// Capacity metrics
print('${license.activeSeats.length} / ${license.totalSeatCapacity} seats in use');
print('${license.utilizationPercentage}% utilization');

Before allocating a seat, verify capacity:

if (license.canAllocateSeat()) {
// Safe to proceed with AllocateSeatDirective
} else {
throw StateError('No available seats on this license');
}

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 seat
if (license.hasActiveSeat(userId)) {
// User is entitled to use the feature
}
// Warn if expiring soon
if (license.isNearExpiry && license.status == 'active') {
// Show renewal notice to admin
}

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);
}
}
// Usage
final id = LicenceId('lic-org-acme-2024');
final fromString = LicenceId.fromString('lic-org-acme-2024');

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);
}
}
// Usage
final seatId = SeatId('seat-alice-2024');

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

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
// }
// }
// }
// Deserialize
final restored = LicenceAggregate.fromJson(json, AggregateId('lic-org-acme-2024'));
assert(restored.isActive);
assert(restored.activeSeats.length == 1);
import 'package:licensing_v1/licensing_v1.dart';
import 'package:contracts_v1/contracts_v1.dart';
import 'package:nomos_core/nomos_core.dart';
// Initialize the domain
registerLicensingV1();
// Step 1: Create a new organization license
final 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 users
final 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 state
final finalLicense = license; // Would reload from store in real application
assert(finalLicense.activeSeats.length == 3);
assert(finalLicense.availableSeats == 47);
assert(finalLicense.utilizationPercentage == 6.0);
// Determine if user can access a premium feature
bool 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 logic
final license = await loadLicense(licenceId);
if (canAccessPremiumFeature(license, currentUser)) {
// Enable premium UI features
showPremiumDashboard();
} else {
// Prompt user to upgrade
showUpgradePrompt();
}
// Release a specific user's seat
final 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 issue
final 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 license
assert(!suspendResult.updatedAggregate.isCurrentlyValid);

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.

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]
ConceptPurposeExample
LicenceAggregateRoot aggregate managing one licenseLicense for ACME Corp with 50 seats
SeatAssignmentIndividual seat allocation to a userAlice has an ‘editor’ seat
LicenceIdStrongly-typed license identifierlic-org-acme-2024
SeatIdStrongly-typed seat identifierseat-alice-2024
License TypeFeature tier (personal, organization, trial)organization
Seat TypeUser role within licensed featureeditor, admin
License StatusLifecycle stateactive, suspended, expired, terminated
EventImmutable record of state changeLicenceCreatedEvent, SeatAllocatedEvent
DirectiveCommand handler enforcing rulesCreateLicenceDirective

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 license
final 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 seats
for (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,
);
}

Track when licenses need renewal and handle expiration:

// Query licenses approaching expiration
final expiringLicenses = allLicenses.where((lic) {
return lic.isActive && lic.isNearExpiry;
}).toList();
// Send renewal reminders
for (final license in expiringLicenses) {
sendRenewalReminder(license.ownerId, license.licenceId);
}
// On renewal date: create new license with same owner
final 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 log
final 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}
''');
}
}