/** * Domain Entity: UserAchievement * * Represents an achievement earned by a specific user. */ import type { IEntity } from '@core/shared/domain'; export interface UserAchievementProps { id: string; userId: string; achievementId: string; earnedAt: Date; notifiedAt?: Date; progress?: number; // For partial progress tracking (0-100) } export class UserAchievement implements IEntity { readonly id: string; readonly userId: string; readonly achievementId: string; readonly earnedAt: Date; readonly notifiedAt: Date | undefined; readonly progress: number; private constructor(props: UserAchievementProps) { this.id = props.id; this.userId = props.userId; this.achievementId = props.achievementId; this.earnedAt = props.earnedAt; this.notifiedAt = props.notifiedAt; this.progress = props.progress ?? 100; } static create(props: UserAchievementProps): UserAchievement { if (!props.id || props.id.trim().length === 0) { throw new Error('UserAchievement ID is required'); } if (!props.userId || props.userId.trim().length === 0) { throw new Error('UserAchievement userId is required'); } if (!props.achievementId || props.achievementId.trim().length === 0) { throw new Error('UserAchievement achievementId is required'); } return new UserAchievement(props); } /** * Mark achievement as notified to user */ markNotified(): UserAchievement { return new UserAchievement({ id: this.id, userId: this.userId, achievementId: this.achievementId, earnedAt: this.earnedAt, notifiedAt: new Date(), progress: this.progress, }); } /** * Update progress towards achievement */ updateProgress(progress: number): UserAchievement { const clampedProgress = Math.max(0, Math.min(100, progress)); return new UserAchievement({ id: this.id, userId: this.userId, achievementId: this.achievementId, earnedAt: this.earnedAt, ...(this.notifiedAt ? { notifiedAt: this.notifiedAt } : {}), progress: clampedProgress, }); } /** * Check if achievement is fully earned */ isComplete(): boolean { return this.progress >= 100; } /** * Check if user has been notified */ isNotified(): boolean { return this.notifiedAt !== undefined; } }