Files
gridpilot.gg/packages/notifications/domain/entities/Notification.ts
2025-12-09 22:22:06 +01:00

135 lines
4.0 KiB
TypeScript

/**
* Domain Entity: Notification
*
* Represents a notification sent to a user.
* Immutable entity with factory methods and domain validation.
*/
import type { NotificationType } from '../value-objects/NotificationType';
import type { NotificationChannel } from '../value-objects/NotificationChannel';
export type NotificationStatus = 'unread' | 'read' | 'dismissed';
export interface NotificationData {
/** Reference to related protest */
protestId?: string;
/** Reference to related race */
raceId?: string;
/** Reference to related league */
leagueId?: string;
/** Reference to related driver (e.g., who filed protest) */
actorDriverId?: string;
/** Reference to related penalty */
penaltyId?: string;
/** Reference to related team */
teamId?: string;
/** Deadline for action (e.g., defense deadline) */
deadline?: Date;
/** Any additional context data */
[key: string]: unknown;
}
export interface NotificationProps {
id: string;
/** Driver who receives this notification */
recipientId: string;
/** Type of notification */
type: NotificationType;
/** Human-readable title */
title: string;
/** Notification body/message */
body: string;
/** Channel this notification was/will be sent through */
channel: NotificationChannel;
/** Current status */
status: NotificationStatus;
/** Structured data for linking/context */
data?: NotificationData;
/** Optional action URL */
actionUrl?: string;
/** When the notification was created */
createdAt: Date;
/** When the notification was read (if applicable) */
readAt?: Date;
}
export class Notification {
private constructor(private readonly props: NotificationProps) {}
static create(props: Omit<NotificationProps, 'status' | 'createdAt'> & {
status?: NotificationStatus;
createdAt?: Date;
}): Notification {
if (!props.id) throw new Error('Notification ID is required');
if (!props.recipientId) throw new Error('Recipient ID is required');
if (!props.type) throw new Error('Notification type is required');
if (!props.title?.trim()) throw new Error('Notification title is required');
if (!props.body?.trim()) throw new Error('Notification body is required');
if (!props.channel) throw new Error('Notification channel is required');
return new Notification({
...props,
status: props.status ?? 'unread',
createdAt: props.createdAt ?? new Date(),
});
}
get id(): string { return this.props.id; }
get recipientId(): string { return this.props.recipientId; }
get type(): NotificationType { return this.props.type; }
get title(): string { return this.props.title; }
get body(): string { return this.props.body; }
get channel(): NotificationChannel { return this.props.channel; }
get status(): NotificationStatus { return this.props.status; }
get data(): NotificationData | undefined { return this.props.data ? { ...this.props.data } : undefined; }
get actionUrl(): string | undefined { return this.props.actionUrl; }
get createdAt(): Date { return this.props.createdAt; }
get readAt(): Date | undefined { return this.props.readAt; }
isUnread(): boolean {
return this.props.status === 'unread';
}
isRead(): boolean {
return this.props.status === 'read';
}
isDismissed(): boolean {
return this.props.status === 'dismissed';
}
/**
* Mark the notification as read
*/
markAsRead(): Notification {
if (this.props.status !== 'unread') {
return this; // Already read or dismissed, no change
}
return new Notification({
...this.props,
status: 'read',
readAt: new Date(),
});
}
/**
* Dismiss the notification
*/
dismiss(): Notification {
if (this.props.status === 'dismissed') {
return this; // Already dismissed
}
return new Notification({
...this.props,
status: 'dismissed',
readAt: this.props.readAt ?? new Date(),
});
}
/**
* Convert to plain object for serialization
*/
toJSON(): NotificationProps {
return { ...this.props };
}
}