/** * InvitationToken Entity * * Represents an invitation token for user registration. * * Business Rules: * - Tokens expire after 7 days by default * - Token can only be used once * - Email must be unique per active (non-used) invitation */ import { UserRole } from './user.entity'; export interface InvitationTokenProps { id: string; token: string; // Unique random token (e.g., UUID) email: string; firstName: string; lastName: string; role: UserRole; organizationId: string; invitedById: string; // User ID who created the invitation expiresAt: Date; usedAt?: Date; isUsed: boolean; createdAt: Date; } export class InvitationToken { private readonly props: InvitationTokenProps; private constructor(props: InvitationTokenProps) { this.props = props; } /** * Factory method to create a new InvitationToken */ static create( props: Omit ): InvitationToken { const now = new Date(); // Validate token if (!props.token || props.token.trim().length === 0) { throw new Error('Invitation token cannot be empty.'); } // Validate email format if (!InvitationToken.isValidEmail(props.email)) { throw new Error('Invalid email format.'); } // Validate expiration date if (props.expiresAt <= now) { throw new Error('Expiration date must be in the future.'); } return new InvitationToken({ ...props, isUsed: false, createdAt: now, }); } /** * Factory method to reconstitute from persistence */ static fromPersistence(props: InvitationTokenProps): InvitationToken { return new InvitationToken(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 token(): string { return this.props.token; } get email(): string { return this.props.email; } get firstName(): string { return this.props.firstName; } get lastName(): string { return this.props.lastName; } get role(): UserRole { return this.props.role; } get organizationId(): string { return this.props.organizationId; } get invitedById(): string { return this.props.invitedById; } get expiresAt(): Date { return this.props.expiresAt; } get usedAt(): Date | undefined { return this.props.usedAt; } get isUsed(): boolean { return this.props.isUsed; } get createdAt(): Date { return this.props.createdAt; } // Business methods isExpired(): boolean { return new Date() > this.props.expiresAt; } isValid(): boolean { return !this.props.isUsed && !this.isExpired(); } markAsUsed(): void { if (this.props.isUsed) { throw new Error('Invitation token has already been used.'); } if (this.isExpired()) { throw new Error('Invitation token has expired.'); } this.props.isUsed = true; this.props.usedAt = new Date(); } /** * Convert to plain object for persistence */ toObject(): InvitationTokenProps { return { ...this.props }; } }