/** * Domain Entity: NotificationPreference * * Represents a user's notification preferences for different channels and types. */ import { Entity } from '@core/shared/domain/Entity'; import { NotificationDomainError } from '../errors/NotificationDomainError'; import type { NotificationChannel, NotificationType } from '../types/NotificationTypes'; import { QuietHours } from '../value-objects/QuietHours'; export interface ChannelPreference { /** Whether this channel is enabled */ enabled: boolean; /** Channel-specific settings (e.g., discord webhook URL, email address) */ settings?: Record; } export interface TypePreference { /** Whether notifications of this type are enabled */ enabled: boolean; /** Which channels to use for this type (overrides global) */ channels?: NotificationChannel[]; } export interface NotificationPreferenceProps { /** Aggregate ID for this preference (usually same as driverId) */ id: string; /** Driver ID this preference belongs to */ driverId: string; /** Global channel preferences */ channels: Record; /** Per-type preferences (optional overrides) */ typePreferences?: Partial>; /** Whether to receive digest emails instead of individual notifications */ digestMode?: boolean; /** Digest frequency in hours (e.g., 24 for daily) */ digestFrequencyHours?: number; /** Quiet hours start (0-23) */ quietHoursStart?: number; /** Quiet hours end (0-23) */ quietHoursEnd?: number; /** Last updated timestamp */ updatedAt: Date; } export class NotificationPreference extends Entity { private constructor(private readonly props: NotificationPreferenceProps) { super(props.id);} static create( props: Omit & { updatedAt?: Date }, ): NotificationPreference { if (!props.id) throw new NotificationDomainError('Preference ID is required'); if (!props.driverId) throw new NotificationDomainError('Driver ID is required'); if (!props.channels) throw new NotificationDomainError('Channel preferences are required'); return new NotificationPreference({ ...props, updatedAt: props.updatedAt ?? new Date(), }); } /** * Create default preferences for a new user */ static createDefault(driverId: string): NotificationPreference { return new NotificationPreference({ id: driverId, driverId, channels: { in_app: { enabled: true }, email: { enabled: false }, discord: { enabled: false }, push: { enabled: false }, }, digestMode: false, updatedAt: new Date(), }); } get driverId(): string { return this.props.driverId; } get channels(): Record { return { ...this.props.channels }; } get typePreferences(): Partial> | undefined { return this.props.typePreferences ? { ...this.props.typePreferences } : undefined; } get digestMode(): boolean { return this.props.digestMode ?? false; } get digestFrequencyHours(): number { return this.props.digestFrequencyHours ?? 24; } get quietHoursStart(): number | undefined { return this.props.quietHoursStart; } get quietHoursEnd(): number | undefined { return this.props.quietHoursEnd; } get updatedAt(): Date { return this.props.updatedAt; } get quietHours(): QuietHours | undefined { if (this.props.quietHoursStart === undefined || this.props.quietHoursEnd === undefined) { return undefined; } return QuietHours.create(this.props.quietHoursStart, this.props.quietHoursEnd); } /** * Check if a specific channel is enabled */ isChannelEnabled(channel: NotificationChannel): boolean { return this.props.channels[channel]?.enabled ?? false; } /** * Check if a specific notification type is enabled */ isTypeEnabled(type: NotificationType): boolean { const typePref = this.props.typePreferences?.[type]; return typePref?.enabled ?? true; // Default to enabled if not specified } /** * Get enabled channels for a specific notification type */ getEnabledChannelsForType(type: NotificationType): NotificationChannel[] { // Check type-specific channel overrides const typePref = this.props.typePreferences?.[type]; if (typePref?.channels && typePref.channels.length > 0) { return typePref.channels.filter(ch => this.isChannelEnabled(ch)); } // Fall back to globally enabled channels return (Object.keys(this.props.channels) as NotificationChannel[]) .filter(ch => this.isChannelEnabled(ch)); } /** * Check if current time is in quiet hours */ isInQuietHours(): boolean { const quietHours = this.quietHours; if (!quietHours) { return false; } const now = new Date(); return quietHours.containsHour(now.getHours()); } /** * Update channel preference */ updateChannel(channel: NotificationChannel, preference: ChannelPreference): NotificationPreference { return new NotificationPreference({ ...this.props, channels: { ...this.props.channels, [channel]: preference, }, updatedAt: new Date(), }); } /** * Update type preference */ updateTypePreference(type: NotificationType, preference: TypePreference): NotificationPreference { return new NotificationPreference({ ...this.props, typePreferences: { ...this.props.typePreferences, [type]: preference, }, updatedAt: new Date(), }); } /** * Update quiet hours */ updateQuietHours(start: number | undefined, end: number | undefined): NotificationPreference { const props = this.toJSON(); if (start === undefined || end === undefined) { delete props.quietHoursStart; delete props.quietHoursEnd; } else { const validated = QuietHours.create(start, end); props.quietHoursStart = validated.props.startHour; props.quietHoursEnd = validated.props.endHour; } props.updatedAt = new Date(); return NotificationPreference.create(props); } /** * Toggle digest mode */ setDigestMode(enabled: boolean, frequencyHours?: number): NotificationPreference { const props = this.toJSON(); props.digestMode = enabled; if (frequencyHours !== undefined) { props.digestFrequencyHours = frequencyHours; } props.updatedAt = new Date(); return NotificationPreference.create(props); } /** * Convert to plain object for serialization */ toJSON(): NotificationPreferenceProps { return { ...this.props }; } }