/** * User Entity * * Represents a user account in the Xpeditis platform. * * Business Rules: * - Email must be valid and unique * - Password must meet complexity requirements (enforced at application layer) * - Users belong to an organization * - Role-based access control (Admin, Manager, User, Viewer) */ export enum UserRole { ADMIN = 'admin', // Full system access MANAGER = 'manager', // Manage bookings and users within organization USER = 'user', // Create and view bookings VIEWER = 'viewer', // Read-only access } export interface UserProps { id: string; organizationId: string; email: string; passwordHash: string; role: UserRole; firstName: string; lastName: string; phoneNumber?: string; totpSecret?: string; // For 2FA isEmailVerified: boolean; isActive: boolean; lastLoginAt?: Date; createdAt: Date; updatedAt: Date; } export class User { private readonly props: UserProps; private constructor(props: UserProps) { this.props = props; } /** * Factory method to create a new User */ static create( props: Omit< UserProps, 'createdAt' | 'updatedAt' | 'isEmailVerified' | 'isActive' | 'lastLoginAt' > ): User { const now = new Date(); // Validate email format (basic validation) if (!User.isValidEmail(props.email)) { throw new Error('Invalid email format.'); } return new User({ ...props, isEmailVerified: false, isActive: true, createdAt: now, updatedAt: now, }); } /** * Factory method to reconstitute from persistence */ static fromPersistence(props: UserProps): User { return new User(props); } /** * Validate email format */ private static isValidEmail(email: string): boolean { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailPattern.test(email); } // Getters get id(): string { return this.props.id; } get organizationId(): string { return this.props.organizationId; } get email(): string { return this.props.email; } get passwordHash(): string { return this.props.passwordHash; } get role(): UserRole { return this.props.role; } get firstName(): string { return this.props.firstName; } get lastName(): string { return this.props.lastName; } get fullName(): string { return `${this.props.firstName} ${this.props.lastName}`; } get phoneNumber(): string | undefined { return this.props.phoneNumber; } get totpSecret(): string | undefined { return this.props.totpSecret; } get isEmailVerified(): boolean { return this.props.isEmailVerified; } get isActive(): boolean { return this.props.isActive; } get lastLoginAt(): Date | undefined { return this.props.lastLoginAt; } get createdAt(): Date { return this.props.createdAt; } get updatedAt(): Date { return this.props.updatedAt; } // Business methods has2FAEnabled(): boolean { return !!this.props.totpSecret; } isAdmin(): boolean { return this.props.role === UserRole.ADMIN; } isManager(): boolean { return this.props.role === UserRole.MANAGER; } isRegularUser(): boolean { return this.props.role === UserRole.USER; } isViewer(): boolean { return this.props.role === UserRole.VIEWER; } canManageUsers(): boolean { return this.props.role === UserRole.ADMIN || this.props.role === UserRole.MANAGER; } canCreateBookings(): boolean { return ( this.props.role === UserRole.ADMIN || this.props.role === UserRole.MANAGER || this.props.role === UserRole.USER ); } updatePassword(newPasswordHash: string): void { this.props.passwordHash = newPasswordHash; this.props.updatedAt = new Date(); } updateRole(newRole: UserRole): void { this.props.role = newRole; this.props.updatedAt = new Date(); } updateFirstName(firstName: string): void { if (!firstName || firstName.trim().length === 0) { throw new Error('First name cannot be empty.'); } this.props.firstName = firstName.trim(); this.props.updatedAt = new Date(); } updateLastName(lastName: string): void { if (!lastName || lastName.trim().length === 0) { throw new Error('Last name cannot be empty.'); } this.props.lastName = lastName.trim(); this.props.updatedAt = new Date(); } updateProfile(firstName: string, lastName: string, phoneNumber?: string): void { if (!firstName || firstName.trim().length === 0) { throw new Error('First name cannot be empty.'); } if (!lastName || lastName.trim().length === 0) { throw new Error('Last name cannot be empty.'); } this.props.firstName = firstName.trim(); this.props.lastName = lastName.trim(); this.props.phoneNumber = phoneNumber; this.props.updatedAt = new Date(); } verifyEmail(): void { this.props.isEmailVerified = true; this.props.updatedAt = new Date(); } enable2FA(totpSecret: string): void { this.props.totpSecret = totpSecret; this.props.updatedAt = new Date(); } disable2FA(): void { this.props.totpSecret = undefined; this.props.updatedAt = new Date(); } recordLogin(): void { this.props.lastLoginAt = new Date(); } deactivate(): void { this.props.isActive = false; this.props.updatedAt = new Date(); } activate(): void { this.props.isActive = true; this.props.updatedAt = new Date(); } /** * Convert to plain object for persistence */ toObject(): UserProps { return { ...this.props }; } }