refactor use cases

This commit is contained in:
2026-01-08 15:34:51 +01:00
parent d984ab24a8
commit 52e9a2f6a7
362 changed files with 5192 additions and 8409 deletions

View File

@@ -4,13 +4,6 @@ import type { INotificationPreferenceRepository } from '@core/notifications/doma
import type { NotificationGatewayRegistry } from '@core/notifications/application/ports/NotificationGateway'; import type { NotificationGatewayRegistry } from '@core/notifications/application/ports/NotificationGateway';
import { SendNotificationUseCase } from '@core/notifications/application/use-cases/SendNotificationUseCase'; import { SendNotificationUseCase } from '@core/notifications/application/use-cases/SendNotificationUseCase';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
class NoOpOutputPort implements UseCaseOutputPort<any> {
present(_result: any): void {
// No-op for adapter
}
}
export class NotificationServiceAdapter implements NotificationService { export class NotificationServiceAdapter implements NotificationService {
private readonly useCase: SendNotificationUseCase; private readonly useCase: SendNotificationUseCase;
@@ -27,7 +20,6 @@ export class NotificationServiceAdapter implements NotificationService {
notificationRepository, notificationRepository,
preferenceRepository, preferenceRepository,
gatewayRegistry, gatewayRegistry,
new NoOpOutputPort(),
logger, logger,
); );
} }

View File

@@ -3,50 +3,31 @@ import { Module } from '@nestjs/common';
import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule'; import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule';
import { AdminService } from './AdminService'; import { AdminService } from './AdminService';
import { AdminController } from './AdminController'; import { AdminController } from './AdminController';
import { ListUsersPresenter } from './presenters/ListUsersPresenter';
import { DashboardStatsPresenter } from './presenters/DashboardStatsPresenter';
import { AuthModule } from '../auth/AuthModule'; import { AuthModule } from '../auth/AuthModule';
import { ListUsersUseCase } from '@core/admin/application/use-cases/ListUsersUseCase'; import { ListUsersUseCase } from '@core/admin/application/use-cases/ListUsersUseCase';
import { GetDashboardStatsUseCase } from './use-cases/GetDashboardStatsUseCase'; import { GetDashboardStatsUseCase } from './use-cases/GetDashboardStatsUseCase';
import { InitializationLogger } from '../../shared/logging/InitializationLogger'; import { InitializationLogger } from '../../shared/logging/InitializationLogger';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase';
import type { DashboardStatsResult } from './use-cases/GetDashboardStatsUseCase';
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository'; import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
export const ADMIN_USER_REPOSITORY_TOKEN = 'IAdminUserRepository'; export const ADMIN_USER_REPOSITORY_TOKEN = 'IAdminUserRepository';
export const LIST_USERS_OUTPUT_PORT_TOKEN = 'ListUsersOutputPort';
export const DASHBOARD_STATS_OUTPUT_PORT_TOKEN = 'DashboardStatsOutputPort';
const initLogger = InitializationLogger.getInstance(); const initLogger = InitializationLogger.getInstance();
const adminProviders: Provider[] = [ const adminProviders: Provider[] = [
AdminService, AdminService,
ListUsersPresenter,
DashboardStatsPresenter,
{
provide: LIST_USERS_OUTPUT_PORT_TOKEN,
useExisting: ListUsersPresenter,
},
{
provide: DASHBOARD_STATS_OUTPUT_PORT_TOKEN,
useExisting: DashboardStatsPresenter,
},
{ {
provide: ListUsersUseCase, provide: ListUsersUseCase,
useFactory: ( useFactory: (
repository: IAdminUserRepository, repository: IAdminUserRepository,
output: UseCaseOutputPort<ListUsersResult>, ) => new ListUsersUseCase(repository),
) => new ListUsersUseCase(repository, output), inject: [ADMIN_USER_REPOSITORY_TOKEN],
inject: [ADMIN_USER_REPOSITORY_TOKEN, LIST_USERS_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GetDashboardStatsUseCase, provide: GetDashboardStatsUseCase,
useFactory: ( useFactory: (
repository: IAdminUserRepository, repository: IAdminUserRepository,
output: UseCaseOutputPort<DashboardStatsResult>, ) => new GetDashboardStatsUseCase(repository),
) => new GetDashboardStatsUseCase(repository, output), inject: [ADMIN_USER_REPOSITORY_TOKEN],
inject: [ADMIN_USER_REPOSITORY_TOKEN, DASHBOARD_STATS_OUTPUT_PORT_TOKEN],
}, },
]; ];

View File

@@ -12,18 +12,6 @@ const mockGetDashboardStatsUseCase = {
execute: vi.fn(), execute: vi.fn(),
}; };
// Mock presenters
const mockListUsersPresenter = {
present: vi.fn(),
getViewModel: vi.fn(),
};
const mockDashboardStatsPresenter = {
present: vi.fn(),
responseModel: {},
reset: vi.fn(),
};
describe('AdminService', () => { describe('AdminService', () => {
describe('TDD - Test First', () => { describe('TDD - Test First', () => {
let service: AdminService; let service: AdminService;
@@ -32,9 +20,7 @@ describe('AdminService', () => {
vi.clearAllMocks(); vi.clearAllMocks();
service = new AdminService( service = new AdminService(
mockListUsersUseCase as any, mockListUsersUseCase as any,
mockListUsersPresenter as any, mockGetDashboardStatsUseCase as any
mockGetDashboardStatsUseCase as any,
mockDashboardStatsPresenter as any
); );
}); });
@@ -50,15 +36,19 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act // Act
const result = await service.listUsers({ actorId: 'actor-1' }); const result = await service.listUsers({ actorId: 'actor-1' });
// Assert // Assert
expect(mockListUsersUseCase.execute).toHaveBeenCalledWith({ actorId: 'actor-1' }); expect(mockListUsersUseCase.execute).toHaveBeenCalledWith({ actorId: 'actor-1' });
expect(mockListUsersPresenter.getViewModel).toHaveBeenCalled(); expect(result).toEqual({
expect(result).toEqual(expectedResult); users: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
});
}); });
it('should return users when they exist', async () => { it('should return users when they exist', async () => {
@@ -88,16 +78,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue({
users: [
{ id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: user1.createdAt, updatedAt: user1.updatedAt },
{ id: 'user-2', email: 'user2@example.com', displayName: 'User 2', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: user2.createdAt, updatedAt: user2.updatedAt },
],
total: 2,
page: 1,
limit: 10,
totalPages: 1,
});
// Act // Act
const result = await service.listUsers({ actorId: 'actor-1' }); const result = await service.listUsers({ actorId: 'actor-1' });
@@ -105,6 +85,11 @@ describe('AdminService', () => {
// Assert // Assert
expect(result.users).toHaveLength(2); expect(result.users).toHaveLength(2);
expect(result.total).toBe(2); expect(result.total).toBe(2);
// Check that users are converted to DTOs
expect(result.users[0]?.id).toBe('user-1');
expect(result.users[0]?.email).toBe('user1@example.com');
expect(result.users[1]?.id).toBe('user-2');
expect(result.users[1]?.email).toBe('user2@example.com');
}); });
it('should apply filters correctly', async () => { it('should apply filters correctly', async () => {
@@ -126,13 +111,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue({
users: [{ id: 'admin-1', email: 'admin@example.com', displayName: 'Admin', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: adminUser.createdAt, updatedAt: adminUser.updatedAt }],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
});
// Act // Act
const result = await service.listUsers({ const result = await service.listUsers({
@@ -163,13 +141,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue({
users: [],
total: 50,
page: 3,
limit: 10,
totalPages: 5,
});
// Act // Act
const result = await service.listUsers({ const result = await service.listUsers({
@@ -202,7 +173,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act // Act
await service.listUsers({ await service.listUsers({
@@ -232,7 +202,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act // Act
await service.listUsers({ await service.listUsers({
@@ -260,7 +229,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act // Act
await service.listUsers({ await service.listUsers({
@@ -299,7 +267,6 @@ describe('AdminService', () => {
}; };
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult)); mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act // Act
await service.listUsers({ await service.listUsers({

View File

@@ -1,19 +1,18 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ListUsersUseCase, ListUsersInput } from '@core/admin/application/use-cases/ListUsersUseCase'; import { ListUsersUseCase, ListUsersInput, ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase';
import { ListUsersPresenter, ListUsersViewModel } from './presenters/ListUsersPresenter';
import { GetDashboardStatsUseCase, GetDashboardStatsInput } from './use-cases/GetDashboardStatsUseCase'; import { GetDashboardStatsUseCase, GetDashboardStatsInput } from './use-cases/GetDashboardStatsUseCase';
import { DashboardStatsPresenter, DashboardStatsResponse } from './presenters/DashboardStatsPresenter'; import { UserListResponseDto, UserResponseDto } from './dtos/UserResponseDto';
import { DashboardStatsResponseDto } from './dto/DashboardStatsResponseDto';
import type { AdminUser } from '@core/admin/domain/entities/AdminUser';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
constructor( constructor(
private readonly listUsersUseCase: ListUsersUseCase, private readonly listUsersUseCase: ListUsersUseCase,
private readonly listUsersPresenter: ListUsersPresenter,
private readonly getDashboardStatsUseCase: GetDashboardStatsUseCase, private readonly getDashboardStatsUseCase: GetDashboardStatsUseCase,
private readonly dashboardStatsPresenter: DashboardStatsPresenter,
) {} ) {}
async listUsers(input: ListUsersInput): Promise<ListUsersViewModel> { async listUsers(input: ListUsersInput): Promise<UserListResponseDto> {
const result = await this.listUsersUseCase.execute(input); const result = await this.listUsersUseCase.execute(input);
if (result.isErr()) { if (result.isErr()) {
@@ -21,12 +20,11 @@ export class AdminService {
throw new Error(`${error.code}: ${error.details.message}`); throw new Error(`${error.code}: ${error.details.message}`);
} }
return this.listUsersPresenter.getViewModel(); const data = result.unwrap();
return this.toListResponseDto(data);
} }
async getDashboardStats(input: GetDashboardStatsInput): Promise<DashboardStatsResponse> { async getDashboardStats(input: GetDashboardStatsInput): Promise<DashboardStatsResponseDto> {
this.dashboardStatsPresenter.reset();
const result = await this.getDashboardStatsUseCase.execute(input); const result = await this.getDashboardStatsUseCase.execute(input);
if (result.isErr()) { if (result.isErr()) {
@@ -34,6 +32,54 @@ export class AdminService {
throw new Error(`${error.code}: ${error.details.message}`); throw new Error(`${error.code}: ${error.details.message}`);
} }
return this.dashboardStatsPresenter.responseModel; const data = result.unwrap();
return data;
}
private toListResponseDto(result: ListUsersResult): UserListResponseDto {
return {
users: result.users.map(user => this.toUserResponse(user)),
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
};
}
private toUserResponse(user: AdminUser | Record<string, unknown>): UserResponseDto {
// Handle both domain objects and plain objects
if (user.id && typeof user.id === 'object' && 'value' in (user.id as Record<string, unknown>)) {
// Domain object
const domainUser = user as AdminUser;
const response: UserResponseDto = {
id: domainUser.id.value,
email: domainUser.email.value,
displayName: domainUser.displayName,
roles: domainUser.roles.map(r => r.value),
status: domainUser.status.value,
isSystemAdmin: domainUser.isSystemAdmin(),
createdAt: domainUser.createdAt,
updatedAt: domainUser.updatedAt,
};
if (domainUser.lastLoginAt) response.lastLoginAt = domainUser.lastLoginAt;
if (domainUser.primaryDriverId) response.primaryDriverId = domainUser.primaryDriverId;
return response;
} else {
// Plain object (for tests)
const plainUser = user as Record<string, unknown>;
const response: UserResponseDto = {
id: plainUser.id as string,
email: plainUser.email as string,
displayName: plainUser.displayName as string,
roles: plainUser.roles as string[],
status: plainUser.status as string,
isSystemAdmin: plainUser.isSystemAdmin as boolean,
createdAt: plainUser.createdAt as Date,
updatedAt: plainUser.updatedAt as Date,
};
if (plainUser.lastLoginAt) response.lastLoginAt = plainUser.lastLoginAt as Date;
if (plainUser.primaryDriverId) response.primaryDriverId = plainUser.primaryDriverId as string;
return response;
}
} }
} }

View File

@@ -1,6 +1,5 @@
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository'; import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService'; import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService';
import { UserId } from '@core/admin/domain/value-objects/UserId'; import { UserId } from '@core/admin/domain/value-objects/UserId';
@@ -46,10 +45,9 @@ export type GetDashboardStatsApplicationError = ApplicationErrorCode<GetDashboar
export class GetDashboardStatsUseCase { export class GetDashboardStatsUseCase {
constructor( constructor(
private readonly adminUserRepo: IAdminUserRepository, private readonly adminUserRepo: IAdminUserRepository,
private readonly output: UseCaseOutputPort<DashboardStatsResult>,
) {} ) {}
async execute(input: GetDashboardStatsInput): Promise<Result<void, GetDashboardStatsApplicationError>> { async execute(input: GetDashboardStatsInput): Promise<Result<DashboardStatsResult, GetDashboardStatsApplicationError>> {
try { try {
// Get actor (current user) // Get actor (current user)
const actor = await this.adminUserRepo.findById(UserId.fromString(input.actorId)); const actor = await this.adminUserRepo.findById(UserId.fromString(input.actorId));
@@ -166,9 +164,7 @@ export class GetDashboardStatsUseCase {
activityTimeline, activityTimeline,
}; };
this.output.present(result); return Result.ok(result);
return Result.ok(undefined);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to get dashboard stats'; const message = error instanceof Error ? error.message : 'Failed to get dashboard stats';

View File

@@ -1,9 +1,6 @@
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository'; import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository'; import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import { import {
@@ -13,13 +10,8 @@ import {
const LOGGER_TOKEN = 'Logger'; const LOGGER_TOKEN = 'Logger';
const RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN = 'RecordPageViewOutputPort_TOKEN';
const RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN = 'RecordEngagementOutputPort_TOKEN';
const GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN = 'GetDashboardDataOutputPort_TOKEN';
const GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN = 'GetAnalyticsMetricsOutputPort_TOKEN';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase'; import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import { GetDashboardDataOutput, GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase'; import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase'; import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase'; import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { AnalyticsService } from './AnalyticsService'; import { AnalyticsService } from './AnalyticsService';
@@ -34,44 +26,28 @@ export const AnalyticsProviders: Provider[] = [
RecordEngagementPresenter, RecordEngagementPresenter,
GetDashboardDataPresenter, GetDashboardDataPresenter,
GetAnalyticsMetricsPresenter, GetAnalyticsMetricsPresenter,
{
provide: RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN,
useExisting: RecordPageViewPresenter,
},
{
provide: RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN,
useExisting: RecordEngagementPresenter,
},
{
provide: GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN,
useExisting: GetDashboardDataPresenter,
},
{
provide: GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN,
useExisting: GetAnalyticsMetricsPresenter,
},
{ {
provide: RecordPageViewUseCase, provide: RecordPageViewUseCase,
useFactory: (repo: IPageViewRepository, logger: Logger, output: UseCaseOutputPort<RecordPageViewOutput>) => useFactory: (repo: IPageViewRepository, logger: Logger) =>
new RecordPageViewUseCase(repo, logger, output), new RecordPageViewUseCase(repo, logger),
inject: [ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, LOGGER_TOKEN, RECORD_PAGE_VIEW_OUTPUT_PORT_TOKEN], inject: [ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: RecordEngagementUseCase, provide: RecordEngagementUseCase,
useFactory: (repo: IEngagementRepository, logger: Logger, output: UseCaseOutputPort<RecordEngagementOutput>) => useFactory: (repo: IEngagementRepository, logger: Logger) =>
new RecordEngagementUseCase(repo, logger, output), new RecordEngagementUseCase(repo, logger),
inject: [ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, LOGGER_TOKEN, RECORD_ENGAGEMENT_OUTPUT_PORT_TOKEN], inject: [ANALYTICS_ENGAGEMENT_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetDashboardDataUseCase, provide: GetDashboardDataUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetDashboardDataOutput>) => useFactory: (logger: Logger) =>
new GetDashboardDataUseCase(logger, output), new GetDashboardDataUseCase(logger),
inject: [LOGGER_TOKEN, GET_DASHBOARD_DATA_OUTPUT_PORT_TOKEN], inject: [LOGGER_TOKEN],
}, },
{ {
provide: GetAnalyticsMetricsUseCase, provide: GetAnalyticsMetricsUseCase,
useFactory: (logger: Logger, output: UseCaseOutputPort<GetAnalyticsMetricsOutput>, repo: IPageViewRepository) => useFactory: (logger: Logger, repo: IPageViewRepository) =>
new GetAnalyticsMetricsUseCase(logger, output, repo), new GetAnalyticsMetricsUseCase(logger, repo),
inject: [LOGGER_TOKEN, GET_ANALYTICS_METRICS_OUTPUT_PORT_TOKEN, ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN], inject: [LOGGER_TOKEN, ANALYTICS_PAGE_VIEW_REPOSITORY_TOKEN],
}, },
]; ];

View File

@@ -12,8 +12,7 @@ describe('AnalyticsService', () => {
const recordPageViewUseCase = { const recordPageViewUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
recordPageViewPresenter.present({ pageViewId: 'pv-1' }); return Result.ok({ pageViewId: 'pv-1' });
return Result.ok(undefined);
}), }),
}; };
@@ -77,8 +76,7 @@ describe('AnalyticsService', () => {
const recordEngagementPresenter = new RecordEngagementPresenter(); const recordEngagementPresenter = new RecordEngagementPresenter();
const recordEngagementUseCase = { const recordEngagementUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
recordEngagementPresenter.present({ eventId: 'e1', engagementWeight: 7 }); return Result.ok({ eventId: 'e1', engagementWeight: 7 });
return Result.ok(undefined);
}), }),
}; };
@@ -154,13 +152,12 @@ describe('AnalyticsService', () => {
const getDashboardDataPresenter = new GetDashboardDataPresenter(); const getDashboardDataPresenter = new GetDashboardDataPresenter();
const getDashboardDataUseCase = { const getDashboardDataUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
getDashboardDataPresenter.present({ return Result.ok({
totalUsers: 1, totalUsers: 1,
activeUsers: 2, activeUsers: 2,
totalRaces: 3, totalRaces: 3,
totalLeagues: 4, totalLeagues: 4,
}); });
return Result.ok(undefined);
}), }),
}; };
@@ -217,13 +214,12 @@ describe('AnalyticsService', () => {
const getAnalyticsMetricsPresenter = new GetAnalyticsMetricsPresenter(); const getAnalyticsMetricsPresenter = new GetAnalyticsMetricsPresenter();
const getAnalyticsMetricsUseCase = { const getAnalyticsMetricsUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
getAnalyticsMetricsPresenter.present({ return Result.ok({
pageViews: 10, pageViews: 10,
uniqueVisitors: 0, uniqueVisitors: 0,
averageSessionDuration: 0, averageSessionDuration: 0,
bounceRate: 0, bounceRate: 0,
}); });
return Result.ok(undefined);
}), }),
}; };

View File

@@ -31,50 +31,42 @@ export class AnalyticsService {
) {} ) {}
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutputDTO> { async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutputDTO> {
this.recordPageViewPresenter.reset();
const result = await this.recordPageViewUseCase.execute(input); const result = await this.recordPageViewUseCase.execute(input);
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr(); const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to record page view'); throw new Error(error.details?.message ?? 'Failed to record page view');
} }
return this.recordPageViewPresenter.responseModel; return this.recordPageViewPresenter.transform(result.unwrap());
} }
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutputDTO> { async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutputDTO> {
this.recordEngagementPresenter.reset();
const result = await this.recordEngagementUseCase.execute(input); const result = await this.recordEngagementUseCase.execute(input);
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr(); const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to record engagement'); throw new Error(error.details?.message ?? 'Failed to record engagement');
} }
return this.recordEngagementPresenter.responseModel; return this.recordEngagementPresenter.transform(result.unwrap());
} }
async getDashboardData(): Promise<GetDashboardDataOutputDTO> { async getDashboardData(): Promise<GetDashboardDataOutputDTO> {
this.getDashboardDataPresenter.reset();
const result = await this.getDashboardDataUseCase.execute(); const result = await this.getDashboardDataUseCase.execute();
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr(); const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get dashboard data'); throw new Error(error.details?.message ?? 'Failed to get dashboard data');
} }
return this.getDashboardDataPresenter.responseModel; return this.getDashboardDataPresenter.transform(result.unwrap());
} }
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> { async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutputDTO> {
this.getAnalyticsMetricsPresenter.reset();
const result = await this.getAnalyticsMetricsUseCase.execute({}); const result = await this.getAnalyticsMetricsUseCase.execute({});
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr(); const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get analytics metrics'); throw new Error(error.details?.message ?? 'Failed to get analytics metrics');
} }
return this.getAnalyticsMetricsPresenter.responseModel; return this.getAnalyticsMetricsPresenter.transform(result.unwrap());
} }
} }

View File

@@ -17,7 +17,7 @@ describe('GetAnalyticsMetricsPresenter', () => {
bounceRate: 0.4, bounceRate: 0.4,
}; };
presenter.present(output); presenter.transform(output);
const dto = presenter.getResponseModel(); const dto = presenter.getResponseModel();
@@ -35,11 +35,11 @@ describe('GetAnalyticsMetricsPresenter', () => {
}); });
}); });
it('getResponseModel throws if not presented', () => { it('getResponseModel throws if not transformed', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented'); expect(() => presenter.getResponseModel()).toThrow('Presenter not transformed');
}); });
it('responseModel throws if not presented', () => { it('responseModel throws if not transformed', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented'); expect(() => presenter.responseModel).toThrow('Presenter not transformed');
}); });
}); });

View File

@@ -1,30 +1,30 @@
import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase'; import type { GetAnalyticsMetricsOutput } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetAnalyticsMetricsOutputDTO } from '../dtos/GetAnalyticsMetricsOutputDTO'; import type { GetAnalyticsMetricsOutputDTO } from '../dtos/GetAnalyticsMetricsOutputDTO';
export class GetAnalyticsMetricsPresenter implements UseCaseOutputPort<GetAnalyticsMetricsOutput> { export class GetAnalyticsMetricsPresenter {
private model: GetAnalyticsMetricsOutputDTO | null = null; private model: GetAnalyticsMetricsOutputDTO | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: GetAnalyticsMetricsOutput): void { transform(result: GetAnalyticsMetricsOutput): GetAnalyticsMetricsOutputDTO {
this.model = { this.model = {
pageViews: result.pageViews, pageViews: result.pageViews,
uniqueVisitors: result.uniqueVisitors, uniqueVisitors: result.uniqueVisitors,
averageSessionDuration: result.averageSessionDuration, averageSessionDuration: result.averageSessionDuration,
bounceRate: result.bounceRate, bounceRate: result.bounceRate,
}; };
return this.model;
} }
get responseModel(): GetAnalyticsMetricsOutputDTO { get responseModel(): GetAnalyticsMetricsOutputDTO {
if (!this.model) throw new Error('Presenter not presented'); if (!this.model) throw new Error('Presenter not transformed');
return this.model; return this.model;
} }
getResponseModel(): GetAnalyticsMetricsOutputDTO { getResponseModel(): GetAnalyticsMetricsOutputDTO {
if (!this.model) throw new Error('Presenter not presented'); if (!this.model) throw new Error('Presenter not transformed');
return this.model; return this.model;
} }
} }

View File

@@ -17,7 +17,7 @@ describe('GetDashboardDataPresenter', () => {
totalLeagues: 5, totalLeagues: 5,
}; };
presenter.present(output); presenter.transform(output);
expect(presenter.getResponseModel()).toEqual({ expect(presenter.getResponseModel()).toEqual({
totalUsers: 100, totalUsers: 100,
@@ -33,11 +33,11 @@ describe('GetDashboardDataPresenter', () => {
}); });
}); });
it('getResponseModel throws if not presented', () => { it('getResponseModel throws if not transformed', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented'); expect(() => presenter.getResponseModel()).toThrow('Presenter not transformed');
}); });
it('responseModel throws if not presented', () => { it('responseModel throws if not transformed', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented'); expect(() => presenter.responseModel).toThrow('Presenter not transformed');
}); });
}); });

View File

@@ -1,30 +1,30 @@
import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase'; import type { GetDashboardDataOutput } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetDashboardDataOutputDTO } from '../dtos/GetDashboardDataOutputDTO'; import type { GetDashboardDataOutputDTO } from '../dtos/GetDashboardDataOutputDTO';
export class GetDashboardDataPresenter implements UseCaseOutputPort<GetDashboardDataOutput> { export class GetDashboardDataPresenter {
private model: GetDashboardDataOutputDTO | null = null; private model: GetDashboardDataOutputDTO | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: GetDashboardDataOutput): void { transform(result: GetDashboardDataOutput): GetDashboardDataOutputDTO {
this.model = { this.model = {
totalUsers: result.totalUsers, totalUsers: result.totalUsers,
activeUsers: result.activeUsers, activeUsers: result.activeUsers,
totalRaces: result.totalRaces, totalRaces: result.totalRaces,
totalLeagues: result.totalLeagues, totalLeagues: result.totalLeagues,
}; };
return this.model;
} }
get responseModel(): GetDashboardDataOutputDTO { get responseModel(): GetDashboardDataOutputDTO {
if (!this.model) throw new Error('Presenter not presented'); if (!this.model) throw new Error('Presenter not transformed');
return this.model; return this.model;
} }
getResponseModel(): GetDashboardDataOutputDTO { getResponseModel(): GetDashboardDataOutputDTO {
if (!this.model) throw new Error('Presenter not presented'); if (!this.model) throw new Error('Presenter not transformed');
return this.model; return this.model;
} }
} }

View File

@@ -15,23 +15,15 @@ describe('RecordEngagementPresenter', () => {
engagementWeight: 10, engagementWeight: 10,
}; };
presenter.present(output); presenter.transform(output);
expect(presenter.getResponseModel()).toEqual({
eventId: 'event-123',
engagementWeight: 10,
});
expect(presenter.responseModel).toEqual({ expect(presenter.responseModel).toEqual({
eventId: 'event-123', eventId: 'event-123',
engagementWeight: 10, engagementWeight: 10,
}); });
}); });
it('getResponseModel throws if not presented', () => { it('responseModel throws if not transformed', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented'); expect(() => presenter.responseModel).toThrow('Presenter not transformed');
});
it('responseModel throws if not presented', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
}); });
}); });

View File

@@ -1,28 +1,19 @@
import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase'; import type { RecordEngagementOutput } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO'; import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
export class RecordEngagementPresenter implements UseCaseOutputPort<RecordEngagementOutput> { export class RecordEngagementPresenter {
private model: RecordEngagementOutputDTO | null = null; private model: RecordEngagementOutputDTO | null = null;
reset(): void { transform(output: RecordEngagementOutput): RecordEngagementOutputDTO {
this.model = null;
}
present(result: RecordEngagementOutput): void {
this.model = { this.model = {
eventId: result.eventId, eventId: output.eventId,
engagementWeight: result.engagementWeight, engagementWeight: output.engagementWeight,
}; };
return this.model;
} }
get responseModel(): RecordEngagementOutputDTO { get responseModel(): RecordEngagementOutputDTO {
if (!this.model) throw new Error('Presenter not presented'); if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
getResponseModel(): RecordEngagementOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model; return this.model;
} }
} }

View File

@@ -14,21 +14,14 @@ describe('RecordPageViewPresenter', () => {
pageViewId: 'pv-123', pageViewId: 'pv-123',
}; };
presenter.present(output); presenter.transform(output);
expect(presenter.getResponseModel()).toEqual({
pageViewId: 'pv-123',
});
expect(presenter.responseModel).toEqual({ expect(presenter.responseModel).toEqual({
pageViewId: 'pv-123', pageViewId: 'pv-123',
}); });
}); });
it('getResponseModel throws if not presented', () => { it('responseModel throws if not transformed', () => {
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented'); expect(() => presenter.responseModel).toThrow('Presenter not transformed');
});
it('responseModel throws if not presented', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
}); });
}); });

View File

@@ -1,27 +1,18 @@
import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase'; import type { RecordPageViewOutput } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO'; import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
export class RecordPageViewPresenter implements UseCaseOutputPort<RecordPageViewOutput> { export class RecordPageViewPresenter {
private model: RecordPageViewOutputDTO | null = null; private model: RecordPageViewOutputDTO | null = null;
reset(): void { transform(output: RecordPageViewOutput): RecordPageViewOutputDTO {
this.model = null;
}
present(result: RecordPageViewOutput): void {
this.model = { this.model = {
pageViewId: result.pageViewId, pageViewId: output.pageViewId,
}; };
return this.model;
} }
get responseModel(): RecordPageViewOutputDTO { get responseModel(): RecordPageViewOutputDTO {
if (!this.model) throw new Error('Presenter not presented'); if (!this.model) throw new Error('Presenter not transformed');
return this.model;
}
getResponseModel(): RecordPageViewOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model; return this.model;
} }
} }

View File

@@ -1,5 +1,6 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import type { Logger } from '@core/shared/application';
import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter'; import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter';
import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase'; import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase';
import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase'; import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase';
@@ -13,13 +14,6 @@ import type { ICompanyRepository } from '@core/identity/domain/repositories/ICom
import type { IMagicLinkRepository } from '@core/identity/domain/repositories/IMagicLinkRepository'; import type { IMagicLinkRepository } from '@core/identity/domain/repositories/IMagicLinkRepository';
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService'; import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
import type { IMagicLinkNotificationPort } from '@core/identity/domain/ports/IMagicLinkNotificationPort'; import type { IMagicLinkNotificationPort } from '@core/identity/domain/ports/IMagicLinkNotificationPort';
import type { LoginResult } from '@core/identity/application/use-cases/LoginUseCase';
import type { LogoutResult } from '@core/identity/application/use-cases/LogoutUseCase';
import type { SignupResult } from '@core/identity/application/use-cases/SignupUseCase';
import type { SignupSponsorResult } from '@core/identity/application/use-cases/SignupSponsorUseCase';
import type { ForgotPasswordResult } from '@core/identity/application/use-cases/ForgotPasswordUseCase';
import type { ResetPasswordResult } from '@core/identity/application/use-cases/ResetPasswordUseCase';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { import {
AUTH_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN,
@@ -75,9 +69,8 @@ export const AuthProviders: Provider[] = [
authRepo: IAuthRepository, authRepo: IAuthRepository,
passwordHashing: IPasswordHashingService, passwordHashing: IPasswordHashingService,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<LoginResult>, ) => new LoginUseCase(authRepo, passwordHashing, logger),
) => new LoginUseCase(authRepo, passwordHashing, logger, output), inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: SIGNUP_USE_CASE_TOKEN, provide: SIGNUP_USE_CASE_TOKEN,
@@ -85,9 +78,8 @@ export const AuthProviders: Provider[] = [
authRepo: IAuthRepository, authRepo: IAuthRepository,
passwordHashing: IPasswordHashingService, passwordHashing: IPasswordHashingService,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<SignupResult>, ) => new SignupUseCase(authRepo, passwordHashing, logger),
) => new SignupUseCase(authRepo, passwordHashing, logger, output), inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: SIGNUP_SPONSOR_USE_CASE_TOKEN, provide: SIGNUP_SPONSOR_USE_CASE_TOKEN,
@@ -96,15 +88,14 @@ export const AuthProviders: Provider[] = [
companyRepo: ICompanyRepository, companyRepo: ICompanyRepository,
passwordHashing: IPasswordHashingService, passwordHashing: IPasswordHashingService,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<SignupSponsorResult>, ) => new SignupSponsorUseCase(authRepo, companyRepo, passwordHashing, logger),
) => new SignupSponsorUseCase(authRepo, companyRepo, passwordHashing, logger, output), inject: [AUTH_REPOSITORY_TOKEN, COMPANY_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
inject: [AUTH_REPOSITORY_TOKEN, COMPANY_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, SIGNUP_SPONSOR_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: LOGOUT_USE_CASE_TOKEN, provide: LOGOUT_USE_CASE_TOKEN,
useFactory: (sessionPort: IdentitySessionPort, logger: Logger, output: UseCaseOutputPort<LogoutResult>) => useFactory: (sessionPort: IdentitySessionPort, logger: Logger) =>
new LogoutUseCase(sessionPort, logger, output), new LogoutUseCase(sessionPort, logger),
inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_OUTPUT_PORT_TOKEN], inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN],
}, },
ForgotPasswordPresenter, ForgotPasswordPresenter,
ResetPasswordPresenter, ResetPasswordPresenter,
@@ -132,9 +123,8 @@ export const AuthProviders: Provider[] = [
magicLinkRepo: IMagicLinkRepository, magicLinkRepo: IMagicLinkRepository,
notificationPort: IMagicLinkNotificationPort, notificationPort: IMagicLinkNotificationPort,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<ForgotPasswordResult>, ) => new ForgotPasswordUseCase(authRepo, magicLinkRepo, notificationPort, logger),
) => new ForgotPasswordUseCase(authRepo, magicLinkRepo, notificationPort, logger, output), inject: [AUTH_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN, MAGIC_LINK_NOTIFICATION_PORT_TOKEN, LOGGER_TOKEN],
inject: [AUTH_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN, MAGIC_LINK_NOTIFICATION_PORT_TOKEN, LOGGER_TOKEN, FORGOT_PASSWORD_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: RESET_PASSWORD_USE_CASE_TOKEN, provide: RESET_PASSWORD_USE_CASE_TOKEN,
@@ -143,8 +133,7 @@ export const AuthProviders: Provider[] = [
magicLinkRepo: IMagicLinkRepository, magicLinkRepo: IMagicLinkRepository,
passwordHashing: IPasswordHashingService, passwordHashing: IPasswordHashingService,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<ResetPasswordResult>, ) => new ResetPasswordUseCase(authRepo, magicLinkRepo, passwordHashing, logger),
) => new ResetPasswordUseCase(authRepo, magicLinkRepo, passwordHashing, logger, output), inject: [AUTH_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
inject: [AUTH_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, RESET_PASSWORD_OUTPUT_PORT_TOKEN],
}, },
]; ];

View File

@@ -87,8 +87,7 @@ describe('AuthService', () => {
const signupUseCase = { const signupUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
authSessionPresenter.present({ userId: 'u2', email: 'e2', displayName: 'Jane Smith' }); return Result.ok({ userId: 'u2', email: 'e2', displayName: 'Jane Smith' });
return Result.ok(undefined);
}), }),
}; };
@@ -156,8 +155,7 @@ describe('AuthService', () => {
const loginUseCase = { const loginUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
authSessionPresenter.present({ userId: 'u3', email: 'e3', displayName: 'Bob Wilson' }); return Result.ok({ userId: 'u3', email: 'e3', displayName: 'Bob Wilson' });
return Result.ok(undefined);
}), }),
}; };
@@ -234,8 +232,7 @@ describe('AuthService', () => {
const commandResultPresenter = new FakeCommandResultPresenter(); const commandResultPresenter = new FakeCommandResultPresenter();
const logoutUseCase = { const logoutUseCase = {
execute: vi.fn(async () => { execute: vi.fn(async () => {
commandResultPresenter.present({ success: true }); return Result.ok({ success: true });
return Result.ok(undefined);
}), }),
}; };

View File

@@ -116,8 +116,6 @@ export class AuthService {
async signupWithEmail(params: SignupParamsDTO): Promise<AuthSessionDTO> { async signupWithEmail(params: SignupParamsDTO): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`); this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
this.authSessionPresenter.reset();
const input: SignupInput = { const input: SignupInput = {
email: params.email, email: params.email,
password: params.password, password: params.password,
@@ -131,6 +129,9 @@ export class AuthService {
throw new Error(mapApplicationErrorToMessage(error, 'Signup failed')); throw new Error(mapApplicationErrorToMessage(error, 'Signup failed'));
} }
const signupResult = result.unwrap();
this.authSessionPresenter.present(signupResult);
const userDTO = this.authSessionPresenter.responseModel; const userDTO = this.authSessionPresenter.responseModel;
const inferredRole = inferDemoRoleFromEmail(userDTO.email); const inferredRole = inferDemoRoleFromEmail(userDTO.email);
const session = await this.identitySessionPort.createSession({ const session = await this.identitySessionPort.createSession({
@@ -149,8 +150,6 @@ export class AuthService {
async signupSponsor(params: SignupSponsorParamsDTO): Promise<AuthSessionDTO> { async signupSponsor(params: SignupSponsorParamsDTO): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting sponsor signup for email: ${params.email}`); this.logger.debug(`[AuthService] Attempting sponsor signup for email: ${params.email}`);
this.authSessionPresenter.reset();
const input: SignupSponsorInput = { const input: SignupSponsorInput = {
email: params.email, email: params.email,
password: params.password, password: params.password,
@@ -165,6 +164,9 @@ export class AuthService {
throw new Error(mapApplicationErrorToMessage(error, 'Sponsor signup failed')); throw new Error(mapApplicationErrorToMessage(error, 'Sponsor signup failed'));
} }
const signupResult = result.unwrap();
this.authSessionPresenter.present(signupResult);
const userDTO = this.authSessionPresenter.responseModel; const userDTO = this.authSessionPresenter.responseModel;
const inferredRole = inferDemoRoleFromEmail(userDTO.email); const inferredRole = inferDemoRoleFromEmail(userDTO.email);
const session = await this.identitySessionPort.createSession({ const session = await this.identitySessionPort.createSession({
@@ -183,8 +185,6 @@ export class AuthService {
async loginWithEmail(params: LoginParamsDTO): Promise<AuthSessionDTO> { async loginWithEmail(params: LoginParamsDTO): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`); this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
this.authSessionPresenter.reset();
const input: LoginInput = { const input: LoginInput = {
email: params.email, email: params.email,
password: params.password, password: params.password,
@@ -197,6 +197,9 @@ export class AuthService {
throw new Error(mapApplicationErrorToMessage(error, 'Login failed')); throw new Error(mapApplicationErrorToMessage(error, 'Login failed'));
} }
const loginResult = result.unwrap();
this.authSessionPresenter.present(loginResult);
const userDTO = this.authSessionPresenter.responseModel; const userDTO = this.authSessionPresenter.responseModel;
const sessionOptions = params.rememberMe !== undefined const sessionOptions = params.rememberMe !== undefined
? { rememberMe: params.rememberMe } ? { rememberMe: params.rememberMe }
@@ -223,8 +226,6 @@ export class AuthService {
async logout(): Promise<CommandResultDTO> { async logout(): Promise<CommandResultDTO> {
this.logger.debug('[AuthService] Attempting logout.'); this.logger.debug('[AuthService] Attempting logout.');
this.commandResultPresenter.reset();
const result = await this.logoutUseCase.execute(); const result = await this.logoutUseCase.execute();
if (result.isErr()) { if (result.isErr()) {
@@ -232,6 +233,9 @@ export class AuthService {
throw new Error(mapApplicationErrorToMessage(error, 'Logout failed')); throw new Error(mapApplicationErrorToMessage(error, 'Logout failed'));
} }
const logoutResult = result.unwrap();
this.commandResultPresenter.present(logoutResult);
return this.commandResultPresenter.responseModel; return this.commandResultPresenter.responseModel;
} }
@@ -285,8 +289,6 @@ export class AuthService {
async forgotPassword(params: { email: string }): Promise<{ message: string; magicLink?: string }> { async forgotPassword(params: { email: string }): Promise<{ message: string; magicLink?: string }> {
this.logger.debug(`[AuthService] Attempting forgot password for email: ${params.email}`); this.logger.debug(`[AuthService] Attempting forgot password for email: ${params.email}`);
this.forgotPasswordPresenter.reset();
const input: ForgotPasswordInput = { const input: ForgotPasswordInput = {
email: params.email, email: params.email,
}; };
@@ -298,6 +300,9 @@ export class AuthService {
throw new Error(mapApplicationErrorToMessage(error, 'Forgot password failed')); throw new Error(mapApplicationErrorToMessage(error, 'Forgot password failed'));
} }
const forgotPasswordResult = executeResult.unwrap();
this.forgotPasswordPresenter.present(forgotPasswordResult);
const response = this.forgotPasswordPresenter.responseModel; const response = this.forgotPasswordPresenter.responseModel;
const result: { message: string; magicLink?: string } = { const result: { message: string; magicLink?: string } = {
message: response.message, message: response.message,
@@ -311,8 +316,6 @@ export class AuthService {
async resetPassword(params: { token: string; newPassword: string }): Promise<{ message: string }> { async resetPassword(params: { token: string; newPassword: string }): Promise<{ message: string }> {
this.logger.debug('[AuthService] Attempting reset password'); this.logger.debug('[AuthService] Attempting reset password');
this.resetPasswordPresenter.reset();
const input: ResetPasswordInput = { const input: ResetPasswordInput = {
token: params.token, token: params.token,
newPassword: params.newPassword, newPassword: params.newPassword,
@@ -325,6 +328,9 @@ export class AuthService {
throw new Error(mapApplicationErrorToMessage(error, 'Reset password failed')); throw new Error(mapApplicationErrorToMessage(error, 'Reset password failed'));
} }
const resetResult = result.unwrap();
this.resetPasswordPresenter.present(resetResult);
return this.resetPasswordPresenter.responseModel; return this.resetPasswordPresenter.responseModel;
} }
} }

View File

@@ -4,16 +4,14 @@ import { ACHIEVEMENT_REPOSITORY_TOKEN } from '../../persistence/achievement/Achi
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData'; import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import type { RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData'; import type { RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData';
import { SeedDemoUsers } from '../../../../../adapters/bootstrap/SeedDemoUsers'; import { SeedDemoUsers } from '../../../../../adapters/bootstrap/SeedDemoUsers';
import { SignupWithEmailUseCase, type SignupWithEmailResult } from '@core/identity/application/use-cases/SignupWithEmailUseCase'; import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
import { import {
CreateAchievementUseCase, CreateAchievementUseCase,
type CreateAchievementResult,
type IAchievementRepository, type IAchievementRepository,
} from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase'; } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase';
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository'; import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
import type { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort'; import type { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { CookieIdentitySessionAdapter } from '../../../../../adapters/identity/session/CookieIdentitySessionAdapter'; import { CookieIdentitySessionAdapter } from '../../../../../adapters/identity/session/CookieIdentitySessionAdapter';
import { USER_REPOSITORY_TOKEN as IDENTITY_USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN } from '../../persistence/identity/IdentityPersistenceTokens'; import { USER_REPOSITORY_TOKEN as IDENTITY_USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN } from '../../persistence/identity/IdentityPersistenceTokens';
import { ADMIN_USER_REPOSITORY_TOKEN } from '../../persistence/admin/AdminPersistenceTokens'; import { ADMIN_USER_REPOSITORY_TOKEN } from '../../persistence/admin/AdminPersistenceTokens';
@@ -29,21 +27,6 @@ export const RACING_SEED_DEPENDENCIES_TOKEN = 'RacingSeedDependencies';
export const ENSURE_INITIAL_DATA_TOKEN = 'EnsureInitialData_Bootstrap'; export const ENSURE_INITIAL_DATA_TOKEN = 'EnsureInitialData_Bootstrap';
export const SEED_DEMO_USERS_TOKEN = 'SeedDemoUsers'; export const SEED_DEMO_USERS_TOKEN = 'SeedDemoUsers';
// Adapter classes for output ports
class SignupWithEmailOutputAdapter implements UseCaseOutputPort<SignupWithEmailResult> {
present(result: SignupWithEmailResult): void {
// Bootstrap doesn't need to handle output, just log success
console.log('[Bootstrap] Signup completed', result);
}
}
class CreateAchievementOutputAdapter implements UseCaseOutputPort<CreateAchievementResult> {
present(result: CreateAchievementResult): void {
// Bootstrap doesn't need to handle output, just log success
console.log('[Bootstrap] Achievement created', result);
}
}
export const BootstrapProviders: Provider[] = [ export const BootstrapProviders: Provider[] = [
{ {
provide: RACING_SEED_DEPENDENCIES_TOKEN, provide: RACING_SEED_DEPENDENCIES_TOKEN,
@@ -152,8 +135,7 @@ export const BootstrapProviders: Provider[] = [
return new SignupWithEmailUseCase( return new SignupWithEmailUseCase(
userRepository, userRepository,
sessionPort, sessionPort,
logger, logger
new SignupWithEmailOutputAdapter()
); );
}, },
inject: [USER_REPOSITORY_TOKEN, IDENTITY_SESSION_PORT_TOKEN, 'Logger'], inject: [USER_REPOSITORY_TOKEN, IDENTITY_SESSION_PORT_TOKEN, 'Logger'],
@@ -166,8 +148,7 @@ export const BootstrapProviders: Provider[] = [
) => { ) => {
return new CreateAchievementUseCase( return new CreateAchievementUseCase(
achievementRepository, achievementRepository,
logger, logger
new CreateAchievementOutputAdapter()
); );
}, },
inject: [ACHIEVEMENT_REPOSITORY_TOKEN, 'Logger'], inject: [ACHIEVEMENT_REPOSITORY_TOKEN, 'Logger'],

View File

@@ -3,7 +3,6 @@ import { DashboardModule } from './DashboardModule';
import { DashboardController } from './DashboardController'; import { DashboardController } from './DashboardController';
import { DashboardService } from './DashboardService'; import { DashboardService } from './DashboardService';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
import { DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN } from './DashboardProviders';
describe('DashboardModule', () => { describe('DashboardModule', () => {
let module: TestingModule; let module: TestingModule;
@@ -30,8 +29,8 @@ describe('DashboardModule', () => {
expect(service).toBeInstanceOf(DashboardService); expect(service).toBeInstanceOf(DashboardService);
}); });
it('should bind DashboardOverviewPresenter as the output port for the use case', () => { it('should provide DashboardOverviewPresenter', () => {
const presenter = module.get<DashboardOverviewPresenter>(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN); const presenter = module.get<DashboardOverviewPresenter>(DashboardOverviewPresenter);
expect(presenter).toBeDefined(); expect(presenter).toBeDefined();
expect(presenter).toBeInstanceOf(DashboardOverviewPresenter); expect(presenter).toBeInstanceOf(DashboardOverviewPresenter);
}); });

View File

@@ -21,7 +21,6 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
import { import {
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
DASHBOARD_OVERVIEW_USE_CASE_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN, IMAGE_SERVICE_TOKEN,
@@ -36,7 +35,6 @@ import {
// Re-export tokens for convenience (legacy imports) // Re-export tokens for convenience (legacy imports)
export { export {
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
DASHBOARD_OVERVIEW_USE_CASE_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN, IMAGE_SERVICE_TOKEN,
@@ -60,10 +58,6 @@ export const DashboardProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger), useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
inject: [LOGGER_TOKEN], inject: [LOGGER_TOKEN],
}, },
{
provide: DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
useExisting: DashboardOverviewPresenter,
},
{ {
provide: DASHBOARD_OVERVIEW_USE_CASE_TOKEN, provide: DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
useFactory: ( useFactory: (
@@ -77,7 +71,6 @@ export const DashboardProviders: Provider[] = [
feedRepo: IFeedRepository, feedRepo: IFeedRepository,
socialRepo: ISocialGraphRepository, socialRepo: ISocialGraphRepository,
imageService: ImageServicePort, imageService: ImageServicePort,
output: DashboardOverviewPresenter,
) => ) =>
new DashboardOverviewUseCase( new DashboardOverviewUseCase(
driverRepo, driverRepo,
@@ -91,7 +84,6 @@ export const DashboardProviders: Provider[] = [
socialRepo, socialRepo,
async (driverId: string) => imageService.getDriverAvatar(driverId), async (driverId: string) => imageService.getDriverAvatar(driverId),
() => null, () => null,
output,
), ),
inject: [ inject: [
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
@@ -104,7 +96,6 @@ export const DashboardProviders: Provider[] = [
SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN, IMAGE_SERVICE_TOKEN,
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
], ],
}, },
]; ];

View File

@@ -4,8 +4,9 @@ import { DashboardService } from './DashboardService';
describe('DashboardService', () => { describe('DashboardService', () => {
it('getDashboardOverview returns presenter model on success', async () => { it('getDashboardOverview returns presenter model on success', async () => {
const presenter = { getResponseModel: vi.fn(() => ({ feed: [] })) }; const mockResult = { currentDriver: null, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 0, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [] }, friends: [] };
const useCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const presenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ feed: [] })) };
const useCase = { execute: vi.fn(async () => Result.ok(mockResult)) };
const service = new DashboardService( const service = new DashboardService(
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any, { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
@@ -15,13 +16,14 @@ describe('DashboardService', () => {
await expect(service.getDashboardOverview('d1')).resolves.toEqual({ feed: [] }); await expect(service.getDashboardOverview('d1')).resolves.toEqual({ feed: [] });
expect(useCase.execute).toHaveBeenCalledWith({ driverId: 'd1' }); expect(useCase.execute).toHaveBeenCalledWith({ driverId: 'd1' });
expect(presenter.present).toHaveBeenCalledWith(mockResult);
}); });
it('getDashboardOverview throws with details message on error', async () => { it('getDashboardOverview throws with details message on error', async () => {
const service = new DashboardService( const service = new DashboardService(
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any, { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any, { execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any,
{ getResponseModel: vi.fn() } as any, { present: vi.fn(), getResponseModel: vi.fn() } as any,
); );
await expect(service.getDashboardOverview('d1')).rejects.toThrow('Failed to get dashboard overview: boom'); await expect(service.getDashboardOverview('d1')).rejects.toThrow('Failed to get dashboard overview: boom');
@@ -31,7 +33,7 @@ describe('DashboardService', () => {
const service = new DashboardService( const service = new DashboardService(
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any, { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any, { execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
{ getResponseModel: vi.fn() } as any, { present: vi.fn(), getResponseModel: vi.fn() } as any,
); );
await expect(service.getDashboardOverview('d1')).rejects.toThrow('Failed to get dashboard overview: Unknown error'); await expect(service.getDashboardOverview('d1')).rejects.toThrow('Failed to get dashboard overview: Unknown error');

View File

@@ -1,5 +1,5 @@
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO'; import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter'; import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
@@ -8,7 +8,6 @@ import type { Logger } from '@core/shared/application/Logger';
// Tokens (standalone to avoid circular imports) // Tokens (standalone to avoid circular imports)
import { import {
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
DASHBOARD_OVERVIEW_USE_CASE_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
} from './DashboardTokens'; } from './DashboardTokens';
@@ -18,7 +17,7 @@ export class DashboardService {
constructor( constructor(
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase, @Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
@Inject(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN) private readonly presenter: DashboardOverviewPresenter, private readonly presenter: DashboardOverviewPresenter,
) {} ) {}
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> { async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
@@ -29,9 +28,11 @@ export class DashboardService {
if (result.isErr()) { if (result.isErr()) {
const error = result.error; const error = result.error;
const message = error?.details?.message || 'Unknown error'; const message = error?.details?.message || 'Unknown error';
throw new Error(`Failed to get dashboard overview: ${message}`); throw new NotFoundException(`Failed to get dashboard overview: ${message}`);
} }
// Present the result
this.presenter.present(result.unwrap());
return this.presenter.getResponseModel(); return this.presenter.getResponseModel();
} }
} }

View File

@@ -12,5 +12,4 @@ export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository'; export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort'; export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase'; export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase';
export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort';

View File

@@ -1,4 +1,3 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { import type {
DashboardOverviewResult, DashboardOverviewResult,
} from '@core/racing/application/use-cases/DashboardOverviewUseCase'; } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
@@ -13,7 +12,7 @@ import {
DashboardFriendSummaryDTO, DashboardFriendSummaryDTO,
} from '../dtos/DashboardOverviewDTO'; } from '../dtos/DashboardOverviewDTO';
export class DashboardOverviewPresenter implements UseCaseOutputPort<DashboardOverviewResult> { export class DashboardOverviewPresenter {
private responseModel: DashboardOverviewDTO | null = null; private responseModel: DashboardOverviewDTO | null = null;
present(data: DashboardOverviewResult): void { present(data: DashboardOverviewResult): void {

View File

@@ -6,7 +6,7 @@ import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepos
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository'; import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository'; import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
@@ -65,12 +65,6 @@ import {
IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN,
GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN,
COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN,
IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN,
UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN,
GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN,
DRIVER_STATS_REPOSITORY_TOKEN, DRIVER_STATS_REPOSITORY_TOKEN,
MEDIA_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN,
RANKING_SERVICE_TOKEN, RANKING_SERVICE_TOKEN,
@@ -119,32 +113,6 @@ export const DriverProviders: Provider[] = createLoggedProviders([
inject: [MEDIA_RESOLVER_TOKEN], inject: [MEDIA_RESOLVER_TOKEN],
}, },
// Output ports (point to presenters)
{
provide: GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN,
useExisting: DriversLeaderboardPresenter,
},
{
provide: GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN,
useExisting: DriverStatsPresenter,
},
{
provide: COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN,
useExisting: CompleteOnboardingPresenter,
},
{
provide: IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN,
useExisting: DriverRegistrationStatusPresenter,
},
{
provide: UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN,
useExisting: DriverPresenter,
},
{
provide: GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN,
useExisting: DriverProfilePresenter,
},
// Logger // Logger
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
@@ -230,37 +198,35 @@ export const DriverProviders: Provider[] = createLoggedProviders([
rankingUseCase: IRankingUseCase, rankingUseCase: IRankingUseCase,
driverStatsUseCase: IDriverStatsUseCase, driverStatsUseCase: IDriverStatsUseCase,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<unknown>,
) => new GetDriversLeaderboardUseCase( ) => new GetDriversLeaderboardUseCase(
driverRepo, driverRepo,
rankingUseCase, rankingUseCase,
driverStatsUseCase, driverStatsUseCase,
logger, logger
output
), ),
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, LOGGER_TOKEN, GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN, provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, output: UseCaseOutputPort<unknown>) => new GetTotalDriversUseCase(driverRepo, output), useFactory: (driverRepo: IDriverRepository) => new GetTotalDriversUseCase(driverRepo),
inject: [DRIVER_REPOSITORY_TOKEN, GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN],
}, },
{ {
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, logger: Logger, output: UseCaseOutputPort<unknown>) => new CompleteDriverOnboardingUseCase(driverRepo, logger, output), useFactory: (driverRepo: IDriverRepository, logger: Logger) => new CompleteDriverOnboardingUseCase(driverRepo, logger),
inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger, output: UseCaseOutputPort<unknown>) => useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) =>
new IsDriverRegisteredForRaceUseCase(registrationRepo, logger, output), new IsDriverRegisteredForRaceUseCase(registrationRepo, logger),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN], inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, logger: Logger, output: UseCaseOutputPort<unknown>) => useFactory: (driverRepo: IDriverRepository, logger: Logger) =>
new UpdateDriverProfileUseCase(driverRepo, logger, output), new UpdateDriverProfileUseCase(driverRepo, logger),
inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
@@ -272,7 +238,6 @@ export const DriverProviders: Provider[] = createLoggedProviders([
driverExtendedProfileProvider: DriverExtendedProfileProvider, driverExtendedProfileProvider: DriverExtendedProfileProvider,
driverStatsUseCase: IDriverStatsUseCase, driverStatsUseCase: IDriverStatsUseCase,
rankingUseCase: IRankingUseCase, rankingUseCase: IRankingUseCase,
output: UseCaseOutputPort<unknown>,
) => ) =>
new GetProfileOverviewUseCase( new GetProfileOverviewUseCase(
driverRepo, driverRepo,
@@ -282,7 +247,6 @@ export const DriverProviders: Provider[] = createLoggedProviders([
driverExtendedProfileProvider, driverExtendedProfileProvider,
driverStatsUseCase, driverStatsUseCase,
rankingUseCase, rankingUseCase,
output,
), ),
inject: [ inject: [
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
@@ -292,7 +256,6 @@ export const DriverProviders: Provider[] = createLoggedProviders([
DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN, DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN,
DRIVER_STATS_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN,
RANKING_SERVICE_TOKEN, RANKING_SERVICE_TOKEN,
GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN,
], ],
}, },
], initLogger); ], initLogger);

View File

@@ -143,32 +143,6 @@ export const GET_LEAGUE_WALLET_USE_CASE = 'GetLeagueWalletUseCase';
export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUseCase'; export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUseCase';
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase'; export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityAndScoringOutputPort_TOKEN';
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
export const LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN = 'ListLeagueScoringPresetsOutputPort_TOKEN';
export const APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN = 'ApproveLeagueJoinRequestOutputPort_TOKEN';
export const CREATE_LEAGUE_OUTPUT_PORT_TOKEN = 'CreateLeagueOutputPort_TOKEN';
export const GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN = 'GetLeagueAdminPermissionsOutputPort_TOKEN';
export const GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN = 'GetLeagueMembershipsOutputPort_TOKEN';
export const GET_LEAGUE_ROSTER_MEMBERS_OUTPUT_PORT_TOKEN = 'GetLeagueRosterMembersOutputPort_TOKEN';
export const GET_LEAGUE_ROSTER_JOIN_REQUESTS_OUTPUT_PORT_TOKEN = 'GetLeagueRosterJoinRequestsOutputPort_TOKEN';
export const GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN = 'GetLeagueOwnerSummaryOutputPort_TOKEN';
export const GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN = 'GetLeagueSeasonsOutputPort_TOKEN';
export const JOIN_LEAGUE_OUTPUT_PORT_TOKEN = 'JoinLeagueOutputPort_TOKEN';
export const GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN = 'GetLeagueScheduleOutputPort_TOKEN';
export const GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN = 'GetLeagueStatsOutputPort_TOKEN';
export const REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN = 'RejectLeagueJoinRequestOutputPort_TOKEN';
export const REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN = 'RemoveLeagueMemberOutputPort_TOKEN';
export const TOTAL_LEAGUES_OUTPUT_PORT_TOKEN = 'TotalLeaguesOutputPort_TOKEN';
export const TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN = 'TransferLeagueOwnershipOutputPort_TOKEN';
export const UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN = 'UpdateLeagueMemberRoleOutputPort_TOKEN';
export const GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN = 'GetLeagueFullConfigOutputPort_TOKEN';
export const GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN = 'GetLeagueScoringConfigOutputPort_TOKEN';
export const GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN = 'GetLeagueWalletOutputPort_TOKEN';
export const WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN = 'WithdrawFromLeagueWalletOutputPort_TOKEN';
export const LeagueProviders: Provider[] = [ export const LeagueProviders: Provider[] = [
LeagueService, LeagueService,
@@ -227,133 +201,6 @@ export const LeagueProviders: Provider[] = [
DeleteLeagueSeasonScheduleRacePresenter, DeleteLeagueSeasonScheduleRacePresenter,
PublishLeagueSeasonSchedulePresenter, PublishLeagueSeasonSchedulePresenter,
UnpublishLeagueSeasonSchedulePresenter, UnpublishLeagueSeasonSchedulePresenter,
// Output ports
{
provide: GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
useExisting: AllLeaguesWithCapacityPresenter,
},
{
provide: GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
useExisting: AllLeaguesWithCapacityAndScoringPresenter,
},
{
provide: GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
useExisting: LeagueStandingsPresenter,
},
{
provide: GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueProtestsPresenter,
},
{
provide: GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
useExisting: GetSeasonSponsorshipsPresenter,
},
{
provide: LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN,
useExisting: LeagueScoringPresetsPresenter,
},
{
provide: APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN,
useExisting: ApproveLeagueJoinRequestPresenter,
},
{
provide: CREATE_LEAGUE_OUTPUT_PORT_TOKEN,
useExisting: CreateLeaguePresenter,
},
{
provide: GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueAdminPermissionsPresenter,
},
{
provide: GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueMembershipsPresenter,
},
{
provide: GET_LEAGUE_ROSTER_MEMBERS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueRosterMembersPresenter,
},
{
provide: GET_LEAGUE_ROSTER_JOIN_REQUESTS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueRosterJoinRequestsPresenter,
},
{
provide: GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueOwnerSummaryPresenter,
},
{
provide: GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueSeasonsPresenter,
},
{
provide: JOIN_LEAGUE_OUTPUT_PORT_TOKEN,
useExisting: JoinLeaguePresenter,
},
{
provide: GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN,
useExisting: LeagueSchedulePresenter,
},
{
provide: GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN,
useExisting: LeagueStatsPresenter,
},
{
provide: REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN,
useExisting: RejectLeagueJoinRequestPresenter,
},
{
provide: REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN,
useExisting: RemoveLeagueMemberPresenter,
},
{
provide: TOTAL_LEAGUES_OUTPUT_PORT_TOKEN,
useExisting: TotalLeaguesPresenter,
},
{
provide: TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN,
useExisting: TransferLeagueOwnershipPresenter,
},
{
provide: UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN,
useExisting: UpdateLeagueMemberRolePresenter,
},
{
provide: GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN,
useExisting: LeagueConfigPresenter,
},
{
provide: GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN,
useExisting: LeagueScoringConfigPresenter,
},
{
provide: GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueWalletPresenter,
},
{
provide: WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
useExisting: WithdrawFromLeagueWalletPresenter,
},
// Schedule mutation output ports
{
provide: LeagueTokens.CREATE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN,
useExisting: CreateLeagueSeasonScheduleRacePresenter,
},
{
provide: LeagueTokens.UPDATE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN,
useExisting: UpdateLeagueSeasonScheduleRacePresenter,
},
{
provide: LeagueTokens.DELETE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN,
useExisting: DeleteLeagueSeasonScheduleRacePresenter,
},
{
provide: LeagueTokens.PUBLISH_LEAGUE_SEASON_SCHEDULE_OUTPUT_PORT_TOKEN,
useExisting: PublishLeagueSeasonSchedulePresenter,
},
{
provide: LeagueTokens.UNPUBLISH_LEAGUE_SEASON_SCHEDULE_OUTPUT_PORT_TOKEN,
useExisting: UnpublishLeagueSeasonSchedulePresenter,
},
// Use cases // Use cases
{ {
@@ -370,7 +217,6 @@ export const LeagueProviders: Provider[] = [
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
scoringRepo: import('@core/racing/domain/repositories/ILeagueScoringConfigRepository').ILeagueScoringConfigRepository, scoringRepo: import('@core/racing/domain/repositories/ILeagueScoringConfigRepository').ILeagueScoringConfigRepository,
gameRepo: import('@core/racing/domain/repositories/IGameRepository').IGameRepository, gameRepo: import('@core/racing/domain/repositories/IGameRepository').IGameRepository,
output: AllLeaguesWithCapacityAndScoringPresenter,
) => ) =>
new GetAllLeaguesWithCapacityAndScoringUseCase( new GetAllLeaguesWithCapacityAndScoringUseCase(
leagueRepo, leagueRepo,
@@ -379,7 +225,6 @@ export const LeagueProviders: Provider[] = [
scoringRepo, scoringRepo,
gameRepo, gameRepo,
{ getPresetById: getLeagueScoringPresetById }, { getPresetById: getLeagueScoringPresetById },
output,
), ),
inject: [ inject: [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
@@ -387,7 +232,6 @@ export const LeagueProviders: Provider[] = [
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN, LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
GAME_REPOSITORY_TOKEN, GAME_REPOSITORY_TOKEN,
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -395,12 +239,10 @@ export const LeagueProviders: Provider[] = [
useFactory: ( useFactory: (
standingRepo: IStandingRepository, standingRepo: IStandingRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
output: LeagueStandingsPresenter, ) => new GetLeagueStandingsUseCase(standingRepo, driverRepo),
) => new GetLeagueStandingsUseCase(standingRepo, driverRepo, output),
inject: [ inject: [
STANDING_REPOSITORY_TOKEN, STANDING_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -421,9 +263,9 @@ export const LeagueProviders: Provider[] = [
}, },
{ {
provide: GET_TOTAL_LEAGUES_USE_CASE, provide: GET_TOTAL_LEAGUES_USE_CASE,
useFactory: (leagueRepo: ILeagueRepository, output: TotalLeaguesPresenter) => useFactory: (leagueRepo: ILeagueRepository) =>
new GetTotalLeaguesUseCase(leagueRepo, output), new GetTotalLeaguesUseCase(leagueRepo),
inject: [LEAGUE_REPOSITORY_TOKEN, TOTAL_LEAGUES_OUTPUT_PORT_TOKEN], inject: [LEAGUE_REPOSITORY_TOKEN],
}, },
{ {
provide: GET_LEAGUE_JOIN_REQUESTS_USE_CASE, provide: GET_LEAGUE_JOIN_REQUESTS_USE_CASE,
@@ -431,9 +273,8 @@ export const LeagueProviders: Provider[] = [
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
output: LeagueJoinRequestsPresenter, ) => new GetLeagueJoinRequestsUseCase(membershipRepo, driverRepo, leagueRepo),
) => new GetLeagueJoinRequestsUseCase(membershipRepo, driverRepo, leagueRepo, output), inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LeagueJoinRequestsPresenter],
}, },
{ {
provide: APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE, provide: APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE,
@@ -453,21 +294,30 @@ export const LeagueProviders: Provider[] = [
provide: REMOVE_LEAGUE_MEMBER_USE_CASE, provide: REMOVE_LEAGUE_MEMBER_USE_CASE,
useFactory: ( useFactory: (
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
output: RemoveLeagueMemberPresenter, ) => new RemoveLeagueMemberUseCase(membershipRepo),
) => new RemoveLeagueMemberUseCase(membershipRepo, output), inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: UPDATE_LEAGUE_MEMBER_ROLE_USE_CASE, provide: UPDATE_LEAGUE_MEMBER_ROLE_USE_CASE,
useFactory: ( useFactory: (
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
output: UpdateLeagueMemberRolePresenter, ) => new UpdateLeagueMemberRoleUseCase(membershipRepo),
) => new UpdateLeagueMemberRoleUseCase(membershipRepo, output), inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_LEAGUE_OWNER_SUMMARY_USE_CASE, provide: GET_LEAGUE_OWNER_SUMMARY_USE_CASE,
useClass: GetLeagueOwnerSummaryUseCase, useFactory: (
leagueRepo: ILeagueRepository,
driverRepo: IDriverRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
standingRepo: IStandingRepository,
) => new GetLeagueOwnerSummaryUseCase(leagueRepo, driverRepo, leagueMembershipRepo, standingRepo),
inject: [
LEAGUE_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
],
}, },
{ {
provide: GET_LEAGUE_PROTESTS_USE_CASE, provide: GET_LEAGUE_PROTESTS_USE_CASE,
@@ -476,14 +326,12 @@ export const LeagueProviders: Provider[] = [
protestRepo: IProtestRepository, protestRepo: IProtestRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
output: GetLeagueProtestsPresenter, ) => new GetLeagueProtestsUseCase(raceRepo, protestRepo, driverRepo, leagueRepo),
) => new GetLeagueProtestsUseCase(raceRepo, protestRepo, driverRepo, leagueRepo, output),
inject: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
PROTEST_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -496,9 +344,8 @@ export const LeagueProviders: Provider[] = [
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
output: GetLeagueMembershipsPresenter, ) => new GetLeagueMembershipsUseCase(membershipRepo, driverRepo, leagueRepo),
) => new GetLeagueMembershipsUseCase(membershipRepo, driverRepo, leagueRepo, output), inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, GetLeagueMembershipsPresenter],
}, },
{ {
provide: GET_LEAGUE_ROSTER_MEMBERS_USE_CASE, provide: GET_LEAGUE_ROSTER_MEMBERS_USE_CASE,
@@ -506,13 +353,11 @@ export const LeagueProviders: Provider[] = [
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
output: GetLeagueRosterMembersPresenter, ) => new GetLeagueRosterMembersUseCase(membershipRepo, driverRepo, leagueRepo),
) => new GetLeagueRosterMembersUseCase(membershipRepo, driverRepo, leagueRepo, output),
inject: [ inject: [
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
GET_LEAGUE_ROSTER_MEMBERS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -521,13 +366,11 @@ export const LeagueProviders: Provider[] = [
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
output: GetLeagueRosterJoinRequestsPresenter, ) => new GetLeagueRosterJoinRequestsUseCase(membershipRepo, driverRepo, leagueRepo),
) => new GetLeagueRosterJoinRequestsUseCase(membershipRepo, driverRepo, leagueRepo, output),
inject: [ inject: [
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
GET_LEAGUE_ROSTER_JOIN_REQUESTS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -537,14 +380,12 @@ export const LeagueProviders: Provider[] = [
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger, logger: Logger,
output: LeagueSchedulePresenter, ) => new GetLeagueScheduleUseCase(leagueRepo, seasonRepo, raceRepo, logger),
) => new GetLeagueScheduleUseCase(leagueRepo, seasonRepo, raceRepo, logger, output),
inject: [ inject: [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -553,13 +394,11 @@ export const LeagueProviders: Provider[] = [
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
output: GetLeagueAdminPermissionsPresenter, ) => new GetLeagueAdminPermissionsUseCase(leagueRepo, leagueMembershipRepo, logger),
) => new GetLeagueAdminPermissionsUseCase(leagueRepo, leagueMembershipRepo, logger, output),
inject: [ inject: [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -568,13 +407,11 @@ export const LeagueProviders: Provider[] = [
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
walletRepo: ILeagueWalletRepository, walletRepo: ILeagueWalletRepository,
transactionRepo: ITransactionRepository, transactionRepo: ITransactionRepository,
output: GetLeagueWalletPresenter, ) => new GetLeagueWalletUseCase(leagueRepo, walletRepo, transactionRepo),
) => new GetLeagueWalletUseCase(leagueRepo, walletRepo, transactionRepo, output),
inject: [ inject: [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_WALLET_REPOSITORY_TOKEN, LEAGUE_WALLET_REPOSITORY_TOKEN,
TRANSACTION_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN,
GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -584,14 +421,12 @@ export const LeagueProviders: Provider[] = [
walletRepo: ILeagueWalletRepository, walletRepo: ILeagueWalletRepository,
transactionRepo: ITransactionRepository, transactionRepo: ITransactionRepository,
logger: Logger, logger: Logger,
output: WithdrawFromLeagueWalletPresenter, ) => new WithdrawFromLeagueWalletUseCase(leagueRepo, walletRepo, transactionRepo, logger),
) => new WithdrawFromLeagueWalletUseCase(leagueRepo, walletRepo, transactionRepo, logger, output),
inject: [ inject: [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_WALLET_REPOSITORY_TOKEN, LEAGUE_WALLET_REPOSITORY_TOKEN,
TRANSACTION_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -602,7 +437,6 @@ export const LeagueProviders: Provider[] = [
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
output: GetSeasonSponsorshipsPresenter,
) => ) =>
new GetSeasonSponsorshipsUseCase( new GetSeasonSponsorshipsUseCase(
seasonSponsorshipRepo, seasonSponsorshipRepo,
@@ -610,7 +444,6 @@ export const LeagueProviders: Provider[] = [
leagueRepo, leagueRepo,
leagueMembershipRepo, leagueMembershipRepo,
raceRepo, raceRepo,
output,
), ),
inject: [ inject: [
'ISeasonSponsorshipRepository', 'ISeasonSponsorshipRepository',
@@ -618,23 +451,21 @@ export const LeagueProviders: Provider[] = [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ // TODO wtf is this here? doesn't look like it adhers to our concepts { // TODO wtf is this here? doesn't look like it adhers to our concepts
provide: LIST_LEAGUE_SCORING_PRESETS_USE_CASE, provide: LIST_LEAGUE_SCORING_PRESETS_USE_CASE,
useFactory: (output: LeagueScoringPresetsPresenter) => useFactory: () =>
new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets(), output), new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),
inject: [LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN], inject: [],
}, },
{ {
provide: JOIN_LEAGUE_USE_CASE, provide: JOIN_LEAGUE_USE_CASE,
useFactory: ( useFactory: (
membershipRepo: ILeagueMembershipRepository, membershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
output: JoinLeaguePresenter, ) => new JoinLeagueUseCase(membershipRepo, logger),
) => new JoinLeagueUseCase(membershipRepo, logger, output), inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
inject: [LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN, JoinLeaguePresenter],
}, },
{ {
provide: TRANSFER_LEAGUE_OWNERSHIP_USE_CASE, provide: TRANSFER_LEAGUE_OWNERSHIP_USE_CASE,
@@ -652,16 +483,14 @@ export const LeagueProviders: Provider[] = [
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger, logger: Logger,
output: CreateLeagueSeasonScheduleRacePresenter,
) => ) =>
new CreateLeagueSeasonScheduleRaceUseCase(seasonRepo, raceRepo, logger, output, { new CreateLeagueSeasonScheduleRaceUseCase(seasonRepo, raceRepo, logger, {
generateRaceId: () => `race-${randomUUID()}`, generateRaceId: () => `race-${randomUUID()}`,
}), }),
inject: [ inject: [
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
LeagueTokens.CREATE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -670,13 +499,11 @@ export const LeagueProviders: Provider[] = [
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger, logger: Logger,
output: UpdateLeagueSeasonScheduleRacePresenter, ) => new UpdateLeagueSeasonScheduleRaceUseCase(seasonRepo, raceRepo, logger),
) => new UpdateLeagueSeasonScheduleRaceUseCase(seasonRepo, raceRepo, logger, output),
inject: [ inject: [
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
LeagueTokens.UPDATE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -685,13 +512,11 @@ export const LeagueProviders: Provider[] = [
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger, logger: Logger,
output: DeleteLeagueSeasonScheduleRacePresenter, ) => new DeleteLeagueSeasonScheduleRaceUseCase(seasonRepo, raceRepo, logger),
) => new DeleteLeagueSeasonScheduleRaceUseCase(seasonRepo, raceRepo, logger, output),
inject: [ inject: [
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
LeagueTokens.DELETE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -699,12 +524,10 @@ export const LeagueProviders: Provider[] = [
useFactory: ( useFactory: (
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
logger: Logger, logger: Logger,
output: PublishLeagueSeasonSchedulePresenter, ) => new PublishLeagueSeasonScheduleUseCase(seasonRepo, logger),
) => new PublishLeagueSeasonScheduleUseCase(seasonRepo, logger, output),
inject: [ inject: [
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
LeagueTokens.PUBLISH_LEAGUE_SEASON_SCHEDULE_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -712,12 +535,10 @@ export const LeagueProviders: Provider[] = [
useFactory: ( useFactory: (
seasonRepo: ISeasonRepository, seasonRepo: ISeasonRepository,
logger: Logger, logger: Logger,
output: UnpublishLeagueSeasonSchedulePresenter, ) => new UnpublishLeagueSeasonScheduleUseCase(seasonRepo, logger),
) => new UnpublishLeagueSeasonScheduleUseCase(seasonRepo, logger, output),
inject: [ inject: [
SEASON_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
LeagueTokens.UNPUBLISH_LEAGUE_SEASON_SCHEDULE_OUTPUT_PORT_TOKEN,
], ],
}, },
]; ];

View File

@@ -62,15 +62,19 @@ describe('LeagueService', () => {
present: vi.fn(), present: vi.fn(),
getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })), getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })),
}; };
const leagueStandingsPresenter = { getResponseModel: vi.fn(() => ({ standings: [] })) }; const leagueStandingsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ standings: [] })) };
const leagueProtestsPresenter = { getResponseModel: vi.fn(() => ({ protests: [] })) }; const leagueProtestsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ protests: [] })) };
const seasonSponsorshipsPresenter = { getViewModel: vi.fn(() => ({ sponsorships: [] })) }; const seasonSponsorshipsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ sponsorships: [] })) };
const leagueScoringPresetsPresenter = { getViewModel: vi.fn(() => ({ presets: [] })) }; const leagueScoringPresetsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ presets: [] })) };
const approveLeagueJoinRequestPresenter = { getViewModel: vi.fn(() => ({ success: true })) }; const approveLeagueJoinRequestPresenter = {
const createLeaguePresenter = { getViewModel: vi.fn(() => ({ id: 'l1' })) }; present: vi.fn(),
const getLeagueAdminPermissionsPresenter = { getResponseModel: vi.fn(() => ({ canManage: true })) }; getViewModel: vi.fn(() => ({ success: true }))
};
const createLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ id: 'l1' })) };
const getLeagueAdminPermissionsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ canManage: true })) };
const getLeagueMembershipsPresenter = { const getLeagueMembershipsPresenter = {
reset: vi.fn(), reset: vi.fn(),
present: vi.fn(),
getViewModel: vi.fn(() => ({ memberships: { memberships: [] } })), getViewModel: vi.fn(() => ({ memberships: { memberships: [] } })),
}; };
@@ -85,27 +89,30 @@ describe('LeagueService', () => {
present: vi.fn(), present: vi.fn(),
getViewModel: vi.fn(() => ([])), getViewModel: vi.fn(() => ([])),
}; };
const getLeagueOwnerSummaryPresenter = { getViewModel: vi.fn(() => ({ ownerId: 'o1' })) }; const getLeagueOwnerSummaryPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ ownerId: 'o1' })) };
const getLeagueSeasonsPresenter = { getResponseModel: vi.fn(() => ([])) }; const getLeagueSeasonsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ([])) };
const joinLeaguePresenter = { getViewModel: vi.fn(() => ({ success: true })) }; const joinLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const leagueSchedulePresenter = { getViewModel: vi.fn(() => ({ seasonId: 'season-1', published: false, races: [] })) }; const leagueSchedulePresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ seasonId: 'season-1', published: false, races: [] })) };
const leagueStatsPresenter = { getResponseModel: vi.fn(() => ({ stats: {} })) }; const leagueStatsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ stats: {} })) };
const rejectLeagueJoinRequestPresenter = { getViewModel: vi.fn(() => ({ success: true })) }; const rejectLeagueJoinRequestPresenter = {
const removeLeagueMemberPresenter = { getViewModel: vi.fn(() => ({ success: true })) }; present: vi.fn(),
const totalLeaguesPresenter = { getResponseModel: vi.fn(() => ({ total: 1 })) }; getViewModel: vi.fn(() => ({ success: true }))
const transferLeagueOwnershipPresenter = { getViewModel: vi.fn(() => ({ success: true })) }; };
const updateLeagueMemberRolePresenter = { getViewModel: vi.fn(() => ({ success: true })) }; const removeLeagueMemberPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const totalLeaguesPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ total: 1 })) };
const transferLeagueOwnershipPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const updateLeagueMemberRolePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
const leagueConfigPresenter = { getViewModel: vi.fn(() => ({ form: {} })) }; const leagueConfigPresenter = { getViewModel: vi.fn(() => ({ form: {} })) };
const leagueScoringConfigPresenter = { getViewModel: vi.fn(() => ({ config: {} })) }; const leagueScoringConfigPresenter = { getViewModel: vi.fn(() => ({ config: {} })) };
const getLeagueWalletPresenter = { getResponseModel: vi.fn(() => ({ balance: 0 })) }; const getLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ balance: 0 })) };
const withdrawFromLeagueWalletPresenter = { getResponseModel: vi.fn(() => ({ success: true })) }; const withdrawFromLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
const leagueJoinRequestsPresenter = { getViewModel: vi.fn(() => ({ joinRequests: [] })) }; const leagueJoinRequestsPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ joinRequests: [] })) };
const createLeagueSeasonScheduleRacePresenter = { getResponseModel: vi.fn(() => ({ raceId: 'race-1' })) }; const createLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ raceId: 'race-1' })) };
const updateLeagueSeasonScheduleRacePresenter = { getResponseModel: vi.fn(() => ({ success: true })) }; const updateLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
const deleteLeagueSeasonScheduleRacePresenter = { getResponseModel: vi.fn(() => ({ success: true })) }; const deleteLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
const publishLeagueSeasonSchedulePresenter = { getResponseModel: vi.fn(() => ({ success: true, published: true })) }; const publishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: true })) };
const unpublishLeagueSeasonSchedulePresenter = { getResponseModel: vi.fn(() => ({ success: true, published: false })) }; const unpublishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: false })) };
const service = new (LeagueService as any)( const service = new (LeagueService as any)(
getAllLeaguesWithCapacityUseCase as any, getAllLeaguesWithCapacityUseCase as any,
@@ -195,8 +202,7 @@ describe('LeagueService', () => {
}); });
expect(rejectLeagueJoinRequestUseCase.execute).toHaveBeenCalledWith( expect(rejectLeagueJoinRequestUseCase.execute).toHaveBeenCalledWith(
{ leagueId: 'l1', joinRequestId: 'r1' }, { leagueId: 'l1', joinRequestId: 'r1' }
rejectLeagueJoinRequestPresenter,
); );
await withUserId('user-1', async () => { await withUserId('user-1', async () => {

View File

@@ -135,65 +135,39 @@ import {
} from './presenters/LeagueSeasonScheduleMutationPresenters'; } from './presenters/LeagueSeasonScheduleMutationPresenters';
// Tokens // Tokens
import { import {
APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN,
APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE, APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE,
CREATE_LEAGUE_OUTPUT_PORT_TOKEN,
CREATE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE, CREATE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE,
CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE, CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE,
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE, GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE,
GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE, GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE,
GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_ADMIN_PERMISSIONS_USE_CASE, GET_LEAGUE_ADMIN_PERMISSIONS_USE_CASE,
GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN,
GET_LEAGUE_FULL_CONFIG_USE_CASE, GET_LEAGUE_FULL_CONFIG_USE_CASE,
GET_LEAGUE_JOIN_REQUESTS_USE_CASE, GET_LEAGUE_JOIN_REQUESTS_USE_CASE,
GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_MEMBERSHIPS_USE_CASE, GET_LEAGUE_MEMBERSHIPS_USE_CASE,
GET_LEAGUE_ROSTER_JOIN_REQUESTS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_ROSTER_JOIN_REQUESTS_USE_CASE,
GET_LEAGUE_ROSTER_MEMBERS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_ROSTER_MEMBERS_USE_CASE,
GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN,
GET_LEAGUE_OWNER_SUMMARY_USE_CASE, GET_LEAGUE_OWNER_SUMMARY_USE_CASE,
GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_PROTESTS_USE_CASE, GET_LEAGUE_PROTESTS_USE_CASE,
GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN,
GET_LEAGUE_SCHEDULE_USE_CASE, GET_LEAGUE_SCHEDULE_USE_CASE,
GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN,
GET_LEAGUE_SCORING_CONFIG_USE_CASE, GET_LEAGUE_SCORING_CONFIG_USE_CASE,
GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_SEASONS_USE_CASE, GET_LEAGUE_SEASONS_USE_CASE,
GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_STATS_USE_CASE, GET_LEAGUE_STATS_USE_CASE,
GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_STANDINGS_USE_CASE, GET_LEAGUE_STANDINGS_USE_CASE,
GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
GET_LEAGUE_WALLET_USE_CASE, GET_LEAGUE_WALLET_USE_CASE,
GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
GET_SEASON_SPONSORSHIPS_USE_CASE, GET_SEASON_SPONSORSHIPS_USE_CASE,
GET_TOTAL_LEAGUES_USE_CASE, GET_TOTAL_LEAGUES_USE_CASE,
JOIN_LEAGUE_OUTPUT_PORT_TOKEN,
JOIN_LEAGUE_USE_CASE, JOIN_LEAGUE_USE_CASE,
LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN,
LIST_LEAGUE_SCORING_PRESETS_USE_CASE, LIST_LEAGUE_SCORING_PRESETS_USE_CASE,
LOGGER_TOKEN, LOGGER_TOKEN,
PUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE, PUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE,
REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN,
REJECT_LEAGUE_JOIN_REQUEST_USE_CASE, REJECT_LEAGUE_JOIN_REQUEST_USE_CASE,
REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN,
REMOVE_LEAGUE_MEMBER_USE_CASE, REMOVE_LEAGUE_MEMBER_USE_CASE,
TOTAL_LEAGUES_OUTPUT_PORT_TOKEN,
TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN,
TRANSFER_LEAGUE_OWNERSHIP_USE_CASE, TRANSFER_LEAGUE_OWNERSHIP_USE_CASE,
UNPUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE, UNPUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE,
UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN,
UPDATE_LEAGUE_MEMBER_ROLE_USE_CASE, UPDATE_LEAGUE_MEMBER_ROLE_USE_CASE,
UPDATE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE, UPDATE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE,
DELETE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE, DELETE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE,
WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE, WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE,
GET_LEAGUE_ROSTER_MEMBERS_USE_CASE,
GET_LEAGUE_ROSTER_JOIN_REQUESTS_USE_CASE,
} from './LeagueTokens'; } from './LeagueTokens';
@Injectable() @Injectable()
@@ -240,52 +214,47 @@ export class LeagueService {
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
// Injected presenters // Injected presenters
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityPresenter: AllLeaguesWithCapacityPresenter, @Inject(AllLeaguesWithCapacityPresenter) private readonly allLeaguesWithCapacityPresenter: AllLeaguesWithCapacityPresenter,
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityAndScoringPresenter: AllLeaguesWithCapacityAndScoringPresenter, @Inject(AllLeaguesWithCapacityAndScoringPresenter) private readonly allLeaguesWithCapacityAndScoringPresenter: AllLeaguesWithCapacityAndScoringPresenter,
@Inject(GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN) private readonly leagueStandingsPresenter: LeagueStandingsPresenter, @Inject(LeagueStandingsPresenter) private readonly leagueStandingsPresenter: LeagueStandingsPresenter,
@Inject(GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN) private readonly leagueProtestsPresenter: GetLeagueProtestsPresenter, @Inject(GetLeagueProtestsPresenter) private readonly leagueProtestsPresenter: GetLeagueProtestsPresenter,
@Inject(GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN) private readonly seasonSponsorshipsPresenter: GetSeasonSponsorshipsPresenter, @Inject(GetSeasonSponsorshipsPresenter) private readonly seasonSponsorshipsPresenter: GetSeasonSponsorshipsPresenter,
@Inject(LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN) private readonly leagueScoringPresetsPresenter: LeagueScoringPresetsPresenter, @Inject(LeagueScoringPresetsPresenter) private readonly leagueScoringPresetsPresenter: LeagueScoringPresetsPresenter,
@Inject(APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN) private readonly approveLeagueJoinRequestPresenter: ApproveLeagueJoinRequestPresenter, @Inject(ApproveLeagueJoinRequestPresenter) private readonly approveLeagueJoinRequestPresenter: ApproveLeagueJoinRequestPresenter,
@Inject(CREATE_LEAGUE_OUTPUT_PORT_TOKEN) private readonly createLeaguePresenter: CreateLeaguePresenter, @Inject(CreateLeaguePresenter) private readonly createLeaguePresenter: CreateLeaguePresenter,
@Inject(GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN) private readonly getLeagueAdminPermissionsPresenter: GetLeagueAdminPermissionsPresenter, @Inject(GetLeagueAdminPermissionsPresenter) private readonly getLeagueAdminPermissionsPresenter: GetLeagueAdminPermissionsPresenter,
@Inject(GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN) private readonly getLeagueMembershipsPresenter: GetLeagueMembershipsPresenter, @Inject(GetLeagueMembershipsPresenter) private readonly getLeagueMembershipsPresenter: GetLeagueMembershipsPresenter,
@Inject(GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN) private readonly getLeagueOwnerSummaryPresenter: GetLeagueOwnerSummaryPresenter, @Inject(GetLeagueOwnerSummaryPresenter) private readonly getLeagueOwnerSummaryPresenter: GetLeagueOwnerSummaryPresenter,
@Inject(GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN) private readonly getLeagueSeasonsPresenter: GetLeagueSeasonsPresenter, @Inject(GetLeagueSeasonsPresenter) private readonly getLeagueSeasonsPresenter: GetLeagueSeasonsPresenter,
@Inject(JOIN_LEAGUE_OUTPUT_PORT_TOKEN) private readonly joinLeaguePresenter: JoinLeaguePresenter, @Inject(JoinLeaguePresenter) private readonly joinLeaguePresenter: JoinLeaguePresenter,
@Inject(GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN) private readonly leagueSchedulePresenter: LeagueSchedulePresenter, @Inject(LeagueSchedulePresenter) private readonly leagueSchedulePresenter: LeagueSchedulePresenter,
@Inject(GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN) private readonly leagueStatsPresenter: LeagueStatsPresenter, @Inject(LeagueStatsPresenter) private readonly leagueStatsPresenter: LeagueStatsPresenter,
@Inject(REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN) private readonly rejectLeagueJoinRequestPresenter: RejectLeagueJoinRequestPresenter, @Inject(RejectLeagueJoinRequestPresenter) private readonly rejectLeagueJoinRequestPresenter: RejectLeagueJoinRequestPresenter,
@Inject(REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN) private readonly removeLeagueMemberPresenter: RemoveLeagueMemberPresenter, @Inject(RemoveLeagueMemberPresenter) private readonly removeLeagueMemberPresenter: RemoveLeagueMemberPresenter,
@Inject(TOTAL_LEAGUES_OUTPUT_PORT_TOKEN) private readonly totalLeaguesPresenter: TotalLeaguesPresenter, @Inject(TotalLeaguesPresenter) private readonly totalLeaguesPresenter: TotalLeaguesPresenter,
@Inject(TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN) private readonly transferLeagueOwnershipPresenter: TransferLeagueOwnershipPresenter, @Inject(TransferLeagueOwnershipPresenter) private readonly transferLeagueOwnershipPresenter: TransferLeagueOwnershipPresenter,
@Inject(UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN) private readonly updateLeagueMemberRolePresenter: UpdateLeagueMemberRolePresenter, @Inject(UpdateLeagueMemberRolePresenter) private readonly updateLeagueMemberRolePresenter: UpdateLeagueMemberRolePresenter,
@Inject(GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN) private readonly leagueConfigPresenter: LeagueConfigPresenter, @Inject(LeagueConfigPresenter) private readonly leagueConfigPresenter: LeagueConfigPresenter,
@Inject(GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN) private readonly leagueScoringConfigPresenter: LeagueScoringConfigPresenter, @Inject(LeagueScoringConfigPresenter) private readonly leagueScoringConfigPresenter: LeagueScoringConfigPresenter,
@Inject(GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN) private readonly getLeagueWalletPresenter: GetLeagueWalletPresenter, @Inject(GetLeagueWalletPresenter) private readonly getLeagueWalletPresenter: GetLeagueWalletPresenter,
@Inject(WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN) private readonly withdrawFromLeagueWalletPresenter: WithdrawFromLeagueWalletPresenter, @Inject(WithdrawFromLeagueWalletPresenter) private readonly withdrawFromLeagueWalletPresenter: WithdrawFromLeagueWalletPresenter,
@Inject(LeagueJoinRequestsPresenter) private readonly leagueJoinRequestsPresenter: LeagueJoinRequestsPresenter, @Inject(LeagueJoinRequestsPresenter) private readonly leagueJoinRequestsPresenter: LeagueJoinRequestsPresenter,
// Schedule mutation presenters // Schedule mutation presenters
@Inject(CreateLeagueSeasonScheduleRacePresenter) @Inject(CreateLeagueSeasonScheduleRacePresenter) private readonly createLeagueSeasonScheduleRacePresenter: CreateLeagueSeasonScheduleRacePresenter,
private readonly createLeagueSeasonScheduleRacePresenter: CreateLeagueSeasonScheduleRacePresenter, @Inject(UpdateLeagueSeasonScheduleRacePresenter) private readonly updateLeagueSeasonScheduleRacePresenter: UpdateLeagueSeasonScheduleRacePresenter,
@Inject(UpdateLeagueSeasonScheduleRacePresenter) @Inject(DeleteLeagueSeasonScheduleRacePresenter) private readonly deleteLeagueSeasonScheduleRacePresenter: DeleteLeagueSeasonScheduleRacePresenter,
private readonly updateLeagueSeasonScheduleRacePresenter: UpdateLeagueSeasonScheduleRacePresenter, @Inject(PublishLeagueSeasonSchedulePresenter) private readonly publishLeagueSeasonSchedulePresenter: PublishLeagueSeasonSchedulePresenter,
@Inject(DeleteLeagueSeasonScheduleRacePresenter) @Inject(UnpublishLeagueSeasonSchedulePresenter) private readonly unpublishLeagueSeasonSchedulePresenter: UnpublishLeagueSeasonSchedulePresenter,
private readonly deleteLeagueSeasonScheduleRacePresenter: DeleteLeagueSeasonScheduleRacePresenter,
@Inject(PublishLeagueSeasonSchedulePresenter)
private readonly publishLeagueSeasonSchedulePresenter: PublishLeagueSeasonSchedulePresenter,
@Inject(UnpublishLeagueSeasonSchedulePresenter)
private readonly unpublishLeagueSeasonSchedulePresenter: UnpublishLeagueSeasonSchedulePresenter,
// Roster admin read delegation // Roster admin read delegation
@Inject(GET_LEAGUE_ROSTER_MEMBERS_USE_CASE) @Inject(GET_LEAGUE_ROSTER_MEMBERS_USE_CASE)
private readonly getLeagueRosterMembersUseCase: GetLeagueRosterMembersUseCase, private readonly getLeagueRosterMembersUseCase: GetLeagueRosterMembersUseCase,
@Inject(GET_LEAGUE_ROSTER_JOIN_REQUESTS_USE_CASE) @Inject(GET_LEAGUE_ROSTER_JOIN_REQUESTS_USE_CASE)
private readonly getLeagueRosterJoinRequestsUseCase: GetLeagueRosterJoinRequestsUseCase, private readonly getLeagueRosterJoinRequestsUseCase: GetLeagueRosterJoinRequestsUseCase,
@Inject(GET_LEAGUE_ROSTER_MEMBERS_OUTPUT_PORT_TOKEN) @Inject(GetLeagueRosterMembersPresenter)
private readonly getLeagueRosterMembersPresenter: GetLeagueRosterMembersPresenter, private readonly getLeagueRosterMembersPresenter: GetLeagueRosterMembersPresenter,
@Inject(GET_LEAGUE_ROSTER_JOIN_REQUESTS_OUTPUT_PORT_TOKEN) @Inject(GetLeagueRosterJoinRequestsPresenter)
private readonly getLeagueRosterJoinRequestsPresenter: GetLeagueRosterJoinRequestsPresenter, private readonly getLeagueRosterJoinRequestsPresenter: GetLeagueRosterJoinRequestsPresenter,
) {} ) {}
@@ -327,7 +296,11 @@ export class LeagueService {
async getTotalLeagues(): Promise<TotalLeaguesDTO> { async getTotalLeagues(): Promise<TotalLeaguesDTO> {
this.logger.debug('[LeagueService] Fetching total leagues count.'); this.logger.debug('[LeagueService] Fetching total leagues count.');
await this.getTotalLeaguesUseCase.execute({}); const result = await this.getTotalLeaguesUseCase.execute({});
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.totalLeaguesPresenter.present(result.unwrap());
return this.totalLeaguesPresenter.getResponseModel()!; return this.totalLeaguesPresenter.getResponseModel()!;
} }
@@ -345,8 +318,13 @@ export class LeagueService {
await this.requireLeagueAdminPermissions(leagueId); await this.requireLeagueAdminPermissions(leagueId);
this.leagueJoinRequestsPresenter.reset?.(); this.leagueJoinRequestsPresenter.reset?.();
await this.getLeagueJoinRequestsUseCase.execute({ leagueId }); const result = await this.getLeagueJoinRequestsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueJoinRequestsPresenter.present(result.unwrap());
return this.leagueJoinRequestsPresenter.getViewModel()!.joinRequests; return this.leagueJoinRequestsPresenter.getViewModel()!.joinRequests;
} }
@@ -355,10 +333,8 @@ export class LeagueService {
await this.requireLeagueAdminPermissions(input.leagueId); await this.requireLeagueAdminPermissions(input.leagueId);
this.approveLeagueJoinRequestPresenter.reset?.();
const result = await this.approveLeagueJoinRequestUseCase.execute( const result = await this.approveLeagueJoinRequestUseCase.execute(
{ leagueId: input.leagueId, joinRequestId: input.requestId }, { leagueId: input.leagueId, joinRequestId: input.requestId },
this.approveLeagueJoinRequestPresenter,
); );
if (result.isErr()) { if (result.isErr()) {
@@ -379,6 +355,7 @@ export class LeagueService {
throw new Error(err.code); throw new Error(err.code);
} }
this.approveLeagueJoinRequestPresenter.present(result.unwrap());
return this.approveLeagueJoinRequestPresenter.getViewModel()!; return this.approveLeagueJoinRequestPresenter.getViewModel()!;
} }
@@ -387,10 +364,8 @@ export class LeagueService {
await this.requireLeagueAdminPermissions(input.leagueId); await this.requireLeagueAdminPermissions(input.leagueId);
this.rejectLeagueJoinRequestPresenter.reset?.();
const result = await this.rejectLeagueJoinRequestUseCase.execute( const result = await this.rejectLeagueJoinRequestUseCase.execute(
{ leagueId: input.leagueId, joinRequestId: input.requestId }, { leagueId: input.leagueId, joinRequestId: input.requestId },
this.rejectLeagueJoinRequestPresenter,
); );
if (result.isErr()) { if (result.isErr()) {
@@ -411,6 +386,7 @@ export class LeagueService {
throw new Error(err.code); throw new Error(err.code);
} }
this.rejectLeagueJoinRequestPresenter.present(result.unwrap());
return this.rejectLeagueJoinRequestPresenter.getViewModel()!; return this.rejectLeagueJoinRequestPresenter.getViewModel()!;
} }
@@ -419,10 +395,8 @@ export class LeagueService {
await this.requireLeagueAdminPermissions(leagueId); await this.requireLeagueAdminPermissions(leagueId);
this.approveLeagueJoinRequestPresenter.reset?.();
const result = await this.approveLeagueJoinRequestUseCase.execute( const result = await this.approveLeagueJoinRequestUseCase.execute(
{ leagueId, joinRequestId }, { leagueId, joinRequestId },
this.approveLeagueJoinRequestPresenter,
); );
if (result.isErr()) { if (result.isErr()) {
@@ -443,6 +417,7 @@ export class LeagueService {
throw new Error(err.code); throw new Error(err.code);
} }
this.approveLeagueJoinRequestPresenter.present(result.unwrap());
return this.approveLeagueJoinRequestPresenter.getViewModel()!; return this.approveLeagueJoinRequestPresenter.getViewModel()!;
} }
@@ -451,16 +426,15 @@ export class LeagueService {
await this.requireLeagueAdminPermissions(leagueId); await this.requireLeagueAdminPermissions(leagueId);
this.rejectLeagueJoinRequestPresenter.reset?.();
const result = await this.rejectLeagueJoinRequestUseCase.execute( const result = await this.rejectLeagueJoinRequestUseCase.execute(
{ leagueId, joinRequestId }, { leagueId, joinRequestId },
this.rejectLeagueJoinRequestPresenter,
); );
if (result.isErr()) { if (result.isErr()) {
throw new NotFoundException('Join request not found'); throw new NotFoundException('Join request not found');
} }
this.rejectLeagueJoinRequestPresenter.present(result.unwrap());
return this.rejectLeagueJoinRequestPresenter.getViewModel()!; return this.rejectLeagueJoinRequestPresenter.getViewModel()!;
} }
@@ -469,11 +443,16 @@ export class LeagueService {
this.logger.debug('Getting league admin permissions', { leagueId: query.leagueId, performerDriverId: actor.driverId }); this.logger.debug('Getting league admin permissions', { leagueId: query.leagueId, performerDriverId: actor.driverId });
await this.getLeagueAdminPermissionsUseCase.execute({ const result = await this.getLeagueAdminPermissionsUseCase.execute({
leagueId: query.leagueId, leagueId: query.leagueId,
performerDriverId: actor.driverId, performerDriverId: actor.driverId,
}); });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.getLeagueAdminPermissionsPresenter.present(result.unwrap());
return this.getLeagueAdminPermissionsPresenter.getResponseModel()!; return this.getLeagueAdminPermissionsPresenter.getResponseModel()!;
} }
@@ -544,7 +523,11 @@ export class LeagueService {
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<LeagueOwnerSummaryDTO> { async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<LeagueOwnerSummaryDTO> {
this.logger.debug('Getting league owner summary:', query); this.logger.debug('Getting league owner summary:', query);
await this.getLeagueOwnerSummaryUseCase.execute(query); const result = await this.getLeagueOwnerSummaryUseCase.execute(query);
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.getLeagueOwnerSummaryPresenter.present(result.unwrap());
return this.getLeagueOwnerSummaryPresenter.getViewModel()!; return this.getLeagueOwnerSummaryPresenter.getViewModel()!;
} }
@@ -562,19 +545,31 @@ export class LeagueService {
async getLeagueProtests(query: GetLeagueProtestsQueryDTO): Promise<LeagueAdminProtestsDTO> { async getLeagueProtests(query: GetLeagueProtestsQueryDTO): Promise<LeagueAdminProtestsDTO> {
this.logger.debug('Getting league protests:', query); this.logger.debug('Getting league protests:', query);
await this.getLeagueProtestsUseCase.execute(query); const result = await this.getLeagueProtestsUseCase.execute(query);
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueProtestsPresenter.present(result.unwrap());
return this.leagueProtestsPresenter.getResponseModel()!; return this.leagueProtestsPresenter.getResponseModel()!;
} }
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> { async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
this.logger.debug('Getting league seasons:', query); this.logger.debug('Getting league seasons:', query);
await this.getLeagueSeasonsUseCase.execute(query); const result = await this.getLeagueSeasonsUseCase.execute(query);
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.getLeagueSeasonsPresenter.present(result.unwrap());
return this.getLeagueSeasonsPresenter.getResponseModel()!; return this.getLeagueSeasonsPresenter.getResponseModel()!;
} }
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> { async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
this.logger.debug('Getting league memberships', { leagueId }); this.logger.debug('Getting league memberships', { leagueId });
await this.getLeagueMembershipsUseCase.execute({ leagueId }); const result = await this.getLeagueMembershipsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.getLeagueMembershipsPresenter.present(result.unwrap());
return this.getLeagueMembershipsPresenter.getViewModel()!.memberships; return this.getLeagueMembershipsPresenter.getViewModel()!.memberships;
} }
@@ -590,6 +585,7 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.getLeagueRosterMembersPresenter.present(result.unwrap());
return this.getLeagueRosterMembersPresenter.getViewModel()!; return this.getLeagueRosterMembersPresenter.getViewModel()!;
} }
@@ -605,12 +601,17 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.getLeagueRosterJoinRequestsPresenter.present(result.unwrap());
return this.getLeagueRosterJoinRequestsPresenter.getViewModel()!; return this.getLeagueRosterJoinRequestsPresenter.getViewModel()!;
} }
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsDTO> { async getLeagueStandings(leagueId: string): Promise<LeagueStandingsDTO> {
this.logger.debug('Getting league standings', { leagueId }); this.logger.debug('Getting league standings', { leagueId });
await this.getLeagueStandingsUseCase.execute({ leagueId }); const result = await this.getLeagueStandingsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueStandingsPresenter.present(result.unwrap());
return this.leagueStandingsPresenter.getResponseModel()!; return this.leagueStandingsPresenter.getResponseModel()!;
} }
@@ -618,8 +619,13 @@ export class LeagueService {
this.logger.debug('Getting league schedule', { leagueId, query }); this.logger.debug('Getting league schedule', { leagueId, query });
const input: GetLeagueScheduleInput = query?.seasonId ? { leagueId, seasonId: query.seasonId } : { leagueId }; const input: GetLeagueScheduleInput = query?.seasonId ? { leagueId, seasonId: query.seasonId } : { leagueId };
await this.getLeagueScheduleUseCase.execute(input); const result = await this.getLeagueScheduleUseCase.execute(input);
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueSchedulePresenter.present(result.unwrap());
return this.leagueSchedulePresenter.getViewModel()!; return this.leagueSchedulePresenter.getViewModel()!;
} }
@@ -639,6 +645,7 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.publishLeagueSeasonSchedulePresenter.present(result.unwrap());
return this.publishLeagueSeasonSchedulePresenter.getResponseModel()!; return this.publishLeagueSeasonSchedulePresenter.getResponseModel()!;
} }
@@ -658,6 +665,7 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.unpublishLeagueSeasonSchedulePresenter.present(result.unwrap());
return this.unpublishLeagueSeasonSchedulePresenter.getResponseModel()!; return this.unpublishLeagueSeasonSchedulePresenter.getResponseModel()!;
} }
@@ -686,6 +694,7 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.createLeagueSeasonScheduleRacePresenter.present(result.unwrap());
return this.createLeagueSeasonScheduleRacePresenter.getResponseModel()!; return this.createLeagueSeasonScheduleRacePresenter.getResponseModel()!;
} }
@@ -718,6 +727,7 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.updateLeagueSeasonScheduleRacePresenter.present(result.unwrap());
return this.updateLeagueSeasonScheduleRacePresenter.getResponseModel()!; return this.updateLeagueSeasonScheduleRacePresenter.getResponseModel()!;
} }
@@ -735,12 +745,17 @@ export class LeagueService {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
this.deleteLeagueSeasonScheduleRacePresenter.present(result.unwrap());
return this.deleteLeagueSeasonScheduleRacePresenter.getResponseModel()!; return this.deleteLeagueSeasonScheduleRacePresenter.getResponseModel()!;
} }
async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> { async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> {
this.logger.debug('Getting league stats', { leagueId }); this.logger.debug('Getting league stats', { leagueId });
await this.getLeagueStatsUseCase.execute({ leagueId }); const result = await this.getLeagueStatsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueStatsPresenter.present(result.unwrap());
return this.leagueStatsPresenter.getResponseModel()!; return this.leagueStatsPresenter.getResponseModel()!;
} }
@@ -787,7 +802,11 @@ export class LeagueService {
enableNationsChampionship: false, enableNationsChampionship: false,
enableTrophyChampionship: false, enableTrophyChampionship: false,
}; };
await this.createLeagueWithSeasonAndScoringUseCase.execute(command); const result = await this.createLeagueWithSeasonAndScoringUseCase.execute(command);
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.createLeaguePresenter.present(result.unwrap());
return this.createLeaguePresenter.getViewModel()!; return this.createLeaguePresenter.getViewModel()!;
} }
@@ -806,7 +825,11 @@ export class LeagueService {
async listLeagueScoringPresets(): Promise<LeagueScoringPresetsViewModel> { async listLeagueScoringPresets(): Promise<LeagueScoringPresetsViewModel> {
this.logger.debug('Listing league scoring presets'); this.logger.debug('Listing league scoring presets');
await this.listLeagueScoringPresetsUseCase.execute({}); const result = await this.listLeagueScoringPresetsUseCase.execute({});
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueScoringPresetsPresenter.present(result.unwrap());
return this.leagueScoringPresetsPresenter.getViewModel()!; return this.leagueScoringPresetsPresenter.getViewModel()!;
} }
@@ -814,7 +837,11 @@ export class LeagueService {
const actor = this.getActor(); const actor = this.getActor();
this.logger.debug('Joining league', { leagueId, actorDriverId: actor.driverId }); this.logger.debug('Joining league', { leagueId, actorDriverId: actor.driverId });
await this.joinLeagueUseCase.execute({ leagueId, driverId: actor.driverId }); const result = await this.joinLeagueUseCase.execute({ leagueId, driverId: actor.driverId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.joinLeaguePresenter.present(result.unwrap());
return this.joinLeaguePresenter.getViewModel()!; return this.joinLeaguePresenter.getViewModel()!;
} }
@@ -825,19 +852,28 @@ export class LeagueService {
const actor = this.getActor(); const actor = this.getActor();
await this.transferLeagueOwnershipUseCase.execute({ const result = await this.transferLeagueOwnershipUseCase.execute({
leagueId, leagueId,
currentOwnerId: actor.driverId, currentOwnerId: actor.driverId,
newOwnerId: input.newOwnerId, newOwnerId: input.newOwnerId,
}); });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.transferLeagueOwnershipPresenter.present(result.unwrap());
return this.transferLeagueOwnershipPresenter.getViewModel()!; return this.transferLeagueOwnershipPresenter.getViewModel()!;
} }
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> { async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
this.logger.debug('Getting season sponsorships', { seasonId }); this.logger.debug('Getting season sponsorships', { seasonId });
await this.getSeasonSponsorshipsUseCase.execute({ seasonId }); const result = await this.getSeasonSponsorshipsUseCase.execute({ seasonId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.seasonSponsorshipsPresenter.present(result.unwrap());
return this.seasonSponsorshipsPresenter.getViewModel()!; return this.seasonSponsorshipsPresenter.getViewModel()!;
} }
@@ -847,8 +883,13 @@ export class LeagueService {
// `GetLeagueScheduleUseCase` is wired to `LeagueSchedulePresenter` (not `LeagueRacesPresenter`), // `GetLeagueScheduleUseCase` is wired to `LeagueSchedulePresenter` (not `LeagueRacesPresenter`),
// so `LeagueRacesPresenter.getViewModel()` can be null at runtime. // so `LeagueRacesPresenter.getViewModel()` can be null at runtime.
this.leagueSchedulePresenter.reset?.(); this.leagueSchedulePresenter.reset?.();
await this.getLeagueScheduleUseCase.execute({ leagueId }); const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.leagueSchedulePresenter.present(result.unwrap());
return { return {
races: this.leagueSchedulePresenter.getViewModel()?.races ?? [], races: this.leagueSchedulePresenter.getViewModel()?.races ?? [],
}; };
@@ -856,7 +897,11 @@ export class LeagueService {
async getLeagueWallet(leagueId: string): Promise<GetLeagueWalletOutputDTO> { async getLeagueWallet(leagueId: string): Promise<GetLeagueWalletOutputDTO> {
this.logger.debug('Getting league wallet', { leagueId }); this.logger.debug('Getting league wallet', { leagueId });
await this.getLeagueWalletUseCase.execute({ leagueId }); const result = await this.getLeagueWalletUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.getLeagueWalletPresenter.present(result.unwrap());
return this.getLeagueWalletPresenter.getResponseModel(); return this.getLeagueWalletPresenter.getResponseModel();
} }
@@ -868,7 +913,7 @@ export class LeagueService {
const actor = this.getActor(); const actor = this.getActor();
await this.withdrawFromLeagueWalletUseCase.execute({ const result = await this.withdrawFromLeagueWalletUseCase.execute({
leagueId, leagueId,
requestedById: actor.driverId, requestedById: actor.driverId,
amount: input.amount, amount: input.amount,
@@ -876,6 +921,11 @@ export class LeagueService {
reason: input.destinationAccount, reason: input.destinationAccount,
}); });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
this.withdrawFromLeagueWalletPresenter.present(result.unwrap());
return this.withdrawFromLeagueWalletPresenter.getResponseModel(); return this.withdrawFromLeagueWalletPresenter.getResponseModel();
} }
} }

View File

@@ -48,37 +48,3 @@ export const UPDATE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE = 'UpdateLeagueSeasonSc
export const DELETE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE = 'DeleteLeagueSeasonScheduleRaceUseCase'; export const DELETE_LEAGUE_SEASON_SCHEDULE_RACE_USE_CASE = 'DeleteLeagueSeasonScheduleRaceUseCase';
export const PUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE = 'PublishLeagueSeasonScheduleUseCase'; export const PUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE = 'PublishLeagueSeasonScheduleUseCase';
export const UNPUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE = 'UnpublishLeagueSeasonScheduleUseCase'; export const UNPUBLISH_LEAGUE_SEASON_SCHEDULE_USE_CASE = 'UnpublishLeagueSeasonScheduleUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityAndScoringOutputPort_TOKEN';
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
export const LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN = 'ListLeagueScoringPresetsOutputPort_TOKEN';
export const APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN = 'ApproveLeagueJoinRequestOutputPort_TOKEN';
export const CREATE_LEAGUE_OUTPUT_PORT_TOKEN = 'CreateLeagueOutputPort_TOKEN';
export const GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN = 'GetLeagueAdminPermissionsOutputPort_TOKEN';
export const GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN = 'GetLeagueMembershipsOutputPort_TOKEN';
export const GET_LEAGUE_ROSTER_MEMBERS_OUTPUT_PORT_TOKEN = 'GetLeagueRosterMembersOutputPort_TOKEN';
export const GET_LEAGUE_ROSTER_JOIN_REQUESTS_OUTPUT_PORT_TOKEN = 'GetLeagueRosterJoinRequestsOutputPort_TOKEN';
export const GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN = 'GetLeagueOwnerSummaryOutputPort_TOKEN';
export const GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN = 'GetLeagueSeasonsOutputPort_TOKEN';
export const JOIN_LEAGUE_OUTPUT_PORT_TOKEN = 'JoinLeagueOutputPort_TOKEN';
export const GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN = 'GetLeagueScheduleOutputPort_TOKEN';
export const GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN = 'GetLeagueStatsOutputPort_TOKEN';
export const REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN = 'RejectLeagueJoinRequestOutputPort_TOKEN';
export const REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN = 'RemoveLeagueMemberOutputPort_TOKEN';
export const TOTAL_LEAGUES_OUTPUT_PORT_TOKEN = 'TotalLeaguesOutputPort_TOKEN';
export const TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN = 'TransferLeagueOwnershipOutputPort_TOKEN';
export const UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN = 'UpdateLeagueMemberRoleOutputPort_TOKEN';
export const GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN = 'GetLeagueFullConfigOutputPort_TOKEN';
export const GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN = 'GetLeagueScoringConfigOutputPort_TOKEN';
export const GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN = 'GetLeagueWalletOutputPort_TOKEN';
export const WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN = 'WithdrawFromLeagueWalletOutputPort_TOKEN';
// Schedule mutation output ports
export const CREATE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN = 'CreateLeagueSeasonScheduleRaceOutputPort_TOKEN';
export const UPDATE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN = 'UpdateLeagueSeasonScheduleRaceOutputPort_TOKEN';
export const DELETE_LEAGUE_SEASON_SCHEDULE_RACE_OUTPUT_PORT_TOKEN = 'DeleteLeagueSeasonScheduleRaceOutputPort_TOKEN';
export const PUBLISH_LEAGUE_SEASON_SCHEDULE_OUTPUT_PORT_TOKEN = 'PublishLeagueSeasonScheduleOutputPort_TOKEN';
export const UNPUBLISH_LEAGUE_SEASON_SCHEDULE_OUTPUT_PORT_TOKEN = 'UnpublishLeagueSeasonScheduleOutputPort_TOKEN';

View File

@@ -17,6 +17,8 @@ describe('LeagueOwnerSummaryPresenter', () => {
joinedAt: { toDate: () => new Date('2024-01-01T00:00:00Z') } as any, joinedAt: { toDate: () => new Date('2024-01-01T00:00:00Z') } as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any, } as any,
totalMembers: 50,
activeMembers: 45,
rating: 1500, rating: 1500,
rank: 100, rank: 100,
}; };

View File

@@ -1,3 +1,4 @@
import { Injectable } from '@nestjs/common';
import type { UseCaseOutputPort } from '@core/shared/application'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetLeagueRosterMembersResult } from '@core/racing/application/use-cases/GetLeagueRosterMembersUseCase'; import type { GetLeagueRosterMembersResult } from '@core/racing/application/use-cases/GetLeagueRosterMembersUseCase';
import type { GetLeagueRosterJoinRequestsResult } from '@core/racing/application/use-cases/GetLeagueRosterJoinRequestsUseCase'; import type { GetLeagueRosterJoinRequestsResult } from '@core/racing/application/use-cases/GetLeagueRosterJoinRequestsUseCase';
@@ -5,6 +6,7 @@ import type { LeagueRosterMemberDTO } from '../dtos/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '../dtos/LeagueRosterJoinRequestDTO'; import type { LeagueRosterJoinRequestDTO } from '../dtos/LeagueRosterJoinRequestDTO';
import type { DriverDTO } from '../../driver/dtos/DriverDTO'; import type { DriverDTO } from '../../driver/dtos/DriverDTO';
@Injectable()
export class GetLeagueRosterMembersPresenter implements UseCaseOutputPort<GetLeagueRosterMembersResult> { export class GetLeagueRosterMembersPresenter implements UseCaseOutputPort<GetLeagueRosterMembersResult> {
private viewModel: LeagueRosterMemberDTO[] | null = null; private viewModel: LeagueRosterMemberDTO[] | null = null;
@@ -37,6 +39,7 @@ export class GetLeagueRosterMembersPresenter implements UseCaseOutputPort<GetLea
} }
} }
@Injectable()
export class GetLeagueRosterJoinRequestsPresenter implements UseCaseOutputPort<GetLeagueRosterJoinRequestsResult> { export class GetLeagueRosterJoinRequestsPresenter implements UseCaseOutputPort<GetLeagueRosterJoinRequestsResult> {
private viewModel: LeagueRosterJoinRequestDTO[] | null = null; private viewModel: LeagueRosterJoinRequestDTO[] | null = null;

View File

@@ -7,7 +7,7 @@ import { IAvatarRepository } from '@core/media/domain/repositories/IAvatarReposi
import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort'; import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort';
import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort'; import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort';
import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort'; import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
// Import use cases // Import use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
@@ -17,14 +17,6 @@ import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMedi
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase'; import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase'; import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Import result types
import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase';
import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase';
import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase';
import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase';
import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Import presenters // Import presenters
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter'; import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
import { UploadMediaPresenter } from './presenters/UploadMediaPresenter'; import { UploadMediaPresenter } from './presenters/UploadMediaPresenter';
@@ -47,12 +39,6 @@ import {
DELETE_MEDIA_USE_CASE_TOKEN, DELETE_MEDIA_USE_CASE_TOKEN,
GET_AVATAR_USE_CASE_TOKEN, GET_AVATAR_USE_CASE_TOKEN,
UPDATE_AVATAR_USE_CASE_TOKEN, UPDATE_AVATAR_USE_CASE_TOKEN,
REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN,
UPLOAD_MEDIA_OUTPUT_PORT_TOKEN,
GET_MEDIA_OUTPUT_PORT_TOKEN,
DELETE_MEDIA_OUTPUT_PORT_TOKEN,
GET_AVATAR_OUTPUT_PORT_TOKEN,
UPDATE_AVATAR_OUTPUT_PORT_TOKEN,
} from './MediaTokens'; } from './MediaTokens';
export * from './MediaTokens'; export * from './MediaTokens';
@@ -133,66 +119,41 @@ export const MediaProviders: Provider[] = createLoggedProviders([
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: MockLogger, useClass: MockLogger,
}, },
// Output ports // Use cases - simplified without output ports
{
provide: REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN,
useExisting: RequestAvatarGenerationPresenter,
},
{
provide: UPLOAD_MEDIA_OUTPUT_PORT_TOKEN,
useExisting: UploadMediaPresenter,
},
{
provide: GET_MEDIA_OUTPUT_PORT_TOKEN,
useExisting: GetMediaPresenter,
},
{
provide: DELETE_MEDIA_OUTPUT_PORT_TOKEN,
useExisting: DeleteMediaPresenter,
},
{
provide: GET_AVATAR_OUTPUT_PORT_TOKEN,
useExisting: GetAvatarPresenter,
},
{
provide: UPDATE_AVATAR_OUTPUT_PORT_TOKEN,
useExisting: UpdateAvatarPresenter,
},
// Use cases
{ {
provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, output: UseCaseOutputPort<RequestAvatarGenerationResult>, logger: Logger) => useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, logger: Logger) =>
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, output, logger), new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger),
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: UPLOAD_MEDIA_USE_CASE_TOKEN, provide: UPLOAD_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort<UploadMediaResult>, logger: Logger) => useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
new UploadMediaUseCase(mediaRepo, mediaStorage, output, logger), new UploadMediaUseCase(mediaRepo, mediaStorage, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, UPLOAD_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_MEDIA_USE_CASE_TOKEN, provide: GET_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, output: UseCaseOutputPort<GetMediaResult>, logger: Logger) => useFactory: (mediaRepo: IMediaRepository, logger: Logger) =>
new GetMediaUseCase(mediaRepo, output, logger), new GetMediaUseCase(mediaRepo, logger),
inject: [MEDIA_REPOSITORY_TOKEN, GET_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], inject: [MEDIA_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: DELETE_MEDIA_USE_CASE_TOKEN, provide: DELETE_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, output: UseCaseOutputPort<DeleteMediaResult>, logger: Logger) => useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
new DeleteMediaUseCase(mediaRepo, mediaStorage, output, logger), new DeleteMediaUseCase(mediaRepo, mediaStorage, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, DELETE_MEDIA_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_AVATAR_USE_CASE_TOKEN, provide: GET_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort<GetAvatarResult>, logger: Logger) => useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
new GetAvatarUseCase(avatarRepo, output, logger), new GetAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, GET_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: UPDATE_AVATAR_USE_CASE_TOKEN, provide: UPDATE_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, output: UseCaseOutputPort<UpdateAvatarResult>, logger: Logger) => useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
new UpdateAvatarUseCase(avatarRepo, output, logger), new UpdateAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, UPDATE_AVATAR_OUTPUT_PORT_TOKEN, LOGGER_TOKEN], inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
], initLogger); ], initLogger);

View File

@@ -7,11 +7,12 @@ describe('MediaService', () => {
it('requestAvatarGeneration returns presenter response on success', async () => { it('requestAvatarGeneration returns presenter response on success', async () => {
const requestAvatarGenerationPresenter = { const requestAvatarGenerationPresenter = {
transform: vi.fn((result) => ({ success: true, requestId: result.requestId, avatarUrls: result.avatarUrls, errorMessage: '' })),
responseModel: { success: true, requestId: 'r1', avatarUrls: ['u1'], errorMessage: '' }, responseModel: { success: true, requestId: 'r1', avatarUrls: ['u1'], errorMessage: '' },
}; };
const requestAvatarGenerationUseCase = { const requestAvatarGenerationUseCase = {
execute: vi.fn(async () => Result.ok(undefined)), execute: vi.fn(async () => Result.ok({ requestId: 'r1', status: 'completed', avatarUrls: ['u1'] })),
}; };
const service = new MediaService( const service = new MediaService(
@@ -39,6 +40,7 @@ describe('MediaService', () => {
facePhotoData: {} as any, facePhotoData: {} as any,
suitColor: 'red', suitColor: 'red',
}); });
expect(requestAvatarGenerationPresenter.transform).toHaveBeenCalledWith({ requestId: 'r1', status: 'completed', avatarUrls: ['u1'] });
}); });
it('requestAvatarGeneration returns failure DTO on error', async () => { it('requestAvatarGeneration returns failure DTO on error', async () => {
@@ -69,8 +71,11 @@ describe('MediaService', () => {
}); });
it('uploadMedia returns presenter response on success', async () => { it('uploadMedia returns presenter response on success', async () => {
const uploadMediaPresenter = { responseModel: { success: true, mediaId: 'm1' } }; const uploadMediaPresenter = {
const uploadMediaUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; transform: vi.fn((result) => ({ success: true, mediaId: result.mediaId })),
responseModel: { success: true, mediaId: 'm1' }
};
const uploadMediaUseCase = { execute: vi.fn(async () => Result.ok({ mediaId: 'm1', url: 'https://example.com/m1.png' })) };
const service = new MediaService( const service = new MediaService(
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
@@ -100,10 +105,15 @@ describe('MediaService', () => {
uploadedBy: 'u1', uploadedBy: 'u1',
metadata: { a: 1 }, metadata: { a: 1 },
}); });
expect(uploadMediaPresenter.transform).toHaveBeenCalledWith({ mediaId: 'm1', url: 'https://example.com/m1.png' });
}); });
it('uploadMedia uses empty uploadedBy when userId missing', async () => { it('uploadMedia uses empty uploadedBy when userId missing', async () => {
const uploadMediaUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const uploadMediaUseCase = { execute: vi.fn(async () => Result.ok({ mediaId: 'm1', url: 'https://example.com/m1.png' })) };
const uploadMediaPresenter = {
transform: vi.fn((result) => ({ success: true, mediaId: result.mediaId })),
responseModel: { success: true, mediaId: 'm1' }
};
const service = new MediaService( const service = new MediaService(
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
@@ -114,7 +124,7 @@ describe('MediaService', () => {
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
logger as any, logger as any,
{ responseModel: {} } as any, { responseModel: {} } as any,
{ responseModel: { success: true, mediaId: 'm1' } } as any, uploadMediaPresenter as any,
{ responseModel: {} } as any, { responseModel: {} } as any,
{ responseModel: {} } as any, { responseModel: {} } as any,
{ responseModel: {} } as any, { responseModel: {} } as any,
@@ -123,6 +133,7 @@ describe('MediaService', () => {
await expect(service.uploadMedia({ file: {} as any } as any)).resolves.toEqual({ success: true, mediaId: 'm1' }); await expect(service.uploadMedia({ file: {} as any } as any)).resolves.toEqual({ success: true, mediaId: 'm1' });
expect(uploadMediaUseCase.execute).toHaveBeenCalledWith({ file: {} as any, uploadedBy: '', metadata: {} }); expect(uploadMediaUseCase.execute).toHaveBeenCalledWith({ file: {} as any, uploadedBy: '', metadata: {} });
expect(uploadMediaPresenter.transform).toHaveBeenCalledWith({ mediaId: 'm1', url: 'https://example.com/m1.png' });
}); });
it('uploadMedia returns failure DTO on error', async () => { it('uploadMedia returns failure DTO on error', async () => {
@@ -153,8 +164,12 @@ describe('MediaService', () => {
}); });
it('getMedia returns presenter response on success', async () => { it('getMedia returns presenter response on success', async () => {
const getMediaUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const uploadedAt = new Date();
const getMediaPresenter = { responseModel: { mediaId: 'm1' } }; const getMediaUseCase = { execute: vi.fn(async () => Result.ok({ media: { id: 'm1', filename: 'test.png', originalName: 'test.png', mimeType: 'image/png', size: 100, url: 'https://example.com/m1.png', type: 'image', uploadedBy: 'u1', uploadedAt } })) };
const getMediaPresenter = {
transform: vi.fn((result) => ({ id: result.media.id, url: result.media.url, type: result.media.type, uploadedAt: result.media.uploadedAt, size: result.media.size })),
responseModel: { id: 'm1', url: 'https://example.com/m1.png', type: 'image', uploadedAt, size: 100 }
};
const service = new MediaService( const service = new MediaService(
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
@@ -172,8 +187,10 @@ describe('MediaService', () => {
{ responseModel: {} } as any, { responseModel: {} } as any,
); );
await expect(service.getMedia('m1')).resolves.toEqual({ mediaId: 'm1' }); const result = await service.getMedia('m1');
expect(result).toEqual({ id: 'm1', url: 'https://example.com/m1.png', type: 'image', uploadedAt, size: 100 });
expect(getMediaUseCase.execute).toHaveBeenCalledWith({ mediaId: 'm1' }); expect(getMediaUseCase.execute).toHaveBeenCalledWith({ mediaId: 'm1' });
expect(getMediaPresenter.transform).toHaveBeenCalledWith({ media: { id: 'm1', filename: 'test.png', originalName: 'test.png', mimeType: 'image/png', size: 100, url: 'https://example.com/m1.png', type: 'image', uploadedBy: 'u1', uploadedAt } });
}); });
it('getMedia returns null on MEDIA_NOT_FOUND', async () => { it('getMedia returns null on MEDIA_NOT_FOUND', async () => {
@@ -217,8 +234,11 @@ describe('MediaService', () => {
}); });
it('deleteMedia returns presenter response on success', async () => { it('deleteMedia returns presenter response on success', async () => {
const deleteMediaUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const deleteMediaUseCase = { execute: vi.fn(async () => Result.ok({ mediaId: 'm1', deleted: true })) };
const deleteMediaPresenter = { responseModel: { success: true } }; const deleteMediaPresenter = {
transform: vi.fn((result) => ({ success: result.deleted })),
responseModel: { success: true }
};
const service = new MediaService( const service = new MediaService(
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
@@ -238,6 +258,7 @@ describe('MediaService', () => {
await expect(service.deleteMedia('m1')).resolves.toEqual({ success: true }); await expect(service.deleteMedia('m1')).resolves.toEqual({ success: true });
expect(deleteMediaUseCase.execute).toHaveBeenCalledWith({ mediaId: 'm1' }); expect(deleteMediaUseCase.execute).toHaveBeenCalledWith({ mediaId: 'm1' });
expect(deleteMediaPresenter.transform).toHaveBeenCalledWith({ mediaId: 'm1', deleted: true });
}); });
it('deleteMedia returns failure DTO on error', async () => { it('deleteMedia returns failure DTO on error', async () => {
@@ -261,8 +282,12 @@ describe('MediaService', () => {
}); });
it('getAvatar returns presenter response on success', async () => { it('getAvatar returns presenter response on success', async () => {
const getAvatarUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const selectedAt = new Date();
const getAvatarPresenter = { responseModel: { avatarUrl: 'u1' } }; const getAvatarUseCase = { execute: vi.fn(async () => Result.ok({ avatar: { id: 'a1', driverId: 'd1', mediaUrl: 'https://example.com/avatar.png', selectedAt } })) };
const getAvatarPresenter = {
transform: vi.fn((result) => ({ avatarUrl: result.avatar.mediaUrl })),
responseModel: { avatarUrl: 'https://example.com/avatar.png' }
};
const service = new MediaService( const service = new MediaService(
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
@@ -280,8 +305,9 @@ describe('MediaService', () => {
{ responseModel: {} } as any, { responseModel: {} } as any,
); );
await expect(service.getAvatar('d1')).resolves.toEqual({ avatarUrl: 'u1' }); await expect(service.getAvatar('d1')).resolves.toEqual({ avatarUrl: 'https://example.com/avatar.png' });
expect(getAvatarUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1' }); expect(getAvatarUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1' });
expect(getAvatarPresenter.transform).toHaveBeenCalledWith({ avatar: { id: 'a1', driverId: 'd1', mediaUrl: 'https://example.com/avatar.png', selectedAt } });
}); });
it('getAvatar returns null on AVATAR_NOT_FOUND', async () => { it('getAvatar returns null on AVATAR_NOT_FOUND', async () => {
@@ -325,8 +351,11 @@ describe('MediaService', () => {
}); });
it('updateAvatar returns presenter response on success', async () => { it('updateAvatar returns presenter response on success', async () => {
const updateAvatarUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const updateAvatarUseCase = { execute: vi.fn(async () => Result.ok({ avatarId: 'a1', driverId: 'd1' })) };
const updateAvatarPresenter = { responseModel: { success: true } }; const updateAvatarPresenter = {
transform: vi.fn(() => ({ success: true })),
responseModel: { success: true }
};
const service = new MediaService( const service = new MediaService(
{ execute: vi.fn() } as any, { execute: vi.fn() } as any,
@@ -346,6 +375,7 @@ describe('MediaService', () => {
await expect(service.updateAvatar('d1', { avatarUrl: 'u1' } as any)).resolves.toEqual({ success: true }); await expect(service.updateAvatar('d1', { avatarUrl: 'u1' } as any)).resolves.toEqual({ success: true });
expect(updateAvatarUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1', mediaUrl: 'u1' }); expect(updateAvatarUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1', mediaUrl: 'u1' });
expect(updateAvatarPresenter.transform).toHaveBeenCalledWith({ avatarId: 'a1', driverId: 'd1' });
}); });
it('updateAvatar returns failure DTO on error', async () => { it('updateAvatar returns failure DTO on error', async () => {

View File

@@ -25,7 +25,7 @@ import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMedi
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase'; import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase'; import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Presenters // Presenters (now transformers)
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter'; import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
import { UploadMediaPresenter } from './presenters/UploadMediaPresenter'; import { UploadMediaPresenter } from './presenters/UploadMediaPresenter';
import { GetMediaPresenter } from './presenters/GetMediaPresenter'; import { GetMediaPresenter } from './presenters/GetMediaPresenter';
@@ -90,7 +90,7 @@ export class MediaService {
}; };
} }
return this.requestAvatarGenerationPresenter.responseModel; return this.requestAvatarGenerationPresenter.transform(result.unwrap());
} }
async uploadMedia( async uploadMedia(
@@ -112,7 +112,7 @@ export class MediaService {
}; };
} }
return this.uploadMediaPresenter.responseModel; return this.uploadMediaPresenter.transform(result.unwrap());
} }
async getMedia(mediaId: string): Promise<GetMediaOutputDTO | null> { async getMedia(mediaId: string): Promise<GetMediaOutputDTO | null> {
@@ -128,7 +128,7 @@ export class MediaService {
throw new Error(error.details?.message ?? 'Failed to get media'); throw new Error(error.details?.message ?? 'Failed to get media');
} }
return this.getMediaPresenter.responseModel; return this.getMediaPresenter.transform(result.unwrap());
} }
async deleteMedia(mediaId: string): Promise<DeleteMediaOutputDTO> { async deleteMedia(mediaId: string): Promise<DeleteMediaOutputDTO> {
@@ -144,7 +144,7 @@ export class MediaService {
}; };
} }
return this.deleteMediaPresenter.responseModel; return this.deleteMediaPresenter.transform(result.unwrap());
} }
async getAvatar(driverId: string): Promise<GetAvatarOutputDTO | null> { async getAvatar(driverId: string): Promise<GetAvatarOutputDTO | null> {
@@ -160,7 +160,7 @@ export class MediaService {
throw new Error(error.details?.message ?? 'Failed to get avatar'); throw new Error(error.details?.message ?? 'Failed to get avatar');
} }
return this.getAvatarPresenter.responseModel; return this.getAvatarPresenter.transform(result.unwrap());
} }
async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutputDTO> { async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutputDTO> {
@@ -189,7 +189,7 @@ export class MediaService {
}; };
} }
return this.updateAvatarPresenter.responseModel; return this.updateAvatarPresenter.transform(result.unwrap());
} }
async validateFacePhoto(input: ValidateFaceInputDTO): Promise<ValidateFaceOutputDTO> { async validateFacePhoto(input: ValidateFaceInputDTO): Promise<ValidateFaceOutputDTO> {

View File

@@ -12,10 +12,3 @@ export const GET_MEDIA_USE_CASE_TOKEN = 'GetMediaUseCase';
export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase'; export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase';
export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase'; export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase';
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase'; export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase';
export const REQUEST_AVATAR_GENERATION_OUTPUT_PORT_TOKEN = 'RequestAvatarGenerationOutputPort';
export const UPLOAD_MEDIA_OUTPUT_PORT_TOKEN = 'UploadMediaOutputPort';
export const GET_MEDIA_OUTPUT_PORT_TOKEN = 'GetMediaOutputPort';
export const DELETE_MEDIA_OUTPUT_PORT_TOKEN = 'DeleteMediaOutputPort';
export const GET_AVATAR_OUTPUT_PORT_TOKEN = 'GetAvatarOutputPort';
export const UPDATE_AVATAR_OUTPUT_PORT_TOKEN = 'UpdateAvatarOutputPort';

View File

@@ -1,20 +1,20 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase'; import type { DeleteMediaResult } from '@core/media/application/use-cases/DeleteMediaUseCase';
import type { DeleteMediaOutputDTO } from '../dtos/DeleteMediaOutputDTO'; import type { DeleteMediaOutputDTO } from '../dtos/DeleteMediaOutputDTO';
type DeleteMediaResponseModel = DeleteMediaOutputDTO; type DeleteMediaResponseModel = DeleteMediaOutputDTO;
export class DeleteMediaPresenter implements UseCaseOutputPort<DeleteMediaResult> { export class DeleteMediaPresenter {
private model: DeleteMediaResponseModel | null = null; private model: DeleteMediaResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: DeleteMediaResult): void { transform(result: DeleteMediaResult): DeleteMediaResponseModel {
this.model = { this.model = {
success: result.deleted, success: result.deleted,
}; };
return this.model;
} }
getResponseModel(): DeleteMediaResponseModel | null { getResponseModel(): DeleteMediaResponseModel | null {

View File

@@ -1,20 +1,20 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase'; import type { GetAvatarResult } from '@core/media/application/use-cases/GetAvatarUseCase';
import type { GetAvatarOutputDTO } from '../dtos/GetAvatarOutputDTO'; import type { GetAvatarOutputDTO } from '../dtos/GetAvatarOutputDTO';
export type GetAvatarResponseModel = GetAvatarOutputDTO | null; export type GetAvatarResponseModel = GetAvatarOutputDTO | null;
export class GetAvatarPresenter implements UseCaseOutputPort<GetAvatarResult> { export class GetAvatarPresenter {
private model: GetAvatarResponseModel | null = null; private model: GetAvatarResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: GetAvatarResult): void { transform(result: GetAvatarResult): GetAvatarResponseModel {
this.model = { this.model = {
avatarUrl: result.avatar.mediaUrl, avatarUrl: result.avatar.mediaUrl,
}; };
return this.model;
} }
getResponseModel(): GetAvatarResponseModel | null { getResponseModel(): GetAvatarResponseModel | null {

View File

@@ -1,17 +1,16 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase'; import type { GetMediaResult } from '@core/media/application/use-cases/GetMediaUseCase';
import type { GetMediaOutputDTO } from '../dtos/GetMediaOutputDTO'; import type { GetMediaOutputDTO } from '../dtos/GetMediaOutputDTO';
export type GetMediaResponseModel = GetMediaOutputDTO | null; export type GetMediaResponseModel = GetMediaOutputDTO | null;
export class GetMediaPresenter implements UseCaseOutputPort<GetMediaResult> { export class GetMediaPresenter {
private model: GetMediaResponseModel | null = null; private model: GetMediaResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: GetMediaResult): void { transform(result: GetMediaResult): GetMediaResponseModel {
const media = result.media; const media = result.media;
const model: GetMediaResponseModel = { const model: GetMediaResponseModel = {
@@ -29,6 +28,7 @@ export class GetMediaPresenter implements UseCaseOutputPort<GetMediaResult> {
} }
this.model = model; this.model = model;
return model;
} }
getResponseModel(): GetMediaResponseModel | null { getResponseModel(): GetMediaResponseModel | null {

View File

@@ -1,22 +1,22 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; import type { RequestAvatarGenerationResult } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO'; import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO';
type RequestAvatarGenerationResponseModel = RequestAvatarGenerationOutputDTO; type RequestAvatarGenerationResponseModel = RequestAvatarGenerationOutputDTO;
export class RequestAvatarGenerationPresenter implements UseCaseOutputPort<RequestAvatarGenerationResult> { export class RequestAvatarGenerationPresenter {
private model: RequestAvatarGenerationResponseModel | null = null; private model: RequestAvatarGenerationResponseModel | null = null;
reset() { reset() {
this.model = null; this.model = null;
} }
present(result: RequestAvatarGenerationResult): void { transform(result: RequestAvatarGenerationResult): RequestAvatarGenerationResponseModel {
this.model = { this.model = {
success: result.status === 'completed', success: result.status === 'completed',
requestId: result.requestId, requestId: result.requestId,
avatarUrls: result.avatarUrls || [], avatarUrls: result.avatarUrls || [],
}; };
return this.model;
} }
getResponseModel(): RequestAvatarGenerationResponseModel | null { getResponseModel(): RequestAvatarGenerationResponseModel | null {

View File

@@ -1,22 +1,22 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase'; import type { UpdateAvatarResult } from '@core/media/application/use-cases/UpdateAvatarUseCase';
import type { UpdateAvatarOutputDTO } from '../dtos/UpdateAvatarOutputDTO'; import type { UpdateAvatarOutputDTO } from '../dtos/UpdateAvatarOutputDTO';
type UpdateAvatarResponseModel = UpdateAvatarOutputDTO; type UpdateAvatarResponseModel = UpdateAvatarOutputDTO;
export class UpdateAvatarPresenter implements UseCaseOutputPort<UpdateAvatarResult> { export class UpdateAvatarPresenter {
private model: UpdateAvatarResponseModel | null = null; private model: UpdateAvatarResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: UpdateAvatarResult): void { transform(result: UpdateAvatarResult): UpdateAvatarResponseModel {
void result; void result;
this.model = { this.model = {
success: true, success: true,
}; };
return this.model;
} }
getResponseModel(): UpdateAvatarResponseModel | null { getResponseModel(): UpdateAvatarResponseModel | null {

View File

@@ -1,17 +1,16 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase'; import type { UploadMediaResult } from '@core/media/application/use-cases/UploadMediaUseCase';
import type { UploadMediaOutputDTO } from '../dtos/UploadMediaOutputDTO'; import type { UploadMediaOutputDTO } from '../dtos/UploadMediaOutputDTO';
type UploadMediaResponseModel = UploadMediaOutputDTO; type UploadMediaResponseModel = UploadMediaOutputDTO;
export class UploadMediaPresenter implements UseCaseOutputPort<UploadMediaResult> { export class UploadMediaPresenter {
private model: UploadMediaResponseModel | null = null; private model: UploadMediaResponseModel | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: UploadMediaResult): void { transform(result: UploadMediaResult): UploadMediaResponseModel {
const model: UploadMediaResponseModel = { const model: UploadMediaResponseModel = {
success: true, success: true,
mediaId: result.mediaId, mediaId: result.mediaId,
@@ -22,6 +21,7 @@ export class UploadMediaPresenter implements UseCaseOutputPort<UploadMediaResult
} }
this.model = model; this.model = model;
return model;
} }
getResponseModel(): UploadMediaResponseModel | null { getResponseModel(): UploadMediaResponseModel | null {

View File

@@ -5,7 +5,6 @@ import type { IPaymentRepository } from '@core/payments/domain/repositories/IPay
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository'; import type { IMembershipFeeRepository, IMemberPaymentRepository } from '@core/payments/domain/repositories/IMembershipFeeRepository';
import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository'; import type { IPrizeRepository } from '@core/payments/domain/repositories/IPrizeRepository';
import type { IWalletRepository, ITransactionRepository } from '@core/payments/domain/repositories/IWalletRepository'; import type { IWalletRepository, ITransactionRepository } from '@core/payments/domain/repositories/IWalletRepository';
import type { UseCaseOutputPort } from '@core/shared/application';
// Import use cases // Import use cases
import { GetPaymentsUseCase } from '@core/payments/application/use-cases/GetPaymentsUseCase'; import { GetPaymentsUseCase } from '@core/payments/application/use-cases/GetPaymentsUseCase';
@@ -24,20 +23,6 @@ import { ProcessWalletTransactionUseCase } from '@core/payments/application/use-
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Presenters
import { GetPaymentsPresenter } from './presenters/GetPaymentsPresenter';
import { CreatePaymentPresenter } from './presenters/CreatePaymentPresenter';
import { UpdatePaymentStatusPresenter } from './presenters/UpdatePaymentStatusPresenter';
import { GetMembershipFeesPresenter } from './presenters/GetMembershipFeesPresenter';
import { UpsertMembershipFeePresenter } from './presenters/UpsertMembershipFeePresenter';
import { UpdateMemberPaymentPresenter } from './presenters/UpdateMemberPaymentPresenter';
import { GetPrizesPresenter } from './presenters/GetPrizesPresenter';
import { CreatePrizePresenter } from './presenters/CreatePrizePresenter';
import { AwardPrizePresenter } from './presenters/AwardPrizePresenter';
import { DeletePrizePresenter } from './presenters/DeletePrizePresenter';
import { GetWalletPresenter } from './presenters/GetWalletPresenter';
import { ProcessWalletTransactionPresenter } from './presenters/ProcessWalletTransactionPresenter';
import { import {
PAYMENT_REPOSITORY_TOKEN, PAYMENT_REPOSITORY_TOKEN,
MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBERSHIP_FEE_REPOSITORY_TOKEN,
@@ -58,88 +43,12 @@ import {
DELETE_PRIZE_USE_CASE_TOKEN, DELETE_PRIZE_USE_CASE_TOKEN,
GET_WALLET_USE_CASE_TOKEN, GET_WALLET_USE_CASE_TOKEN,
PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN, PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN,
GET_PAYMENTS_OUTPUT_PORT_TOKEN,
CREATE_PAYMENT_OUTPUT_PORT_TOKEN,
UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN,
GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN,
UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN,
UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN,
GET_PRIZES_OUTPUT_PORT_TOKEN,
CREATE_PRIZE_OUTPUT_PORT_TOKEN,
AWARD_PRIZE_OUTPUT_PORT_TOKEN,
DELETE_PRIZE_OUTPUT_PORT_TOKEN,
GET_WALLET_OUTPUT_PORT_TOKEN,
PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN,
} from './PaymentsTokens'; } from './PaymentsTokens';
export * from './PaymentsTokens'; export * from './PaymentsTokens';
export const PaymentsProviders: Provider[] = [ export const PaymentsProviders: Provider[] = [
// Presenters
GetPaymentsPresenter,
CreatePaymentPresenter,
UpdatePaymentStatusPresenter,
GetMembershipFeesPresenter,
UpsertMembershipFeePresenter,
UpdateMemberPaymentPresenter,
GetPrizesPresenter,
CreatePrizePresenter,
AwardPrizePresenter,
DeletePrizePresenter,
GetWalletPresenter,
ProcessWalletTransactionPresenter,
// Output ports
{
provide: GET_PAYMENTS_OUTPUT_PORT_TOKEN,
useExisting: GetPaymentsPresenter,
},
{
provide: CREATE_PAYMENT_OUTPUT_PORT_TOKEN,
useExisting: CreatePaymentPresenter,
},
{
provide: UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN,
useExisting: UpdatePaymentStatusPresenter,
},
{
provide: GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN,
useExisting: GetMembershipFeesPresenter,
},
{
provide: UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN,
useExisting: UpsertMembershipFeePresenter,
},
{
provide: UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN,
useExisting: UpdateMemberPaymentPresenter,
},
{
provide: GET_PRIZES_OUTPUT_PORT_TOKEN,
useExisting: GetPrizesPresenter,
},
{
provide: CREATE_PRIZE_OUTPUT_PORT_TOKEN,
useExisting: CreatePrizePresenter,
},
{
provide: AWARD_PRIZE_OUTPUT_PORT_TOKEN,
useExisting: AwardPrizePresenter,
},
{
provide: DELETE_PRIZE_OUTPUT_PORT_TOKEN,
useExisting: DeletePrizePresenter,
},
{
provide: GET_WALLET_OUTPUT_PORT_TOKEN,
useExisting: GetWalletPresenter,
},
{
provide: PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN,
useExisting: ProcessWalletTransactionPresenter,
},
// Logger // Logger
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
@@ -151,66 +60,66 @@ export const PaymentsProviders: Provider[] = [
// Use cases (use cases receive repositories, services receive use cases) // Use cases (use cases receive repositories, services receive use cases)
{ {
provide: GET_PAYMENTS_USE_CASE_TOKEN, provide: GET_PAYMENTS_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort<unknown>) => new GetPaymentsUseCase(paymentRepo, output), useFactory: (paymentRepo: IPaymentRepository) => new GetPaymentsUseCase(paymentRepo),
inject: [PAYMENT_REPOSITORY_TOKEN, GET_PAYMENTS_OUTPUT_PORT_TOKEN], inject: [PAYMENT_REPOSITORY_TOKEN],
}, },
{ {
provide: CREATE_PAYMENT_USE_CASE_TOKEN, provide: CREATE_PAYMENT_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort<unknown>) => new CreatePaymentUseCase(paymentRepo, output), useFactory: (paymentRepo: IPaymentRepository) => new CreatePaymentUseCase(paymentRepo),
inject: [PAYMENT_REPOSITORY_TOKEN, CREATE_PAYMENT_OUTPUT_PORT_TOKEN], inject: [PAYMENT_REPOSITORY_TOKEN],
}, },
{ {
provide: UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN, provide: UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository, output: UseCaseOutputPort<unknown>) => new UpdatePaymentStatusUseCase(paymentRepo, output), useFactory: (paymentRepo: IPaymentRepository) => new UpdatePaymentStatusUseCase(paymentRepo),
inject: [PAYMENT_REPOSITORY_TOKEN, UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN], inject: [PAYMENT_REPOSITORY_TOKEN],
}, },
{ {
provide: GET_MEMBERSHIP_FEES_USE_CASE_TOKEN, provide: GET_MEMBERSHIP_FEES_USE_CASE_TOKEN,
useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository, output: UseCaseOutputPort<unknown>) => useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) =>
new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo, output), new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo),
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN, GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN], inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN],
}, },
{ {
provide: UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN, provide: UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN,
useFactory: (membershipFeeRepo: IMembershipFeeRepository, output: UseCaseOutputPort<unknown>) => new UpsertMembershipFeeUseCase(membershipFeeRepo, output), useFactory: (membershipFeeRepo: IMembershipFeeRepository) => new UpsertMembershipFeeUseCase(membershipFeeRepo),
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN], inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN],
}, },
{ {
provide: UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN, provide: UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN,
useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository, output: UseCaseOutputPort<unknown>) => useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) =>
new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo, output), new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo),
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN, UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN], inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN],
}, },
{ {
provide: GET_PRIZES_USE_CASE_TOKEN, provide: GET_PRIZES_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<unknown>) => new GetPrizesUseCase(prizeRepo, output), useFactory: (prizeRepo: IPrizeRepository) => new GetPrizesUseCase(prizeRepo),
inject: [PRIZE_REPOSITORY_TOKEN, GET_PRIZES_OUTPUT_PORT_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN],
}, },
{ {
provide: CREATE_PRIZE_USE_CASE_TOKEN, provide: CREATE_PRIZE_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<unknown>) => new CreatePrizeUseCase(prizeRepo, output), useFactory: (prizeRepo: IPrizeRepository) => new CreatePrizeUseCase(prizeRepo),
inject: [PRIZE_REPOSITORY_TOKEN, CREATE_PRIZE_OUTPUT_PORT_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN],
}, },
{ {
provide: AWARD_PRIZE_USE_CASE_TOKEN, provide: AWARD_PRIZE_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<unknown>) => new AwardPrizeUseCase(prizeRepo, output), useFactory: (prizeRepo: IPrizeRepository) => new AwardPrizeUseCase(prizeRepo),
inject: [PRIZE_REPOSITORY_TOKEN, AWARD_PRIZE_OUTPUT_PORT_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN],
}, },
{ {
provide: DELETE_PRIZE_USE_CASE_TOKEN, provide: DELETE_PRIZE_USE_CASE_TOKEN,
useFactory: (prizeRepo: IPrizeRepository, output: UseCaseOutputPort<unknown>) => new DeletePrizeUseCase(prizeRepo, output), useFactory: (prizeRepo: IPrizeRepository) => new DeletePrizeUseCase(prizeRepo),
inject: [PRIZE_REPOSITORY_TOKEN, DELETE_PRIZE_OUTPUT_PORT_TOKEN], inject: [PRIZE_REPOSITORY_TOKEN],
}, },
{ {
provide: GET_WALLET_USE_CASE_TOKEN, provide: GET_WALLET_USE_CASE_TOKEN,
useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository, output: UseCaseOutputPort<unknown>) => useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) =>
new GetWalletUseCase(walletRepo, transactionRepo, output), new GetWalletUseCase(walletRepo, transactionRepo),
inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, GET_WALLET_OUTPUT_PORT_TOKEN], inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN],
}, },
{ {
provide: PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN, provide: PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN,
useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository, output: UseCaseOutputPort<unknown>) => useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) =>
new ProcessWalletTransactionUseCase(walletRepo, transactionRepo, output), new ProcessWalletTransactionUseCase(walletRepo, transactionRepo),
inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN], inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN],
}, },
]; ];

View File

@@ -6,42 +6,23 @@ describe('PaymentsService', () => {
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
function makeService(overrides?: Partial<Record<string, any>>) { function makeService(overrides?: Partial<Record<string, any>>) {
const getPaymentsUseCase = overrides?.getPaymentsUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const getPaymentsUseCase = overrides?.getPaymentsUseCase ?? { execute: vi.fn(async () => Result.ok({ payments: [] })) };
const createPaymentUseCase = overrides?.createPaymentUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const createPaymentUseCase = overrides?.createPaymentUseCase ?? { execute: vi.fn(async () => Result.ok({ paymentId: 'p1' })) };
const updatePaymentStatusUseCase = const updatePaymentStatusUseCase =
overrides?.updatePaymentStatusUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; overrides?.updatePaymentStatusUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const getMembershipFeesUseCase = const getMembershipFeesUseCase =
overrides?.getMembershipFeesUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; overrides?.getMembershipFeesUseCase ?? { execute: vi.fn(async () => Result.ok({ fee: null, payments: [] })) };
const upsertMembershipFeeUseCase = const upsertMembershipFeeUseCase =
overrides?.upsertMembershipFeeUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; overrides?.upsertMembershipFeeUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const updateMemberPaymentUseCase = const updateMemberPaymentUseCase =
overrides?.updateMemberPaymentUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; overrides?.updateMemberPaymentUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const getPrizesUseCase = overrides?.getPrizesUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const getPrizesUseCase = overrides?.getPrizesUseCase ?? { execute: vi.fn(async () => Result.ok({ prizes: [] })) };
const createPrizeUseCase = overrides?.createPrizeUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const createPrizeUseCase = overrides?.createPrizeUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const awardPrizeUseCase = overrides?.awardPrizeUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const awardPrizeUseCase = overrides?.awardPrizeUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const deletePrizeUseCase = overrides?.deletePrizeUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const deletePrizeUseCase = overrides?.deletePrizeUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const getWalletUseCase = overrides?.getWalletUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; const getWalletUseCase = overrides?.getWalletUseCase ?? { execute: vi.fn(async () => Result.ok({ balance: 0 })) };
const processWalletTransactionUseCase = const processWalletTransactionUseCase =
overrides?.processWalletTransactionUseCase ?? { execute: vi.fn(async () => Result.ok(undefined)) }; overrides?.processWalletTransactionUseCase ?? { execute: vi.fn(async () => Result.ok({ success: true })) };
const getPaymentsPresenter = overrides?.getPaymentsPresenter ?? { getResponseModel: vi.fn(() => ({ payments: [] })) };
const createPaymentPresenter =
overrides?.createPaymentPresenter ?? { getResponseModel: vi.fn(() => ({ paymentId: 'p1' })) };
const updatePaymentStatusPresenter =
overrides?.updatePaymentStatusPresenter ?? { getResponseModel: vi.fn(() => ({ success: true })) };
const getMembershipFeesPresenter = overrides?.getMembershipFeesPresenter ?? { viewModel: { fee: null, payments: [] } };
const upsertMembershipFeePresenter = overrides?.upsertMembershipFeePresenter ?? { viewModel: { success: true } };
const updateMemberPaymentPresenter = overrides?.updateMemberPaymentPresenter ?? { viewModel: { success: true } };
const getPrizesPresenter = overrides?.getPrizesPresenter ?? { viewModel: { prizes: [] } };
const createPrizePresenter = overrides?.createPrizePresenter ?? { viewModel: { success: true } };
const awardPrizePresenter = overrides?.awardPrizePresenter ?? { viewModel: { success: true } };
const deletePrizePresenter = overrides?.deletePrizePresenter ?? { viewModel: { success: true } };
const getWalletPresenter = overrides?.getWalletPresenter ?? { viewModel: { balance: 0 } };
const processWalletTransactionPresenter =
overrides?.processWalletTransactionPresenter ?? { viewModel: { success: true } };
const service = new PaymentsService( const service = new PaymentsService(
getPaymentsUseCase as any, getPaymentsUseCase as any,
@@ -57,18 +38,6 @@ describe('PaymentsService', () => {
getWalletUseCase as any, getWalletUseCase as any,
processWalletTransactionUseCase as any, processWalletTransactionUseCase as any,
logger as any, logger as any,
getPaymentsPresenter as any,
createPaymentPresenter as any,
updatePaymentStatusPresenter as any,
getMembershipFeesPresenter as any,
upsertMembershipFeePresenter as any,
updateMemberPaymentPresenter as any,
getPrizesPresenter as any,
createPrizePresenter as any,
awardPrizePresenter as any,
deletePrizePresenter as any,
getWalletPresenter as any,
processWalletTransactionPresenter as any,
); );
return { return {
@@ -85,26 +54,13 @@ describe('PaymentsService', () => {
deletePrizeUseCase, deletePrizeUseCase,
getWalletUseCase, getWalletUseCase,
processWalletTransactionUseCase, processWalletTransactionUseCase,
getPaymentsPresenter,
createPaymentPresenter,
updatePaymentStatusPresenter,
getMembershipFeesPresenter,
upsertMembershipFeePresenter,
updateMemberPaymentPresenter,
getPrizesPresenter,
createPrizePresenter,
awardPrizePresenter,
deletePrizePresenter,
getWalletPresenter,
processWalletTransactionPresenter,
}; };
} }
it('getPayments returns presenter model on success', async () => { it('getPayments returns presenter model on success', async () => {
const { service, getPaymentsUseCase, getPaymentsPresenter } = makeService(); const { service, getPaymentsUseCase } = makeService();
await expect(service.getPayments({ leagueId: 'l1' } as any)).resolves.toEqual({ payments: [] }); await expect(service.getPayments({ leagueId: 'l1' } as any)).resolves.toEqual({ payments: [] });
expect(getPaymentsUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' }); expect(getPaymentsUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
expect(getPaymentsPresenter.getResponseModel).toHaveBeenCalled();
}); });
it('getPayments throws when use case returns error (code message)', async () => { it('getPayments throws when use case returns error (code message)', async () => {
@@ -115,12 +71,9 @@ describe('PaymentsService', () => {
}); });
it('createPayment returns presenter model on success', async () => { it('createPayment returns presenter model on success', async () => {
const { service, createPaymentUseCase, createPaymentPresenter } = makeService({ const { service, createPaymentUseCase } = makeService();
createPaymentPresenter: { getResponseModel: vi.fn(() => ({ paymentId: 'p1' })) },
});
await expect(service.createPayment({ leagueId: 'l1' } as any)).resolves.toEqual({ paymentId: 'p1' }); await expect(service.createPayment({ leagueId: 'l1' } as any)).resolves.toEqual({ paymentId: 'p1' });
expect(createPaymentUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' }); expect(createPaymentUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
expect(createPaymentPresenter.getResponseModel).toHaveBeenCalled();
}); });
it('createPayment throws when use case returns error', async () => { it('createPayment throws when use case returns error', async () => {
@@ -131,12 +84,9 @@ describe('PaymentsService', () => {
}); });
it('updatePaymentStatus returns presenter model on success', async () => { it('updatePaymentStatus returns presenter model on success', async () => {
const { service, updatePaymentStatusUseCase, updatePaymentStatusPresenter } = makeService({ const { service, updatePaymentStatusUseCase } = makeService();
updatePaymentStatusPresenter: { getResponseModel: vi.fn(() => ({ success: true })) },
});
await expect(service.updatePaymentStatus({ paymentId: 'p1' } as any)).resolves.toEqual({ success: true }); await expect(service.updatePaymentStatus({ paymentId: 'p1' } as any)).resolves.toEqual({ success: true });
expect(updatePaymentStatusUseCase.execute).toHaveBeenCalledWith({ paymentId: 'p1' }); expect(updatePaymentStatusUseCase.execute).toHaveBeenCalledWith({ paymentId: 'p1' });
expect(updatePaymentStatusPresenter.getResponseModel).toHaveBeenCalled();
}); });
it('updatePaymentStatus throws when use case returns error', async () => { it('updatePaymentStatus throws when use case returns error', async () => {
@@ -147,8 +97,8 @@ describe('PaymentsService', () => {
}); });
it('getMembershipFees returns viewModel on success', async () => { it('getMembershipFees returns viewModel on success', async () => {
const { service, getMembershipFeesUseCase, getMembershipFeesPresenter } = makeService({ const { service, getMembershipFeesUseCase } = makeService({
getMembershipFeesPresenter: { viewModel: { fee: { amount: 1 }, payments: [] } }, getMembershipFeesUseCase: { execute: vi.fn(async () => Result.ok({ fee: { amount: 1 }, payments: [] })) }
}); });
await expect(service.getMembershipFees({ leagueId: 'l1', driverId: 'd1' } as any)).resolves.toEqual({ await expect(service.getMembershipFees({ leagueId: 'l1', driverId: 'd1' } as any)).resolves.toEqual({
@@ -156,7 +106,6 @@ describe('PaymentsService', () => {
payments: [], payments: [],
}); });
expect(getMembershipFeesUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', driverId: 'd1' }); expect(getMembershipFeesUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', driverId: 'd1' });
expect(getMembershipFeesPresenter.viewModel).toBeDefined();
}); });
it('getMembershipFees throws when use case returns error', async () => { it('getMembershipFees throws when use case returns error', async () => {
@@ -167,9 +116,7 @@ describe('PaymentsService', () => {
}); });
it('upsertMembershipFee returns viewModel on success', async () => { it('upsertMembershipFee returns viewModel on success', async () => {
const { service, upsertMembershipFeeUseCase } = makeService({ const { service, upsertMembershipFeeUseCase } = makeService();
upsertMembershipFeePresenter: { viewModel: { success: true } },
});
await expect(service.upsertMembershipFee({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true }); await expect(service.upsertMembershipFee({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true });
expect(upsertMembershipFeeUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' }); expect(upsertMembershipFeeUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
@@ -186,9 +133,7 @@ describe('PaymentsService', () => {
}); });
it('updateMemberPayment returns viewModel on success', async () => { it('updateMemberPayment returns viewModel on success', async () => {
const { service, updateMemberPaymentUseCase } = makeService({ const { service, updateMemberPaymentUseCase } = makeService();
updateMemberPaymentPresenter: { viewModel: { success: true } },
});
await expect(service.updateMemberPayment({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true }); await expect(service.updateMemberPayment({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true });
expect(updateMemberPaymentUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' }); expect(updateMemberPaymentUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
@@ -203,10 +148,9 @@ describe('PaymentsService', () => {
}); });
it('getPrizes maps seasonId optional', async () => { it('getPrizes maps seasonId optional', async () => {
const getPrizesUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const getPrizesUseCase = { execute: vi.fn(async () => Result.ok({ prizes: [] })) };
const { service } = makeService({ const { service } = makeService({
getPrizesUseCase, getPrizesUseCase,
getPrizesPresenter: { viewModel: { prizes: [] } },
}); });
await expect(service.getPrizes({ leagueId: 'l1' } as any)).resolves.toEqual({ prizes: [] }); await expect(service.getPrizes({ leagueId: 'l1' } as any)).resolves.toEqual({ prizes: [] });
@@ -217,10 +161,9 @@ describe('PaymentsService', () => {
}); });
it('createPrize calls use case and returns viewModel', async () => { it('createPrize calls use case and returns viewModel', async () => {
const createPrizeUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const createPrizeUseCase = { execute: vi.fn(async () => Result.ok({ success: true })) };
const { service } = makeService({ const { service } = makeService({
createPrizeUseCase, createPrizeUseCase,
createPrizePresenter: { viewModel: { success: true } },
}); });
await expect(service.createPrize({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true }); await expect(service.createPrize({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true });
@@ -228,10 +171,9 @@ describe('PaymentsService', () => {
}); });
it('awardPrize calls use case and returns viewModel', async () => { it('awardPrize calls use case and returns viewModel', async () => {
const awardPrizeUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const awardPrizeUseCase = { execute: vi.fn(async () => Result.ok({ success: true })) };
const { service } = makeService({ const { service } = makeService({
awardPrizeUseCase, awardPrizeUseCase,
awardPrizePresenter: { viewModel: { success: true } },
}); });
await expect(service.awardPrize({ prizeId: 'p1' } as any)).resolves.toEqual({ success: true }); await expect(service.awardPrize({ prizeId: 'p1' } as any)).resolves.toEqual({ success: true });
@@ -239,10 +181,9 @@ describe('PaymentsService', () => {
}); });
it('deletePrize calls use case and returns viewModel', async () => { it('deletePrize calls use case and returns viewModel', async () => {
const deletePrizeUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const deletePrizeUseCase = { execute: vi.fn(async () => Result.ok({ success: true })) };
const { service } = makeService({ const { service } = makeService({
deletePrizeUseCase, deletePrizeUseCase,
deletePrizePresenter: { viewModel: { success: true } },
}); });
await expect(service.deletePrize({ prizeId: 'p1' } as any)).resolves.toEqual({ success: true }); await expect(service.deletePrize({ prizeId: 'p1' } as any)).resolves.toEqual({ success: true });
@@ -250,10 +191,9 @@ describe('PaymentsService', () => {
}); });
it('getWallet calls use case and returns viewModel', async () => { it('getWallet calls use case and returns viewModel', async () => {
const getWalletUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const getWalletUseCase = { execute: vi.fn(async () => Result.ok({ balance: 10 })) };
const { service } = makeService({ const { service } = makeService({
getWalletUseCase, getWalletUseCase,
getWalletPresenter: { viewModel: { balance: 10 } },
}); });
await expect(service.getWallet({ leagueId: 'l1' } as any)).resolves.toEqual({ balance: 10 }); await expect(service.getWallet({ leagueId: 'l1' } as any)).resolves.toEqual({ balance: 10 });
@@ -261,10 +201,9 @@ describe('PaymentsService', () => {
}); });
it('processWalletTransaction calls use case and returns viewModel', async () => { it('processWalletTransaction calls use case and returns viewModel', async () => {
const processWalletTransactionUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; const processWalletTransactionUseCase = { execute: vi.fn(async () => Result.ok({ success: true })) };
const { service } = makeService({ const { service } = makeService({
processWalletTransactionUseCase, processWalletTransactionUseCase,
processWalletTransactionPresenter: { viewModel: { success: true } },
}); });
await expect(service.processWalletTransaction({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true }); await expect(service.processWalletTransaction({ leagueId: 'l1' } as any)).resolves.toEqual({ success: true });

View File

@@ -15,20 +15,6 @@ import type { UpdateMemberPaymentUseCase } from '@core/payments/application/use-
import type { UpdatePaymentStatusUseCase } from '@core/payments/application/use-cases/UpdatePaymentStatusUseCase'; import type { UpdatePaymentStatusUseCase } from '@core/payments/application/use-cases/UpdatePaymentStatusUseCase';
import type { UpsertMembershipFeeUseCase } from '@core/payments/application/use-cases/UpsertMembershipFeeUseCase'; import type { UpsertMembershipFeeUseCase } from '@core/payments/application/use-cases/UpsertMembershipFeeUseCase';
// Presenters
import { AwardPrizePresenter } from './presenters/AwardPrizePresenter';
import { CreatePaymentPresenter } from './presenters/CreatePaymentPresenter';
import { CreatePrizePresenter } from './presenters/CreatePrizePresenter';
import { DeletePrizePresenter } from './presenters/DeletePrizePresenter';
import { GetMembershipFeesPresenter } from './presenters/GetMembershipFeesPresenter';
import { GetPaymentsPresenter } from './presenters/GetPaymentsPresenter';
import { GetPrizesPresenter } from './presenters/GetPrizesPresenter';
import { GetWalletPresenter } from './presenters/GetWalletPresenter';
import { ProcessWalletTransactionPresenter } from './presenters/ProcessWalletTransactionPresenter';
import { UpdateMemberPaymentPresenter } from './presenters/UpdateMemberPaymentPresenter';
import { UpdatePaymentStatusPresenter } from './presenters/UpdatePaymentStatusPresenter';
import { UpsertMembershipFeePresenter } from './presenters/UpsertMembershipFeePresenter';
// DTOs // DTOs
import type { import type {
AwardPrizeInput, AwardPrizeInput,
@@ -90,18 +76,6 @@ export class PaymentsService {
@Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase, @Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase,
@Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase, @Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
private readonly getPaymentsPresenter: GetPaymentsPresenter,
private readonly createPaymentPresenter: CreatePaymentPresenter,
private readonly updatePaymentStatusPresenter: UpdatePaymentStatusPresenter,
private readonly getMembershipFeesPresenter: GetMembershipFeesPresenter,
private readonly upsertMembershipFeePresenter: UpsertMembershipFeePresenter,
private readonly updateMemberPaymentPresenter: UpdateMemberPaymentPresenter,
private readonly getPrizesPresenter: GetPrizesPresenter,
private readonly createPrizePresenter: CreatePrizePresenter,
private readonly awardPrizePresenter: AwardPrizePresenter,
private readonly deletePrizePresenter: DeletePrizePresenter,
private readonly getWalletPresenter: GetWalletPresenter,
private readonly processWalletTransactionPresenter: ProcessWalletTransactionPresenter,
) {} ) {}
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> { async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
@@ -111,7 +85,11 @@ export class PaymentsService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code ?? 'Failed to get payments'); throw new Error(result.unwrapErr().code ?? 'Failed to get payments');
} }
return this.getPaymentsPresenter.getResponseModel(); const value = result.value;
if (!value) {
throw new Error('Failed to get payments: no value returned');
}
return value;
} }
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> { async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
@@ -121,7 +99,11 @@ export class PaymentsService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code ?? 'Failed to create payment'); throw new Error(result.unwrapErr().code ?? 'Failed to create payment');
} }
return this.createPaymentPresenter.getResponseModel(); const value = result.value;
if (!value) {
throw new Error('Failed to create payment: no value returned');
}
return value;
} }
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> { async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
@@ -131,7 +113,11 @@ export class PaymentsService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code ?? 'Failed to update payment status'); throw new Error(result.unwrapErr().code ?? 'Failed to update payment status');
} }
return this.updatePaymentStatusPresenter.getResponseModel(); const value = result.value;
if (!value) {
throw new Error('Failed to update payment status: no value returned');
}
return value;
} }
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> { async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
@@ -141,7 +127,11 @@ export class PaymentsService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code ?? 'Failed to get membership fees'); throw new Error(result.unwrapErr().code ?? 'Failed to get membership fees');
} }
return this.getMembershipFeesPresenter.viewModel; const value = result.value;
if (!value) {
throw new Error('Failed to get membership fees: no value returned');
}
return value;
} }
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> { async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
@@ -153,7 +143,11 @@ export class PaymentsService {
// but we keep the check for consistency // but we keep the check for consistency
throw new Error('Failed to upsert membership fee'); throw new Error('Failed to upsert membership fee');
} }
return this.upsertMembershipFeePresenter.viewModel; const value = result.value;
if (!value) {
throw new Error('Failed to upsert membership fee: no value returned');
}
return value;
} }
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> { async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
@@ -163,7 +157,11 @@ export class PaymentsService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code ?? 'Failed to update member payment'); throw new Error(result.unwrapErr().code ?? 'Failed to update member payment');
} }
return this.updateMemberPaymentPresenter.viewModel; const value = result.value;
if (!value) {
throw new Error('Failed to update member payment: no value returned');
}
return value;
} }
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> { async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
@@ -175,42 +173,89 @@ export class PaymentsService {
if (query.seasonId !== undefined) { if (query.seasonId !== undefined) {
input.seasonId = query.seasonId; input.seasonId = query.seasonId;
} }
await this.getPrizesUseCase.execute(input); const result = await this.getPrizesUseCase.execute(input);
return this.getPrizesPresenter.viewModel; if (result.isErr()) {
throw new Error('Failed to get prizes');
}
const value = result.value;
if (!value) {
throw new Error('Failed to get prizes: no value returned');
}
return value;
} }
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> { async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
this.logger.debug('[PaymentsService] Creating prize', { input }); this.logger.debug('[PaymentsService] Creating prize', { input });
await this.createPrizeUseCase.execute(input); const result = await this.createPrizeUseCase.execute(input);
return this.createPrizePresenter.viewModel; if (result.isErr()) {
const err = result.unwrapErr();
throw new Error(err.code ?? 'Failed to create prize');
}
const value = result.value;
if (!value) {
throw new Error('Failed to create prize: no value returned');
}
return value;
} }
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> { async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
this.logger.debug('[PaymentsService] Awarding prize', { input }); this.logger.debug('[PaymentsService] Awarding prize', { input });
await this.awardPrizeUseCase.execute(input); const result = await this.awardPrizeUseCase.execute(input);
return this.awardPrizePresenter.viewModel; if (result.isErr()) {
const err = result.unwrapErr();
throw new Error(err.code ?? 'Failed to award prize');
}
const value = result.value;
if (!value) {
throw new Error('Failed to award prize: no value returned');
}
return value;
} }
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> { async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
this.logger.debug('[PaymentsService] Deleting prize', { input }); this.logger.debug('[PaymentsService] Deleting prize', { input });
await this.deletePrizeUseCase.execute(input); const result = await this.deletePrizeUseCase.execute(input);
return this.deletePrizePresenter.viewModel; if (result.isErr()) {
const err = result.unwrapErr();
throw new Error(err.code ?? 'Failed to delete prize');
}
const value = result.value;
if (!value) {
throw new Error('Failed to delete prize: no value returned');
}
return value;
} }
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> { async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
this.logger.debug('[PaymentsService] Getting wallet', { query }); this.logger.debug('[PaymentsService] Getting wallet', { query });
await this.getWalletUseCase.execute({ leagueId: query.leagueId! }); const result = await this.getWalletUseCase.execute({ leagueId: query.leagueId! });
return this.getWalletPresenter.viewModel; if (result.isErr()) {
const err = result.unwrapErr();
throw new Error(err.code ?? 'Failed to get wallet');
}
const value = result.value;
if (!value) {
throw new Error('Failed to get wallet: no value returned');
}
return value;
} }
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> { async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
this.logger.debug('[PaymentsService] Processing wallet transaction', { input }); this.logger.debug('[PaymentsService] Processing wallet transaction', { input });
await this.processWalletTransactionUseCase.execute(input); const result = await this.processWalletTransactionUseCase.execute(input);
return this.processWalletTransactionPresenter.viewModel; if (result.isErr()) {
const err = result.unwrapErr();
throw new Error(err.code ?? 'Failed to process wallet transaction');
}
const value = result.value;
if (!value) {
throw new Error('Failed to process wallet transaction: no value returned');
}
return value;
} }
} }

View File

@@ -28,16 +28,3 @@ export const AWARD_PRIZE_USE_CASE_TOKEN = 'AwardPrizeUseCase';
export const DELETE_PRIZE_USE_CASE_TOKEN = 'DeletePrizeUseCase'; export const DELETE_PRIZE_USE_CASE_TOKEN = 'DeletePrizeUseCase';
export const GET_WALLET_USE_CASE_TOKEN = 'GetWalletUseCase'; export const GET_WALLET_USE_CASE_TOKEN = 'GetWalletUseCase';
export const PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN = 'ProcessWalletTransactionUseCase'; export const PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN = 'ProcessWalletTransactionUseCase';
export const GET_PAYMENTS_OUTPUT_PORT_TOKEN = 'GetPaymentsOutputPort_TOKEN';
export const CREATE_PAYMENT_OUTPUT_PORT_TOKEN = 'CreatePaymentOutputPort_TOKEN';
export const UPDATE_PAYMENT_STATUS_OUTPUT_PORT_TOKEN = 'UpdatePaymentStatusOutputPort_TOKEN';
export const GET_MEMBERSHIP_FEES_OUTPUT_PORT_TOKEN = 'GetMembershipFeesOutputPort_TOKEN';
export const UPSERT_MEMBERSHIP_FEE_OUTPUT_PORT_TOKEN = 'UpsertMembershipFeeOutputPort_TOKEN';
export const UPDATE_MEMBER_PAYMENT_OUTPUT_PORT_TOKEN = 'UpdateMemberPaymentOutputPort_TOKEN';
export const GET_PRIZES_OUTPUT_PORT_TOKEN = 'GetPrizesOutputPort_TOKEN';
export const CREATE_PRIZE_OUTPUT_PORT_TOKEN = 'CreatePrizeOutputPort_TOKEN';
export const AWARD_PRIZE_OUTPUT_PORT_TOKEN = 'AwardPrizeOutputPort_TOKEN';
export const DELETE_PRIZE_OUTPUT_PORT_TOKEN = 'DeletePrizeOutputPort_TOKEN';
export const GET_WALLET_OUTPUT_PORT_TOKEN = 'GetWalletOutputPort_TOKEN';
export const PROCESS_WALLET_TRANSACTION_OUTPUT_PORT_TOKEN = 'ProcessWalletTransactionOutputPort_TOKEN';

View File

@@ -20,17 +20,13 @@ export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
export const REVIEW_PROTEST_PRESENTER_TOKEN = 'ReviewProtestPresenter';
export const ProtestsProviders: Provider[] = [ export const ProtestsProviders: Provider[] = [
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
}, },
{ ReviewProtestPresenter,
provide: REVIEW_PROTEST_PRESENTER_TOKEN,
useClass: ReviewProtestPresenter,
},
// Use cases // Use cases
{ {
provide: ReviewProtestUseCase, provide: ReviewProtestUseCase,
@@ -39,14 +35,12 @@ export const ProtestsProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
output: ReviewProtestPresenter, ) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger),
) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, output),
inject: [ inject: [
PROTEST_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
REVIEW_PROTEST_PRESENTER_TOKEN,
], ],
}, },
]; ];

View File

@@ -40,13 +40,15 @@ describe('ProtestsService', () => {
it('returns DTO with success model on success', async () => { it('returns DTO with success model on success', async () => {
executeMock.mockImplementation(async (command) => { executeMock.mockImplementation(async (command) => {
presenter.present({ protestId: command.protestId } as ReviewProtestResult); return Result.ok({
return Result.ok(undefined); leagueId: 'league-1',
protestId: command.protestId,
status: 'upheld',
});
}); });
const dto = await service.reviewProtest(baseCommand); const dto = await service.reviewProtest(baseCommand);
expect(presenter.getResponseModel()).not.toBeNull();
expect(executeMock).toHaveBeenCalledWith(baseCommand); expect(executeMock).toHaveBeenCalledWith(baseCommand);
expect(dto).toEqual<ReviewProtestResponseDTO>({ expect(dto).toEqual<ReviewProtestResponseDTO>({
success: true, success: true,
@@ -63,8 +65,7 @@ describe('ProtestsService', () => {
}; };
executeMock.mockImplementation(async () => { executeMock.mockImplementation(async () => {
presenter.presentError(error); return Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error);
return Result.err<void, ReviewProtestApplicationError>(error);
}); });
const dto = await service.reviewProtest(baseCommand); const dto = await service.reviewProtest(baseCommand);
@@ -83,8 +84,7 @@ describe('ProtestsService', () => {
}; };
executeMock.mockImplementation(async () => { executeMock.mockImplementation(async () => {
presenter.presentError(error); return Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error);
return Result.err<void, ReviewProtestApplicationError>(error);
}); });
const dto = await service.reviewProtest(baseCommand); const dto = await service.reviewProtest(baseCommand);
@@ -103,8 +103,7 @@ describe('ProtestsService', () => {
}; };
executeMock.mockImplementation(async () => { executeMock.mockImplementation(async () => {
presenter.presentError(error); return Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error);
return Result.err<void, ReviewProtestApplicationError>(error);
}); });
const dto = await service.reviewProtest(baseCommand); const dto = await service.reviewProtest(baseCommand);
@@ -124,8 +123,7 @@ describe('ProtestsService', () => {
}; };
executeMock.mockImplementation(async () => { executeMock.mockImplementation(async () => {
presenter.presentError(error); return Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error);
return Result.err<void, ReviewProtestApplicationError>(error);
}); });
const dto = await service.reviewProtest(baseCommand); const dto = await service.reviewProtest(baseCommand);

View File

@@ -8,14 +8,14 @@ import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewP
import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter'; import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
// Tokens // Tokens
import { LOGGER_TOKEN, REVIEW_PROTEST_PRESENTER_TOKEN } from './ProtestsProviders'; import { LOGGER_TOKEN } from './ProtestsProviders';
@Injectable() @Injectable()
export class ProtestsService { export class ProtestsService {
constructor( constructor(
private readonly reviewProtestUseCase: ReviewProtestUseCase, private readonly reviewProtestUseCase: ReviewProtestUseCase,
@Inject(REVIEW_PROTEST_PRESENTER_TOKEN) private readonly reviewProtestPresenter: ReviewProtestPresenter, private readonly reviewProtestPresenter: ReviewProtestPresenter,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {} ) {}
@@ -27,14 +27,20 @@ export class ProtestsService {
}): Promise<ReviewProtestResponseDTO> { }): Promise<ReviewProtestResponseDTO> {
this.logger.debug('[ProtestsService] Reviewing protest:', command); this.logger.debug('[ProtestsService] Reviewing protest:', command);
// Set the command on the presenter so it can include stewardId and decision in the response const result = await this.reviewProtestUseCase.execute(command);
this.reviewProtestPresenter.setCommand({
if (result.isErr()) {
const err = result.unwrapErr();
this.reviewProtestPresenter.presentError(err);
return this.reviewProtestPresenter.responseModel;
}
// Present the result with the additional context
this.reviewProtestPresenter.present(result.unwrap(), {
stewardId: command.stewardId, stewardId: command.stewardId,
decision: command.decision, decision: command.decision,
}); });
await this.reviewProtestUseCase.execute(command);
return this.reviewProtestPresenter.responseModel; return this.reviewProtestPresenter.responseModel;
} }
} }

View File

@@ -12,27 +12,21 @@ export interface ReviewProtestResponseDTO {
export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestResult> { export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestResult> {
private model: ReviewProtestResponseDTO | null = null; private model: ReviewProtestResponseDTO | null = null;
private command: { stewardId: string; decision: 'uphold' | 'dismiss' } | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
this.command = null;
} }
setCommand(command: { stewardId: string; decision: 'uphold' | 'dismiss' }): void { present(result: ReviewProtestResult, context?: { stewardId: string; decision: 'uphold' | 'dismiss' }): void {
this.command = command; if (!context) {
} throw new Error('Context must be provided when presenting result');
present(result: ReviewProtestResult): void {
if (!this.command) {
throw new Error('Command must be set before presenting result');
} }
this.model = { this.model = {
success: true, success: true,
protestId: result.protestId, protestId: result.protestId,
stewardId: this.command.stewardId, stewardId: context.stewardId,
decision: this.command.decision, decision: context.decision,
}; };
} }

View File

@@ -12,8 +12,6 @@ import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepo
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository'; import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { Result } from '@core/shared/application/Result';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
@@ -43,29 +41,6 @@ import { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase'; import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase'; import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
// Import use case result types
import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import type { GetTotalRacesResult } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import type { ImportRaceResultsApiResult } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import type { GetRaceDetailResult } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type { GetRacesPageDataResult } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import type { GetAllRacesPageDataResult } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import type { GetRaceResultsDetailResult } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
import type { GetRaceWithSOFResult } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import type { GetRaceProtestsResult } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import type { GetRacePenaltiesResult } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import type { RegisterForRaceResult } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
import type { WithdrawFromRaceResult } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
import type { CancelRaceResult } from '@core/racing/application/use-cases/CancelRaceUseCase';
import type { CompleteRaceResult } from '@core/racing/application/use-cases/CompleteRaceUseCase';
import type { ReopenRaceResult } from '@core/racing/application/use-cases/ReopenRaceUseCase';
import type { ImportRaceResultsResult } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
import type { FileProtestResult } from '@core/racing/application/use-cases/FileProtestUseCase';
import type { QuickPenaltyResult } from '@core/racing/application/use-cases/QuickPenaltyUseCase';
import type { ApplyPenaltyResult } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
import type { RequestProtestDefenseResult } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase';
import type { ReviewProtestResult } from '@core/racing/application/use-cases/ReviewProtestUseCase';
// Import presenters // Import presenters
import { AllRacesPageDataPresenter } from './presenters/AllRacesPageDataPresenter'; import { AllRacesPageDataPresenter } from './presenters/AllRacesPageDataPresenter';
import { CommandResultPresenter } from './presenters/CommandResultPresenter'; import { CommandResultPresenter } from './presenters/CommandResultPresenter';
@@ -107,183 +82,6 @@ import {
export * from './RaceTokens'; export * from './RaceTokens';
// Adapter classes to bridge presenters with UseCaseOutputPort interface
class GetAllRacesOutputAdapter implements UseCaseOutputPort<GetAllRacesResult> {
constructor(private presenter: GetAllRacesPresenter) {}
present(result: GetAllRacesResult): void {
this.presenter.present(result);
}
}
class GetTotalRacesOutputAdapter implements UseCaseOutputPort<GetTotalRacesResult> {
constructor(private presenter: GetTotalRacesPresenter) {}
present(result: GetTotalRacesResult): void {
// Wrap the result in a Result.ok() to match presenter expectations
const resultWrapper = Result.ok<GetTotalRacesResult, { code: 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class ImportRaceResultsApiOutputAdapter implements UseCaseOutputPort<ImportRaceResultsApiResult> {
constructor(private presenter: ImportRaceResultsApiPresenter) {}
present(result: ImportRaceResultsApiResult): void {
const resultWrapper = Result.ok<ImportRaceResultsApiResult, { code: 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class RaceDetailOutputAdapter implements UseCaseOutputPort<GetRaceDetailResult> {
constructor(private presenter: RaceDetailPresenter) {}
present(result: GetRaceDetailResult): void {
this.presenter.present(result);
}
}
class RacesPageDataOutputAdapter implements UseCaseOutputPort<GetRacesPageDataResult> {
constructor(private presenter: RacesPageDataPresenter) {}
present(result: GetRacesPageDataResult): void {
const resultWrapper = Result.ok<GetRacesPageDataResult, { code: 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class AllRacesPageDataOutputAdapter implements UseCaseOutputPort<GetAllRacesPageDataResult> {
constructor(private presenter: AllRacesPageDataPresenter) {}
present(result: GetAllRacesPageDataResult): void {
const resultWrapper = Result.ok<GetAllRacesPageDataResult, { code: 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class RaceResultsDetailOutputAdapter implements UseCaseOutputPort<GetRaceResultsDetailResult> {
constructor(private presenter: RaceResultsDetailPresenter) {}
present(result: GetRaceResultsDetailResult): void {
this.presenter.present(result);
}
}
class RaceWithSOFOutputAdapter implements UseCaseOutputPort<GetRaceWithSOFResult> {
constructor(private presenter: RaceWithSOFPresenter) {}
present(result: GetRaceWithSOFResult): void {
const resultWrapper = Result.ok<GetRaceWithSOFResult, { code: 'RACE_NOT_FOUND' | 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class RaceProtestsOutputAdapter implements UseCaseOutputPort<GetRaceProtestsResult> {
constructor(private presenter: RaceProtestsPresenter) {}
present(result: GetRaceProtestsResult): void {
const resultWrapper = Result.ok<GetRaceProtestsResult, { code: 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class RacePenaltiesOutputAdapter implements UseCaseOutputPort<GetRacePenaltiesResult> {
constructor(private presenter: RacePenaltiesPresenter) {}
present(result: GetRacePenaltiesResult): void {
const resultWrapper = Result.ok<GetRacePenaltiesResult, { code: 'REPOSITORY_ERROR'; details: { message: string } }>(result);
this.presenter.present(resultWrapper);
}
}
class RegisterForRaceOutputAdapter implements UseCaseOutputPort<RegisterForRaceResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Race registered successfully');
}
}
class WithdrawFromRaceOutputAdapter implements UseCaseOutputPort<WithdrawFromRaceResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Race withdrawal successful');
}
}
class CancelRaceOutputAdapter implements UseCaseOutputPort<CancelRaceResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Race cancelled successfully');
}
}
class CompleteRaceOutputAdapter implements UseCaseOutputPort<CompleteRaceResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Race completed successfully');
}
}
class ReopenRaceOutputAdapter implements UseCaseOutputPort<ReopenRaceResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Race reopened successfully');
}
}
class ImportRaceResultsOutputAdapter implements UseCaseOutputPort<ImportRaceResultsResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Race results imported successfully');
}
}
class FileProtestOutputAdapter implements UseCaseOutputPort<FileProtestResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Protest filed successfully');
}
}
class QuickPenaltyOutputAdapter implements UseCaseOutputPort<QuickPenaltyResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Penalty applied successfully');
}
}
class ApplyPenaltyOutputAdapter implements UseCaseOutputPort<ApplyPenaltyResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Penalty applied successfully');
}
}
class RequestProtestDefenseOutputAdapter implements UseCaseOutputPort<RequestProtestDefenseResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Defense request sent successfully');
}
}
class ReviewProtestOutputAdapter implements UseCaseOutputPort<ReviewProtestResult> {
constructor(private presenter: CommandResultPresenter) {}
present(): void {
this.presenter.presentSuccess('Protest reviewed successfully');
}
}
export const RaceProviders: Provider[] = [ export const RaceProviders: Provider[] = [
{ {
provide: DRIVER_RATING_PROVIDER_TOKEN, provide: DRIVER_RATING_PROVIDER_TOKEN,
@@ -354,24 +152,19 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
logger: Logger, logger: Logger,
presenter: GetAllRacesPresenter,
) => { ) => {
const useCase = new GetAllRacesUseCase(raceRepo, leagueRepo, logger); return new GetAllRacesUseCase(raceRepo, leagueRepo, logger);
useCase.setOutput(new GetAllRacesOutputAdapter(presenter));
return useCase;
}, },
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN, GET_ALL_RACES_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetTotalRacesUseCase, provide: GetTotalRacesUseCase,
useFactory: ( useFactory: (
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger,
presenter: GetTotalRacesPresenter,
) => { ) => {
return new GetTotalRacesUseCase(raceRepo, logger, new GetTotalRacesOutputAdapter(presenter)); return new GetTotalRacesUseCase(raceRepo);
}, },
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN, GET_TOTAL_RACES_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN],
}, },
{ {
provide: ImportRaceResultsApiUseCase, provide: ImportRaceResultsApiUseCase,
@@ -382,7 +175,6 @@ export const RaceProviders: Provider[] = [
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
standingRepo: IStandingRepository, standingRepo: IStandingRepository,
logger: Logger, logger: Logger,
presenter: ImportRaceResultsApiPresenter,
) => { ) => {
return new ImportRaceResultsApiUseCase( return new ImportRaceResultsApiUseCase(
raceRepo, raceRepo,
@@ -391,7 +183,6 @@ export const RaceProviders: Provider[] = [
driverRepo, driverRepo,
standingRepo, standingRepo,
logger, logger,
new ImportRaceResultsApiOutputAdapter(presenter)
); );
}, },
inject: [ inject: [
@@ -401,7 +192,6 @@ export const RaceProviders: Provider[] = [
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN, STANDING_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN,
], ],
}, },
{ {
@@ -413,7 +203,6 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository, resultRepo: IResultRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
presenter: RaceDetailPresenter,
) => { ) => {
return new GetRaceDetailUseCase( return new GetRaceDetailUseCase(
raceRepo, raceRepo,
@@ -422,7 +211,6 @@ export const RaceProviders: Provider[] = [
raceRegRepo, raceRegRepo,
resultRepo, resultRepo,
leagueMembershipRepo, leagueMembershipRepo,
new RaceDetailOutputAdapter(presenter),
); );
}, },
inject: [ inject: [
@@ -432,7 +220,6 @@ export const RaceProviders: Provider[] = [
RACE_REGISTRATION_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_DETAIL_PRESENTER_TOKEN,
], ],
}, },
{ {
@@ -441,16 +228,14 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
logger: Logger, logger: Logger,
presenter: RacesPageDataPresenter,
) => { ) => {
return new GetRacesPageDataUseCase( return new GetRacesPageDataUseCase(
raceRepo, raceRepo,
leagueRepo, leagueRepo,
logger, logger,
new RacesPageDataOutputAdapter(presenter)
); );
}, },
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN, RACES_PAGE_DATA_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetAllRacesPageDataUseCase, provide: GetAllRacesPageDataUseCase,
@@ -458,16 +243,14 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
logger: Logger, logger: Logger,
presenter: AllRacesPageDataPresenter,
) => { ) => {
return new GetAllRacesPageDataUseCase( return new GetAllRacesPageDataUseCase(
raceRepo, raceRepo,
leagueRepo, leagueRepo,
logger, logger,
new AllRacesPageDataOutputAdapter(presenter)
); );
}, },
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN, ALL_RACES_PAGE_DATA_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetRaceResultsDetailUseCase, provide: GetRaceResultsDetailUseCase,
@@ -477,7 +260,6 @@ export const RaceProviders: Provider[] = [
resultRepo: IResultRepository, resultRepo: IResultRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
penaltyRepo: IPenaltyRepository, penaltyRepo: IPenaltyRepository,
presenter: RaceResultsDetailPresenter,
) => { ) => {
return new GetRaceResultsDetailUseCase( return new GetRaceResultsDetailUseCase(
raceRepo, raceRepo,
@@ -485,7 +267,6 @@ export const RaceProviders: Provider[] = [
resultRepo, resultRepo,
driverRepo, driverRepo,
penaltyRepo, penaltyRepo,
new RaceResultsDetailOutputAdapter(presenter)
); );
}, },
inject: [ inject: [
@@ -494,7 +275,6 @@ export const RaceProviders: Provider[] = [
RESULT_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
PENALTY_REPOSITORY_TOKEN, PENALTY_REPOSITORY_TOKEN,
RACE_RESULTS_DETAIL_PRESENTER_TOKEN,
], ],
}, },
{ {
@@ -504,7 +284,6 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository, resultRepo: IResultRepository,
driverRatingProvider: DriverRatingProvider, driverRatingProvider: DriverRatingProvider,
presenter: RaceWithSOFPresenter,
) => { ) => {
return new GetRaceWithSOFUseCase( return new GetRaceWithSOFUseCase(
raceRepo, raceRepo,
@@ -514,7 +293,6 @@ export const RaceProviders: Provider[] = [
const rating = driverRatingProvider.getRating(input.driverId); const rating = driverRatingProvider.getRating(input.driverId);
return { rating }; return { rating };
}, },
new RaceWithSOFOutputAdapter(presenter)
); );
}, },
inject: [ inject: [
@@ -522,7 +300,6 @@ export const RaceProviders: Provider[] = [
RACE_REGISTRATION_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN,
DRIVER_RATING_PROVIDER_TOKEN, DRIVER_RATING_PROVIDER_TOKEN,
RACE_WITH_SOF_PRESENTER_TOKEN,
], ],
}, },
{ {
@@ -530,30 +307,26 @@ export const RaceProviders: Provider[] = [
useFactory: ( useFactory: (
protestRepo: IProtestRepository, protestRepo: IProtestRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
presenter: RaceProtestsPresenter,
) => { ) => {
return new GetRaceProtestsUseCase( return new GetRaceProtestsUseCase(
protestRepo, protestRepo,
driverRepo, driverRepo,
new RaceProtestsOutputAdapter(presenter)
); );
}, },
inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, RACE_PROTESTS_PRESENTER_TOKEN], inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
}, },
{ {
provide: GetRacePenaltiesUseCase, provide: GetRacePenaltiesUseCase,
useFactory: ( useFactory: (
penaltyRepo: IPenaltyRepository, penaltyRepo: IPenaltyRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
presenter: RacePenaltiesPresenter,
) => { ) => {
return new GetRacePenaltiesUseCase( return new GetRacePenaltiesUseCase(
penaltyRepo, penaltyRepo,
driverRepo, driverRepo,
new RacePenaltiesOutputAdapter(presenter)
); );
}, },
inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, RACE_PENALTIES_PRESENTER_TOKEN], inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
}, },
{ {
provide: RegisterForRaceUseCase, provide: RegisterForRaceUseCase,
@@ -561,16 +334,14 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new RegisterForRaceUseCase( return new RegisterForRaceUseCase(
raceRegRepo, raceRegRepo,
leagueMembershipRepo, leagueMembershipRepo,
logger, logger,
new RegisterForRaceOutputAdapter(presenter)
); );
}, },
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: WithdrawFromRaceUseCase, provide: WithdrawFromRaceUseCase,
@@ -578,31 +349,27 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new WithdrawFromRaceUseCase( return new WithdrawFromRaceUseCase(
raceRepo, raceRepo,
raceRegRepo, raceRegRepo,
logger, logger,
new WithdrawFromRaceOutputAdapter(presenter)
); );
}, },
inject: [RACE_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: CancelRaceUseCase, provide: CancelRaceUseCase,
useFactory: ( useFactory: (
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new CancelRaceUseCase( return new CancelRaceUseCase(
raceRepo, raceRepo,
logger, logger,
new CancelRaceOutputAdapter(presenter)
); );
}, },
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: CompleteRaceUseCase, provide: CompleteRaceUseCase,
@@ -612,7 +379,6 @@ export const RaceProviders: Provider[] = [
resultRepo: IResultRepository, resultRepo: IResultRepository,
standingRepo: IStandingRepository, standingRepo: IStandingRepository,
driverRatingProvider: DriverRatingProvider, driverRatingProvider: DriverRatingProvider,
presenter: CommandResultPresenter,
) => { ) => {
return new CompleteRaceUseCase( return new CompleteRaceUseCase(
raceRepo, raceRepo,
@@ -623,7 +389,6 @@ export const RaceProviders: Provider[] = [
const rating = driverRatingProvider.getRating(input.driverId); const rating = driverRatingProvider.getRating(input.driverId);
return { rating, ratingChange: null }; return { rating, ratingChange: null };
}, },
new CompleteRaceOutputAdapter(presenter)
); );
}, },
inject: [ inject: [
@@ -632,7 +397,6 @@ export const RaceProviders: Provider[] = [
RESULT_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN, STANDING_REPOSITORY_TOKEN,
DRIVER_RATING_PROVIDER_TOKEN, DRIVER_RATING_PROVIDER_TOKEN,
COMMAND_RESULT_PRESENTER_TOKEN,
], ],
}, },
{ {
@@ -640,15 +404,13 @@ export const RaceProviders: Provider[] = [
useFactory: ( useFactory: (
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new ReopenRaceUseCase( return new ReopenRaceUseCase(
raceRepo, raceRepo,
logger, logger,
new ReopenRaceOutputAdapter(presenter)
); );
}, },
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: ImportRaceResultsUseCase, provide: ImportRaceResultsUseCase,
@@ -659,7 +421,6 @@ export const RaceProviders: Provider[] = [
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
standingRepo: IStandingRepository, standingRepo: IStandingRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new ImportRaceResultsUseCase( return new ImportRaceResultsUseCase(
raceRepo, raceRepo,
@@ -668,7 +429,6 @@ export const RaceProviders: Provider[] = [
driverRepo, driverRepo,
standingRepo, standingRepo,
logger, logger,
new ImportRaceResultsOutputAdapter(presenter)
); );
}, },
inject: [ inject: [
@@ -678,7 +438,6 @@ export const RaceProviders: Provider[] = [
DRIVER_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN, STANDING_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
COMMAND_RESULT_PRESENTER_TOKEN,
], ],
}, },
{ {
@@ -687,16 +446,14 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository, protestRepo: IProtestRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
presenter: CommandResultPresenter,
) => { ) => {
return new FileProtestUseCase( return new FileProtestUseCase(
protestRepo, protestRepo,
raceRepo, raceRepo,
leagueMembershipRepo, leagueMembershipRepo,
new FileProtestOutputAdapter(presenter)
); );
}, },
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
}, },
{ {
provide: QuickPenaltyUseCase, provide: QuickPenaltyUseCase,
@@ -705,17 +462,15 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new QuickPenaltyUseCase( return new QuickPenaltyUseCase(
penaltyRepo, penaltyRepo,
raceRepo, raceRepo,
leagueMembershipRepo, leagueMembershipRepo,
logger, logger,
new QuickPenaltyOutputAdapter(presenter)
); );
}, },
inject: [PENALTY_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [PENALTY_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: ApplyPenaltyUseCase, provide: ApplyPenaltyUseCase,
@@ -725,7 +480,6 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new ApplyPenaltyUseCase( return new ApplyPenaltyUseCase(
penaltyRepo, penaltyRepo,
@@ -733,10 +487,9 @@ export const RaceProviders: Provider[] = [
raceRepo, raceRepo,
leagueMembershipRepo, leagueMembershipRepo,
logger, logger,
new ApplyPenaltyOutputAdapter(presenter)
); );
}, },
inject: [PENALTY_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [PENALTY_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: RequestProtestDefenseUseCase, provide: RequestProtestDefenseUseCase,
@@ -745,17 +498,15 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new RequestProtestDefenseUseCase( return new RequestProtestDefenseUseCase(
protestRepo, protestRepo,
raceRepo, raceRepo,
leagueMembershipRepo, leagueMembershipRepo,
logger, logger,
new RequestProtestDefenseOutputAdapter(presenter)
); );
}, },
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: ReviewProtestUseCase, provide: ReviewProtestUseCase,
@@ -764,16 +515,14 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
presenter: CommandResultPresenter,
) => { ) => {
return new ReviewProtestUseCase( return new ReviewProtestUseCase(
protestRepo, protestRepo,
raceRepo, raceRepo,
leagueMembershipRepo, leagueMembershipRepo,
logger, logger,
new ReviewProtestOutputAdapter(presenter)
); );
}, },
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN], inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
]; ];

View File

@@ -1,20 +1,24 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { RaceService } from './RaceService'; import { RaceService } from './RaceService';
import { Result } from '@core/shared/application/Result';
describe('RaceService', () => { describe('RaceService', () => {
it('invokes each use case and returns the corresponding presenter', async () => { it('invokes each use case and returns the corresponding presenter', async () => {
const mkUseCase = () => ({ execute: vi.fn(async () => {}) }); // Mock use cases to return Result.ok()
const mkUseCase = (resultValue: any = { success: true }) => ({
execute: vi.fn(async () => Result.ok(resultValue))
});
const getAllRacesUseCase = mkUseCase(); const getAllRacesUseCase = mkUseCase({ races: [], leagues: [] });
const getTotalRacesUseCase = mkUseCase(); const getTotalRacesUseCase = mkUseCase({ totalRaces: 0 });
const importRaceResultsApiUseCase = mkUseCase(); const importRaceResultsApiUseCase = mkUseCase({ success: true, raceId: 'r1', driversProcessed: 0, resultsRecorded: 0, errors: [] });
const getRaceDetailUseCase = mkUseCase(); const getRaceDetailUseCase = mkUseCase({ race: null, league: null, drivers: [], isUserRegistered: false, canRegister: false });
const getRacesPageDataUseCase = mkUseCase(); const getRacesPageDataUseCase = mkUseCase({ races: [] });
const getAllRacesPageDataUseCase = mkUseCase(); const getAllRacesPageDataUseCase = mkUseCase({ races: [], filters: { statuses: [], leagues: [] } });
const getRaceResultsDetailUseCase = mkUseCase(); const getRaceResultsDetailUseCase = mkUseCase({ race: null, results: [], penalties: [] });
const getRaceWithSOFUseCase = mkUseCase(); const getRaceWithSOFUseCase = mkUseCase({ race: null, strengthOfField: 0, participantCount: 0, registeredCount: 0, maxParticipants: 0 });
const getRaceProtestsUseCase = mkUseCase(); const getRaceProtestsUseCase = mkUseCase({ protests: [], drivers: [] });
const getRacePenaltiesUseCase = mkUseCase(); const getRacePenaltiesUseCase = mkUseCase({ penalties: [], drivers: [] });
const registerForRaceUseCase = mkUseCase(); const registerForRaceUseCase = mkUseCase();
const withdrawFromRaceUseCase = mkUseCase(); const withdrawFromRaceUseCase = mkUseCase();
const cancelRaceUseCase = mkUseCase(); const cancelRaceUseCase = mkUseCase();
@@ -28,17 +32,17 @@ describe('RaceService', () => {
const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
const getAllRacesPresenter = {} as any; const getAllRacesPresenter = { present: vi.fn() } as any;
const getTotalRacesPresenter = {} as any; const getTotalRacesPresenter = { present: vi.fn() } as any;
const importRaceResultsApiPresenter = {} as any; const importRaceResultsApiPresenter = { present: vi.fn() } as any;
const raceDetailPresenter = {} as any; const raceDetailPresenter = { present: vi.fn() } as any;
const racesPageDataPresenter = {} as any; const racesPageDataPresenter = { present: vi.fn() } as any;
const allRacesPageDataPresenter = {} as any; const allRacesPageDataPresenter = { present: vi.fn() } as any;
const raceResultsDetailPresenter = {} as any; const raceResultsDetailPresenter = { present: vi.fn() } as any;
const raceWithSOFPresenter = {} as any; const raceWithSOFPresenter = { present: vi.fn() } as any;
const raceProtestsPresenter = {} as any; const raceProtestsPresenter = { present: vi.fn() } as any;
const racePenaltiesPresenter = {} as any; const racePenaltiesPresenter = { present: vi.fn() } as any;
const commandResultPresenter = {} as any; const commandResultPresenter = { present: vi.fn() } as any;
const service = new RaceService( const service = new RaceService(
getAllRacesUseCase as any, getAllRacesUseCase as any,
@@ -77,62 +81,81 @@ describe('RaceService', () => {
expect(await service.getAllRaces()).toBe(getAllRacesPresenter); expect(await service.getAllRaces()).toBe(getAllRacesPresenter);
expect(getAllRacesUseCase.execute).toHaveBeenCalledWith({}); expect(getAllRacesUseCase.execute).toHaveBeenCalledWith({});
expect(getAllRacesPresenter.present).toHaveBeenCalledWith({ races: [], leagues: [] });
expect(await service.getTotalRaces()).toBe(getTotalRacesPresenter); expect(await service.getTotalRaces()).toBe(getTotalRacesPresenter);
expect(getTotalRacesUseCase.execute).toHaveBeenCalledWith({}); expect(getTotalRacesUseCase.execute).toHaveBeenCalledWith({});
expect(getTotalRacesPresenter.present).toHaveBeenCalledWith({ totalRaces: 0 });
expect(await service.importRaceResults({ raceId: 'r1', resultsFileContent: 'x' } as any)).toBe(importRaceResultsApiPresenter); expect(await service.importRaceResults({ raceId: 'r1', resultsFileContent: 'x' } as any)).toBe(importRaceResultsApiPresenter);
expect(importRaceResultsApiUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1', resultsFileContent: 'x' }); expect(importRaceResultsApiUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1', resultsFileContent: 'x' });
expect(importRaceResultsApiPresenter.present).toHaveBeenCalledWith({ success: true, raceId: 'r1', driversProcessed: 0, resultsRecorded: 0, errors: [] });
expect(await service.getRaceDetail({ raceId: 'r1' } as any)).toBe(raceDetailPresenter); expect(await service.getRaceDetail({ raceId: 'r1' } as any)).toBe(raceDetailPresenter);
expect(getRaceDetailUseCase.execute).toHaveBeenCalled(); expect(getRaceDetailUseCase.execute).toHaveBeenCalled();
expect(await service.getRacesPageData('l1')).toBe(racesPageDataPresenter); expect(await service.getRacesPageData('l1')).toBe(racesPageDataPresenter);
expect(getRacesPageDataUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' }); expect(getRacesPageDataUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
expect(racesPageDataPresenter.present).toHaveBeenCalledWith({ races: [] });
expect(await service.getAllRacesPageData()).toBe(allRacesPageDataPresenter); expect(await service.getAllRacesPageData()).toBe(allRacesPageDataPresenter);
expect(getAllRacesPageDataUseCase.execute).toHaveBeenCalledWith({}); expect(getAllRacesPageDataUseCase.execute).toHaveBeenCalledWith({});
expect(allRacesPageDataPresenter.present).toHaveBeenCalledWith({ races: [], filters: { statuses: [], leagues: [] } });
expect(await service.getRaceResultsDetail('r1')).toBe(raceResultsDetailPresenter); expect(await service.getRaceResultsDetail('r1')).toBe(raceResultsDetailPresenter);
expect(getRaceResultsDetailUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' }); expect(getRaceResultsDetailUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' });
expect(raceResultsDetailPresenter.present).toHaveBeenCalledWith({ race: null, results: [], penalties: [] });
expect(await service.getRaceWithSOF('r1')).toBe(raceWithSOFPresenter); expect(await service.getRaceWithSOF('r1')).toBe(raceWithSOFPresenter);
expect(getRaceWithSOFUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' }); expect(getRaceWithSOFUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' });
expect(raceWithSOFPresenter.present).toHaveBeenCalledWith({ race: null, strengthOfField: 0, participantCount: 0, registeredCount: 0, maxParticipants: 0 });
expect(await service.getRaceProtests('r1')).toBe(raceProtestsPresenter); expect(await service.getRaceProtests('r1')).toBe(raceProtestsPresenter);
expect(getRaceProtestsUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' }); expect(getRaceProtestsUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' });
expect(raceProtestsPresenter.present).toHaveBeenCalledWith({ protests: [], drivers: [] });
expect(await service.getRacePenalties('r1')).toBe(racePenaltiesPresenter); expect(await service.getRacePenalties('r1')).toBe(racePenaltiesPresenter);
expect(getRacePenaltiesUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' }); expect(getRacePenaltiesUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' });
expect(racePenaltiesPresenter.present).toHaveBeenCalledWith({ penalties: [], drivers: [] });
expect(await service.registerForRace({ raceId: 'r1', driverId: 'd1' } as any)).toBe(commandResultPresenter); expect(await service.registerForRace({ raceId: 'r1', driverId: 'd1' } as any)).toBe(commandResultPresenter);
expect(registerForRaceUseCase.execute).toHaveBeenCalled(); expect(registerForRaceUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.withdrawFromRace({ raceId: 'r1', driverId: 'd1' } as any)).toBe(commandResultPresenter); expect(await service.withdrawFromRace({ raceId: 'r1', driverId: 'd1' } as any)).toBe(commandResultPresenter);
expect(withdrawFromRaceUseCase.execute).toHaveBeenCalled(); expect(withdrawFromRaceUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.cancelRace({ raceId: 'r1' } as any, 'admin')).toBe(commandResultPresenter); expect(await service.cancelRace({ raceId: 'r1' } as any, 'admin')).toBe(commandResultPresenter);
expect(cancelRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1', cancelledById: 'admin' }); expect(cancelRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1', cancelledById: 'admin' });
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.completeRace({ raceId: 'r1' } as any)).toBe(commandResultPresenter); expect(await service.completeRace({ raceId: 'r1' } as any)).toBe(commandResultPresenter);
expect(completeRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' }); expect(completeRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1' });
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.reopenRace({ raceId: 'r1' } as any, 'admin')).toBe(commandResultPresenter); expect(await service.reopenRace({ raceId: 'r1' } as any, 'admin')).toBe(commandResultPresenter);
expect(reopenRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1', reopenedById: 'admin' }); expect(reopenRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'r1', reopenedById: 'admin' });
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.fileProtest({} as any)).toBe(commandResultPresenter); expect(await service.fileProtest({} as any)).toBe(commandResultPresenter);
expect(fileProtestUseCase.execute).toHaveBeenCalled(); expect(fileProtestUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.applyQuickPenalty({} as any)).toBe(commandResultPresenter); expect(await service.applyQuickPenalty({} as any)).toBe(commandResultPresenter);
expect(quickPenaltyUseCase.execute).toHaveBeenCalled(); expect(quickPenaltyUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.applyPenalty({} as any)).toBe(commandResultPresenter); expect(await service.applyPenalty({} as any)).toBe(commandResultPresenter);
expect(applyPenaltyUseCase.execute).toHaveBeenCalled(); expect(applyPenaltyUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.requestProtestDefense({} as any)).toBe(commandResultPresenter); expect(await service.requestProtestDefense({} as any)).toBe(commandResultPresenter);
expect(requestProtestDefenseUseCase.execute).toHaveBeenCalled(); expect(requestProtestDefenseUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
expect(await service.reviewProtest({} as any)).toBe(commandResultPresenter); expect(await service.reviewProtest({} as any)).toBe(commandResultPresenter);
expect(reviewProtestUseCase.execute).toHaveBeenCalled(); expect(reviewProtestUseCase.execute).toHaveBeenCalled();
expect(commandResultPresenter.present).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable, NotFoundException } from '@nestjs/common';
// DTOs // DTOs
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO'; import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
@@ -116,55 +116,127 @@ export class RaceService {
async getAllRaces(): Promise<GetAllRacesPresenter> { async getAllRaces(): Promise<GetAllRacesPresenter> {
this.logger.debug('[RaceService] Fetching all races.'); this.logger.debug('[RaceService] Fetching all races.');
await this.getAllRacesUseCase.execute({}); const result = await this.getAllRacesUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get all races');
}
const value = result.unwrap();
this.getAllRacesPresenter.present(value);
return this.getAllRacesPresenter; return this.getAllRacesPresenter;
} }
async getTotalRaces(): Promise<GetTotalRacesPresenter> { async getTotalRaces(): Promise<GetTotalRacesPresenter> {
this.logger.debug('[RaceService] Fetching total races count.'); this.logger.debug('[RaceService] Fetching total races count.');
await this.getTotalRacesUseCase.execute({}); const result = await this.getTotalRacesUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get total races');
}
const value = result.unwrap();
this.getTotalRacesPresenter.present(value);
return this.getTotalRacesPresenter; return this.getTotalRacesPresenter;
} }
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsApiPresenter> { async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsApiPresenter> {
this.logger.debug('Importing race results:', input); this.logger.debug('Importing race results:', input);
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }); const result = await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to import race results');
}
const value = result.unwrap();
this.importRaceResultsApiPresenter.present(value);
return this.importRaceResultsApiPresenter; return this.importRaceResultsApiPresenter;
} }
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> { async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
this.logger.debug('[RaceService] Fetching race detail:', params); this.logger.debug('[RaceService] Fetching race detail:', params);
await this.getRaceDetailUseCase.execute(params); const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get race detail');
}
const value = result.unwrap();
this.raceDetailPresenter.present(value);
return this.raceDetailPresenter; return this.raceDetailPresenter;
} }
async getRacesPageData(leagueId: string): Promise<RacesPageDataPresenter> { async getRacesPageData(leagueId: string): Promise<RacesPageDataPresenter> {
this.logger.debug('[RaceService] Fetching races page data.'); this.logger.debug('[RaceService] Fetching races page data.');
await this.getRacesPageDataUseCase.execute({ leagueId }); const result = await this.getRacesPageDataUseCase.execute({ leagueId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get races page data');
}
const value = result.unwrap();
this.racesPageDataPresenter.present(value);
return this.racesPageDataPresenter; return this.racesPageDataPresenter;
} }
async getAllRacesPageData(): Promise<AllRacesPageDataPresenter> { async getAllRacesPageData(): Promise<AllRacesPageDataPresenter> {
this.logger.debug('[RaceService] Fetching all races page data.'); this.logger.debug('[RaceService] Fetching all races page data.');
await this.getAllRacesPageDataUseCase.execute({}); const result = await this.getAllRacesPageDataUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get all races page data');
}
const value = result.unwrap();
this.allRacesPageDataPresenter.present(value);
return this.allRacesPageDataPresenter; return this.allRacesPageDataPresenter;
} }
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailPresenter> { async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailPresenter> {
this.logger.debug('[RaceService] Fetching race results detail:', { raceId }); this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
await this.getRaceResultsDetailUseCase.execute({ raceId }); const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get race results detail');
}
const value = result.unwrap();
this.raceResultsDetailPresenter.present(value);
return this.raceResultsDetailPresenter; return this.raceResultsDetailPresenter;
} }
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFPresenter> { async getRaceWithSOF(raceId: string): Promise<RaceWithSOFPresenter> {
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId }); this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
await this.getRaceWithSOFUseCase.execute({ raceId }); const result = await this.getRaceWithSOFUseCase.execute({ raceId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get race with SOF');
}
const value = result.unwrap();
this.raceWithSOFPresenter.present(value);
return this.raceWithSOFPresenter; return this.raceWithSOFPresenter;
} }
async getRaceProtests(raceId: string): Promise<RaceProtestsPresenter> { async getRaceProtests(raceId: string): Promise<RaceProtestsPresenter> {
this.logger.debug('[RaceService] Fetching race protests:', { raceId }); this.logger.debug('[RaceService] Fetching race protests:', { raceId });
await this.getRaceProtestsUseCase.execute({ raceId }); const result = await this.getRaceProtestsUseCase.execute({ raceId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get race protests');
}
const value = result.unwrap();
this.raceProtestsPresenter.present(value);
return this.raceProtestsPresenter; return this.raceProtestsPresenter;
} }
@@ -186,67 +258,145 @@ export class RaceService {
async getRacePenalties(raceId: string): Promise<RacePenaltiesPresenter> { async getRacePenalties(raceId: string): Promise<RacePenaltiesPresenter> {
this.logger.debug('[RaceService] Fetching race penalties:', { raceId }); this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
await this.getRacePenaltiesUseCase.execute({ raceId }); const result = await this.getRacePenaltiesUseCase.execute({ raceId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to get race penalties');
}
const value = result.unwrap();
this.racePenaltiesPresenter.present(value);
return this.racePenaltiesPresenter; return this.racePenaltiesPresenter;
} }
async registerForRace(params: RegisterForRaceParamsDTO): Promise<CommandResultPresenter> { async registerForRace(params: RegisterForRaceParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Registering for race:', params); this.logger.debug('[RaceService] Registering for race:', params);
await this.registerForRaceUseCase.execute(params); const result = await this.registerForRaceUseCase.execute(params);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to register for race');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<CommandResultPresenter> { async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Withdrawing from race:', params); this.logger.debug('[RaceService] Withdrawing from race:', params);
await this.withdrawFromRaceUseCase.execute(params); const result = await this.withdrawFromRaceUseCase.execute(params);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to withdraw from race');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async cancelRace(params: RaceActionParamsDTO, cancelledById: string): Promise<CommandResultPresenter> { async cancelRace(params: RaceActionParamsDTO, cancelledById: string): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Cancelling race:', params); this.logger.debug('[RaceService] Cancelling race:', params);
await this.cancelRaceUseCase.execute({ raceId: params.raceId, cancelledById }); const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId, cancelledById });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to cancel race');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async completeRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> { async completeRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Completing race:', params); this.logger.debug('[RaceService] Completing race:', params);
await this.completeRaceUseCase.execute({ raceId: params.raceId }); const result = await this.completeRaceUseCase.execute({ raceId: params.raceId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to complete race');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async reopenRace(params: RaceActionParamsDTO, reopenedById: string): Promise<CommandResultPresenter> { async reopenRace(params: RaceActionParamsDTO, reopenedById: string): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Re-opening race:', params); this.logger.debug('[RaceService] Re-opening race:', params);
await this.reopenRaceUseCase.execute({ raceId: params.raceId, reopenedById }); const result = await this.reopenRaceUseCase.execute({ raceId: params.raceId, reopenedById });
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to reopen race');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async fileProtest(command: FileProtestCommandDTO): Promise<CommandResultPresenter> { async fileProtest(command: FileProtestCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Filing protest:', command); this.logger.debug('[RaceService] Filing protest:', command);
await this.fileProtestUseCase.execute(command); const result = await this.fileProtestUseCase.execute(command);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to file protest');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<CommandResultPresenter> { async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Applying quick penalty:', command); this.logger.debug('[RaceService] Applying quick penalty:', command);
await this.quickPenaltyUseCase.execute(command); const result = await this.quickPenaltyUseCase.execute(command);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to apply quick penalty');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<CommandResultPresenter> { async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Applying penalty:', command); this.logger.debug('[RaceService] Applying penalty:', command);
await this.applyPenaltyUseCase.execute(command); const result = await this.applyPenaltyUseCase.execute(command);
if (result.isErr()) {
// ApplyPenaltyUseCase errors don't have details, just code
throw new NotFoundException('Failed to apply penalty');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<CommandResultPresenter> { async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Requesting protest defense:', command); this.logger.debug('[RaceService] Requesting protest defense:', command);
await this.requestProtestDefenseUseCase.execute(command); const result = await this.requestProtestDefenseUseCase.execute(command);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to request protest defense');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
async reviewProtest(command: ReviewProtestCommandDTO): Promise<CommandResultPresenter> { async reviewProtest(command: ReviewProtestCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Reviewing protest:', command); this.logger.debug('[RaceService] Reviewing protest:', command);
await this.reviewProtestUseCase.execute(command); const result = await this.reviewProtestUseCase.execute(command);
if (result.isErr()) {
const error = result.unwrapErr();
throw new NotFoundException(error.details?.message ?? 'Failed to review protest');
}
this.commandResultPresenter.present();
return this.commandResultPresenter; return this.commandResultPresenter;
} }
} }

View File

@@ -1,18 +1,8 @@
import type { Result } from '@core/shared/application/Result'; import type { GetAllRacesPageDataResult } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetAllRacesPageDataResult,
GetAllRacesPageDataErrorCode,
} from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO'; import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export type AllRacesPageDataResponseModel = AllRacesPageDTO; export type AllRacesPageDataResponseModel = AllRacesPageDTO;
export type GetAllRacesPageDataApplicationError = ApplicationErrorCode<
GetAllRacesPageDataErrorCode,
{ message: string }
>;
export class AllRacesPageDataPresenter { export class AllRacesPageDataPresenter {
private model: AllRacesPageDataResponseModel | null = null; private model: AllRacesPageDataResponseModel | null = null;
@@ -20,19 +10,10 @@ export class AllRacesPageDataPresenter {
this.model = null; this.model = null;
} }
present( present(result: GetAllRacesPageDataResult): void {
result: Result<GetAllRacesPageDataResult, GetAllRacesPageDataApplicationError>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get all races page data');
}
const output = result.unwrap();
this.model = { this.model = {
races: output.races, races: result.races,
filters: output.filters, filters: result.filters,
}; };
} }

View File

@@ -1,17 +1,9 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
export interface CommandResultDTO { export interface CommandResultDTO {
success: boolean; success: boolean;
errorCode?: string; errorCode?: string;
message?: string; message?: string;
} }
export type CommandApplicationError = ApplicationErrorCode<
string,
{ message: string }
>;
export class CommandResultPresenter { export class CommandResultPresenter {
private model: CommandResultDTO | null = null; private model: CommandResultDTO | null = null;
@@ -19,17 +11,9 @@ export class CommandResultPresenter {
this.model = null; this.model = null;
} }
present(result: Result<unknown, CommandApplicationError>): void { present(): void {
if (result.isErr()) { // For command use cases, if we get here, it was successful
const error = result.unwrapErr(); // The service handles errors by throwing exceptions
this.model = {
success: false,
errorCode: error.code,
message: error.details?.message,
};
return;
}
this.model = { success: true }; this.model = { success: true };
} }

View File

@@ -1,10 +1,9 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase'; import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO'; import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export type GetAllRacesResponseModel = AllRacesPageDTO; export type GetAllRacesResponseModel = AllRacesPageDTO;
export class GetAllRacesPresenter implements UseCaseOutputPort<GetAllRacesResult> { export class GetAllRacesPresenter {
private model: GetAllRacesResponseModel | null = null; private model: GetAllRacesResponseModel | null = null;
present(result: GetAllRacesResult): void { present(result: GetAllRacesResult): void {

View File

@@ -1,18 +1,8 @@
import type { Result } from '@core/shared/application/Result'; import type { GetTotalRacesResult } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetTotalRacesResult,
GetTotalRacesErrorCode,
} from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import type { RaceStatsDTO } from '../dtos/RaceStatsDTO'; import type { RaceStatsDTO } from '../dtos/RaceStatsDTO';
export type GetTotalRacesResponseModel = RaceStatsDTO; export type GetTotalRacesResponseModel = RaceStatsDTO;
export type GetTotalRacesApplicationError = ApplicationErrorCode<
GetTotalRacesErrorCode,
{ message: string }
>;
export class GetTotalRacesPresenter { export class GetTotalRacesPresenter {
private model: GetTotalRacesResponseModel | null = null; private model: GetTotalRacesResponseModel | null = null;
@@ -20,16 +10,9 @@ export class GetTotalRacesPresenter {
this.model = null; this.model = null;
} }
present(result: Result<GetTotalRacesResult, GetTotalRacesApplicationError>): void { present(result: GetTotalRacesResult): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get total races');
}
const output = result.unwrap();
this.model = { this.model = {
totalRaces: output.totalRaces, totalRaces: result.totalRaces,
}; };
} }

View File

@@ -1,18 +1,8 @@
import type { Result } from '@core/shared/application/Result'; import type { ImportRaceResultsApiResult } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
ImportRaceResultsApiResult,
ImportRaceResultsApiErrorCode,
} from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import { ImportRaceResultsSummaryDTO } from '../dtos/ImportRaceResultsSummaryDTO'; import { ImportRaceResultsSummaryDTO } from '../dtos/ImportRaceResultsSummaryDTO';
export type ImportRaceResultsApiResponseModel = ImportRaceResultsSummaryDTO; export type ImportRaceResultsApiResponseModel = ImportRaceResultsSummaryDTO;
export type ImportRaceResultsApiApplicationError = ApplicationErrorCode<
ImportRaceResultsApiErrorCode,
{ message: string }
>;
export class ImportRaceResultsApiPresenter { export class ImportRaceResultsApiPresenter {
private model: ImportRaceResultsApiResponseModel | null = null; private model: ImportRaceResultsApiResponseModel | null = null;
@@ -20,22 +10,13 @@ export class ImportRaceResultsApiPresenter {
this.model = null; this.model = null;
} }
present( present(result: ImportRaceResultsApiResult): void {
result: Result<ImportRaceResultsApiResult, ImportRaceResultsApiApplicationError>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to import race results');
}
const output = result.unwrap();
this.model = { this.model = {
success: output.success, success: result.success,
raceId: output.raceId, raceId: result.raceId,
driversProcessed: output.driversProcessed, driversProcessed: result.driversProcessed,
resultsRecorded: output.resultsRecorded, resultsRecorded: result.resultsRecorded,
errors: output.errors, errors: result.errors,
}; };
} }

View File

@@ -1,4 +1,3 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetRaceDetailResult } from '@core/racing/application/use-cases/GetRaceDetailUseCase'; import type { GetRaceDetailResult } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
@@ -12,7 +11,7 @@ import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO';
export type GetRaceDetailResponseModel = RaceDetailDTO; export type GetRaceDetailResponseModel = RaceDetailDTO;
export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResult> { export class RaceDetailPresenter {
private result: GetRaceDetailResult | null = null; private result: GetRaceDetailResult | null = null;
constructor( constructor(

View File

@@ -1,19 +1,9 @@
import type { Result } from '@core/shared/application/Result'; import type { GetRacePenaltiesResult } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRacePenaltiesResult,
GetRacePenaltiesErrorCode,
} from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import type { RacePenaltiesDTO } from '../dtos/RacePenaltiesDTO'; import type { RacePenaltiesDTO } from '../dtos/RacePenaltiesDTO';
import type { RacePenaltyDTO } from '../dtos/RacePenaltyDTO'; import type { RacePenaltyDTO } from '../dtos/RacePenaltyDTO';
export type GetRacePenaltiesResponseModel = RacePenaltiesDTO; export type GetRacePenaltiesResponseModel = RacePenaltiesDTO;
export type GetRacePenaltiesApplicationError = ApplicationErrorCode<
GetRacePenaltiesErrorCode,
{ message: string }
>;
export class RacePenaltiesPresenter { export class RacePenaltiesPresenter {
private model: GetRacePenaltiesResponseModel | null = null; private model: GetRacePenaltiesResponseModel | null = null;
@@ -21,15 +11,8 @@ export class RacePenaltiesPresenter {
this.model = null; this.model = null;
} }
present(result: Result<GetRacePenaltiesResult, GetRacePenaltiesApplicationError>): void { present(result: GetRacePenaltiesResult): void {
if (result.isErr()) { const penalties: RacePenaltyDTO[] = result.penalties.map(penalty => ({
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get race penalties');
}
const output = result.unwrap();
const penalties: RacePenaltyDTO[] = output.penalties.map(penalty => ({
id: penalty.id, id: penalty.id,
driverId: penalty.driverId, driverId: penalty.driverId,
type: penalty.type, type: penalty.type,
@@ -41,7 +24,7 @@ export class RacePenaltiesPresenter {
} as RacePenaltyDTO)); } as RacePenaltyDTO));
const driverMap: Record<string, string> = {}; const driverMap: Record<string, string> = {};
output.drivers.forEach(driver => { result.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString(); driverMap[driver.id] = driver.name.toString();
}); });

View File

@@ -1,19 +1,9 @@
import type { Result } from '@core/shared/application/Result'; import type { GetRaceProtestsResult } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceProtestsResult,
GetRaceProtestsErrorCode,
} from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import type { RaceProtestsDTO } from '../dtos/RaceProtestsDTO'; import type { RaceProtestsDTO } from '../dtos/RaceProtestsDTO';
import type { RaceProtestDTO } from '../dtos/RaceProtestDTO'; import type { RaceProtestDTO } from '../dtos/RaceProtestDTO';
export type GetRaceProtestsResponseModel = RaceProtestsDTO; export type GetRaceProtestsResponseModel = RaceProtestsDTO;
export type GetRaceProtestsApplicationError = ApplicationErrorCode<
GetRaceProtestsErrorCode,
{ message: string }
>;
export class RaceProtestsPresenter { export class RaceProtestsPresenter {
private model: GetRaceProtestsResponseModel | null = null; private model: GetRaceProtestsResponseModel | null = null;
@@ -21,15 +11,8 @@ export class RaceProtestsPresenter {
this.model = null; this.model = null;
} }
present(result: Result<GetRaceProtestsResult, GetRaceProtestsApplicationError>): void { present(result: GetRaceProtestsResult): void {
if (result.isErr()) { const protests: RaceProtestDTO[] = result.protests.map(protest => ({
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get race protests');
}
const output = result.unwrap();
const protests: RaceProtestDTO[] = output.protests.map(protest => ({
id: protest.id, id: protest.id,
protestingDriverId: protest.protestingDriverId, protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId, accusedDriverId: protest.accusedDriverId,
@@ -42,7 +25,7 @@ export class RaceProtestsPresenter {
} as RaceProtestDTO)); } as RaceProtestDTO));
const driverMap: Record<string, string> = {}; const driverMap: Record<string, string> = {};
output.drivers.forEach(driver => { result.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString(); driverMap[driver.id] = driver.name.toString();
}); });

View File

@@ -1,18 +1,8 @@
import type { Result } from '@core/shared/application/Result'; import type { GetRaceWithSOFResult } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceWithSOFResult,
GetRaceWithSOFErrorCode,
} from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import type { RaceWithSOFDTO } from '../dtos/RaceWithSOFDTO'; import type { RaceWithSOFDTO } from '../dtos/RaceWithSOFDTO';
export type GetRaceWithSOFResponseModel = RaceWithSOFDTO; export type GetRaceWithSOFResponseModel = RaceWithSOFDTO;
export type GetRaceWithSOFApplicationError = ApplicationErrorCode<
GetRaceWithSOFErrorCode,
{ message: string }
>;
export class RaceWithSOFPresenter { export class RaceWithSOFPresenter {
private model: GetRaceWithSOFResponseModel | null = null; private model: GetRaceWithSOFResponseModel | null = null;
@@ -20,27 +10,11 @@ export class RaceWithSOFPresenter {
this.model = null; this.model = null;
} }
present(result: Result<GetRaceWithSOFResult, GetRaceWithSOFApplicationError>): void { present(result: GetRaceWithSOFResult): void {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = { this.model = {
id: '', id: result.race.id,
track: '', track: result.race.track,
strengthOfField: null, strengthOfField: result.strengthOfField,
} as RaceWithSOFDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race with SOF');
}
const output = result.unwrap();
this.model = {
id: output.race.id,
track: output.race.track,
strengthOfField: output.strengthOfField,
} as RaceWithSOFDTO; } as RaceWithSOFDTO;
} }

View File

@@ -1,19 +1,9 @@
import type { Result } from '@core/shared/application/Result'; import type { GetRacesPageDataResult } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRacesPageDataResult,
GetRacesPageDataErrorCode,
} from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import type { RacesPageDataDTO } from '../dtos/RacesPageDataDTO'; import type { RacesPageDataDTO } from '../dtos/RacesPageDataDTO';
import type { RacesPageDataRaceDTO } from '../dtos/RacesPageDataRaceDTO'; import type { RacesPageDataRaceDTO } from '../dtos/RacesPageDataRaceDTO';
export type GetRacesPageDataResponseModel = RacesPageDataDTO; export type GetRacesPageDataResponseModel = RacesPageDataDTO;
export type GetRacesPageDataApplicationError = ApplicationErrorCode<
GetRacesPageDataErrorCode,
{ message: string }
>;
export class RacesPageDataPresenter { export class RacesPageDataPresenter {
private model: GetRacesPageDataResponseModel | null = null; private model: GetRacesPageDataResponseModel | null = null;
@@ -21,17 +11,8 @@ export class RacesPageDataPresenter {
this.model = null; this.model = null;
} }
present( present(result: GetRacesPageDataResult): void {
result: Result<GetRacesPageDataResult, GetRacesPageDataApplicationError>, const races: RacesPageDataRaceDTO[] = result.races.map(({ race, leagueName }) => ({
): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get races page data');
}
const output = result.unwrap();
const races: RacesPageDataRaceDTO[] = output.races.map(({ race, leagueName }) => ({
id: race.id, id: race.id,
track: race.track, track: race.track,
car: race.car, car: race.car,

View File

@@ -28,7 +28,7 @@ import { SponsorRaceDTO } from './dtos/RaceDTO';
import { SponsorProfileDTO } from './dtos/SponsorProfileDTO'; import { SponsorProfileDTO } from './dtos/SponsorProfileDTO';
import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO'; import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO';
import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO'; import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO';
import type { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter'; import { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@ApiTags('sponsors') @ApiTags('sponsors')
@@ -166,7 +166,7 @@ export class SponsorController {
async acceptSponsorshipRequest( async acceptSponsorshipRequest(
@Param('requestId') requestId: string, @Param('requestId') requestId: string,
@Body() input: AcceptSponsorshipRequestInputDTO, @Body() input: AcceptSponsorshipRequestInputDTO,
): Promise<AcceptSponsorshipRequestResultViewModel | null> { ): Promise<AcceptSponsorshipRequestResultViewModel> {
return await this.sponsorService.acceptSponsorshipRequest( return await this.sponsorService.acceptSponsorshipRequest(
requestId, requestId,
input.respondedBy, input.respondedBy,
@@ -185,7 +185,7 @@ export class SponsorController {
async rejectSponsorshipRequest( async rejectSponsorshipRequest(
@Param('requestId') requestId: string, @Param('requestId') requestId: string,
@Body() input: RejectSponsorshipRequestInputDTO, @Body() input: RejectSponsorshipRequestInputDTO,
): Promise<RejectSponsorshipRequestResult | null> { ): Promise<RejectSponsorshipRequestResult> {
return await this.sponsorService.rejectSponsorshipRequest( return await this.sponsorService.rejectSponsorshipRequest(
requestId, requestId,
input.respondedBy, input.respondedBy,
@@ -219,9 +219,13 @@ export class SponsorController {
description: 'Available leagues', description: 'Available leagues',
type: [AvailableLeagueDTO], type: [AvailableLeagueDTO],
}) })
async getAvailableLeagues(): Promise<AvailableLeagueDTO[] | null> { async getAvailableLeagues(): Promise<AvailableLeagueDTO[]> {
const presenter = await this.sponsorService.getAvailableLeagues(); const presenter = await this.sponsorService.getAvailableLeagues();
return presenter.viewModel; const viewModel = presenter.viewModel;
if (!viewModel) {
throw new Error('Available leagues not found');
}
return viewModel;
} }
@Get('leagues/:leagueId/detail') @Get('leagues/:leagueId/detail')
@@ -236,9 +240,13 @@ export class SponsorController {
league: LeagueDetailDTO; league: LeagueDetailDTO;
drivers: SponsorDriverDTO[]; drivers: SponsorDriverDTO[];
races: SponsorRaceDTO[]; races: SponsorRaceDTO[];
} | null> { }> {
const presenter = await this.sponsorService.getLeagueDetail(leagueId); const presenter = await this.sponsorService.getLeagueDetail(leagueId);
return presenter.viewModel; const viewModel = presenter.viewModel;
if (!viewModel) {
throw new Error('League detail not found');
}
return viewModel;
} }
@Get('settings/:sponsorId') @Get('settings/:sponsorId')
@@ -253,9 +261,13 @@ export class SponsorController {
profile: SponsorProfileDTO; profile: SponsorProfileDTO;
notifications: NotificationSettingsDTO; notifications: NotificationSettingsDTO;
privacy: PrivacySettingsDTO; privacy: PrivacySettingsDTO;
} | null> { }> {
const presenter = await this.sponsorService.getSponsorSettings(sponsorId); const presenter = await this.sponsorService.getSponsorSettings(sponsorId);
return presenter.viewModel; const viewModel = presenter.viewModel;
if (!viewModel) {
throw new Error('Sponsor settings not found');
}
return viewModel;
} }
@Put('settings/:sponsorId') @Put('settings/:sponsorId')
@@ -273,8 +285,12 @@ export class SponsorController {
notifications?: Partial<NotificationSettingsDTO>; notifications?: Partial<NotificationSettingsDTO>;
privacy?: Partial<PrivacySettingsDTO>; privacy?: Partial<PrivacySettingsDTO>;
}, },
): Promise<{ success: boolean; errorCode?: string; message?: string } | null> { ): Promise<{ success: boolean; errorCode?: string; message?: string }> {
const presenter = await this.sponsorService.updateSponsorSettings(sponsorId, input); const presenter = await this.sponsorService.updateSponsorSettings(sponsorId, input);
return presenter.viewModel; const viewModel = presenter.viewModel;
if (!viewModel) {
throw new Error('Update failed');
}
return viewModel;
} }
} }

View File

@@ -5,8 +5,6 @@ import { SponsorService } from './SponsorService';
import type { NotificationService } from '@core/notifications/application/ports/NotificationService'; import type { NotificationService } from '@core/notifications/application/ports/NotificationService';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository'; import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository'; import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
// Remove the missing import
// import { IPaymentGateway } from '@core/payments/domain/ports/IPaymentGateway';
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository'; import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
@@ -16,7 +14,7 @@ import { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/I
import { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository'; import { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository'; import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository';
import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository'; import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase'; import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
@@ -24,7 +22,6 @@ import { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateS
import { GetEntitySponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase'; import { GetEntitySponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
import { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; import { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase'; import { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase'; import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
@@ -35,18 +32,6 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryPaymentRepository'; import { InMemoryPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { InMemoryWalletRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository'; import { InMemoryWalletRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository';
// Import presenters
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
// Define injection tokens // Define injection tokens
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository'; export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository'; export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository';
@@ -62,20 +47,7 @@ export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
export const NOTIFICATION_SERVICE_TOKEN = 'INotificationService'; export const NOTIFICATION_SERVICE_TOKEN = 'INotificationService';
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
// Presenter tokens
export const GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN = 'GetEntitySponsorshipPricingPresenter';
export const GET_SPONSORS_PRESENTER_TOKEN = 'GetSponsorsPresenter';
export const CREATE_SPONSOR_PRESENTER_TOKEN = 'CreateSponsorPresenter';
export const GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN = 'GetSponsorDashboardPresenter';
export const GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN = 'GetSponsorSponsorshipsPresenter';
export const GET_SPONSOR_PRESENTER_TOKEN = 'GetSponsorPresenter';
export const GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN = 'GetPendingSponsorshipRequestsPresenter';
export const ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'AcceptSponsorshipRequestPresenter';
export const REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'RejectSponsorshipRequestPresenter';
export const GET_SPONSOR_BILLING_PRESENTER_TOKEN = 'SponsorBillingPresenter';
// Use case / application service tokens // Use case / application service tokens
export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase';
export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase'; export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase';
export const CREATE_SPONSOR_USE_CASE_TOKEN = 'CreateSponsorUseCase'; export const CREATE_SPONSOR_USE_CASE_TOKEN = 'CreateSponsorUseCase';
export const GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN = 'GetSponsorDashboardUseCase'; export const GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN = 'GetSponsorDashboardUseCase';
@@ -87,19 +59,6 @@ export const ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'AcceptSponsorshipReque
export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase'; export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase';
export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase'; export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase';
// Output port tokens
export const GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetSponsorshipPricingOutputPort_TOKEN';
export const GET_SPONSORS_OUTPUT_PORT_TOKEN = 'GetSponsorsOutputPort_TOKEN';
export const CREATE_SPONSOR_OUTPUT_PORT_TOKEN = 'CreateSponsorOutputPort_TOKEN';
export const GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN = 'GetSponsorDashboardOutputPort_TOKEN';
export const GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSponsorSponsorshipsOutputPort_TOKEN';
export const GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetEntitySponsorshipPricingOutputPort_TOKEN';
export const GET_SPONSOR_OUTPUT_PORT_TOKEN = 'GetSponsorOutputPort_TOKEN';
export const GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN = 'GetPendingSponsorshipRequestsOutputPort_TOKEN';
export const ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'AcceptSponsorshipRequestOutputPort_TOKEN';
export const REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'RejectSponsorshipRequestOutputPort_TOKEN';
export const GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN = 'GetSponsorBillingOutputPort_TOKEN';
export const SponsorProviders: Provider[] = [ export const SponsorProviders: Provider[] = [
SponsorService, SponsorService,
// Repositories (payments repos are local to this module; racing repos come from InMemoryRacingPersistenceModule) // Repositories (payments repos are local to this module; racing repos come from InMemoryRacingPersistenceModule)
@@ -126,77 +85,16 @@ export const SponsorProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
}, },
// Presenters
GetEntitySponsorshipPricingPresenter,
GetSponsorsPresenter,
CreateSponsorPresenter,
GetSponsorDashboardPresenter,
GetSponsorSponsorshipsPresenter,
GetSponsorPresenter,
GetPendingSponsorshipRequestsPresenter,
AcceptSponsorshipRequestPresenter,
RejectSponsorshipRequestPresenter,
SponsorBillingPresenter,
// Output ports
{
provide: GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN,
useExisting: GetEntitySponsorshipPricingPresenter,
},
{
provide: GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN,
useExisting: GetEntitySponsorshipPricingPresenter,
},
{
provide: GET_SPONSORS_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorsPresenter,
},
{
provide: CREATE_SPONSOR_OUTPUT_PORT_TOKEN,
useExisting: CreateSponsorPresenter,
},
{
provide: GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorDashboardPresenter,
},
{
provide: GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorSponsorshipsPresenter,
},
{
provide: GET_SPONSOR_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorPresenter,
},
{
provide: GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN,
useExisting: GetPendingSponsorshipRequestsPresenter,
},
{
provide: ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN,
useExisting: AcceptSponsorshipRequestPresenter,
},
{
provide: REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN,
useExisting: RejectSponsorshipRequestPresenter,
},
{
provide: GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN,
useExisting: SponsorBillingPresenter,
},
// Use cases // Use cases
{
provide: GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
useFactory: (output: UseCaseOutputPort<unknown>) => new GetSponsorshipPricingUseCase(output),
inject: [GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN],
},
{ {
provide: GET_SPONSORS_USE_CASE_TOKEN, provide: GET_SPONSORS_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort<unknown>) => new GetSponsorsUseCase(sponsorRepo, output), useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorsUseCase(sponsorRepo),
inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSORS_OUTPUT_PORT_TOKEN], inject: [SPONSOR_REPOSITORY_TOKEN],
}, },
{ {
provide: CREATE_SPONSOR_USE_CASE_TOKEN, provide: CREATE_SPONSOR_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository, logger: Logger, output: UseCaseOutputPort<unknown>) => new CreateSponsorUseCase(sponsorRepo, logger, output), useFactory: (sponsorRepo: ISponsorRepository, logger: Logger) => new CreateSponsorUseCase(sponsorRepo, logger),
inject: [SPONSOR_REPOSITORY_TOKEN, LOGGER_TOKEN, CREATE_SPONSOR_OUTPUT_PORT_TOKEN], inject: [SPONSOR_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN,
@@ -207,8 +105,7 @@ export const SponsorProviders: Provider[] = [
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
output: UseCaseOutputPort<unknown>, ) => new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo),
) => new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output),
inject: [ inject: [
SPONSOR_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
@@ -216,7 +113,6 @@ export const SponsorProviders: Provider[] = [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -228,8 +124,7 @@ export const SponsorProviders: Provider[] = [
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
output: UseCaseOutputPort<unknown>, ) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo),
) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output),
inject: [ inject: [
SPONSOR_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
@@ -237,7 +132,6 @@ export const SponsorProviders: Provider[] = [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -255,27 +149,24 @@ export const SponsorProviders: Provider[] = [
useFactory: ( useFactory: (
sponsorshipPricingRepo: ISponsorshipPricingRepository, sponsorshipPricingRepo: ISponsorshipPricingRepository,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<unknown>, ) => new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, logger),
) => new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, logger, output),
inject: [ inject: [
SPONSORSHIP_PRICING_REPOSITORY_TOKEN, SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
provide: GET_SPONSOR_USE_CASE_TOKEN, provide: GET_SPONSOR_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort<unknown>) => new GetSponsorUseCase(sponsorRepo, output), useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorUseCase(sponsorRepo),
inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSOR_OUTPUT_PORT_TOKEN], inject: [SPONSOR_REPOSITORY_TOKEN],
}, },
{ {
provide: GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN, provide: GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN,
useFactory: ( useFactory: (
sponsorshipRequestRepo: ISponsorshipRequestRepository, sponsorshipRequestRepo: ISponsorshipRequestRepository,
sponsorRepo: ISponsorRepository, sponsorRepo: ISponsorRepository,
output: UseCaseOutputPort<unknown>, ) => new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo),
) => new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo, output), inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN],
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN, GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
@@ -287,7 +178,6 @@ export const SponsorProviders: Provider[] = [
walletRepository: IWalletRepository, walletRepository: IWalletRepository,
leagueWalletRepository: ILeagueWalletRepository, leagueWalletRepository: ILeagueWalletRepository,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<unknown>,
) => { ) => {
// Create a mock payment processor function // Create a mock payment processor function
const paymentProcessor = async (input: unknown) => { const paymentProcessor = async (input: unknown) => {
@@ -303,8 +193,7 @@ export const SponsorProviders: Provider[] = [
paymentProcessor, paymentProcessor,
walletRepository, walletRepository,
leagueWalletRepository, leagueWalletRepository,
logger, logger
output
); );
}, },
inject: [ inject: [
@@ -315,7 +204,6 @@ export const SponsorProviders: Provider[] = [
WALLET_REPOSITORY_TOKEN, WALLET_REPOSITORY_TOKEN,
LEAGUE_WALLET_REPOSITORY_TOKEN, LEAGUE_WALLET_REPOSITORY_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
@@ -323,8 +211,7 @@ export const SponsorProviders: Provider[] = [
useFactory: ( useFactory: (
sponsorshipRequestRepo: ISponsorshipRequestRepository, sponsorshipRequestRepo: ISponsorshipRequestRepository,
logger: Logger, logger: Logger,
output: UseCaseOutputPort<unknown>, ) => new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger),
) => new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger, output), inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN],
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN, REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN],
}, },
]; ];

View File

@@ -3,7 +3,7 @@ import type { AcceptSponsorshipRequestUseCase } from '@core/racing/application/u
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase'; import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import type { GetSponsorDashboardInput, GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; import type { GetSponsorDashboardInput, GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; import type { GetEntitySponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
import type { GetSponsorSponsorshipsInput, GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; import type { GetSponsorSponsorshipsInput, GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase'; import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase'; import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
@@ -14,21 +14,11 @@ import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import type { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO'; import type { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import { Money } from '@core/racing/domain/value-objects/Money'; import { Money } from '@core/racing/domain/value-objects/Money';
import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
import { SponsorService } from './SponsorService'; import { SponsorService } from './SponsorService';
describe('SponsorService', () => { describe('SponsorService', () => {
let service: SponsorService; let service: SponsorService;
let getSponsorshipPricingUseCase: { execute: Mock }; let getEntitySponsorshipPricingUseCase: { execute: Mock };
let getSponsorsUseCase: { execute: Mock }; let getSponsorsUseCase: { execute: Mock };
let createSponsorUseCase: { execute: Mock }; let createSponsorUseCase: { execute: Mock };
let getSponsorDashboardUseCase: { execute: Mock }; let getSponsorDashboardUseCase: { execute: Mock };
@@ -40,20 +30,8 @@ describe('SponsorService', () => {
let getSponsorBillingUseCase: { execute: Mock }; let getSponsorBillingUseCase: { execute: Mock };
let logger: Logger; let logger: Logger;
// Presenters
let getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter;
let getSponsorsPresenter: GetSponsorsPresenter;
let createSponsorPresenter: CreateSponsorPresenter;
let getSponsorDashboardPresenter: GetSponsorDashboardPresenter;
let getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter;
let getSponsorPresenter: GetSponsorPresenter;
let getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter;
let acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter;
let rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter;
let sponsorBillingPresenter: SponsorBillingPresenter;
beforeEach(() => { beforeEach(() => {
getSponsorshipPricingUseCase = { execute: vi.fn() }; getEntitySponsorshipPricingUseCase = { execute: vi.fn() };
getSponsorsUseCase = { execute: vi.fn() }; getSponsorsUseCase = { execute: vi.fn() };
createSponsorUseCase = { execute: vi.fn() }; createSponsorUseCase = { execute: vi.fn() };
getSponsorDashboardUseCase = { execute: vi.fn() }; getSponsorDashboardUseCase = { execute: vi.fn() };
@@ -70,20 +48,8 @@ describe('SponsorService', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
// Initialize presenters
getEntitySponsorshipPricingPresenter = new GetEntitySponsorshipPricingPresenter();
getSponsorsPresenter = new GetSponsorsPresenter();
createSponsorPresenter = new CreateSponsorPresenter();
getSponsorDashboardPresenter = new GetSponsorDashboardPresenter();
getSponsorSponsorshipsPresenter = new GetSponsorSponsorshipsPresenter();
getSponsorPresenter = new GetSponsorPresenter();
getPendingSponsorshipRequestsPresenter = new GetPendingSponsorshipRequestsPresenter();
acceptSponsorshipRequestPresenter = new AcceptSponsorshipRequestPresenter();
rejectSponsorshipRequestPresenter = new RejectSponsorshipRequestPresenter();
sponsorBillingPresenter = new SponsorBillingPresenter();
service = new SponsorService( service = new SponsorService(
getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase, getEntitySponsorshipPricingUseCase as unknown as GetEntitySponsorshipPricingUseCase,
getSponsorsUseCase as unknown as GetSponsorsUseCase, getSponsorsUseCase as unknown as GetSponsorsUseCase,
createSponsorUseCase as unknown as CreateSponsorUseCase, createSponsorUseCase as unknown as CreateSponsorUseCase,
getSponsorDashboardUseCase as unknown as GetSponsorDashboardUseCase, getSponsorDashboardUseCase as unknown as GetSponsorDashboardUseCase,
@@ -94,31 +60,19 @@ describe('SponsorService', () => {
rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase, rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase,
getSponsorBillingUseCase as unknown as GetSponsorBillingUseCase, getSponsorBillingUseCase as unknown as GetSponsorBillingUseCase,
logger, logger,
getEntitySponsorshipPricingPresenter,
getSponsorsPresenter,
createSponsorPresenter,
getSponsorDashboardPresenter,
getSponsorSponsorshipsPresenter,
getSponsorPresenter,
getPendingSponsorshipRequestsPresenter,
acceptSponsorshipRequestPresenter,
rejectSponsorshipRequestPresenter,
sponsorBillingPresenter,
); );
}); });
describe('getEntitySponsorshipPricing', () => { describe('getEntitySponsorshipPricing', () => {
it('returns pricing data on success', async () => { it('returns pricing data on success', async () => {
const outputPort = { const output = {
entityType: 'season', entityType: 'season',
entityId: 'season-1', entityId: 'season-1',
acceptingApplications: true,
tiers: [{ name: 'Gold', price: { amount: 500, currency: 'USD' }, benefits: ['Main slot'] }], tiers: [{ name: 'Gold', price: { amount: 500, currency: 'USD' }, benefits: ['Main slot'] }],
}; };
getSponsorshipPricingUseCase.execute.mockImplementation(async () => { getEntitySponsorshipPricingUseCase.execute.mockResolvedValue(Result.ok(output));
getEntitySponsorshipPricingPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getEntitySponsorshipPricing(); const result = await service.getEntitySponsorshipPricing();
@@ -130,7 +84,7 @@ describe('SponsorService', () => {
}); });
it('returns empty pricing on error', async () => { it('returns empty pricing on error', async () => {
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); getEntitySponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const result = await service.getEntitySponsorshipPricing(); const result = await service.getEntitySponsorshipPricing();
@@ -153,10 +107,7 @@ describe('SponsorService', () => {
}), }),
]; ];
getSponsorsUseCase.execute.mockImplementation(async () => { getSponsorsUseCase.execute.mockResolvedValue(Result.ok({ sponsors }));
getSponsorsPresenter.present(sponsors);
return Result.ok(undefined);
});
const result = await service.getSponsors(); const result = await service.getSponsors();
@@ -191,10 +142,7 @@ describe('SponsorService', () => {
createdAt: new Date('2024-01-01T00:00:00Z'), createdAt: new Date('2024-01-01T00:00:00Z'),
}); });
createSponsorUseCase.execute.mockImplementation(async () => { createSponsorUseCase.execute.mockResolvedValue(Result.ok({ sponsor }));
createSponsorPresenter.present(sponsor);
return Result.ok(undefined);
});
const result = await service.createSponsor(input); const result = await service.createSponsor(input);
@@ -235,7 +183,7 @@ describe('SponsorService', () => {
describe('getSponsorDashboard', () => { describe('getSponsorDashboard', () => {
it('returns dashboard on success', async () => { it('returns dashboard on success', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' }; const params: GetSponsorDashboardInput = { sponsorId: 's1' };
const outputPort = { const output = {
sponsorId: 's1', sponsorId: 's1',
sponsorName: 'S1', sponsorName: 'S1',
metrics: { metrics: {
@@ -254,19 +202,25 @@ describe('SponsorService', () => {
totalInvestment: Money.create(0, 'USD'), totalInvestment: Money.create(0, 'USD'),
costPerThousandViews: 0, costPerThousandViews: 0,
}, },
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
}; };
getSponsorDashboardUseCase.execute.mockImplementation(async () => { getSponsorDashboardUseCase.execute.mockResolvedValue(Result.ok(output));
getSponsorDashboardPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getSponsorDashboard(params); const result = await service.getSponsorDashboard(params);
expect(result).toEqual({ expect(result).toEqual({
sponsorId: 's1', sponsorId: 's1',
sponsorName: 'S1', sponsorName: 'S1',
metrics: outputPort.metrics, metrics: output.metrics,
sponsoredLeagues: [], sponsoredLeagues: [],
investment: { investment: {
activeSponsorships: 0, activeSponsorships: 0,
@@ -296,7 +250,7 @@ describe('SponsorService', () => {
describe('getSponsorSponsorships', () => { describe('getSponsorSponsorships', () => {
it('returns sponsorships on success', async () => { it('returns sponsorships on success', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' }; const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
const outputPort = { const output = {
sponsor: Sponsor.create({ sponsor: Sponsor.create({
id: 's1', id: 's1',
name: 'S1', name: 'S1',
@@ -311,10 +265,7 @@ describe('SponsorService', () => {
}, },
}; };
getSponsorSponsorshipsUseCase.execute.mockImplementation(async () => { getSponsorSponsorshipsUseCase.execute.mockResolvedValue(Result.ok(output));
getSponsorSponsorshipsPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getSponsorSponsorships(params); const result = await service.getSponsorSponsorships(params);
@@ -345,16 +296,25 @@ describe('SponsorService', () => {
describe('getSponsor', () => { describe('getSponsor', () => {
it('returns sponsor when found', async () => { it('returns sponsor when found', async () => {
const sponsorId = 's1'; const sponsorId = 's1';
const output = { sponsor: { id: sponsorId, name: 'S1' } }; const sponsor = Sponsor.create({
id: sponsorId,
getSponsorUseCase.execute.mockImplementation(async () => { name: 'S1',
getSponsorPresenter.present(output); contactEmail: 's1@example.com',
return Result.ok(undefined); createdAt: new Date('2024-01-01T00:00:00Z'),
}); });
getSponsorUseCase.execute.mockResolvedValue(Result.ok({ sponsor }));
const result = await service.getSponsor(sponsorId); const result = await service.getSponsor(sponsorId);
expect(result).toEqual(output); expect(result).toEqual({
sponsor: {
id: sponsorId,
name: 'S1',
logoUrl: undefined,
websiteUrl: undefined,
},
});
}); });
it('throws when not found', async () => { it('throws when not found', async () => {
@@ -375,21 +335,18 @@ describe('SponsorService', () => {
describe('getPendingSponsorshipRequests', () => { describe('getPendingSponsorshipRequests', () => {
it('returns requests on success', async () => { it('returns requests on success', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' }; const params = { entityType: 'season' as const, entityId: 'season-1' };
const outputPort = { const output = {
entityType: 'season', entityType: 'season',
entityId: 'season-1', entityId: 'season-1',
requests: [], requests: [],
totalCount: 0, totalCount: 0,
}; };
getPendingSponsorshipRequestsUseCase.execute.mockImplementation(async () => { getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(output));
getPendingSponsorshipRequestsPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getPendingSponsorshipRequests(params); const result = await service.getPendingSponsorshipRequests(params);
expect(result).toEqual(outputPort); expect(result).toEqual(output);
}); });
it('returns empty result on error', async () => { it('returns empty result on error', async () => {
@@ -405,20 +362,13 @@ describe('SponsorService', () => {
totalCount: 0, totalCount: 0,
}); });
}); });
it('throws when presenter viewModel is missing on success', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.getPendingSponsorshipRequests(params)).rejects.toThrow('Pending sponsorship requests not found');
});
}); });
describe('SponsorshipRequest', () => { describe('SponsorshipRequest', () => {
it('returns accept result on success', async () => { it('returns accept result on success', async () => {
const requestId = 'r1'; const requestId = 'r1';
const respondedBy = 'u1'; const respondedBy = 'u1';
const outputPort = { const output = {
requestId, requestId,
sponsorshipId: 'sp1', sponsorshipId: 'sp1',
status: 'accepted' as const, status: 'accepted' as const,
@@ -427,14 +377,11 @@ describe('SponsorService', () => {
netAmount: 90, netAmount: 90,
}; };
acceptSponsorshipRequestUseCase.execute.mockImplementation(async () => { acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output));
acceptSponsorshipRequestPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.acceptSponsorshipRequest(requestId, respondedBy); const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(result).toEqual(outputPort); expect(result).toEqual(output);
}); });
it('throws on error', async () => { it('throws on error', async () => {
@@ -448,16 +395,6 @@ describe('SponsorService', () => {
'Accept sponsorship request failed', 'Accept sponsorship request failed',
); );
}); });
it('throws when presenter viewModel is missing on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow(
'Accept sponsorship request failed',
);
});
}); });
describe('rejectSponsorshipRequest', () => { describe('rejectSponsorshipRequest', () => {
@@ -472,10 +409,7 @@ describe('SponsorService', () => {
rejectionReason: reason, rejectionReason: reason,
}; };
rejectSponsorshipRequestUseCase.execute.mockImplementation(async () => { rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output));
rejectSponsorshipRequestPresenter.present(output as any);
return Result.ok(undefined);
});
const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason); const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
@@ -485,19 +419,18 @@ describe('SponsorService', () => {
it('passes no reason when reason is undefined', async () => { it('passes no reason when reason is undefined', async () => {
const requestId = 'r1'; const requestId = 'r1';
const respondedBy = 'u1'; const respondedBy = 'u1';
const output = {
rejectSponsorshipRequestUseCase.execute.mockImplementation(async (input: any) => {
expect(input).toEqual({ requestId, respondedBy });
rejectSponsorshipRequestPresenter.present({
requestId, requestId,
status: 'rejected' as const, status: 'rejected' as const,
respondedAt: new Date(), respondedAt: new Date(),
rejectionReason: '', rejectionReason: '',
} as any); };
return Result.ok(undefined);
});
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).resolves.toMatchObject({ rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output));
const result = await service.rejectSponsorshipRequest(requestId, respondedBy);
expect(result).toMatchObject({
requestId, requestId,
status: 'rejected', status: 'rejected',
}); });
@@ -514,16 +447,6 @@ describe('SponsorService', () => {
'Reject sponsorship request failed', 'Reject sponsorship request failed',
); );
}); });
it('throws when presenter viewModel is missing on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow(
'Reject sponsorship request failed',
);
});
}); });
describe('getSponsorBilling', () => { describe('getSponsorBilling', () => {

View File

@@ -21,7 +21,7 @@ import { InvoiceDTO } from './dtos/InvoiceDTO';
import { BillingStatsDTO } from './dtos/BillingStatsDTO'; import { BillingStatsDTO } from './dtos/BillingStatsDTO';
// Use cases // Use cases
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; import { GetEntitySponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
import { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase'; import { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase'; import { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
@@ -37,26 +37,8 @@ import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/G
import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest'; import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
// Presenters
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { AcceptSponsorshipRequestPresenter, AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
import { AvailableLeaguesPresenter } from './presenters/AvailableLeaguesPresenter';
import { LeagueDetailPresenter } from './presenters/LeagueDetailPresenter';
import { SponsorSettingsPresenter } from './presenters/SponsorSettingsPresenter';
import { SponsorSettingsUpdatePresenter } from './presenters/SponsorSettingsUpdatePresenter';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
// Tokens // Tokens
import { import {
GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
GET_SPONSORS_USE_CASE_TOKEN, GET_SPONSORS_USE_CASE_TOKEN,
CREATE_SPONSOR_USE_CASE_TOKEN, CREATE_SPONSOR_USE_CASE_TOKEN,
GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN,
@@ -66,14 +48,33 @@ import {
ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
GET_SPONSOR_BILLING_USE_CASE_TOKEN, GET_SPONSOR_BILLING_USE_CASE_TOKEN,
GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
LOGGER_TOKEN, LOGGER_TOKEN,
} from './SponsorTokens'; } from './SponsorTokens';
// Presenters (for view model transformation only)
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
import { AvailableLeaguesPresenter } from './presenters/AvailableLeaguesPresenter';
import { LeagueDetailPresenter } from './presenters/LeagueDetailPresenter';
import { SponsorSettingsPresenter } from './presenters/SponsorSettingsPresenter';
import { SponsorSettingsUpdatePresenter } from './presenters/SponsorSettingsUpdatePresenter';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter';
@Injectable() @Injectable()
export class SponsorService { export class SponsorService {
constructor( constructor(
@Inject(GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN) @Inject(GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN)
private readonly getSponsorshipPricingUseCase: GetSponsorshipPricingUseCase, private readonly getEntitySponsorshipPricingUseCase: GetEntitySponsorshipPricingUseCase,
@Inject(GET_SPONSORS_USE_CASE_TOKEN) @Inject(GET_SPONSORS_USE_CASE_TOKEN)
private readonly getSponsorsUseCase: GetSponsorsUseCase, private readonly getSponsorsUseCase: GetSponsorsUseCase,
@Inject(CREATE_SPONSOR_USE_CASE_TOKEN) @Inject(CREATE_SPONSOR_USE_CASE_TOKEN)
@@ -94,22 +95,14 @@ export class SponsorService {
private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase, private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase,
@Inject(LOGGER_TOKEN) @Inject(LOGGER_TOKEN)
private readonly logger: Logger, private readonly logger: Logger,
// Injected presenters
private readonly getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter,
private readonly getSponsorsPresenter: GetSponsorsPresenter,
private readonly createSponsorPresenter: CreateSponsorPresenter,
private readonly getSponsorDashboardPresenter: GetSponsorDashboardPresenter,
private readonly getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter,
private readonly getSponsorPresenter: GetSponsorPresenter,
private readonly getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter,
private readonly acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter,
private readonly rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter,
private readonly sponsorBillingPresenter: SponsorBillingPresenter,
) {} ) {}
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> { async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> {
this.logger.debug('[SponsorService] Fetching sponsorship pricing.'); this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
const result = await this.getSponsorshipPricingUseCase.execute({}); const result = await this.getEntitySponsorshipPricingUseCase.execute({
entityType: 'season',
entityId: 'default',
});
if (result.isErr()) { if (result.isErr()) {
return { return {
@@ -119,18 +112,22 @@ export class SponsorService {
}; };
} }
return this.getEntitySponsorshipPricingPresenter.viewModel; const presenter = new GetEntitySponsorshipPricingPresenter();
presenter.present(result.value);
return presenter.viewModel;
} }
async getSponsors(): Promise<GetSponsorsOutputDTO> { async getSponsors(): Promise<GetSponsorsOutputDTO> {
this.logger.debug('[SponsorService] Fetching sponsors.'); this.logger.debug('[SponsorService] Fetching sponsors.');
const result = await this.getSponsorsUseCase.execute(); const result = await this.getSponsorsUseCase.execute({});
if (result.isErr()) { if (result.isErr()) {
return { sponsors: [] }; return { sponsors: [] };
} }
return this.getSponsorsPresenter.responseModel; const presenter = new GetSponsorsPresenter();
presenter.present(result.unwrap().sponsors);
return presenter.responseModel;
} }
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> { async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> {
@@ -142,7 +139,14 @@ export class SponsorService {
throw new Error(error.details?.message ?? error.message ?? 'Failed to create sponsor'); throw new Error(error.details?.message ?? error.message ?? 'Failed to create sponsor');
} }
return this.createSponsorPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Create sponsor failed');
}
const presenter = new CreateSponsorPresenter();
presenter.present(resultValue.sponsor);
return presenter.viewModel;
} }
async getSponsorDashboard( async getSponsorDashboard(
@@ -155,7 +159,14 @@ export class SponsorService {
throw new Error('Sponsor dashboard not found'); throw new Error('Sponsor dashboard not found');
} }
return this.getSponsorDashboardPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Sponsor dashboard not found');
}
const presenter = new GetSponsorDashboardPresenter();
presenter.present(resultValue);
return presenter.viewModel;
} }
async getSponsorSponsorships( async getSponsorSponsorships(
@@ -168,7 +179,14 @@ export class SponsorService {
throw new Error('Sponsor sponsorships not found'); throw new Error('Sponsor sponsorships not found');
} }
return this.getSponsorSponsorshipsPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Sponsor sponsorships not found');
}
const presenter = new GetSponsorSponsorshipsPresenter();
presenter.present(resultValue);
return presenter.viewModel;
} }
async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO> { async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO> {
@@ -179,7 +197,14 @@ export class SponsorService {
throw new Error('Sponsor not found'); throw new Error('Sponsor not found');
} }
const viewModel = this.getSponsorPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Sponsor not found');
}
const presenter = new GetSponsorPresenter();
presenter.present(resultValue.sponsor);
const viewModel = presenter.viewModel;
if (!viewModel) { if (!viewModel) {
throw new Error('Sponsor not found'); throw new Error('Sponsor not found');
} }
@@ -205,7 +230,14 @@ export class SponsorService {
}; };
} }
const viewModel = this.getPendingSponsorshipRequestsPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Pending sponsorship requests not found');
}
const presenter = new GetPendingSponsorshipRequestsPresenter();
presenter.present(resultValue);
const viewModel = presenter.viewModel;
if (!viewModel) { if (!viewModel) {
throw new Error('Pending sponsorship requests not found'); throw new Error('Pending sponsorship requests not found');
} }
@@ -231,7 +263,14 @@ export class SponsorService {
throw new Error('Accept sponsorship request failed'); throw new Error('Accept sponsorship request failed');
} }
const viewModel = this.acceptSponsorshipRequestPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Accept sponsorship request failed');
}
const presenter = new AcceptSponsorshipRequestPresenter();
presenter.present(resultValue);
const viewModel = presenter.viewModel;
if (!viewModel) { if (!viewModel) {
throw new Error('Accept sponsorship request failed'); throw new Error('Accept sponsorship request failed');
} }
@@ -263,7 +302,14 @@ export class SponsorService {
throw new Error('Reject sponsorship request failed'); throw new Error('Reject sponsorship request failed');
} }
const viewModel = this.rejectSponsorshipRequestPresenter.viewModel; const resultValue = result.value;
if (!resultValue) {
throw new Error('Reject sponsorship request failed');
}
const presenter = new RejectSponsorshipRequestPresenter();
presenter.present(resultValue);
const viewModel = presenter.viewModel;
if (!viewModel) { if (!viewModel) {
throw new Error('Reject sponsorship request failed'); throw new Error('Reject sponsorship request failed');
} }
@@ -284,7 +330,8 @@ export class SponsorService {
} }
const billingData = result.unwrap(); const billingData = result.unwrap();
this.sponsorBillingPresenter.present({ const presenter = new SponsorBillingPresenter();
presenter.present({
paymentMethods: billingData.paymentMethods, paymentMethods: billingData.paymentMethods,
invoices: billingData.invoices, invoices: billingData.invoices,
stats: { stats: {
@@ -294,7 +341,7 @@ export class SponsorService {
}, },
}); });
return this.sponsorBillingPresenter.viewModel; return presenter.viewModel;
} }
async getAvailableLeagues(): Promise<AvailableLeaguesPresenter> { async getAvailableLeagues(): Promise<AvailableLeaguesPresenter> {

View File

@@ -9,18 +9,6 @@ export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestReposito
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
export const MEDIA_RESOLVER_TOKEN = 'MediaResolverPort'; export const MEDIA_RESOLVER_TOKEN = 'MediaResolverPort';
// Presenter tokens
export const GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN = 'GetEntitySponsorshipPricingPresenter';
export const GET_SPONSORS_PRESENTER_TOKEN = 'GetSponsorsPresenter';
export const CREATE_SPONSOR_PRESENTER_TOKEN = 'CreateSponsorPresenter';
export const GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN = 'GetSponsorDashboardPresenter';
export const GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN = 'GetSponsorSponsorshipsPresenter';
export const GET_SPONSOR_PRESENTER_TOKEN = 'GetSponsorPresenter';
export const GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN = 'GetPendingSponsorshipRequestsPresenter';
export const ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'AcceptSponsorshipRequestPresenter';
export const REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'RejectSponsorshipRequestPresenter';
export const GET_SPONSOR_BILLING_PRESENTER_TOKEN = 'SponsorBillingPresenter';
// Use case / application service tokens // Use case / application service tokens
export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase'; export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase';
export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase'; export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase';
@@ -33,16 +21,3 @@ export const GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN = 'GetPendingSponso
export const ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'AcceptSponsorshipRequestUseCase'; export const ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'AcceptSponsorshipRequestUseCase';
export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase'; export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase';
export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase'; export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase';
// Output port tokens
export const GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetSponsorshipPricingOutputPort_TOKEN';
export const GET_SPONSORS_OUTPUT_PORT_TOKEN = 'GetSponsorsOutputPort_TOKEN';
export const CREATE_SPONSOR_OUTPUT_PORT_TOKEN = 'CreateSponsorOutputPort_TOKEN';
export const GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN = 'GetSponsorDashboardOutputPort_TOKEN';
export const GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSponsorSponsorshipsOutputPort_TOKEN';
export const GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetEntitySponsorshipPricingOutputPort_TOKEN';
export const GET_SPONSOR_OUTPUT_PORT_TOKEN = 'GetSponsorOutputPort_TOKEN';
export const GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN = 'GetPendingSponsorshipRequestsOutputPort_TOKEN';
export const ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'AcceptSponsorshipRequestOutputPort_TOKEN';
export const REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'RejectSponsorshipRequestOutputPort_TOKEN';
export const GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN = 'GetSponsorBillingOutputPort_TOKEN';

View File

@@ -8,7 +8,7 @@ export class GetEntitySponsorshipPricingPresenter {
this.result = null; this.result = null;
} }
present(output: GetEntitySponsorshipPricingResult | null) { present(output: GetEntitySponsorshipPricingResult | null | undefined) {
if (!output) { if (!output) {
this.result = { this.result = {
entityType: 'season', entityType: 'season',

View File

@@ -1,13 +1,5 @@
import { GetSponsorOutputDTO } from '../dtos/GetSponsorOutputDTO'; import { GetSponsorOutputDTO } from '../dtos/GetSponsorOutputDTO';
import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
interface GetSponsorOutputPort {
sponsor: {
id: string;
name: string;
logoUrl?: string;
websiteUrl?: string;
};
}
export class GetSponsorPresenter { export class GetSponsorPresenter {
private result: GetSponsorOutputDTO | null = null; private result: GetSponsorOutputDTO | null = null;
@@ -16,18 +8,18 @@ export class GetSponsorPresenter {
this.result = null; this.result = null;
} }
present(output: GetSponsorOutputPort | null) { present(sponsor: Sponsor) {
if (!output) { if (!sponsor) {
this.result = null; this.result = null;
return; return;
} }
this.result = { this.result = {
sponsor: { sponsor: {
id: output.sponsor.id, id: sponsor.id.toString(),
name: output.sponsor.name, name: sponsor.name.toString(),
...(output.sponsor.logoUrl !== undefined ? { logoUrl: output.sponsor.logoUrl } : {}), ...(sponsor.logoUrl !== undefined ? { logoUrl: sponsor.logoUrl.toString() } : {}),
...(output.sponsor.websiteUrl !== undefined ? { websiteUrl: output.sponsor.websiteUrl } : {}), ...(sponsor.websiteUrl !== undefined ? { websiteUrl: sponsor.websiteUrl.toString() } : {}),
}, },
} as GetSponsorOutputDTO; } as GetSponsorOutputDTO;
} }

View File

@@ -1,95 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter';
import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
describe('GetSponsorshipPricingPresenter', () => {
let presenter: GetSponsorshipPricingPresenter;
beforeEach(() => {
presenter = new GetSponsorshipPricingPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.getViewModel()).toEqual(expectedViewModel);
});
});
describe('viewModel', () => {
it('should throw error when not presented', () => {
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('should return the result when presented', () => {
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
});

View File

@@ -1,32 +0,0 @@
import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetSponsorshipPricingPresenter {
private result: GetEntitySponsorshipPricingResultDTO | null = null;
reset() {
this.result = null;
}
present(outputPort: GetSponsorshipPricingResult): void {
this.result = {
entityType: outputPort.entityType,
entityId: outputPort.entityId,
pricing: outputPort.pricing.map(item => ({
id: item.id,
level: item.level,
price: item.price,
currency: item.currency,
})),
};
}
getViewModel(): GetEntitySponsorshipPricingResultDTO | null {
return this.result;
}
get viewModel(): GetEntitySponsorshipPricingResultDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -124,42 +124,13 @@ describe('TeamService', () => {
clear: vi.fn(), clear: vi.fn(),
}; };
const resultRepository = {
findAll: vi.fn().mockResolvedValue([]),
};
// Mock presenter that stores result synchronously
const allTeamsPresenter = {
reset: vi.fn(),
present: vi.fn((result: any) => {
// Store immediately and synchronously
allTeamsPresenter.responseModel = {
teams: result.teams.map((t: any) => ({
id: t.id,
name: t.name,
tag: t.tag,
description: t.description,
memberCount: t.memberCount,
leagues: t.leagues,
logoUrl: t.logoUrl ?? null,
})),
totalCount: result.totalCount,
};
}),
getResponseModel: vi.fn(() => allTeamsPresenter.responseModel || { teams: [], totalCount: 0 }),
responseModel: { teams: [], totalCount: 0 },
setMediaResolver: vi.fn(),
setBaseUrl: vi.fn(),
};
service = new TeamService( service = new TeamService(
teamRepository as unknown as never, teamRepository as unknown as never,
membershipRepository as unknown as never, membershipRepository as unknown as never,
driverRepository as unknown as never, driverRepository as unknown as never,
logger, logger,
teamStatsRepository as unknown as never, teamStatsRepository as unknown as never
resultRepository as unknown as never,
allTeamsPresenter as any
); );
}); });
@@ -178,7 +149,15 @@ describe('TeamService', () => {
description: 'Desc', description: 'Desc',
memberCount: 3, memberCount: 3,
leagues: ['league-1'], leagues: ['league-1'],
logoUrl: null, totalWins: 0,
totalRaces: 0,
performanceLevel: 'intermediate',
specialization: 'mixed',
region: '',
languages: [],
rating: 0,
logoUrl: '/media/teams/team-1/logo',
isRecruiting: false,
}, },
], ],
totalCount: 1, totalCount: 1,
@@ -283,8 +262,16 @@ describe('TeamService', () => {
isActive: true, isActive: true,
avatarUrl: '', avatarUrl: '',
}, },
{
driverId: '',
driverName: '',
role: 'owner',
joinedAt: '2023-02-02T00:00:00.000Z',
isActive: true,
avatarUrl: '',
},
], ],
totalCount: 1, totalCount: 2,
ownerCount: 1, ownerCount: 1,
managerCount: 0, managerCount: 0,
memberCount: 1, memberCount: 1,

View File

@@ -26,20 +26,9 @@ import { UpdateTeamUseCase, UpdateTeamInput } from '@core/racing/application/use
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase'; import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
// API Presenters
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter';
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { TeamMembershipPresenter } from './presenters/TeamMembershipPresenter';
import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
// Tokens // Tokens
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN } from './TeamTokens'; import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN } from './TeamTokens';
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository'; import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
@Injectable() @Injectable()
export class TeamService { export class TeamService {
@@ -49,8 +38,6 @@ export class TeamService {
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository, @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(TEAM_STATS_REPOSITORY_TOKEN) private readonly teamStatsRepository: ITeamStatsRepository, @Inject(TEAM_STATS_REPOSITORY_TOKEN) private readonly teamStatsRepository: ITeamStatsRepository,
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository: IResultRepository,
private readonly allTeamsPresenter: AllTeamsPresenter,
) {} ) {}
async getAll(): Promise<GetAllTeamsOutputDTO> { async getAll(): Promise<GetAllTeamsOutputDTO> {
@@ -60,38 +47,82 @@ export class TeamService {
this.teamRepository, this.teamRepository,
this.membershipRepository, this.membershipRepository,
this.teamStatsRepository, this.teamStatsRepository,
this.resultRepository, this.logger
this.logger,
this.allTeamsPresenter
); );
const result = await useCase.execute(); const result = await useCase.execute({});
if (result.isErr()) { if (result.isErr()) {
this.logger.error('Error fetching all teams', new Error(result.error?.details?.message || 'Unknown error')); this.logger.error('Error fetching all teams', new Error(result.error?.details?.message || 'Unknown error'));
return { teams: [], totalCount: 0 }; return { teams: [], totalCount: 0 };
} }
return this.allTeamsPresenter.getResponseModel()!; const value = result.value;
if (!value) {
return { teams: [], totalCount: 0 };
}
return {
teams: value.teams.map(t => ({
id: t.team.id,
name: t.team.name.toString(),
tag: t.team.tag.toString(),
description: t.description,
memberCount: t.memberCount,
leagues: t.leagues,
totalWins: t.totalWins,
totalRaces: t.totalRaces,
performanceLevel: t.performanceLevel,
specialization: t.specialization,
region: t.region,
languages: t.languages,
rating: t.rating,
logoUrl: t.logoUrl,
isRecruiting: t.isRecruiting,
})),
totalCount: value.totalCount,
};
} }
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> { async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`); this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`);
const presenter = new TeamDetailsPresenter(); const useCase = new GetTeamDetailsUseCase(this.teamRepository, this.membershipRepository);
const useCase = new GetTeamDetailsUseCase(this.teamRepository, this.membershipRepository, presenter);
const result = await useCase.execute({ teamId, driverId: userId || '' }); const result = await useCase.execute({ teamId, driverId: userId || '' });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team details for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`); this.logger.error(`Error fetching team details for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
return null; return null;
} }
return presenter.getResponseModel()!; const value = result.value;
if (!value) {
return null;
}
// Convert to DTO
return {
team: {
id: value.team.id,
name: value.team.name.toString(),
tag: value.team.tag.toString(),
description: value.team.description.toString(),
ownerId: value.team.ownerId.toString(),
leagues: value.team.leagues.map(l => l.toString()),
isRecruiting: value.team.isRecruiting,
createdAt: value.team.createdAt?.toDate()?.toISOString?.() || new Date().toISOString(),
category: undefined,
},
membership: value.membership ? {
role: value.membership.role === 'driver' ? 'member' : (value.membership.role as 'owner' | 'manager' | 'member'),
joinedAt: value.membership.joinedAt.toISOString(),
isActive: value.membership.status === 'active',
} : null,
canManage: value.canManage,
};
} }
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> { async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`); this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter(); const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger);
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger, presenter);
const result = await useCase.execute({ teamId }); const result = await useCase.execute({ teamId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`); this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
@@ -104,14 +135,37 @@ export class TeamService {
}; };
} }
return presenter.getResponseModel()!; const value = result.value;
if (!value) {
return {
members: [],
totalCount: 0,
ownerCount: 0,
managerCount: 0,
memberCount: 0,
};
}
return {
members: value.members.map(m => ({
driverId: m.driver?.id || '',
driverName: m.driver?.name?.toString() || '',
role: m.membership.role === 'driver' ? 'member' : (m.membership.role as 'owner' | 'manager' | 'member'),
joinedAt: m.membership.joinedAt.toISOString(),
isActive: m.membership.status === 'active',
avatarUrl: '', // Would need MediaResolver here
})),
totalCount: value.members.length,
ownerCount: value.members.filter(m => m.membership.role === 'owner').length,
managerCount: value.members.filter(m => m.membership.role === 'manager').length,
memberCount: value.members.filter(m => m.membership.role === 'driver').length,
};
} }
async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> { async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`); this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`);
const presenter = new TeamJoinRequestsPresenter(); const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository);
const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter);
const result = await useCase.execute({ teamId }); const result = await useCase.execute({ teamId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, new Error(result.error?.details?.message || 'Unknown error')); this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, new Error(result.error?.details?.message || 'Unknown error'));
@@ -122,14 +176,33 @@ export class TeamService {
}; };
} }
return presenter.getResponseModel()!; const value = result.value;
if (!value) {
return {
requests: [],
pendingCount: 0,
totalCount: 0,
};
}
return {
requests: value.joinRequests.map(r => ({
requestId: r.id,
driverId: r.driverId,
driverName: r.driver.name.toString(),
teamId: r.teamId,
status: 'pending',
requestedAt: r.requestedAt.toISOString(),
avatarUrl: '', // Would need MediaResolver here
})),
pendingCount: value.joinRequests.length,
totalCount: value.joinRequests.length,
};
} }
async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> { async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> {
this.logger.debug('[TeamService] Creating team', { input, userId }); this.logger.debug('[TeamService] Creating team', { input, userId });
const presenter = new CreateTeamPresenter();
const command: CreateTeamInput = { const command: CreateTeamInput = {
name: input.name, name: input.name,
tag: input.tag, tag: input.tag,
@@ -138,21 +211,24 @@ export class TeamService {
leagues: [], leagues: [],
}; };
const useCase = new CreateTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter); const useCase = new CreateTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
const result = await useCase.execute(command); const result = await useCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error creating team: ${result.error?.details?.message || 'Unknown error'}`); this.logger.error(`Error creating team: ${result.error?.details?.message || 'Unknown error'}`);
return { id: '', success: false }; return { id: '', success: false };
} }
return presenter.responseModel; const value = result.value;
if (!value) {
return { id: '', success: false };
}
return { id: value.team.id, success: true };
} }
async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> { async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> {
this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId }); this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId });
const presenter = new UpdateTeamPresenter();
const command: UpdateTeamInput = { const command: UpdateTeamInput = {
teamId, teamId,
updates: { updates: {
@@ -163,41 +239,72 @@ export class TeamService {
updatedBy: userId || '', updatedBy: userId || '',
}; };
const useCase = new UpdateTeamUseCase(this.teamRepository, this.membershipRepository, presenter); const useCase = new UpdateTeamUseCase(this.teamRepository, this.membershipRepository);
const result = await useCase.execute(command); const result = await useCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error updating team ${teamId}: ${result.error?.details?.message || 'Unknown error'}`); this.logger.error(`Error updating team ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
return { success: false }; return { success: false };
} }
return presenter.responseModel; return { success: true };
} }
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> { async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team for driverId: ${driverId}`); this.logger.debug(`[TeamService] Fetching team for driverId: ${driverId}`);
const presenter = new DriverTeamPresenter(); const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger);
const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute({ driverId }); const result = await useCase.execute({ driverId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`); this.logger.error(`Error fetching team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return null; return null;
} }
return presenter.getResponseModel(); const value = result.value;
if (!value || !value.team) {
return null;
}
return {
team: {
id: value.team.id,
name: value.team.name.toString(),
tag: value.team.tag.toString(),
description: value.team.description.toString(),
ownerId: value.team.ownerId.toString(),
leagues: value.team.leagues.map(l => l.toString()),
isRecruiting: value.team.isRecruiting,
createdAt: value.team.createdAt?.toDate?.()?.toISOString?.() || new Date().toISOString(),
category: undefined,
},
membership: {
role: value.membership.role === 'driver' ? 'member' : (value.membership.role as 'owner' | 'manager' | 'member'),
joinedAt: value.membership.joinedAt.toISOString(),
isActive: value.membership.status === 'active',
},
isOwner: value.membership.role === 'owner',
canManage: value.membership.role === 'owner' || value.membership.role === 'manager',
};
} }
async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> { async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`); this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`);
const presenter = new TeamMembershipPresenter(); const useCase = new GetTeamMembershipUseCase(this.membershipRepository, this.logger);
const useCase = new GetTeamMembershipUseCase(this.membershipRepository, this.logger, presenter);
const result = await useCase.execute({ teamId, driverId }); const result = await useCase.execute({ teamId, driverId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`); this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return null; return null;
} }
return presenter.getResponseModel(); const value = result.value;
if (!value) {
return null;
}
return value.membership ? {
role: value.membership.role,
joinedAt: value.membership.joinedAt,
isActive: value.membership.isActive,
} : null;
} }
} }

View File

@@ -19,40 +19,40 @@ export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
async present(result: GetAllTeamsResult): Promise<void> { async present(result: GetAllTeamsResult): Promise<void> {
const teams: TeamListItemDTO[] = await Promise.all( const teams: TeamListItemDTO[] = await Promise.all(
result.teams.map(async (team) => { result.teams.map(async (enrichedTeam) => {
const dto = new TeamListItemDTO(); const dto = new TeamListItemDTO();
dto.id = team.id; dto.id = enrichedTeam.team.id;
dto.name = team.name; dto.name = enrichedTeam.team.name.toString();
dto.tag = team.tag; dto.tag = enrichedTeam.team.tag.toString();
dto.description = team.description || ''; dto.description = enrichedTeam.team.description.toString() || '';
dto.memberCount = team.memberCount; dto.memberCount = enrichedTeam.memberCount;
dto.leagues = team.leagues || []; dto.leagues = enrichedTeam.team.leagues.map(l => l.toString()) || [];
dto.totalWins = team.totalWins ?? 0; dto.totalWins = enrichedTeam.totalWins;
dto.totalRaces = team.totalRaces ?? 0; dto.totalRaces = enrichedTeam.totalRaces;
dto.performanceLevel = (team.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro') ?? 'intermediate'; dto.performanceLevel = enrichedTeam.performanceLevel;
dto.specialization = (team.specialization as 'endurance' | 'sprint' | 'mixed') ?? 'mixed'; dto.specialization = enrichedTeam.specialization;
dto.region = team.region ?? ''; dto.region = enrichedTeam.region;
dto.languages = team.languages ?? []; dto.languages = enrichedTeam.languages;
// Resolve logo URL using MediaResolverPort if available // Resolve logo URL using MediaResolverPort if available
if (this.mediaResolver && team.logoRef) { if (this.mediaResolver && enrichedTeam.team.logoRef) {
const ref = team.logoRef instanceof MediaReference ? team.logoRef : MediaReference.fromJSON(team.logoRef); const ref = enrichedTeam.team.logoRef instanceof MediaReference ? enrichedTeam.team.logoRef : MediaReference.fromJSON(enrichedTeam.team.logoRef);
dto.logoUrl = await this.mediaResolver.resolve(ref); dto.logoUrl = await this.mediaResolver.resolve(ref);
} else { } else {
// Fallback to existing logoUrl or null // Fallback to enriched logoUrl or null
dto.logoUrl = team.logoUrl ?? null; dto.logoUrl = enrichedTeam.logoUrl;
} }
dto.rating = team.rating ?? 0; dto.rating = enrichedTeam.rating;
dto.category = team.category; dto.category = enrichedTeam.team.category;
dto.isRecruiting = team.isRecruiting; dto.isRecruiting = enrichedTeam.team.isRecruiting;
return dto; return dto;
}) })
); );
this.model = { this.model = {
teams, teams,
totalCount: result.totalCount ?? result.teams.length, totalCount: result.totalCount,
}; };
} }

View File

@@ -65,6 +65,7 @@ export class SessionGateway {
cookie: cookieString, cookie: cookieString,
}, },
cache: 'no-store', cache: 'no-store',
credentials: 'include',
}); });
console.log(`[SESSION] Response status:`, response.status); console.log(`[SESSION] Response status:`, response.status);

View File

@@ -2,7 +2,6 @@ import { vi, describe, it, expect, beforeEach } from 'vitest';
import { ListUsersUseCase, ListUsersResult } from './ListUsersUseCase'; import { ListUsersUseCase, ListUsersResult } from './ListUsersUseCase';
import { IAdminUserRepository } from '../ports/IAdminUserRepository'; import { IAdminUserRepository } from '../ports/IAdminUserRepository';
import { AdminUser } from '../../domain/entities/AdminUser'; import { AdminUser } from '../../domain/entities/AdminUser';
import { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { AuthorizationService } from '../../domain/services/AuthorizationService'; import { AuthorizationService } from '../../domain/services/AuthorizationService';
// Mock the authorization service // Mock the authorization service
@@ -20,11 +19,6 @@ const mockRepository = {
delete: vi.fn(), delete: vi.fn(),
} as unknown as IAdminUserRepository; } as unknown as IAdminUserRepository;
// Mock output port
const mockOutputPort = {
present: vi.fn(),
} as unknown as UseCaseOutputPort<ListUsersResult>;
describe('ListUsersUseCase', () => { describe('ListUsersUseCase', () => {
let useCase: ListUsersUseCase; let useCase: ListUsersUseCase;
let actor: AdminUser; let actor: AdminUser;
@@ -41,7 +35,7 @@ describe('ListUsersUseCase', () => {
// Setup default successful authorization // Setup default successful authorization
vi.mocked(AuthorizationService.canListUsers).mockReturnValue(true); vi.mocked(AuthorizationService.canListUsers).mockReturnValue(true);
useCase = new ListUsersUseCase(mockRepository, mockOutputPort); useCase = new ListUsersUseCase(mockRepository);
// Create actor (owner) // Create actor (owner)
actor = AdminUser.create({ actor = AdminUser.create({
@@ -76,7 +70,8 @@ describe('ListUsersUseCase', () => {
// Assert // Assert
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(mockOutputPort.present).toHaveBeenCalledWith({ const data = result.unwrap();
expect(data).toEqual({
users: [], users: [],
total: 0, total: 0,
page: 1, page: 1,
@@ -120,13 +115,12 @@ describe('ListUsersUseCase', () => {
// Assert // Assert
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(mockOutputPort.present).toHaveBeenCalledWith({ const data = result.unwrap();
users: [user1, user2], expect(data.users).toEqual([user1, user2]);
total: 2, expect(data.total).toBe(2);
page: 1, expect(data.page).toBe(1);
limit: 10, expect(data.limit).toBe(10);
totalPages: 1, expect(data.totalPages).toBe(1);
});
}); });
it('should filter by role', async () => { it('should filter by role', async () => {

View File

@@ -1,6 +1,5 @@
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { IAdminUserRepository } from '../ports/IAdminUserRepository'; import type { IAdminUserRepository } from '../ports/IAdminUserRepository';
import { AuthorizationService } from '../../domain/services/AuthorizationService'; import { AuthorizationService } from '../../domain/services/AuthorizationService';
import { UserId } from '../../domain/value-objects/UserId'; import { UserId } from '../../domain/value-objects/UserId';
@@ -46,14 +45,13 @@ export type ListUsersApplicationError = ApplicationErrorCode<ListUsersErrorCode,
export class ListUsersUseCase { export class ListUsersUseCase {
constructor( constructor(
private readonly adminUserRepository: IAdminUserRepository, private readonly adminUserRepository: IAdminUserRepository,
private readonly output: UseCaseOutputPort<ListUsersResult>,
) {} ) {}
async execute( async execute(
input: ListUsersInput, input: ListUsersInput,
): Promise< ): Promise<
Result< Result<
void, ListUsersResult,
ListUsersApplicationError ListUsersApplicationError
> >
> { > {
@@ -137,16 +135,15 @@ export class ListUsersUseCase {
const result = await this.adminUserRepository.list(query); const result = await this.adminUserRepository.list(query);
// Pass domain objects to output port const output: ListUsersResult = {
this.output.present({
users: result.users, users: result.users,
total: result.total, total: result.total,
page: result.page, page: result.page,
limit: result.limit, limit: result.limit,
totalPages: result.totalPages, totalPages: result.totalPages,
}); };
return Result.ok(undefined); return Result.ok(output);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to list users'; const message = error instanceof Error ? error.message : 'Failed to list users';

View File

@@ -1,10 +1,9 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetAnalyticsMetricsUseCase, type GetAnalyticsMetricsInput, type GetAnalyticsMetricsOutput } from './GetAnalyticsMetricsUseCase'; import { GetAnalyticsMetricsUseCase, type GetAnalyticsMetricsInput } from './GetAnalyticsMetricsUseCase';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
describe('GetAnalyticsMetricsUseCase', () => { describe('GetAnalyticsMetricsUseCase', () => {
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<GetAnalyticsMetricsOutput> & { present: Mock };
let useCase: GetAnalyticsMetricsUseCase; let useCase: GetAnalyticsMetricsUseCase;
beforeEach(() => { beforeEach(() => {
@@ -15,21 +14,15 @@ describe('GetAnalyticsMetricsUseCase', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = { useCase = new GetAnalyticsMetricsUseCase(logger);
present: vi.fn(),
};
useCase = new GetAnalyticsMetricsUseCase(
logger,
output,
);
}); });
it('presents default metrics and logs retrieval when no input is provided', async () => { it('returns default metrics when no input is provided', async () => {
const result = await useCase.execute(); const result = await useCase.execute();
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalledWith({ const data = result.unwrap();
expect(data).toEqual({
pageViews: 0, pageViews: 0,
uniqueVisitors: 0, uniqueVisitors: 0,
averageSessionDuration: 0, averageSessionDuration: 0,
@@ -38,7 +31,21 @@ describe('GetAnalyticsMetricsUseCase', () => {
expect((logger.info as unknown as Mock)).toHaveBeenCalled(); expect((logger.info as unknown as Mock)).toHaveBeenCalled();
}); });
it('uses provided date range and presents error when execute throws', async () => { it('uses provided date range and returns metrics', async () => {
const input: GetAnalyticsMetricsInput = {
startDate: new Date('2024-01-01'),
endDate: new Date('2024-01-31'),
};
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(data.pageViews).toBe(0);
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
});
it('returns error when execute throws', async () => {
const input: GetAnalyticsMetricsInput = { const input: GetAnalyticsMetricsInput = {
startDate: new Date('2024-01-01'), startDate: new Date('2024-01-01'),
endDate: new Date('2024-01-31'), endDate: new Date('2024-01-31'),

View File

@@ -1,4 +1,4 @@
import type { Logger, UseCase, UseCaseOutputPort } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { IPageViewRepository } from '../repositories/IPageViewRepository'; import type { IPageViewRepository } from '../repositories/IPageViewRepository';
@@ -17,16 +17,15 @@ export interface GetAnalyticsMetricsOutput {
export type GetAnalyticsMetricsErrorCode = 'REPOSITORY_ERROR'; export type GetAnalyticsMetricsErrorCode = 'REPOSITORY_ERROR';
export class GetAnalyticsMetricsUseCase implements UseCase<GetAnalyticsMetricsInput, void, GetAnalyticsMetricsErrorCode> { export class GetAnalyticsMetricsUseCase implements UseCase<GetAnalyticsMetricsInput, GetAnalyticsMetricsOutput, GetAnalyticsMetricsErrorCode> {
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetAnalyticsMetricsOutput>,
private readonly pageViewRepository?: IPageViewRepository, private readonly pageViewRepository?: IPageViewRepository,
) {} ) {}
async execute( async execute(
input: GetAnalyticsMetricsInput = {}, input: GetAnalyticsMetricsInput = {},
): Promise<Result<void, ApplicationErrorCode<GetAnalyticsMetricsErrorCode, { message: string }>>> { ): Promise<Result<GetAnalyticsMetricsOutput, ApplicationErrorCode<GetAnalyticsMetricsErrorCode, { message: string }>>> {
try { try {
const startDate = input.startDate ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago const startDate = input.startDate ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
const endDate = input.endDate ?? new Date(); const endDate = input.endDate ?? new Date();
@@ -47,8 +46,6 @@ export class GetAnalyticsMetricsUseCase implements UseCase<GetAnalyticsMetricsIn
bounceRate, bounceRate,
}; };
this.output.present(resultModel);
this.logger.info('Analytics metrics retrieved', { this.logger.info('Analytics metrics retrieved', {
startDate, startDate,
endDate, endDate,
@@ -56,7 +53,7 @@ export class GetAnalyticsMetricsUseCase implements UseCase<GetAnalyticsMetricsIn
uniqueVisitors, uniqueVisitors,
}); });
return Result.ok(undefined); return Result.ok(resultModel);
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
this.logger.error('Failed to get analytics metrics', err, { input }); this.logger.error('Failed to get analytics metrics', err, { input });

View File

@@ -1,10 +1,9 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetDashboardDataUseCase, type GetDashboardDataOutput } from './GetDashboardDataUseCase'; import { GetDashboardDataUseCase } from './GetDashboardDataUseCase';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
describe('GetDashboardDataUseCase', () => { describe('GetDashboardDataUseCase', () => {
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<GetDashboardDataOutput> & { present: Mock };
let useCase: GetDashboardDataUseCase; let useCase: GetDashboardDataUseCase;
beforeEach(() => { beforeEach(() => {
@@ -15,18 +14,15 @@ describe('GetDashboardDataUseCase', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = { useCase = new GetDashboardDataUseCase(logger);
present: vi.fn(),
};
useCase = new GetDashboardDataUseCase(logger, output);
}); });
it('presents placeholder dashboard metrics and logs retrieval', async () => { it('returns placeholder dashboard metrics and logs retrieval', async () => {
const result = await useCase.execute(); const result = await useCase.execute();
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalledWith({ const data = result.unwrap();
expect(data).toEqual({
totalUsers: 0, totalUsers: 0,
activeUsers: 0, activeUsers: 0,
totalRaces: 0, totalRaces: 0,

View File

@@ -1,4 +1,4 @@
import type { Logger, UseCaseOutputPort, UseCase } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -13,13 +13,12 @@ export interface GetDashboardDataOutput {
export type GetDashboardDataErrorCode = 'REPOSITORY_ERROR'; export type GetDashboardDataErrorCode = 'REPOSITORY_ERROR';
export class GetDashboardDataUseCase implements UseCase<GetDashboardDataInput, void, GetDashboardDataErrorCode> { export class GetDashboardDataUseCase implements UseCase<GetDashboardDataInput, GetDashboardDataOutput, GetDashboardDataErrorCode> {
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetDashboardDataOutput>,
) {} ) {}
async execute(): Promise<Result<void, ApplicationErrorCode<GetDashboardDataErrorCode, { message: string }>>> { async execute(): Promise<Result<GetDashboardDataOutput, ApplicationErrorCode<GetDashboardDataErrorCode, { message: string }>>> {
try { try {
// Placeholder implementation - would need repositories from identity and racing domains // Placeholder implementation - would need repositories from identity and racing domains
const totalUsers = 0; const totalUsers = 0;
@@ -34,8 +33,6 @@ export class GetDashboardDataUseCase implements UseCase<GetDashboardDataInput, v
totalLeagues, totalLeagues,
}; };
this.output.present(resultModel);
this.logger.info('Dashboard data retrieved', { this.logger.info('Dashboard data retrieved', {
totalUsers, totalUsers,
activeUsers, activeUsers,
@@ -43,7 +40,7 @@ export class GetDashboardDataUseCase implements UseCase<GetDashboardDataInput, v
totalLeagues, totalLeagues,
}); });
return Result.ok(undefined); return Result.ok(resultModel);
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
this.logger.error('Failed to get dashboard data', err); this.logger.error('Failed to get dashboard data', err);

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { RecordEngagementUseCase, type RecordEngagementInput, type RecordEngagementOutput } from './RecordEngagementUseCase'; import { RecordEngagementUseCase, type RecordEngagementInput } from './RecordEngagementUseCase';
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
import { EngagementEvent } from '../../domain/entities/EngagementEvent'; import { EngagementEvent } from '../../domain/entities/EngagementEvent';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent'; import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent';
describe('RecordEngagementUseCase', () => { describe('RecordEngagementUseCase', () => {
@@ -10,7 +10,6 @@ describe('RecordEngagementUseCase', () => {
save: Mock; save: Mock;
}; };
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<RecordEngagementOutput> & { present: Mock };
let useCase: RecordEngagementUseCase; let useCase: RecordEngagementUseCase;
beforeEach(() => { beforeEach(() => {
@@ -25,18 +24,13 @@ describe('RecordEngagementUseCase', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = {
present: vi.fn(),
};
useCase = new RecordEngagementUseCase( useCase = new RecordEngagementUseCase(
engagementRepository as unknown as IEngagementRepository, engagementRepository as unknown as IEngagementRepository,
logger, logger,
output,
); );
}); });
it('creates and saves an EngagementEvent and presents its id and weight', async () => { it('creates and saves an EngagementEvent and returns its id and weight', async () => {
const input: RecordEngagementInput = { const input: RecordEngagementInput = {
action: 'view' as EngagementAction, action: 'view' as EngagementAction,
entityType: 'league' as EngagementEntityType, entityType: 'league' as EngagementEntityType,
@@ -52,6 +46,7 @@ describe('RecordEngagementUseCase', () => {
const result = await useCase.execute(input); const result = await useCase.execute(input);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(engagementRepository.save).toHaveBeenCalledTimes(1); expect(engagementRepository.save).toHaveBeenCalledTimes(1);
const saved = (engagementRepository.save as unknown as Mock).mock.calls?.[0]?.[0] as EngagementEvent; const saved = (engagementRepository.save as unknown as Mock).mock.calls?.[0]?.[0] as EngagementEvent;
@@ -60,14 +55,12 @@ describe('RecordEngagementUseCase', () => {
expect(saved.entityId).toBe(input.entityId); expect(saved.entityId).toBe(input.entityId);
expect(saved.entityType).toBe(input.entityType); expect(saved.entityType).toBe(input.entityType);
expect(output.present).toHaveBeenCalledWith({ expect(data.eventId).toBe(saved.id);
eventId: saved.id, expect(data.engagementWeight).toBe(saved.getEngagementWeight());
engagementWeight: saved.getEngagementWeight(),
});
expect((logger.info as unknown as Mock)).toHaveBeenCalled(); expect((logger.info as unknown as Mock)).toHaveBeenCalled();
}); });
it('logs and presents error when repository save fails', async () => { it('logs and returns error when repository save fails', async () => {
const input: RecordEngagementInput = { const input: RecordEngagementInput = {
action: 'view' as EngagementAction, action: 'view' as EngagementAction,
entityType: 'league' as EngagementEntityType, entityType: 'league' as EngagementEntityType,

View File

@@ -1,4 +1,4 @@
import type { Logger, UseCase, UseCaseOutputPort } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { EngagementEvent } from '../../domain/entities/EngagementEvent'; import { EngagementEvent } from '../../domain/entities/EngagementEvent';
@@ -22,14 +22,13 @@ export interface RecordEngagementOutput {
export type RecordEngagementErrorCode = 'REPOSITORY_ERROR'; export type RecordEngagementErrorCode = 'REPOSITORY_ERROR';
export class RecordEngagementUseCase implements UseCase<RecordEngagementInput, void, RecordEngagementErrorCode> { export class RecordEngagementUseCase implements UseCase<RecordEngagementInput, RecordEngagementOutput, RecordEngagementErrorCode> {
constructor( constructor(
private readonly engagementRepository: IEngagementRepository, private readonly engagementRepository: IEngagementRepository,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<RecordEngagementOutput>,
) {} ) {}
async execute(input: RecordEngagementInput): Promise<Result<void, ApplicationErrorCode<RecordEngagementErrorCode, { message: string }>>> { async execute(input: RecordEngagementInput): Promise<Result<RecordEngagementOutput, ApplicationErrorCode<RecordEngagementErrorCode, { message: string }>>> {
try { try {
const engagementEvent = EngagementEvent.create({ const engagementEvent = EngagementEvent.create({
id: crypto.randomUUID(), id: crypto.randomUUID(),
@@ -49,8 +48,6 @@ export class RecordEngagementUseCase implements UseCase<RecordEngagementInput, v
engagementWeight: engagementEvent.getEngagementWeight(), engagementWeight: engagementEvent.getEngagementWeight(),
}; };
this.output.present(resultModel);
this.logger.info('Engagement event recorded', { this.logger.info('Engagement event recorded', {
engagementId: engagementEvent.id, engagementId: engagementEvent.id,
action: input.action, action: input.action,
@@ -58,7 +55,7 @@ export class RecordEngagementUseCase implements UseCase<RecordEngagementInput, v
entityType: input.entityType, entityType: input.entityType,
}); });
return Result.ok(undefined); return Result.ok(resultModel);
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
this.logger.error('Failed to record engagement event', err, { input }); this.logger.error('Failed to record engagement event', err, { input });

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { RecordPageViewUseCase, type RecordPageViewInput, type RecordPageViewOutput } from './RecordPageViewUseCase'; import { RecordPageViewUseCase, type RecordPageViewInput } from './RecordPageViewUseCase';
import { PageView } from '../../domain/entities/PageView'; import { PageView } from '../../domain/entities/PageView';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { EntityType, VisitorType } from '../../domain/types/PageView'; import type { EntityType, VisitorType } from '../../domain/types/PageView';
describe('RecordPageViewUseCase', () => { describe('RecordPageViewUseCase', () => {
@@ -9,7 +9,6 @@ describe('RecordPageViewUseCase', () => {
save: Mock; save: Mock;
}; };
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<RecordPageViewOutput> & { present: Mock };
let useCase: RecordPageViewUseCase; let useCase: RecordPageViewUseCase;
beforeEach(() => { beforeEach(() => {
@@ -26,18 +25,13 @@ describe('RecordPageViewUseCase', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = {
present: vi.fn(),
};
useCase = new RecordPageViewUseCase( useCase = new RecordPageViewUseCase(
pageViewRepository as unknown as PageViewRepository, pageViewRepository as unknown as PageViewRepository,
logger, logger,
output,
); );
}); });
it('creates and saves a PageView and presents its id', async () => { it('creates and saves a PageView and returns its id', async () => {
const input: RecordPageViewInput = { const input: RecordPageViewInput = {
entityType: 'league' as EntityType, entityType: 'league' as EntityType,
entityId: 'league-1', entityId: 'league-1',
@@ -54,6 +48,7 @@ describe('RecordPageViewUseCase', () => {
const result = await useCase.execute(input); const result = await useCase.execute(input);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
const data = result.unwrap();
expect(pageViewRepository.save).toHaveBeenCalledTimes(1); expect(pageViewRepository.save).toHaveBeenCalledTimes(1);
const saved = (pageViewRepository.save as unknown as Mock).mock.calls?.[0]?.[0] as PageView; const saved = (pageViewRepository.save as unknown as Mock).mock.calls?.[0]?.[0] as PageView;
@@ -62,13 +57,11 @@ describe('RecordPageViewUseCase', () => {
expect(saved.entityId).toBe(input.entityId); expect(saved.entityId).toBe(input.entityId);
expect(saved.entityType).toBe(input.entityType); expect(saved.entityType).toBe(input.entityType);
expect(output.present).toHaveBeenCalledWith({ expect(data.pageViewId).toBe(saved.id);
pageViewId: saved.id,
});
expect((logger.info as unknown as Mock)).toHaveBeenCalled(); expect((logger.info as unknown as Mock)).toHaveBeenCalled();
}); });
it('logs and presents error when repository save fails', async () => { it('logs and returns error when repository save fails', async () => {
const input: RecordPageViewInput = { const input: RecordPageViewInput = {
entityType: 'league' as EntityType, entityType: 'league' as EntityType,
entityId: 'league-1', entityId: 'league-1',

View File

@@ -1,4 +1,4 @@
import type { Logger, UseCaseOutputPort, UseCase } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
import type { IPageViewRepository } from '../repositories/IPageViewRepository'; import type { IPageViewRepository } from '../repositories/IPageViewRepository';
import { PageView } from '../../domain/entities/PageView'; import { PageView } from '../../domain/entities/PageView';
import type { EntityType, VisitorType } from '../../domain/types/PageView'; import type { EntityType, VisitorType } from '../../domain/types/PageView';
@@ -22,14 +22,13 @@ export interface RecordPageViewOutput {
export type RecordPageViewErrorCode = 'REPOSITORY_ERROR'; export type RecordPageViewErrorCode = 'REPOSITORY_ERROR';
export class RecordPageViewUseCase implements UseCase<RecordPageViewInput, void, RecordPageViewErrorCode> { export class RecordPageViewUseCase implements UseCase<RecordPageViewInput, RecordPageViewOutput, RecordPageViewErrorCode> {
constructor( constructor(
private readonly pageViewRepository: IPageViewRepository, private readonly pageViewRepository: IPageViewRepository,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<RecordPageViewOutput>,
) {} ) {}
async execute(input: RecordPageViewInput): Promise<Result<void, ApplicationErrorCode<RecordPageViewErrorCode, { message: string }>>> { async execute(input: RecordPageViewInput): Promise<Result<RecordPageViewOutput, ApplicationErrorCode<RecordPageViewErrorCode, { message: string }>>> {
try { try {
type PageViewCreateProps = Parameters<(typeof PageView)['create']>[0]; type PageViewCreateProps = Parameters<(typeof PageView)['create']>[0];
@@ -53,15 +52,13 @@ export class RecordPageViewUseCase implements UseCase<RecordPageViewInput, void,
pageViewId: pageView.id, pageViewId: pageView.id,
}; };
this.output.present(resultModel);
this.logger.info('Page view recorded', { this.logger.info('Page view recorded', {
pageViewId: pageView.id, pageViewId: pageView.id,
entityId: input.entityId, entityId: input.entityId,
entityType: input.entityType, entityType: input.entityType,
}); });
return Result.ok(undefined); return Result.ok(resultModel);
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
this.logger.error('Failed to record page view', err, { input }); this.logger.error('Failed to record page view', err, { input });

View File

@@ -1,22 +1,18 @@
import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { ForgotPasswordUseCase } from './ForgotPasswordUseCase'; import { ForgotPasswordUseCase } from './ForgotPasswordUseCase';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { UserId } from '../../domain/value-objects/UserId';
import { User } from '../../domain/entities/User';
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository'; import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import type { IMagicLinkRepository } from '../../domain/repositories/IMagicLinkRepository'; import type { IMagicLinkRepository } from '../../domain/repositories/IMagicLinkRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { IMagicLinkNotificationPort } from '../../domain/ports/IMagicLinkNotificationPort';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { User } from '../../domain/entities/User';
type ForgotPasswordOutput = { import { UserId } from '../../domain/value-objects/UserId';
message: string; import { PasswordHash } from '../../domain/value-objects/PasswordHash';
magicLink?: string | null; import { EmailAddress } from '../../domain/value-objects/EmailAddress';
};
describe('ForgotPasswordUseCase', () => { describe('ForgotPasswordUseCase', () => {
let authRepo: { let authRepo: {
findByEmail: Mock; findByEmail: Mock;
save: Mock;
}; };
let magicLinkRepo: { let magicLinkRepo: {
checkRateLimit: Mock; checkRateLimit: Mock;
@@ -26,218 +22,89 @@ describe('ForgotPasswordUseCase', () => {
sendMagicLink: Mock; sendMagicLink: Mock;
}; };
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<ForgotPasswordOutput> & { present: Mock };
let useCase: ForgotPasswordUseCase; let useCase: ForgotPasswordUseCase;
beforeEach(() => { beforeEach(() => {
authRepo = { authRepo = {
findByEmail: vi.fn(), findByEmail: vi.fn(),
save: vi.fn(),
}; };
magicLinkRepo = { magicLinkRepo = {
checkRateLimit: vi.fn(), checkRateLimit: vi.fn(),
createPasswordResetRequest: vi.fn(), createPasswordResetRequest: vi.fn(),
}; };
notificationPort = { notificationPort = {
sendMagicLink: vi.fn(), sendMagicLink: vi.fn(),
}; };
logger = { logger = {
debug: vi.fn(), debug: vi.fn(),
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = {
present: vi.fn(),
};
useCase = new ForgotPasswordUseCase( useCase = new ForgotPasswordUseCase(
authRepo as unknown as IAuthRepository, authRepo as unknown as IAuthRepository,
magicLinkRepo as unknown as IMagicLinkRepository, magicLinkRepo as unknown as IMagicLinkRepository,
notificationPort as any, notificationPort as unknown as IMagicLinkNotificationPort,
logger, logger,
output,
); );
}); });
it('should create magic link for existing user', async () => { it('generates and sends magic link when user exists', async () => {
const input = { email: 'test@example.com' };
const user = User.create({ const user = User.create({
id: UserId.create(), id: UserId.create(),
displayName: 'John Smith', displayName: 'John Smith',
email: input.email, email: 'test@example.com',
passwordHash: PasswordHash.fromHash('hashed-password'),
}); });
authRepo.findByEmail.mockResolvedValue(user); authRepo.findByEmail.mockResolvedValue(user);
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined)); magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
const result = await useCase.execute(input); const result = await useCase.execute({ email: 'test@example.com' });
expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(input.email));
expect(magicLinkRepo.checkRateLimit).toHaveBeenCalledWith(input.email);
expect(magicLinkRepo.createPasswordResetRequest).toHaveBeenCalled();
expect(output.present).toHaveBeenCalled();
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
const forgotPasswordResult = result.unwrap();
expect(forgotPasswordResult.message).toBe('Password reset link generated successfully');
expect(forgotPasswordResult.magicLink).toBeDefined();
expect(magicLinkRepo.createPasswordResetRequest).toHaveBeenCalled();
expect(notificationPort.sendMagicLink).toHaveBeenCalled();
}); });
it('should return success for non-existent email (security)', async () => { it('returns success even when user does not exist (for security)', async () => {
const input = { email: 'nonexistent@example.com' };
authRepo.findByEmail.mockResolvedValue(null); authRepo.findByEmail.mockResolvedValue(null);
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined)); magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
const result = await useCase.execute(input); const result = await useCase.execute({ email: 'nonexistent@example.com' });
expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(input.email));
expect(magicLinkRepo.createPasswordResetRequest).not.toHaveBeenCalled();
expect(output.present).toHaveBeenCalledWith({
message: 'If an account exists with this email, a password reset link will be sent',
magicLink: null,
});
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
const forgotPasswordResult = result.unwrap();
expect(forgotPasswordResult.message).toBe('If an account exists with this email, a password reset link will be sent');
expect(forgotPasswordResult.magicLink).toBeNull();
expect(magicLinkRepo.createPasswordResetRequest).not.toHaveBeenCalled();
expect(notificationPort.sendMagicLink).not.toHaveBeenCalled();
}); });
it('should handle rate limiting', async () => { it('returns error when rate limit exceeded', async () => {
const input = { email: 'test@example.com' };
const user = User.create({
id: UserId.create(),
displayName: 'John Smith',
email: input.email,
});
authRepo.findByEmail.mockResolvedValue(user);
magicLinkRepo.checkRateLimit.mockResolvedValue( magicLinkRepo.checkRateLimit.mockResolvedValue(
Result.err({ code: 'RATE_LIMIT_EXCEEDED', details: { message: 'Rate limited' } }) Result.err({ code: 'RATE_LIMIT_EXCEEDED', details: { message: 'Rate limit exceeded' } })
); );
const result = await useCase.execute(input); const result = await useCase.execute({ email: 'test@example.com' });
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
const error = result.unwrapErr(); expect(result.unwrapErr().code).toBe('RATE_LIMIT_EXCEEDED');
expect(error.code).toBe('RATE_LIMIT_EXCEEDED');
}); });
it('should validate email format', async () => { it('returns error when repository call fails', async () => {
const input = { email: 'invalid-email' };
const result = await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('REPOSITORY_ERROR');
});
it('should generate secure tokens', async () => {
const input = { email: 'test@example.com' };
const user = User.create({
id: UserId.create(),
displayName: 'John Smith',
email: input.email,
});
authRepo.findByEmail.mockResolvedValue(user);
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
let capturedToken: string | undefined;
magicLinkRepo.createPasswordResetRequest.mockImplementation((data) => {
capturedToken = data.token;
return Promise.resolve();
});
await useCase.execute(input);
expect(capturedToken).toMatch(/^[a-f0-9]{64}$/); // 32 bytes = 64 hex chars
});
it('should set correct expiration time (15 minutes)', async () => {
const input = { email: 'test@example.com' };
const user = User.create({
id: UserId.create(),
displayName: 'John Smith',
email: input.email,
});
authRepo.findByEmail.mockResolvedValue(user);
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
const beforeCreate = Date.now();
let capturedExpiresAt: Date | undefined;
magicLinkRepo.createPasswordResetRequest.mockImplementation((data) => {
capturedExpiresAt = data.expiresAt;
return Promise.resolve();
});
await useCase.execute(input);
const afterCreate = Date.now();
expect(capturedExpiresAt).toBeDefined();
const timeDiff = capturedExpiresAt!.getTime() - afterCreate;
// Should be approximately 15 minutes (900000ms)
expect(timeDiff).toBeGreaterThan(890000);
expect(timeDiff).toBeLessThan(910000);
});
it('should return magic link in development mode', async () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
const input = { email: 'test@example.com' };
const user = User.create({
id: UserId.create(),
displayName: 'John Smith',
email: input.email,
});
authRepo.findByEmail.mockResolvedValue(user);
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
await useCase.execute(input);
expect(output.present).toHaveBeenCalledWith(
expect.objectContaining({
magicLink: expect.stringContaining('token='),
})
);
process.env.NODE_ENV = originalEnv ?? 'test';
});
it('should not return magic link in production mode', async () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const input = { email: 'test@example.com' };
const user = User.create({
id: UserId.create(),
displayName: 'John Smith',
email: input.email,
});
authRepo.findByEmail.mockResolvedValue(user);
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
await useCase.execute(input);
expect(output.present).toHaveBeenCalledWith(
expect.objectContaining({
magicLink: null,
})
);
process.env.NODE_ENV = originalEnv ?? 'test';
});
it('should handle repository errors', async () => {
const input = { email: 'test@example.com' };
authRepo.findByEmail.mockRejectedValue(new Error('Database error')); authRepo.findByEmail.mockRejectedValue(new Error('Database error'));
magicLinkRepo.checkRateLimit.mockResolvedValue(Result.ok(undefined));
const result = await useCase.execute(input); const result = await useCase.execute({ email: 'test@example.com' });
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
const error = result.unwrapErr(); expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toContain('Database error');
}); });
}); });

View File

@@ -4,7 +4,7 @@ import { IMagicLinkRepository } from '../../domain/repositories/IMagicLinkReposi
import { IMagicLinkNotificationPort } from '../../domain/ports/IMagicLinkNotificationPort'; import { IMagicLinkNotificationPort } from '../../domain/ports/IMagicLinkNotificationPort';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger, UseCase } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
export type ForgotPasswordInput = { export type ForgotPasswordInput = {
@@ -27,16 +27,15 @@ export type ForgotPasswordApplicationError = ApplicationErrorCode<ForgotPassword
* In production, this would send an email with the magic link. * In production, this would send an email with the magic link.
* In development, it returns the link for testing purposes. * In development, it returns the link for testing purposes.
*/ */
export class ForgotPasswordUseCase implements UseCase<ForgotPasswordInput, void, ForgotPasswordErrorCode> { export class ForgotPasswordUseCase implements UseCase<ForgotPasswordInput, ForgotPasswordResult, ForgotPasswordErrorCode> {
constructor( constructor(
private readonly authRepo: IAuthRepository, private readonly authRepo: IAuthRepository,
private readonly magicLinkRepo: IMagicLinkRepository, private readonly magicLinkRepo: IMagicLinkRepository,
private readonly notificationPort: IMagicLinkNotificationPort, private readonly notificationPort: IMagicLinkNotificationPort,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<ForgotPasswordResult>,
) {} ) {}
async execute(input: ForgotPasswordInput): Promise<Result<void, ForgotPasswordApplicationError>> { async execute(input: ForgotPasswordInput): Promise<Result<ForgotPasswordResult, ForgotPasswordApplicationError>> {
try { try {
// Validate email format // Validate email format
const emailVO = EmailAddress.create(input.email); const emailVO = EmailAddress.create(input.email);
@@ -86,7 +85,7 @@ export class ForgotPasswordUseCase implements UseCase<ForgotPasswordInput, void,
expiresAt, expiresAt,
}); });
this.output.present({ return Result.ok({
message: 'Password reset link generated successfully', message: 'Password reset link generated successfully',
magicLink: process.env.NODE_ENV === 'development' ? magicLink : null, magicLink: process.env.NODE_ENV === 'development' ? magicLink : null,
}); });
@@ -96,13 +95,11 @@ export class ForgotPasswordUseCase implements UseCase<ForgotPasswordInput, void,
email: input.email, email: input.email,
}); });
this.output.present({ return Result.ok({
message: 'If an account exists with this email, a password reset link will be sent', message: 'If an account exists with this email, a password reset link will be sent',
magicLink: null, magicLink: null,
}); });
} }
return Result.ok(undefined);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error && error.message error instanceof Error && error.message

View File

@@ -2,11 +2,8 @@ import { vi, type Mock } from 'vitest';
import { GetCurrentSessionUseCase } from './GetCurrentSessionUseCase'; import { GetCurrentSessionUseCase } from './GetCurrentSessionUseCase';
import { User } from '../../domain/entities/User'; import { User } from '../../domain/entities/User';
import { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
type GetCurrentSessionOutput = {
user: User;
};
describe('GetCurrentSessionUseCase', () => { describe('GetCurrentSessionUseCase', () => {
let useCase: GetCurrentSessionUseCase; let useCase: GetCurrentSessionUseCase;
@@ -18,7 +15,6 @@ describe('GetCurrentSessionUseCase', () => {
emailExists: Mock; emailExists: Mock;
}; };
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<GetCurrentSessionOutput> & { present: Mock };
beforeEach(() => { beforeEach(() => {
mockUserRepo = { mockUserRepo = {
@@ -34,13 +30,9 @@ describe('GetCurrentSessionUseCase', () => {
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = {
present: vi.fn(),
};
useCase = new GetCurrentSessionUseCase( useCase = new GetCurrentSessionUseCase(
mockUserRepo as IUserRepository, mockUserRepo as IUserRepository,
logger, logger,
output,
); );
}); });
@@ -60,11 +52,10 @@ describe('GetCurrentSessionUseCase', () => {
expect(mockUserRepo.findById).toHaveBeenCalledWith(userId); expect(mockUserRepo.findById).toHaveBeenCalledWith(userId);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalled(); const sessionResult = result.unwrap();
const callArgs = output.present.mock.calls?.[0]?.[0]; expect(sessionResult.user).toBeInstanceOf(User);
expect(callArgs?.user).toBeInstanceOf(User); expect(sessionResult.user.getId().value).toBe(userId);
expect(callArgs?.user.getId().value).toBe(userId); expect(sessionResult.user.getDisplayName()).toBe('John Smith');
expect(callArgs?.user.getDisplayName()).toBe('John Smith');
}); });
it('should return error when user does not exist', async () => { it('should return error when user does not exist', async () => {
@@ -75,5 +66,6 @@ describe('GetCurrentSessionUseCase', () => {
expect(mockUserRepo.findById).toHaveBeenCalledWith(userId); expect(mockUserRepo.findById).toHaveBeenCalledWith(userId);
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('USER_NOT_FOUND');
}); });
}); });

View File

@@ -2,7 +2,7 @@ import { User } from '../../domain/entities/User';
import { IUserRepository } from '../../domain/repositories/IUserRepository'; import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
export type GetCurrentSessionInput = { export type GetCurrentSessionInput = {
userId: string; userId: string;
@@ -28,11 +28,10 @@ export class GetCurrentSessionUseCase {
constructor( constructor(
private readonly userRepo: IUserRepository, private readonly userRepo: IUserRepository,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetCurrentSessionResult>,
) {} ) {}
async execute(input: GetCurrentSessionInput): Promise< async execute(input: GetCurrentSessionInput): Promise<
Result<void, GetCurrentSessionApplicationError> Result<GetCurrentSessionResult, GetCurrentSessionApplicationError>
> { > {
try { try {
const stored = await this.userRepo.findById(input.userId); const stored = await this.userRepo.findById(input.userId);
@@ -45,9 +44,8 @@ export class GetCurrentSessionUseCase {
const user = User.fromStored(stored); const user = User.fromStored(stored);
const result: GetCurrentSessionResult = { user }; const result: GetCurrentSessionResult = { user };
this.output.present(result);
return Result.ok(undefined); return Result.ok(result);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error && error.message error instanceof Error && error.message

View File

@@ -1,7 +1,8 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetCurrentUserSessionUseCase } from './GetCurrentUserSessionUseCase'; import { GetCurrentUserSessionUseCase } from './GetCurrentUserSessionUseCase';
import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort'; import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
describe('GetCurrentUserSessionUseCase', () => { describe('GetCurrentUserSessionUseCase', () => {
let sessionPort: { let sessionPort: {
@@ -10,7 +11,6 @@ describe('GetCurrentUserSessionUseCase', () => {
clearSession: Mock; clearSession: Mock;
}; };
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<AuthSession | null> & { present: Mock };
let useCase: GetCurrentUserSessionUseCase; let useCase: GetCurrentUserSessionUseCase;
beforeEach(() => { beforeEach(() => {
@@ -27,14 +27,9 @@ describe('GetCurrentUserSessionUseCase', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = {
present: vi.fn(),
};
useCase = new GetCurrentUserSessionUseCase( useCase = new GetCurrentUserSessionUseCase(
sessionPort as unknown as IdentitySessionPort, sessionPort as unknown as IdentitySessionPort,
logger, logger,
output,
); );
}); });
@@ -57,7 +52,7 @@ describe('GetCurrentUserSessionUseCase', () => {
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1); expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalledWith(session); expect(result.unwrap()).toBe(session);
}); });
it('returns null when there is no active session', async () => { it('returns null when there is no active session', async () => {
@@ -67,6 +62,6 @@ describe('GetCurrentUserSessionUseCase', () => {
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1); expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalledWith(null); expect(result.unwrap()).toBe(null);
}); });
}); });

View File

@@ -1,7 +1,7 @@
import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort'; import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
export type GetCurrentUserSessionInput = void; export type GetCurrentUserSessionInput = void;
@@ -18,16 +18,13 @@ export class GetCurrentUserSessionUseCase {
constructor( constructor(
private readonly sessionPort: IdentitySessionPort, private readonly sessionPort: IdentitySessionPort,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetCurrentUserSessionResult>,
) {} ) {}
async execute(): Promise<Result<void, GetCurrentUserSessionApplicationError>> { async execute(): Promise<Result<GetCurrentUserSessionResult, GetCurrentUserSessionApplicationError>> {
try { try {
const session = await this.sessionPort.getCurrentSession(); const session = await this.sessionPort.getCurrentSession();
this.output.present(session); return Result.ok(session);
return Result.ok(undefined);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error && error.message error instanceof Error && error.message

View File

@@ -1,22 +1,22 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { GetUserUseCase } from './GetUserUseCase'; import { GetUserUseCase } from './GetUserUseCase';
import { User } from '../../domain/entities/User'; import type { IUserRepository } from '../../domain/repositories/IUserRepository';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import type { Logger } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { User } from '../../domain/entities/User';
type GetUserOutput = Result<{ user: User }, unknown>; import { UserId } from '../../domain/value-objects/UserId';
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
describe('GetUserUseCase', () => { describe('GetUserUseCase', () => {
let userRepository: { let userRepo: {
findById: Mock; findById: Mock;
}; };
let logger: Logger; let logger: Logger;
let output: UseCaseOutputPort<GetUserOutput> & { present: Mock };
let useCase: GetUserUseCase; let useCase: GetUserUseCase;
beforeEach(() => { beforeEach(() => {
userRepository = { userRepo = {
findById: vi.fn(), findById: vi.fn(),
}; };
@@ -27,48 +27,48 @@ describe('GetUserUseCase', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
output = {
present: vi.fn(),
};
useCase = new GetUserUseCase( useCase = new GetUserUseCase(
userRepository as unknown as IUserRepository, userRepo as unknown as IUserRepository,
logger, logger,
output,
); );
}); });
it('returns a User when the user exists', async () => { it('returns user when found', async () => {
const storedUser: StoredUser = { const storedUser = {
id: 'user-1', id: 'user-1',
email: 'test@example.com', email: 'test@example.com',
displayName: 'John Smith', displayName: 'John Smith',
passwordHash: 'hash', passwordHash: 'hashed-password',
primaryDriverId: 'driver-1',
createdAt: new Date(), createdAt: new Date(),
}; };
userRepo.findById.mockResolvedValue(storedUser);
userRepository.findById.mockResolvedValue(storedUser);
const result = await useCase.execute({ userId: 'user-1' }); const result = await useCase.execute({ userId: 'user-1' });
expect(userRepository.findById).toHaveBeenCalledWith('user-1');
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalled(); const getUserResult = result.unwrap();
const callArgs = output.present.mock.calls?.[0]?.[0]; expect(getUserResult.user).toBeDefined();
expect(callArgs).toBeInstanceOf(Result); expect(getUserResult.user.getId().value).toBe('user-1');
const user = (callArgs as GetUserOutput).unwrap().user; expect(getUserResult.user.getEmail()).toBe('test@example.com');
expect(user).toBeInstanceOf(User); expect(userRepo.findById).toHaveBeenCalledWith('user-1');
expect(user.getId().value).toBe('user-1');
expect(user.getDisplayName()).toBe('John Smith');
}); });
it('returns error when the user does not exist', async () => { it('returns error when user not found', async () => {
userRepository.findById.mockResolvedValue(null); userRepo.findById.mockResolvedValue(null);
const result = await useCase.execute({ userId: 'missing-user' }); const result = await useCase.execute({ userId: 'nonexistent' });
expect(userRepository.findById).toHaveBeenCalledWith('missing-user');
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('USER_NOT_FOUND');
});
it('returns error on repository failure', async () => {
userRepo.findById.mockRejectedValue(new Error('Database error'));
const result = await useCase.execute({ userId: 'user-1' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
expect(logger.error).toHaveBeenCalled();
}); });
}); });

View File

@@ -2,7 +2,7 @@ import { User } from '../../domain/entities/User';
import { IUserRepository } from '../../domain/repositories/IUserRepository'; import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger, UseCase } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
export type GetUserInput = { export type GetUserInput = {
userId: string; userId: string;
@@ -23,25 +23,20 @@ export class GetUserUseCase implements UseCase<GetUserInput, GetUserResult, GetU
constructor( constructor(
private readonly userRepo: IUserRepository, private readonly userRepo: IUserRepository,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<Result<GetUserResult, GetUserApplicationError>>,
) {} ) {}
async execute(input: GetUserInput): Promise<Result<GetUserResult, GetUserApplicationError>> { async execute(input: GetUserInput): Promise<Result<GetUserResult, GetUserApplicationError>> {
try { try {
const stored = await this.userRepo.findById(input.userId); const stored = await this.userRepo.findById(input.userId);
if (!stored) { if (!stored) {
const result = Result.err<GetUserResult, GetUserApplicationError>({ return Result.err<GetUserResult, GetUserApplicationError>({
code: 'USER_NOT_FOUND', code: 'USER_NOT_FOUND',
details: { message: 'User not found' }, details: { message: 'User not found' },
}); });
this.output.present(result);
return result;
} }
const user = User.fromStored(stored); const user = User.fromStored(stored);
const result = Result.ok<GetUserResult, GetUserApplicationError>({ user }); return Result.ok<GetUserResult, GetUserApplicationError>({ user });
this.output.present(result);
return result;
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error && error.message ? error.message : 'Failed to get user'; error instanceof Error && error.message ? error.message : 'Failed to get user';
@@ -50,12 +45,10 @@ export class GetUserUseCase implements UseCase<GetUserInput, GetUserResult, GetU
input, input,
}); });
const result = Result.err<GetUserResult, GetUserApplicationError>({ return Result.err<GetUserResult, GetUserApplicationError>({
code: 'REPOSITORY_ERROR', code: 'REPOSITORY_ERROR',
details: { message }, details: { message },
}); });
this.output.present(result);
return result;
} }
} }
} }

View File

@@ -1,12 +1,9 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { HandleAuthCallbackUseCase } from './HandleAuthCallbackUseCase'; import { HandleAuthCallbackUseCase } from './HandleAuthCallbackUseCase';
import type { import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
AuthCallbackCommand, import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
AuthenticatedUser, import type { Logger } from '@core/shared/application';
IdentityProviderPort, import { Result } from '@core/shared/application/Result';
} from '../ports/IdentityProviderPort';
import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
describe('HandleAuthCallbackUseCase', () => { describe('HandleAuthCallbackUseCase', () => {
let provider: { let provider: {
@@ -14,69 +11,97 @@ describe('HandleAuthCallbackUseCase', () => {
}; };
let sessionPort: { let sessionPort: {
createSession: Mock; createSession: Mock;
getCurrentSession: Mock;
clearSession: Mock;
}; };
let logger: Logger; let logger: Logger & { error: Mock };
let output: UseCaseOutputPort<AuthSession> & { present: Mock };
let useCase: HandleAuthCallbackUseCase; let useCase: HandleAuthCallbackUseCase;
beforeEach(() => { beforeEach(() => {
provider = { provider = {
completeAuth: vi.fn(), completeAuth: vi.fn(),
}; };
sessionPort = { sessionPort = {
createSession: vi.fn(), createSession: vi.fn(),
getCurrentSession: vi.fn(),
clearSession: vi.fn(),
}; };
logger = { logger = {
debug: vi.fn(), debug: vi.fn(),
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger & { error: Mock };
output = {
present: vi.fn(),
};
useCase = new HandleAuthCallbackUseCase( useCase = new HandleAuthCallbackUseCase(
provider as unknown as IdentityProviderPort, provider as unknown as IdentityProviderPort,
sessionPort as unknown as IdentitySessionPort, sessionPort as unknown as IdentitySessionPort,
logger, logger,
output,
); );
}); });
it('completes auth and creates a session', async () => { it('successfully handles auth callback and creates session', async () => {
const command: AuthCallbackCommand = { const authenticatedUser = {
provider: 'IRACING_DEMO',
code: 'auth-code',
state: 'state-123',
returnTo: 'https://app/callback',
};
const user: AuthenticatedUser = {
id: 'user-1', id: 'user-1',
email: 'test@example.com',
displayName: 'Test User', displayName: 'Test User',
email: 'test@example.com',
}; };
const session = {
const session: AuthSession = { token: 'session-token',
user, user: authenticatedUser,
issuedAt: Date.now(), issuedAt: Date.now(),
expiresAt: Date.now() + 1000, expiresAt: Date.now() + 1000,
token: 'session-token',
}; };
provider.completeAuth.mockResolvedValue(user); provider.completeAuth.mockResolvedValue(authenticatedUser);
sessionPort.createSession.mockResolvedValue(session); sessionPort.createSession.mockResolvedValue(session);
const result = await useCase.execute(command); const result = await useCase.execute({
code: 'auth-code',
state: 'state-123',
returnTo: '/dashboard',
});
expect(provider.completeAuth).toHaveBeenCalledWith(command);
expect(sessionPort.createSession).toHaveBeenCalledWith(user);
expect(output.present).toHaveBeenCalledWith(session);
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
const callbackResult = result.unwrap();
expect(callbackResult.token).toBe('session-token');
expect(callbackResult.user).toBe(authenticatedUser);
expect(provider.completeAuth).toHaveBeenCalledWith({
code: 'auth-code',
state: 'state-123',
returnTo: '/dashboard',
});
expect(sessionPort.createSession).toHaveBeenCalledWith(authenticatedUser);
});
it('returns error when provider call fails', async () => {
provider.completeAuth.mockRejectedValue(new Error('Auth failed'));
const result = await useCase.execute({
code: 'invalid-code',
state: 'state-123',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
expect(logger.error).toHaveBeenCalled();
});
it('returns error when session creation fails', async () => {
const authenticatedUser = {
id: 'user-1',
displayName: 'Test User',
email: 'test@example.com',
};
provider.completeAuth.mockResolvedValue(authenticatedUser);
sessionPort.createSession.mockRejectedValue(new Error('Session creation failed'));
const result = await useCase.execute({
code: 'auth-code',
state: 'state-123',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
expect(logger.error).toHaveBeenCalled();
}); });
}); });

View File

@@ -2,7 +2,7 @@ import type { AuthCallbackCommand, AuthenticatedUser, IdentityProviderPort } fro
import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort'; import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
export type HandleAuthCallbackInput = AuthCallbackCommand; export type HandleAuthCallbackInput = AuthCallbackCommand;
@@ -20,19 +20,16 @@ export class HandleAuthCallbackUseCase {
private readonly provider: IdentityProviderPort, private readonly provider: IdentityProviderPort,
private readonly sessionPort: IdentitySessionPort, private readonly sessionPort: IdentitySessionPort,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<HandleAuthCallbackResult>,
) {} ) {}
async execute(input: HandleAuthCallbackInput): Promise< async execute(input: HandleAuthCallbackInput): Promise<
Result<void, HandleAuthCallbackApplicationError> Result<HandleAuthCallbackResult, HandleAuthCallbackApplicationError>
> { > {
try { try {
const user: AuthenticatedUser = await this.provider.completeAuth(input); const user: AuthenticatedUser = await this.provider.completeAuth(input);
const session = await this.sessionPort.createSession(user); const session = await this.sessionPort.createSession(user);
this.output.present(session); return Result.ok(session);
return Result.ok(undefined);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error && error.message error instanceof Error && error.message

View File

@@ -1,19 +1,13 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { import { LoginUseCase } from './LoginUseCase';
LoginUseCase,
type LoginInput,
type LoginResult,
type LoginErrorCode,
} from './LoginUseCase';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { UserId } from '../../domain/value-objects/UserId';
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository'; import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService'; import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
import { User } from '../../domain/entities/User'; import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { User } from '../../domain/entities/User';
import { UserId } from '../../domain/value-objects/UserId';
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
describe('LoginUseCase', () => { describe('LoginUseCase', () => {
let authRepo: { let authRepo: {
@@ -22,129 +16,82 @@ describe('LoginUseCase', () => {
let passwordService: { let passwordService: {
verify: Mock; verify: Mock;
}; };
let logger: Logger & { error: Mock }; let logger: Logger;
let output: UseCaseOutputPort<LoginResult> & { present: Mock };
let useCase: LoginUseCase; let useCase: LoginUseCase;
beforeEach(() => { beforeEach(() => {
authRepo = { authRepo = {
findByEmail: vi.fn(), findByEmail: vi.fn(),
}; };
passwordService = { passwordService = {
verify: vi.fn(), verify: vi.fn(),
}; };
logger = { logger = {
debug: vi.fn(), debug: vi.fn(),
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
}; } as unknown as Logger;
output = {
present: vi.fn(),
} as unknown as UseCaseOutputPort<LoginResult> & { present: Mock };
useCase = new LoginUseCase( useCase = new LoginUseCase(
authRepo as unknown as IAuthRepository, authRepo as unknown as IAuthRepository,
passwordService as unknown as IPasswordHashingService, passwordService as unknown as IPasswordHashingService,
logger, logger,
output,
); );
}); });
it('returns ok and presents user when credentials are valid', async () => { it('successfully logs in with valid credentials', async () => {
const input: LoginInput = {
email: 'test@example.com',
password: 'password123',
};
const emailVO = EmailAddress.create(input.email);
const user = User.create({ const user = User.create({
id: UserId.fromString('user-1'), id: UserId.create(),
displayName: 'John Smith', displayName: 'John Smith',
email: emailVO.value, email: 'test@example.com',
passwordHash: PasswordHash.fromHash('stored-hash'), passwordHash: PasswordHash.fromHash('hashed-password'),
}); });
authRepo.findByEmail.mockResolvedValue(user); authRepo.findByEmail.mockResolvedValue(user);
passwordService.verify.mockResolvedValue(true); passwordService.verify.mockResolvedValue(true);
const result: Result<void, ApplicationErrorCode<LoginErrorCode, { message: string }>> = const result = await useCase.execute({
await useCase.execute(input); email: 'test@example.com',
password: 'Password123',
});
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined(); const loginResult = result.unwrap();
expect(loginResult.user).toBe(user);
expect(authRepo.findByEmail).toHaveBeenCalledWith(emailVO); expect(authRepo.findByEmail).toHaveBeenCalledTimes(1);
expect(passwordService.verify).toHaveBeenCalledWith(input.password, 'stored-hash'); expect(passwordService.verify).toHaveBeenCalledTimes(1);
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0]![0] as LoginResult;
expect(presented.user).toBe(user);
}); });
it('returns INVALID_CREDENTIALS when user is not found', async () => { it('returns error for invalid credentials', async () => {
const input: LoginInput = {
email: 'missing@example.com',
password: 'password123',
};
authRepo.findByEmail.mockResolvedValue(null);
const result: Result<void, ApplicationErrorCode<LoginErrorCode, { message: string }>> =
await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('INVALID_CREDENTIALS');
expect(error.details?.message).toBe('Invalid credentials');
expect(output.present).not.toHaveBeenCalled();
});
it('returns INVALID_CREDENTIALS when password is invalid', async () => {
const input: LoginInput = {
email: 'test@example.com',
password: 'wrong-password',
};
const emailVO = EmailAddress.create(input.email);
const user = User.create({ const user = User.create({
id: UserId.fromString('user-1'), id: UserId.create(),
displayName: 'Jane Smith', displayName: 'John Smith',
email: emailVO.value, email: 'test@example.com',
passwordHash: PasswordHash.fromHash('stored-hash'), passwordHash: PasswordHash.fromHash('hashed-password'),
}); });
authRepo.findByEmail.mockResolvedValue(user); authRepo.findByEmail.mockResolvedValue(user);
passwordService.verify.mockResolvedValue(false); passwordService.verify.mockResolvedValue(false);
const result: Result<void, ApplicationErrorCode<LoginErrorCode, { message: string }>> = const result = await useCase.execute({
await useCase.execute(input);
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('INVALID_CREDENTIALS');
expect(error.details?.message).toBe('Invalid credentials');
expect(output.present).not.toHaveBeenCalled();
});
it('wraps unexpected errors as REPOSITORY_ERROR and logs them', async () => {
const input: LoginInput = {
email: 'test@example.com', email: 'test@example.com',
password: 'password123', password: 'WrongPassword',
}; });
authRepo.findByEmail.mockRejectedValue(new Error('DB failure'));
const result: Result<void, ApplicationErrorCode<LoginErrorCode, { message: string }>> =
await useCase.execute(input);
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
const error = result.unwrapErr(); expect(result.unwrapErr().code).toBe('INVALID_CREDENTIALS');
});
expect(error.code).toBe('REPOSITORY_ERROR'); it('returns error when user does not exist', async () => {
expect(error.details?.message).toBe('DB failure'); authRepo.findByEmail.mockResolvedValue(null);
expect(output.present).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalled(); const result = await useCase.execute({
email: 'nonexistent@example.com',
password: 'Password123',
});
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('INVALID_CREDENTIALS');
}); });
}); });

View File

@@ -4,7 +4,7 @@ import { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import { IPasswordHashingService } from '../../domain/services/PasswordHashingService'; import { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger, UseCase } from '@core/shared/application'; import type { Logger, UseCase } from '@core/shared/application';
export type LoginInput = { export type LoginInput = {
email: string; email: string;
@@ -24,15 +24,14 @@ export type LoginApplicationError = ApplicationErrorCode<LoginErrorCode, { messa
* *
* Handles user login by verifying credentials. * Handles user login by verifying credentials.
*/ */
export class LoginUseCase implements UseCase<LoginInput, void, LoginErrorCode> { export class LoginUseCase implements UseCase<LoginInput, LoginResult, LoginErrorCode> {
constructor( constructor(
private readonly authRepo: IAuthRepository, private readonly authRepo: IAuthRepository,
private readonly passwordService: IPasswordHashingService, private readonly passwordService: IPasswordHashingService,
private readonly logger: Logger, private readonly logger: Logger,
private readonly output: UseCaseOutputPort<LoginResult>,
) {} ) {}
async execute(input: LoginInput): Promise<Result<void, LoginApplicationError>> { async execute(input: LoginInput): Promise<Result<LoginResult, LoginApplicationError>> {
try { try {
const emailVO = EmailAddress.create(input.email); const emailVO = EmailAddress.create(input.email);
const user = await this.authRepo.findByEmail(emailVO); const user = await this.authRepo.findByEmail(emailVO);
@@ -48,14 +47,13 @@ export class LoginUseCase implements UseCase<LoginInput, void, LoginErrorCode> {
const isValid = await this.passwordService.verify(input.password, passwordHash.value); const isValid = await this.passwordService.verify(input.password, passwordHash.value);
if (!isValid) { if (!isValid) {
return Result.err<void, LoginApplicationError>({ return Result.err<LoginResult, LoginApplicationError>({
code: 'INVALID_CREDENTIALS', code: 'INVALID_CREDENTIALS',
details: { message: 'Invalid credentials' }, details: { message: 'Invalid credentials' },
}); });
} }
this.output.present({ user }); return Result.ok({ user });
return Result.ok(undefined);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error && error.message error instanceof Error && error.message
@@ -66,7 +64,7 @@ export class LoginUseCase implements UseCase<LoginInput, void, LoginErrorCode> {
input, input,
}); });
return Result.err<void, LoginApplicationError>({ return Result.err<LoginResult, LoginApplicationError>({
code: 'REPOSITORY_ERROR', code: 'REPOSITORY_ERROR',
details: { message }, details: { message },
}); });

View File

@@ -1,15 +1,18 @@
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest';
import { import { LoginWithEmailUseCase } from './LoginWithEmailUseCase';
LoginWithEmailUseCase, import type { IUserRepository } from '../../domain/repositories/IUserRepository';
type LoginWithEmailInput,
type LoginWithEmailResult,
type LoginWithEmailErrorCode,
} from './LoginWithEmailUseCase';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { UseCaseOutputPort, Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result'; // Mock the PasswordHash module
vi.mock('@core/identity/domain/value-objects/PasswordHash', () => ({
PasswordHash: {
fromHash: vi.fn((hash: string) => ({
verify: vi.fn().mockResolvedValue(hash === 'hashed-password'),
value: hash,
})),
},
}));
describe('LoginWithEmailUseCase', () => { describe('LoginWithEmailUseCase', () => {
let userRepository: { let userRepository: {
@@ -17,169 +20,119 @@ describe('LoginWithEmailUseCase', () => {
}; };
let sessionPort: { let sessionPort: {
createSession: Mock; createSession: Mock;
getCurrentSession: Mock;
clearSession: Mock;
}; };
let logger: Logger & { error: Mock }; let logger: Logger;
let output: UseCaseOutputPort<LoginWithEmailResult> & { present: Mock };
let useCase: LoginWithEmailUseCase; let useCase: LoginWithEmailUseCase;
beforeEach(() => { beforeEach(() => {
userRepository = { userRepository = {
findByEmail: vi.fn(), findByEmail: vi.fn(),
}; };
sessionPort = { sessionPort = {
createSession: vi.fn(), createSession: vi.fn(),
getCurrentSession: vi.fn(),
clearSession: vi.fn(),
}; };
logger = { logger = {
debug: vi.fn(), debug: vi.fn(),
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
} as unknown as Logger & { error: Mock }; } as unknown as Logger;
output = {
present: vi.fn(),
} as unknown as UseCaseOutputPort<LoginWithEmailResult> & { present: Mock };
useCase = new LoginWithEmailUseCase( useCase = new LoginWithEmailUseCase(
userRepository as unknown as IUserRepository, userRepository as unknown as IUserRepository,
sessionPort as unknown as IdentitySessionPort, sessionPort as unknown as IdentitySessionPort,
logger, logger,
output,
); );
}); });
it('returns ok and presents session result for valid credentials', async () => { it('returns ok and presents session result for valid credentials', async () => {
const input: LoginWithEmailInput = { const storedUser = {
email: 'Test@Example.com',
password: 'password123',
};
// Import PasswordHash to create a proper hash
const { PasswordHash } = await import('@core/identity/domain/value-objects/PasswordHash');
const passwordHash = await PasswordHash.create('password123');
const storedUser: StoredUser = {
id: 'user-1', id: 'user-1',
email: 'test@example.com', email: 'test@example.com',
displayName: 'Test User', displayName: 'John Smith',
passwordHash: passwordHash.value, passwordHash: 'hashed-password',
createdAt: new Date(), createdAt: new Date(),
}; };
userRepository.findByEmail.mockResolvedValue(storedUser);
const session = { sessionPort.createSession.mockResolvedValue({
token: 'token-123',
user: { user: {
id: storedUser.id, id: 'user-1',
email: storedUser.email, email: 'test@example.com',
displayName: storedUser.displayName, displayName: 'John Smith',
}, },
issuedAt: Date.now(), issuedAt: Date.now(),
expiresAt: Date.now() + 1000, expiresAt: Date.now() + 1000,
token: 'token-123',
};
userRepository.findByEmail.mockResolvedValue(storedUser);
sessionPort.createSession.mockResolvedValue(session);
const result: Result<void, ApplicationErrorCode<LoginWithEmailErrorCode, { message: string }>> =
await useCase.execute(input);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
expect(sessionPort.createSession).toHaveBeenCalledWith({
id: storedUser.id,
displayName: storedUser.displayName,
email: storedUser.email,
}); });
expect(output.present).toHaveBeenCalledTimes(1); const result = await useCase.execute({
const presented = output.present.mock.calls[0]![0] as LoginWithEmailResult; email: 'test@example.com',
expect(presented.sessionToken).toBe('token-123'); password: 'Password123',
expect(presented.userId).toBe(storedUser.id); });
expect(presented.displayName).toBe(storedUser.displayName);
expect(presented.email).toBe(storedUser.email); expect(result.isOk()).toBe(true);
const loginResult = result.unwrap();
expect(loginResult.sessionToken).toBe('token-123');
expect(loginResult.userId).toBe('user-1');
expect(loginResult.displayName).toBe('John Smith');
expect(loginResult.email).toBe('test@example.com');
expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
expect(sessionPort.createSession).toHaveBeenCalled();
}); });
it('returns INVALID_INPUT when email or password is missing', async () => { it('returns INVALID_INPUT when email or password is missing', async () => {
const result1 = await useCase.execute({ email: '', password: 'x' }); const result = await useCase.execute({
const result2 = await useCase.execute({ email: 'a@example.com', password: '' }); email: '',
password: 'Password123',
});
expect(result1.isErr()).toBe(true); expect(result.isErr()).toBe(true);
expect(result1.unwrapErr().code).toBe('INVALID_INPUT'); expect(result.unwrapErr().code).toBe('INVALID_INPUT');
expect(result2.isErr()).toBe(true);
expect(result2.unwrapErr().code).toBe('INVALID_INPUT');
expect(output.present).not.toHaveBeenCalled();
}); });
it('returns INVALID_CREDENTIALS when user does not exist', async () => { it('returns INVALID_CREDENTIALS when user does not exist', async () => {
const input: LoginWithEmailInput = {
email: 'missing@example.com',
password: 'password',
};
userRepository.findByEmail.mockResolvedValue(null); userRepository.findByEmail.mockResolvedValue(null);
const result: Result<void, ApplicationErrorCode<LoginWithEmailErrorCode, { message: string }>> = const result = await useCase.execute({
await useCase.execute(input); email: 'nonexistent@example.com',
password: 'Password123',
});
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
const error = result.unwrapErr(); expect(result.unwrapErr().code).toBe('INVALID_CREDENTIALS');
expect(error.code).toBe('INVALID_CREDENTIALS');
expect(error.details.message).toBe('Invalid email or password');
expect(output.present).not.toHaveBeenCalled();
}); });
it('returns INVALID_CREDENTIALS when password is invalid', async () => { it('returns INVALID_CREDENTIALS when password is invalid', async () => {
const input: LoginWithEmailInput = { const storedUser = {
email: 'test@example.com',
password: 'wrong',
};
// Create a hash for a different password
const { PasswordHash } = await import('@core/identity/domain/value-objects/PasswordHash');
const passwordHash = await PasswordHash.create('correct-password');
const storedUser: StoredUser = {
id: 'user-1', id: 'user-1',
email: 'test@example.com', email: 'test@example.com',
displayName: 'Test User', displayName: 'John Smith',
passwordHash: passwordHash.value, passwordHash: 'wrong-hash', // Different hash to simulate wrong password
createdAt: new Date(), createdAt: new Date(),
}; };
userRepository.findByEmail.mockResolvedValue(storedUser); userRepository.findByEmail.mockResolvedValue(storedUser);
const result: Result<void, ApplicationErrorCode<LoginWithEmailErrorCode, { message: string }>> = const result = await useCase.execute({
await useCase.execute(input); email: 'test@example.com',
password: 'WrongPassword',
});
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
const error = result.unwrapErr(); expect(result.unwrapErr().code).toBe('INVALID_CREDENTIALS');
expect(error.code).toBe('INVALID_CREDENTIALS');
expect(error.details.message).toBe('Invalid email or password');
expect(output.present).not.toHaveBeenCalled();
}); });
it('wraps unexpected errors as REPOSITORY_ERROR and logs them', async () => { it('wraps unexpected errors as REPOSITORY_ERROR and logs them', async () => {
const input: LoginWithEmailInput = { userRepository.findByEmail.mockRejectedValue(new Error('Database connection failed'));
const result = await useCase.execute({
email: 'test@example.com', email: 'test@example.com',
password: 'password123', password: 'Password123',
}; });
userRepository.findByEmail.mockRejectedValue(new Error('DB failure'));
const result: Result<void, ApplicationErrorCode<LoginWithEmailErrorCode, { message: string }>> =
await useCase.execute(input);
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
const error = result.unwrapErr(); expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
expect(error.code).toBe('REPOSITORY_ERROR');
expect(error.details.message).toBe('DB failure');
expect(output.present).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More