209 lines
6.6 KiB
TypeScript
209 lines
6.6 KiB
TypeScript
/**
|
|
* 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<string, string>;
|
|
}
|
|
|
|
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<NotificationChannel, ChannelPreference>;
|
|
/** Per-type preferences (optional overrides) */
|
|
typePreferences?: Partial<Record<NotificationType, TypePreference>>;
|
|
/** 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<string> {
|
|
private constructor(private readonly props: NotificationPreferenceProps) {
|
|
super(props.id);}
|
|
|
|
static create(
|
|
props: Omit<NotificationPreferenceProps, 'updatedAt'> & { 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<NotificationChannel, ChannelPreference> { return { ...this.props.channels }; }
|
|
get typePreferences(): Partial<Record<NotificationType, TypePreference>> | 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 };
|
|
}
|
|
} |