import type { IEntity } from '@core/shared/domain'; import { UserId } from '../value-objects/UserId'; import { Email } from '../value-objects/Email'; import { UserRole } from '../value-objects/UserRole'; import { UserStatus } from '../value-objects/UserStatus'; import { AdminDomainValidationError, AdminDomainInvariantError } from '../errors/AdminDomainError'; export interface AdminUserProps { id: UserId; email: Email; roles: UserRole[]; status: UserStatus; displayName: string; createdAt: Date; updatedAt: Date; lastLoginAt: Date | undefined; primaryDriverId: string | undefined; } export class AdminUser implements IEntity { readonly id: UserId; private _email: Email; private _roles: UserRole[]; private _status: UserStatus; private _displayName: string; private _createdAt: Date; private _updatedAt: Date; private _lastLoginAt: Date | undefined; private _primaryDriverId: string | undefined; private constructor(props: AdminUserProps) { this.id = props.id; this._email = props.email; this._roles = props.roles; this._status = props.status; this._displayName = props.displayName; this._createdAt = props.createdAt; this._updatedAt = props.updatedAt; this._lastLoginAt = props.lastLoginAt; this._primaryDriverId = props.primaryDriverId; } /** * Factory method to create a new AdminUser * Validates all business rules and invariants */ static create(props: { id: string; email: string; roles: string[]; status: string; displayName: string; createdAt?: Date; updatedAt?: Date; lastLoginAt?: Date; primaryDriverId?: string; }): AdminUser { // Validate required fields if (!props.id || props.id.trim().length === 0) { throw new AdminDomainValidationError('User ID is required'); } if (!props.email || props.email.trim().length === 0) { throw new AdminDomainValidationError('Email is required'); } if (!props.roles || props.roles.length === 0) { throw new AdminDomainValidationError('At least one role is required'); } if (!props.status || props.status.trim().length === 0) { throw new AdminDomainValidationError('Status is required'); } if (!props.displayName || props.displayName.trim().length === 0) { throw new AdminDomainValidationError('Display name is required'); } // Validate display name length const trimmedName = props.displayName.trim(); if (trimmedName.length < 2 || trimmedName.length > 100) { throw new AdminDomainValidationError('Display name must be between 2 and 100 characters'); } // Create value objects const id = UserId.fromString(props.id); const email = Email.fromString(props.email); const roles = props.roles.map(role => UserRole.fromString(role)); const status = UserStatus.fromString(props.status); // Validate role hierarchy - ensure no duplicate roles const uniqueRoles = new Set(roles.map(r => r.toString())); if (uniqueRoles.size !== roles.length) { throw new AdminDomainValidationError('Duplicate roles are not allowed'); } const now = props.createdAt ?? new Date(); return new AdminUser({ id, email, roles, status, displayName: trimmedName, createdAt: now, updatedAt: props.updatedAt ?? now, lastLoginAt: props.lastLoginAt ?? undefined, primaryDriverId: props.primaryDriverId ?? undefined, }); } /** * Rehydrate from storage */ static rehydrate(props: { id: string; email: string; roles: string[]; status: string; displayName: string; createdAt: Date; updatedAt: Date; lastLoginAt?: Date; primaryDriverId?: string; }): AdminUser { return this.create(props); } // Getters get email(): Email { return this._email; } get roles(): UserRole[] { return [...this._roles]; } get status(): UserStatus { return this._status; } get displayName(): string { return this._displayName; } get createdAt(): Date { return new Date(this._createdAt.getTime()); } get updatedAt(): Date { return new Date(this._updatedAt.getTime()); } get lastLoginAt(): Date | undefined { return this._lastLoginAt ? new Date(this._lastLoginAt.getTime()) : undefined; } get primaryDriverId(): string | undefined { return this._primaryDriverId; } // Domain methods /** * Add a role to the user * Cannot add duplicate roles * Cannot add owner role if user already has other roles */ addRole(role: UserRole): void { if (this._roles.some(r => r.equals(role))) { throw new AdminDomainInvariantError(`Role ${role.value} is already assigned`); } // If adding owner role, user must have no other roles if (role.value === 'owner' && this._roles.length > 0) { throw new AdminDomainInvariantError('Cannot add owner role to user with existing roles'); } // If user has owner role, cannot add other roles if (this._roles.some(r => r.value === 'owner')) { throw new AdminDomainInvariantError('Owner cannot have additional roles'); } this._roles.push(role); this._updatedAt = new Date(); } /** * Remove a role from the user * Cannot remove the last role * Cannot remove owner role (must be transferred first) */ removeRole(role: UserRole): void { const roleIndex = this._roles.findIndex(r => r.equals(role)); if (roleIndex === -1) { throw new AdminDomainInvariantError(`Role ${role.value} not found`); } if (this._roles.length === 1) { throw new AdminDomainInvariantError('Cannot remove the last role from user'); } if (role.value === 'owner') { throw new AdminDomainInvariantError('Cannot remove owner role. Transfer ownership first.'); } this._roles.splice(roleIndex, 1); this._updatedAt = new Date(); } /** * Update user status */ updateStatus(newStatus: UserStatus): void { if (this._status.equals(newStatus)) { throw new AdminDomainInvariantError(`User already has status ${newStatus.value}`); } this._status = newStatus; this._updatedAt = new Date(); } /** * Check if user has a specific role */ hasRole(roleValue: string): boolean { return this._roles.some(r => r.value === roleValue); } /** * Check if user is a system administrator */ isSystemAdmin(): boolean { return this._roles.some(r => r.isSystemAdmin()); } /** * Check if user has higher authority than another user */ hasHigherAuthorityThan(other: AdminUser): boolean { // Get highest role for each user const hierarchy: Record = { user: 0, admin: 1, owner: 2, }; const myHighest = Math.max(...this._roles.map(r => hierarchy[r.value] ?? 0)); const otherHighest = Math.max(...other._roles.map(r => hierarchy[r.value] ?? 0)); return myHighest > otherHighest; } /** * Update last login timestamp */ recordLogin(): void { this._lastLoginAt = new Date(); this._updatedAt = new Date(); } /** * Update display name (only for admin operations) */ updateDisplayName(newName: string): void { const trimmed = newName.trim(); if (trimmed.length < 2 || trimmed.length > 100) { throw new AdminDomainValidationError('Display name must be between 2 and 100 characters'); } this._displayName = trimmed; this._updatedAt = new Date(); } /** * Update email */ updateEmail(newEmail: Email): void { if (this._email.equals(newEmail)) { throw new AdminDomainInvariantError('Email is already the same'); } this._email = newEmail; this._updatedAt = new Date(); } /** * Check if user is active */ isActive(): boolean { return this._status.isActive(); } /** * Suspend user */ suspend(): void { if (this._status.isSuspended()) { throw new AdminDomainInvariantError('User is already suspended'); } if (this._status.isDeleted()) { throw new AdminDomainInvariantError('Cannot suspend a deleted user'); } this._status = UserStatus.create('suspended'); this._updatedAt = new Date(); } /** * Activate user */ activate(): void { if (this._status.isActive()) { throw new AdminDomainInvariantError('User is already active'); } if (this._status.isDeleted()) { throw new AdminDomainInvariantError('Cannot activate a deleted user'); } this._status = UserStatus.create('active'); this._updatedAt = new Date(); } /** * Soft delete user */ delete(): void { if (this._status.isDeleted()) { throw new AdminDomainInvariantError('User is already deleted'); } this._status = UserStatus.create('deleted'); this._updatedAt = new Date(); } /** * Get role display names */ getRoleDisplayNames(): string[] { return this._roles.map(r => { switch (r.value) { case 'owner': return 'Owner'; case 'admin': return 'Admin'; case 'user': return 'User'; default: return r.value; } }); } /** * Check if this user can manage another user * Owner can manage everyone (including self) * Admin can manage users but not admins/owners (including self) * User can manage self only */ canManage(target: AdminUser): boolean { // Owner can manage everyone if (this.hasRole('owner')) { return true; } // Admin can manage non-admin users if (this.hasRole('admin')) { // Cannot manage admins/owners (including self) if (target.isSystemAdmin()) { return false; } // Can manage non-admin users return true; } // User can only manage self return this.id.equals(target.id); } /** * Check if this user can modify roles of target user * Only owner can modify roles */ canModifyRoles(target: AdminUser): boolean { // Only owner can modify roles if (!this.hasRole('owner')) { return false; } // Cannot modify own roles (prevents accidental lockout) if (this.id.equals(target.id)) { return false; } return true; } /** * Check if this user can change status of target user * Owner can change anyone's status * Admin can change user status but not other admins/owners */ canChangeStatus(target: AdminUser): boolean { if (this.id.equals(target.id)) { return false; // Cannot change own status } if (this.hasRole('owner')) { return true; } if (this.hasRole('admin')) { return !target.isSystemAdmin(); } return false; } /** * Check if this user can delete target user * Owner can delete anyone except self * Admin can delete users but not admins/owners */ canDelete(target: AdminUser): boolean { if (this.id.equals(target.id)) { return false; // Cannot delete self } if (this.hasRole('owner')) { return true; } if (this.hasRole('admin')) { return !target.isSystemAdmin(); } return false; } /** * Get summary for display */ toSummary(): { id: string; email: string; displayName: string; roles: string[]; status: string; isSystemAdmin: boolean; lastLoginAt?: Date; } { const summary: { id: string; email: string; displayName: string; roles: string[]; status: string; isSystemAdmin: boolean; lastLoginAt?: Date; } = { id: this.id.value, email: this._email.value, displayName: this._displayName, roles: this._roles.map(r => r.value), status: this._status.value, isSystemAdmin: this.isSystemAdmin(), }; if (this._lastLoginAt) { summary.lastLoginAt = this._lastLoginAt; } return summary; } /** * Equals comparison */ equals(other?: AdminUser): boolean { if (!other) { return false; } return this.id.equals(other.id); } }