This commit is contained in:
2025-12-11 11:25:22 +01:00
parent 6a427eab57
commit e4c1be628d
86 changed files with 1222 additions and 736 deletions

View File

@@ -7,7 +7,7 @@
// Use Cases
export * from './use-cases/SendNotificationUseCase';
export * from './use-cases/MarkNotificationReadUseCase';
export * from './use-cases/GetUnreadNotificationsQuery';
export * from './use-cases/GetUnreadNotificationsUseCase';
export * from './use-cases/NotificationPreferencesUseCases';
// Ports

View File

@@ -1,5 +1,5 @@
/**
* Application Query: GetUnreadNotificationsQuery
* Application Use Case: GetUnreadNotificationsUseCase
*
* Retrieves unread notifications for a recipient.
*/
@@ -12,7 +12,7 @@ export interface UnreadNotificationsResult {
totalCount: number;
}
export class GetUnreadNotificationsQuery {
export class GetUnreadNotificationsUseCase {
constructor(
private readonly notificationRepository: INotificationRepository,
) {}
@@ -28,6 +28,6 @@ export class GetUnreadNotificationsQuery {
}
/**
* Additional notification query use cases (e.g., listing or counting notifications)
* Additional notification query/use case types (e.g., listing or counting notifications)
* can be added here in the future as needed.
*/

View File

@@ -5,6 +5,7 @@
*/
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
export interface MarkNotificationReadCommand {
notificationId: string;
@@ -20,11 +21,11 @@ export class MarkNotificationReadUseCase {
const notification = await this.notificationRepository.findById(command.notificationId);
if (!notification) {
throw new Error('Notification not found');
throw new NotificationDomainError('Notification not found');
}
if (notification.recipientId !== command.recipientId) {
throw new Error('Cannot mark another user\'s notification as read');
throw new NotificationDomainError('Cannot mark another user\'s notification as read');
}
if (!notification.isUnread()) {
@@ -70,11 +71,11 @@ export class DismissNotificationUseCase {
const notification = await this.notificationRepository.findById(command.notificationId);
if (!notification) {
throw new Error('Notification not found');
throw new NotificationDomainError('Notification not found');
}
if (notification.recipientId !== command.recipientId) {
throw new Error('Cannot dismiss another user\'s notification');
throw new NotificationDomainError('Cannot dismiss another user\'s notification');
}
if (notification.isDismissed()) {

View File

@@ -82,10 +82,10 @@ export class UpdateQuietHoursUseCase {
async execute(command: UpdateQuietHoursCommand): Promise<void> {
// Validate hours if provided
if (command.startHour !== undefined && (command.startHour < 0 || command.startHour > 23)) {
throw new Error('Start hour must be between 0 and 23');
throw new NotificationDomainError('Start hour must be between 0 and 23');
}
if (command.endHour !== undefined && (command.endHour < 0 || command.endHour > 23)) {
throw new Error('End hour must be between 0 and 23');
throw new NotificationDomainError('End hour must be between 0 and 23');
}
const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId);
@@ -110,7 +110,7 @@ export class SetDigestModeUseCase {
async execute(command: SetDigestModeCommand): Promise<void> {
if (command.frequencyHours !== undefined && command.frequencyHours < 1) {
throw new Error('Digest frequency must be at least 1 hour');
throw new NotificationDomainError('Digest frequency must be at least 1 hour');
}
const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId);

View File

@@ -5,6 +5,8 @@
* Immutable entity with factory methods and domain validation.
*/
import { NotificationDomainError } from '../errors/NotificationDomainError';
import type { NotificationType } from '../value-objects/NotificationType';
import type { NotificationChannel } from '../value-objects/NotificationChannel';
@@ -91,12 +93,12 @@ export class Notification {
createdAt?: Date;
urgency?: NotificationUrgency;
}): 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');
if (!props.id) throw new NotificationDomainError('Notification ID is required');
if (!props.recipientId) throw new NotificationDomainError('Recipient ID is required');
if (!props.type) throw new NotificationDomainError('Notification type is required');
if (!props.title?.trim()) throw new NotificationDomainError('Notification title is required');
if (!props.body?.trim()) throw new NotificationDomainError('Notification body is required');
if (!props.channel) throw new NotificationDomainError('Notification channel is required');
// Modal notifications that require response start with action_required status
const defaultStatus = props.requiresResponse ? 'action_required' : 'unread';
@@ -196,7 +198,7 @@ export class Notification {
}
// Cannot dismiss action_required notifications without responding
if (this.props.requiresResponse && this.props.status === 'action_required') {
throw new Error('Cannot dismiss notification that requires response');
throw new NotificationDomainError('Cannot dismiss notification that requires response');
}
return new Notification({
...this.props,

View File

@@ -6,6 +6,7 @@
import type { NotificationType } from '../value-objects/NotificationType';
import type { NotificationChannel } from '../value-objects/NotificationChannel';
import { NotificationDomainError } from '../errors/NotificationDomainError';
import { DEFAULT_ENABLED_CHANNELS } from '../value-objects/NotificationChannel';
export interface ChannelPreference {
@@ -45,8 +46,8 @@ export class NotificationPreference {
private constructor(private readonly props: NotificationPreferenceProps) {}
static create(props: Omit<NotificationPreferenceProps, 'updatedAt'> & { updatedAt?: Date }): NotificationPreference {
if (!props.driverId) throw new Error('Driver ID is required');
if (!props.channels) throw new Error('Channel preferences are 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,

View File

@@ -0,0 +1,8 @@
export class NotificationDomainError extends Error {
readonly name: string = 'NotificationDomainError';
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}