refactor use cases
This commit is contained in:
@@ -2,10 +2,9 @@ import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import {
|
||||
GetUnreadNotificationsUseCase,
|
||||
type GetUnreadNotificationsInput,
|
||||
type GetUnreadNotificationsResult,
|
||||
} from './GetUnreadNotificationsUseCase';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Notification } from '../../domain/entities/Notification';
|
||||
@@ -14,14 +13,9 @@ interface NotificationRepositoryMock {
|
||||
findUnreadByRecipientId: Mock;
|
||||
}
|
||||
|
||||
interface OutputPortMock extends UseCaseOutputPort<GetUnreadNotificationsResult> {
|
||||
present: Mock;
|
||||
}
|
||||
|
||||
describe('GetUnreadNotificationsUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let logger: Logger;
|
||||
let output: OutputPortMock;
|
||||
let useCase: GetUnreadNotificationsUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -36,13 +30,8 @@ describe('GetUnreadNotificationsUseCase', () => {
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
output = {
|
||||
present: vi.fn(),
|
||||
} as unknown as OutputPortMock;
|
||||
|
||||
useCase = new GetUnreadNotificationsUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
@@ -69,10 +58,10 @@ describe('GetUnreadNotificationsUseCase', () => {
|
||||
expect(notificationRepository.findUnreadByRecipientId).toHaveBeenCalledWith(recipientId);
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
notifications,
|
||||
totalCount: 1,
|
||||
});
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.notifications).toEqual(notifications);
|
||||
expect(successResult.totalCount).toBe(1);
|
||||
});
|
||||
|
||||
it('handles repository errors by logging and returning error result', async () => {
|
||||
@@ -89,6 +78,5 @@ describe('GetUnreadNotificationsUseCase', () => {
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
expect(err.details.message).toBe('DB error');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Retrieves unread notifications for a recipient.
|
||||
*/
|
||||
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Notification } from '../../domain/entities/Notification';
|
||||
@@ -24,13 +24,12 @@ export type GetUnreadNotificationsErrorCode = 'REPOSITORY_ERROR';
|
||||
export class GetUnreadNotificationsUseCase {
|
||||
constructor(
|
||||
private readonly notificationRepository: INotificationRepository,
|
||||
private readonly output: UseCaseOutputPort<GetUnreadNotificationsResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetUnreadNotificationsInput,
|
||||
): Promise<Result<void, ApplicationErrorCode<GetUnreadNotificationsErrorCode, { message: string }>>> {
|
||||
): Promise<Result<GetUnreadNotificationsResult, ApplicationErrorCode<GetUnreadNotificationsErrorCode, { message: string }>>> {
|
||||
const { recipientId } = input;
|
||||
this.logger.debug(
|
||||
`Attempting to retrieve unread notifications for recipient ID: ${recipientId}`,
|
||||
@@ -48,14 +47,10 @@ export class GetUnreadNotificationsUseCase {
|
||||
this.logger.warn(`No unread notifications found for recipient ID: ${recipientId}`);
|
||||
}
|
||||
|
||||
this.output.present({
|
||||
return Result.ok<GetUnreadNotificationsResult, ApplicationErrorCode<GetUnreadNotificationsErrorCode, { message: string }>>({
|
||||
notifications,
|
||||
totalCount: notifications.length,
|
||||
});
|
||||
|
||||
return Result.ok<void, ApplicationErrorCode<GetUnreadNotificationsErrorCode, { message: string }>>(
|
||||
undefined,
|
||||
);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(
|
||||
@@ -63,7 +58,7 @@ export class GetUnreadNotificationsUseCase {
|
||||
err,
|
||||
);
|
||||
|
||||
return Result.err<void, ApplicationErrorCode<GetUnreadNotificationsErrorCode, { message: string }>>({
|
||||
return Result.err<GetUnreadNotificationsResult, ApplicationErrorCode<GetUnreadNotificationsErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -74,4 +69,4 @@ export class GetUnreadNotificationsUseCase {
|
||||
/**
|
||||
* Additional notification query/use case types (e.g., listing or counting notifications)
|
||||
* can be added here in the future as needed.
|
||||
*/
|
||||
*/
|
||||
|
||||
@@ -2,10 +2,13 @@ import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import {
|
||||
MarkNotificationReadUseCase,
|
||||
type MarkNotificationReadCommand,
|
||||
type MarkNotificationReadResult,
|
||||
MarkAllNotificationsReadUseCase,
|
||||
type MarkAllNotificationsReadInput,
|
||||
DismissNotificationUseCase,
|
||||
type DismissNotificationCommand,
|
||||
} from './MarkNotificationReadUseCase';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Notification } from '../../domain/entities/Notification';
|
||||
@@ -16,14 +19,9 @@ interface NotificationRepositoryMock {
|
||||
markAllAsReadByRecipientId: Mock;
|
||||
}
|
||||
|
||||
interface OutputPortMock extends UseCaseOutputPort<MarkNotificationReadResult> {
|
||||
present: Mock;
|
||||
}
|
||||
|
||||
describe('MarkNotificationReadUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let logger: Logger;
|
||||
let output: OutputPortMock;
|
||||
let useCase: MarkNotificationReadUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -40,13 +38,8 @@ describe('MarkNotificationReadUseCase', () => {
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
output = {
|
||||
present: vi.fn(),
|
||||
} as unknown as OutputPortMock;
|
||||
|
||||
useCase = new MarkNotificationReadUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
@@ -65,7 +58,6 @@ describe('MarkNotificationReadUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'NOTIFICATION_NOT_FOUND', { message: string }>;
|
||||
expect(err.code).toBe('NOTIFICATION_NOT_FOUND');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns RECIPIENT_MISMATCH when recipientId does not match', async () => {
|
||||
@@ -90,10 +82,9 @@ describe('MarkNotificationReadUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'RECIPIENT_MISMATCH', { message: string }>;
|
||||
expect(err.code).toBe('RECIPIENT_MISMATCH');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('marks notification as read when unread and presents result', async () => {
|
||||
it('marks notification as read when unread and returns success result', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
@@ -114,10 +105,220 @@ describe('MarkNotificationReadUseCase', () => {
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(notificationRepository.update).toHaveBeenCalled();
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.notificationId).toBe('n1');
|
||||
expect(successResult.recipientId).toBe('driver-1');
|
||||
expect(successResult.wasAlreadyRead).toBe(false);
|
||||
});
|
||||
|
||||
it('returns already read when notification is already read', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
// Mark as read
|
||||
const readNotification = notification.markAsRead();
|
||||
notificationRepository.findById.mockResolvedValue(readNotification);
|
||||
|
||||
const command: MarkNotificationReadCommand = {
|
||||
notificationId: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
wasAlreadyRead: false,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(notificationRepository.update).not.toHaveBeenCalled();
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.wasAlreadyRead).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkAllNotificationsReadUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let useCase: MarkAllNotificationsReadUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationRepository = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
markAllAsReadByRecipientId: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new MarkAllNotificationsReadUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('marks all notifications as read', async () => {
|
||||
const input: MarkAllNotificationsReadInput = {
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(notificationRepository.markAllAsReadByRecipientId).toHaveBeenCalledWith('driver-1');
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.recipientId).toBe('driver-1');
|
||||
});
|
||||
|
||||
it('handles repository errors', async () => {
|
||||
notificationRepository.markAllAsReadByRecipientId.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
const input: MarkAllNotificationsReadInput = {
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>;
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DismissNotificationUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let useCase: DismissNotificationUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationRepository = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
markAllAsReadByRecipientId: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new DismissNotificationUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns NOTIFICATION_NOT_FOUND when notification is not found', async () => {
|
||||
notificationRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const command: DismissNotificationCommand = {
|
||||
notificationId: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'NOTIFICATION_NOT_FOUND', { message: string }>;
|
||||
expect(err.code).toBe('NOTIFICATION_NOT_FOUND');
|
||||
});
|
||||
|
||||
it('returns RECIPIENT_MISMATCH when recipientId does not match', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-2',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
notificationRepository.findById.mockResolvedValue(notification);
|
||||
|
||||
const command: DismissNotificationCommand = {
|
||||
notificationId: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'RECIPIENT_MISMATCH', { message: string }>;
|
||||
expect(err.code).toBe('RECIPIENT_MISMATCH');
|
||||
});
|
||||
|
||||
it('dismisses notification and returns success result', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
notificationRepository.findById.mockResolvedValue(notification);
|
||||
|
||||
const command: DismissNotificationCommand = {
|
||||
notificationId: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(notificationRepository.update).toHaveBeenCalled();
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.notificationId).toBe('n1');
|
||||
expect(successResult.recipientId).toBe('driver-1');
|
||||
expect(successResult.wasAlreadyDismissed).toBe(false);
|
||||
});
|
||||
|
||||
it('returns already dismissed when notification is already dismissed', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
// Dismiss it
|
||||
const dismissedNotification = notification.dismiss();
|
||||
notificationRepository.findById.mockResolvedValue(dismissedNotification);
|
||||
|
||||
const command: DismissNotificationCommand = {
|
||||
notificationId: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(notificationRepository.update).not.toHaveBeenCalled();
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.wasAlreadyDismissed).toBe(true);
|
||||
});
|
||||
|
||||
it('returns CANNOT_DISMISS_REQUIRING_RESPONSE when notification requires response', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'system_announcement',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
requiresResponse: true,
|
||||
});
|
||||
|
||||
notificationRepository.findById.mockResolvedValue(notification);
|
||||
|
||||
const command: DismissNotificationCommand = {
|
||||
notificationId: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
};
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr() as ApplicationErrorCode<'CANNOT_DISMISS_REQUIRING_RESPONSE', { message: string }>;
|
||||
expect(err.code).toBe('CANNOT_DISMISS_REQUIRING_RESPONSE');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
* Marks a notification as read.
|
||||
*/
|
||||
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
// import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||
|
||||
export interface MarkNotificationReadCommand {
|
||||
notificationId: string;
|
||||
@@ -29,13 +28,12 @@ export type MarkNotificationReadErrorCode =
|
||||
export class MarkNotificationReadUseCase {
|
||||
constructor(
|
||||
private readonly notificationRepository: INotificationRepository,
|
||||
private readonly output: UseCaseOutputPort<MarkNotificationReadResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: MarkNotificationReadCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>> {
|
||||
): Promise<Result<MarkNotificationReadResult, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>> {
|
||||
this.logger.debug(
|
||||
`Attempting to mark notification ${command.notificationId} as read for recipient ${command.recipientId}`,
|
||||
);
|
||||
@@ -45,7 +43,7 @@ export class MarkNotificationReadUseCase {
|
||||
|
||||
if (!notification) {
|
||||
this.logger.warn(`Notification not found for ID: ${command.notificationId}`);
|
||||
return Result.err<void, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
return Result.err<MarkNotificationReadResult, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
code: 'NOTIFICATION_NOT_FOUND',
|
||||
details: { message: 'Notification not found' },
|
||||
});
|
||||
@@ -55,7 +53,7 @@ export class MarkNotificationReadUseCase {
|
||||
this.logger.warn(
|
||||
`Unauthorized attempt to mark notification ${command.notificationId}. Recipient ID mismatch.`,
|
||||
);
|
||||
return Result.err<void, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
return Result.err<MarkNotificationReadResult, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
code: 'RECIPIENT_MISMATCH',
|
||||
details: { message: "Cannot mark another user's notification as read" },
|
||||
});
|
||||
@@ -65,12 +63,11 @@ export class MarkNotificationReadUseCase {
|
||||
this.logger.info(
|
||||
`Notification ${command.notificationId} is already read. Skipping update.`,
|
||||
);
|
||||
this.output.present({
|
||||
return Result.ok<MarkNotificationReadResult, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
notificationId: command.notificationId,
|
||||
recipientId: command.recipientId,
|
||||
wasAlreadyRead: true,
|
||||
});
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
const updatedNotification = notification.markAsRead();
|
||||
@@ -79,19 +76,17 @@ export class MarkNotificationReadUseCase {
|
||||
`Notification ${command.notificationId} successfully marked as read.`,
|
||||
);
|
||||
|
||||
this.output.present({
|
||||
return Result.ok<MarkNotificationReadResult, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
notificationId: command.notificationId,
|
||||
recipientId: command.recipientId,
|
||||
wasAlreadyRead: false,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(
|
||||
`Failed to mark notification ${command.notificationId} as read: ${err.message}`,
|
||||
);
|
||||
return Result.err<void, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
return Result.err<MarkNotificationReadResult, ApplicationErrorCode<MarkNotificationReadErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -117,19 +112,19 @@ export type MarkAllNotificationsReadErrorCode = 'REPOSITORY_ERROR';
|
||||
export class MarkAllNotificationsReadUseCase {
|
||||
constructor(
|
||||
private readonly notificationRepository: INotificationRepository,
|
||||
private readonly output: UseCaseOutputPort<MarkAllNotificationsReadResult>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: MarkAllNotificationsReadInput,
|
||||
): Promise<Result<void, ApplicationErrorCode<MarkAllNotificationsReadErrorCode, { message: string }>>> {
|
||||
): Promise<Result<MarkAllNotificationsReadResult, ApplicationErrorCode<MarkAllNotificationsReadErrorCode, { message: string }>>> {
|
||||
try {
|
||||
await this.notificationRepository.markAllAsReadByRecipientId(input.recipientId);
|
||||
this.output.present({ recipientId: input.recipientId });
|
||||
return Result.ok(undefined);
|
||||
return Result.ok<MarkAllNotificationsReadResult, ApplicationErrorCode<MarkAllNotificationsReadErrorCode, { message: string }>>({
|
||||
recipientId: input.recipientId,
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
return Result.err<void, ApplicationErrorCode<MarkAllNotificationsReadErrorCode, { message: string }>>({
|
||||
return Result.err<MarkAllNotificationsReadResult, ApplicationErrorCode<MarkAllNotificationsReadErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -162,42 +157,40 @@ export type DismissNotificationErrorCode =
|
||||
export class DismissNotificationUseCase {
|
||||
constructor(
|
||||
private readonly notificationRepository: INotificationRepository,
|
||||
private readonly output: UseCaseOutputPort<DismissNotificationResult>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: DismissNotificationCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>> {
|
||||
): Promise<Result<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>> {
|
||||
try {
|
||||
const notification = await this.notificationRepository.findById(
|
||||
command.notificationId,
|
||||
);
|
||||
|
||||
if (!notification) {
|
||||
return Result.err<void, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
return Result.err<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
code: 'NOTIFICATION_NOT_FOUND',
|
||||
details: { message: 'Notification not found' },
|
||||
});
|
||||
}
|
||||
|
||||
if (notification.recipientId !== command.recipientId) {
|
||||
return Result.err<void, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
return Result.err<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
code: 'RECIPIENT_MISMATCH',
|
||||
details: { message: "Cannot dismiss another user's notification" },
|
||||
});
|
||||
}
|
||||
|
||||
if (notification.isDismissed()) {
|
||||
this.output.present({
|
||||
return Result.ok<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
notificationId: command.notificationId,
|
||||
recipientId: command.recipientId,
|
||||
wasAlreadyDismissed: true,
|
||||
});
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
if (!notification.canDismiss()) {
|
||||
return Result.err<void, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
return Result.err<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
code: 'CANNOT_DISMISS_REQUIRING_RESPONSE',
|
||||
details: { message: 'Cannot dismiss notification that requires response' },
|
||||
});
|
||||
@@ -206,19 +199,17 @@ export class DismissNotificationUseCase {
|
||||
const updatedNotification = notification.dismiss();
|
||||
await this.notificationRepository.update(updatedNotification);
|
||||
|
||||
this.output.present({
|
||||
return Result.ok<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
notificationId: command.notificationId,
|
||||
recipientId: command.recipientId,
|
||||
wasAlreadyDismissed: false,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
return Result.err<void, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
return Result.err<DismissNotificationResult, ApplicationErrorCode<DismissNotificationErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { describe, expect, it, vi, type Mock } from 'vitest';
|
||||
import { describe, expect, it, vi, type Mock, beforeEach } from 'vitest';
|
||||
import type { ChannelPreference, NotificationPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
import type { NotificationChannel, NotificationType } from '../../domain/types/NotificationTypes';
|
||||
@@ -12,15 +12,10 @@ import {
|
||||
UpdateQuietHoursUseCase,
|
||||
UpdateTypePreferenceUseCase,
|
||||
type GetNotificationPreferencesInput,
|
||||
type GetNotificationPreferencesResult,
|
||||
type SetDigestModeCommand,
|
||||
type SetDigestModeResult,
|
||||
type UpdateChannelPreferenceCommand,
|
||||
type UpdateChannelPreferenceResult,
|
||||
type UpdateQuietHoursCommand,
|
||||
type UpdateQuietHoursResult,
|
||||
type UpdateTypePreferenceCommand,
|
||||
type UpdateTypePreferenceResult,
|
||||
} from './NotificationPreferencesUseCases';
|
||||
|
||||
describe('NotificationPreferencesUseCases', () => {
|
||||
@@ -30,46 +25,43 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
};
|
||||
let logger: Logger;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
preferenceRepository = {
|
||||
getOrCreateDefault: vi.fn(),
|
||||
save: vi.fn(),
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
});
|
||||
|
||||
it('GetNotificationPreferencesQuery returns preferences from repository', async () => {
|
||||
const preference = {
|
||||
id: 'pref-1',
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const output: UseCaseOutputPort<GetNotificationPreferencesResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<GetNotificationPreferencesResult> & { present: Mock };
|
||||
|
||||
const useCase = new GetNotificationPreferencesQuery(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
|
||||
const input: GetNotificationPreferencesInput = { driverId: 'driver-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(preferenceRepository.getOrCreateDefault).toHaveBeenCalledWith('driver-1');
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(output.present).toHaveBeenCalledWith({ preference });
|
||||
});
|
||||
beforeEach(() => {
|
||||
preferenceRepository = {
|
||||
getOrCreateDefault: vi.fn(),
|
||||
save: vi.fn(),
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
});
|
||||
|
||||
it('GetNotificationPreferencesQuery returns preferences from repository', async () => {
|
||||
const preference = {
|
||||
id: 'pref-1',
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const useCase = new GetNotificationPreferencesQuery(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
const input: GetNotificationPreferencesInput = { driverId: 'driver-1' };
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(preferenceRepository.getOrCreateDefault).toHaveBeenCalledWith('driver-1');
|
||||
expect(result).toBeInstanceOf(Result);
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.preference).toEqual(preference);
|
||||
});
|
||||
|
||||
it('UpdateChannelPreferenceUseCase updates channel preference', async () => {
|
||||
const preference = {
|
||||
updateChannel: vi.fn().mockReturnThis(),
|
||||
@@ -77,13 +69,8 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const output: UseCaseOutputPort<UpdateChannelPreferenceResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<UpdateChannelPreferenceResult> & { present: Mock };
|
||||
|
||||
const useCase = new UpdateChannelPreferenceUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -98,7 +85,10 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(preference.updateChannel).toHaveBeenCalled();
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
expect(output.present).toHaveBeenCalledWith({ driverId: 'driver-1', channel: 'email' });
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.driverId).toBe('driver-1');
|
||||
expect(successResult.channel).toBe('email');
|
||||
});
|
||||
|
||||
it('UpdateTypePreferenceUseCase updates type preference', async () => {
|
||||
@@ -108,13 +98,8 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const output: UseCaseOutputPort<UpdateTypePreferenceResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<UpdateTypePreferenceResult> & { present: Mock };
|
||||
|
||||
const useCase = new UpdateTypePreferenceUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -129,7 +114,10 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(preference.updateTypePreference).toHaveBeenCalled();
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
expect(output.present).toHaveBeenCalledWith({ driverId: 'driver-1', type: 'system_announcement' });
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.driverId).toBe('driver-1');
|
||||
expect(successResult.type).toBe('system_announcement');
|
||||
});
|
||||
|
||||
it('UpdateQuietHoursUseCase validates hours and updates preferences', async () => {
|
||||
@@ -139,13 +127,8 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const output: UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock };
|
||||
|
||||
const useCase = new UpdateQuietHoursUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -160,21 +143,16 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(preference.updateQuietHours).toHaveBeenCalledWith(22, 7);
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
driverId: 'driver-1',
|
||||
startHour: 22,
|
||||
endHour: 7,
|
||||
});
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.driverId).toBe('driver-1');
|
||||
expect(successResult.startHour).toBe(22);
|
||||
expect(successResult.endHour).toBe(7);
|
||||
});
|
||||
|
||||
it('UpdateQuietHoursUseCase returns error on invalid hours', async () => {
|
||||
const output: UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock };
|
||||
|
||||
const useCase = new UpdateQuietHoursUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -198,13 +176,8 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const output: UseCaseOutputPort<SetDigestModeResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<SetDigestModeResult> & { present: Mock };
|
||||
|
||||
const useCase = new SetDigestModeUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
);
|
||||
|
||||
const command: SetDigestModeCommand = {
|
||||
@@ -218,21 +191,16 @@ describe('NotificationPreferencesUseCases', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(preference.setDigestMode).toHaveBeenCalledWith(true, 4);
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
driverId: 'driver-1',
|
||||
enabled: true,
|
||||
frequencyHours: 4,
|
||||
});
|
||||
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.driverId).toBe('driver-1');
|
||||
expect(successResult.enabled).toBe(true);
|
||||
expect(successResult.frequencyHours).toBe(4);
|
||||
});
|
||||
|
||||
it('SetDigestModeUseCase returns error on invalid frequency', async () => {
|
||||
const output: UseCaseOutputPort<SetDigestModeResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<SetDigestModeResult> & { present: Mock };
|
||||
|
||||
const useCase = new SetDigestModeUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
output,
|
||||
);
|
||||
|
||||
const command: SetDigestModeCommand = {
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
* Manages user notification preferences.
|
||||
*/
|
||||
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
import type { NotificationType, NotificationChannel } from '../../domain/types/NotificationTypes';
|
||||
// import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||
|
||||
/**
|
||||
* Query: GetNotificationPreferencesQuery
|
||||
@@ -29,24 +28,22 @@ export type GetNotificationPreferencesErrorCode = 'REPOSITORY_ERROR';
|
||||
export class GetNotificationPreferencesQuery {
|
||||
constructor(
|
||||
private readonly preferenceRepository: INotificationPreferenceRepository,
|
||||
private readonly output: UseCaseOutputPort<GetNotificationPreferencesResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetNotificationPreferencesInput,
|
||||
): Promise<Result<void, ApplicationErrorCode<GetNotificationPreferencesErrorCode, { message: string }>>> {
|
||||
): Promise<Result<GetNotificationPreferencesResult, ApplicationErrorCode<GetNotificationPreferencesErrorCode, { message: string }>>> {
|
||||
const { driverId } = input;
|
||||
this.logger.debug(`Fetching notification preferences for driver: ${driverId}`);
|
||||
try {
|
||||
const preferences = await this.preferenceRepository.getOrCreateDefault(driverId);
|
||||
this.logger.info(`Successfully fetched preferences for driver: ${driverId}`);
|
||||
this.output.present({ preference: preferences });
|
||||
return Result.ok(undefined);
|
||||
return Result.ok<GetNotificationPreferencesResult, ApplicationErrorCode<GetNotificationPreferencesErrorCode, { message: string }>>({ preference: preferences });
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(`Failed to fetch preferences for driver: ${driverId}`, err);
|
||||
return Result.err<void, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>({
|
||||
return Result.err<GetNotificationPreferencesResult, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -74,13 +71,12 @@ export type UpdateChannelPreferenceErrorCode =
|
||||
export class UpdateChannelPreferenceUseCase {
|
||||
constructor(
|
||||
private readonly preferenceRepository: INotificationPreferenceRepository,
|
||||
private readonly output: UseCaseOutputPort<UpdateChannelPreferenceResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: UpdateChannelPreferenceCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<UpdateChannelPreferenceErrorCode, { message: string }>>> {
|
||||
): Promise<Result<UpdateChannelPreferenceResult, ApplicationErrorCode<UpdateChannelPreferenceErrorCode, { message: string }>>> {
|
||||
this.logger.debug(
|
||||
`Updating channel preference for driver: ${command.driverId}, channel: ${command.channel}, preference: ${JSON.stringify(command.preference)}`,
|
||||
);
|
||||
@@ -93,15 +89,17 @@ export class UpdateChannelPreferenceUseCase {
|
||||
this.logger.info(
|
||||
`Successfully updated channel preference for driver: ${command.driverId}`,
|
||||
);
|
||||
this.output.present({ driverId: command.driverId, channel: command.channel });
|
||||
return Result.ok(undefined);
|
||||
return Result.ok<UpdateChannelPreferenceResult, ApplicationErrorCode<UpdateChannelPreferenceErrorCode, { message: string }>>({
|
||||
driverId: command.driverId,
|
||||
channel: command.channel,
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(
|
||||
`Failed to update channel preference for driver: ${command.driverId}, channel: ${command.channel}`,
|
||||
err,
|
||||
);
|
||||
return Result.err<void, ApplicationErrorCode<UpdateChannelPreferenceErrorCode, { message: string }>>({
|
||||
return Result.err<UpdateChannelPreferenceResult, ApplicationErrorCode<UpdateChannelPreferenceErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -128,13 +126,12 @@ export type UpdateTypePreferenceErrorCode = 'REPOSITORY_ERROR';
|
||||
export class UpdateTypePreferenceUseCase {
|
||||
constructor(
|
||||
private readonly preferenceRepository: INotificationPreferenceRepository,
|
||||
private readonly output: UseCaseOutputPort<UpdateTypePreferenceResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: UpdateTypePreferenceCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<UpdateTypePreferenceErrorCode, { message: string }>>> {
|
||||
): Promise<Result<UpdateTypePreferenceResult, ApplicationErrorCode<UpdateTypePreferenceErrorCode, { message: string }>>> {
|
||||
this.logger.debug(
|
||||
`Updating type preference for driver: ${command.driverId}, type: ${command.type}, preference: ${JSON.stringify(command.preference)}`,
|
||||
);
|
||||
@@ -147,15 +144,17 @@ export class UpdateTypePreferenceUseCase {
|
||||
this.logger.info(
|
||||
`Successfully updated type preference for driver: ${command.driverId}`,
|
||||
);
|
||||
this.output.present({ driverId: command.driverId, type: command.type });
|
||||
return Result.ok(undefined);
|
||||
return Result.ok<UpdateTypePreferenceResult, ApplicationErrorCode<UpdateTypePreferenceErrorCode, { message: string }>>({
|
||||
driverId: command.driverId,
|
||||
type: command.type,
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(
|
||||
`Failed to update type preference for driver: ${command.driverId}, type: ${command.type}`,
|
||||
err,
|
||||
);
|
||||
return Result.err<void, ApplicationErrorCode<GetNotificationPreferencesErrorCode, { message: string }>>({
|
||||
return Result.err<UpdateTypePreferenceResult, ApplicationErrorCode<GetNotificationPreferencesErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -186,13 +185,12 @@ export type UpdateQuietHoursErrorCode =
|
||||
export class UpdateQuietHoursUseCase {
|
||||
constructor(
|
||||
private readonly preferenceRepository: INotificationPreferenceRepository,
|
||||
private readonly output: UseCaseOutputPort<UpdateQuietHoursResult>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: UpdateQuietHoursCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>> {
|
||||
): Promise<Result<UpdateQuietHoursResult, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>> {
|
||||
this.logger.debug(
|
||||
`Updating quiet hours for driver: ${command.driverId}, startHour: ${command.startHour}, endHour: ${command.endHour}`,
|
||||
);
|
||||
@@ -202,7 +200,7 @@ export class UpdateQuietHoursUseCase {
|
||||
this.logger.warn(
|
||||
`Invalid start hour provided for driver: ${command.driverId}. startHour: ${command.startHour}`,
|
||||
);
|
||||
return Result.err<void, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>({
|
||||
return Result.err<UpdateQuietHoursResult, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>({
|
||||
code: 'INVALID_START_HOUR',
|
||||
details: { message: 'Start hour must be between 0 and 23' },
|
||||
});
|
||||
@@ -211,7 +209,7 @@ export class UpdateQuietHoursUseCase {
|
||||
this.logger.warn(
|
||||
`Invalid end hour provided for driver: ${command.driverId}. endHour: ${command.endHour}`,
|
||||
);
|
||||
return Result.err<void, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>({
|
||||
return Result.err<UpdateQuietHoursResult, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>({
|
||||
code: 'INVALID_END_HOUR',
|
||||
details: { message: 'End hour must be between 0 and 23' },
|
||||
});
|
||||
@@ -226,16 +224,15 @@ export class UpdateQuietHoursUseCase {
|
||||
);
|
||||
await this.preferenceRepository.save(updated);
|
||||
this.logger.info(`Successfully updated quiet hours for driver: ${command.driverId}`);
|
||||
this.output.present({
|
||||
return Result.ok<UpdateQuietHoursResult, ApplicationErrorCode<UpdateQuietHoursErrorCode, { message: string }>>({
|
||||
driverId: command.driverId,
|
||||
startHour: command.startHour,
|
||||
endHour: command.endHour,
|
||||
});
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(`Failed to update quiet hours for driver: ${command.driverId}`, err);
|
||||
return Result.err<void, ApplicationErrorCode<UpdateTypePreferenceErrorCode, { message: string }>>({
|
||||
return Result.err<UpdateQuietHoursResult, ApplicationErrorCode<UpdateTypePreferenceErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
@@ -265,14 +262,13 @@ export type SetDigestModeErrorCode =
|
||||
export class SetDigestModeUseCase {
|
||||
constructor(
|
||||
private readonly preferenceRepository: INotificationPreferenceRepository,
|
||||
private readonly output: UseCaseOutputPort<SetDigestModeResult>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: SetDigestModeCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>> {
|
||||
): Promise<Result<SetDigestModeResult, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>> {
|
||||
if (command.frequencyHours !== undefined && command.frequencyHours < 1) {
|
||||
return Result.err<void, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>({
|
||||
return Result.err<SetDigestModeResult, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>({
|
||||
code: 'INVALID_FREQUENCY',
|
||||
details: { message: 'Digest frequency must be at least 1 hour' },
|
||||
});
|
||||
@@ -292,14 +288,13 @@ export class SetDigestModeUseCase {
|
||||
enabled: command.enabled,
|
||||
frequencyHours: command.frequencyHours,
|
||||
};
|
||||
this.output.present(result);
|
||||
return Result.ok(undefined);
|
||||
return Result.ok<SetDigestModeResult, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>(result);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
return Result.err<void, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>({
|
||||
return Result.err<SetDigestModeResult, ApplicationErrorCode<SetDigestModeErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -19,17 +19,11 @@ vi.mock('uuid', () => ({
|
||||
v4: () => 'notif-1',
|
||||
}));
|
||||
|
||||
interface TestOutputPort extends UseCaseOutputPort<SendNotificationResult> {
|
||||
present: Mock;
|
||||
result?: SendNotificationResult;
|
||||
}
|
||||
|
||||
describe('SendNotificationUseCase', () => {
|
||||
let notificationRepository: { create: Mock };
|
||||
let preferenceRepository: { getOrCreateDefault: Mock };
|
||||
let gatewayRegistry: { send: Mock };
|
||||
let logger: Logger;
|
||||
let output: TestOutputPort;
|
||||
let useCase: SendNotificationUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -52,17 +46,10 @@ describe('SendNotificationUseCase', () => {
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
output = {
|
||||
present: vi.fn((result: SendNotificationResult) => {
|
||||
output.result = result;
|
||||
}),
|
||||
} as unknown as TestOutputPort;
|
||||
|
||||
useCase = new SendNotificationUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
gatewayRegistry as unknown as NotificationGatewayRegistry,
|
||||
output,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
@@ -92,10 +79,10 @@ describe('SendNotificationUseCase', () => {
|
||||
expect(notificationRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(gatewayRegistry.send).not.toHaveBeenCalled();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
expect(output.result?.deliveryResults).toEqual([]);
|
||||
expect(output.result?.notification.channel).toBe('in_app');
|
||||
expect(output.result?.notification.status).toBe('dismissed');
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.deliveryResults).toEqual([]);
|
||||
expect(successResult.notification.channel).toBe('in_app');
|
||||
expect(successResult.notification.status).toBe('dismissed');
|
||||
});
|
||||
|
||||
it('ensures in_app is used and sends external channels when enabled', async () => {
|
||||
@@ -128,11 +115,11 @@ describe('SendNotificationUseCase', () => {
|
||||
expect(notificationRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(gatewayRegistry.send).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
expect(output.result?.notification.channel).toBe('in_app');
|
||||
expect(output.result?.deliveryResults.length).toBe(2);
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.notification.channel).toBe('in_app');
|
||||
expect(successResult.deliveryResults.length).toBe(2);
|
||||
|
||||
const channels = output.result!.deliveryResults.map(r => r.channel).sort();
|
||||
const channels = successResult.deliveryResults.map(r => r.channel).sort();
|
||||
expect(channels).toEqual(['email', 'in_app']);
|
||||
});
|
||||
|
||||
@@ -158,8 +145,9 @@ describe('SendNotificationUseCase', () => {
|
||||
expect(notificationRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(gatewayRegistry.send).not.toHaveBeenCalled();
|
||||
|
||||
expect(output.result?.deliveryResults.length).toBe(1);
|
||||
expect(output.result?.deliveryResults[0]?.channel).toBe('in_app');
|
||||
const successResult = result.unwrap();
|
||||
expect(successResult.deliveryResults.length).toBe(1);
|
||||
expect(successResult.deliveryResults[0]?.channel).toBe('in_app');
|
||||
});
|
||||
|
||||
it('returns REPOSITORY_ERROR when preference repository throws', async () => {
|
||||
@@ -172,13 +160,12 @@ describe('SendNotificationUseCase', () => {
|
||||
body: 'World',
|
||||
};
|
||||
|
||||
const result: Result<void, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>> =
|
||||
const result: Result<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>> =
|
||||
await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
expect(err.details?.message).toBe('DB error');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* based on their preferences.
|
||||
*/
|
||||
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -52,7 +52,6 @@ export class SendNotificationUseCase {
|
||||
private readonly notificationRepository: INotificationRepository,
|
||||
private readonly preferenceRepository: INotificationPreferenceRepository,
|
||||
private readonly gatewayRegistry: NotificationGatewayRegistry,
|
||||
private readonly output: UseCaseOutputPort<SendNotificationResult>,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
this.logger.debug('SendNotificationUseCase initialized.');
|
||||
@@ -60,7 +59,7 @@ export class SendNotificationUseCase {
|
||||
|
||||
async execute(
|
||||
command: SendNotificationCommand,
|
||||
): Promise<Result<void, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>> {
|
||||
): Promise<Result<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>> {
|
||||
this.logger.debug('Executing SendNotificationUseCase', { command });
|
||||
try {
|
||||
// Get recipient's preferences
|
||||
@@ -84,12 +83,10 @@ export class SendNotificationUseCase {
|
||||
|
||||
await this.notificationRepository.create(notification);
|
||||
|
||||
this.output.present({
|
||||
return Result.ok<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>({
|
||||
notification,
|
||||
deliveryResults: [],
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
// Determine which channels to use
|
||||
@@ -142,20 +139,18 @@ export class SendNotificationUseCase {
|
||||
deliveryResults.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
this.output.present({
|
||||
|
||||
return Result.ok<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>({
|
||||
notification: primaryNotification!,
|
||||
deliveryResults,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error('Error sending notification', err);
|
||||
return Result.err({
|
||||
return Result.err<SendNotificationResult, ApplicationErrorCode<SendNotificationErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user