xpeditis2.0/apps/backend/src/domain/entities/invitation-token.entity.ts
2025-11-30 13:39:32 +01:00

159 lines
3.2 KiB
TypeScript

/**
* 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<InvitationTokenProps, 'createdAt' | 'isUsed' | 'usedAt'>
): 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 };
}
}