module cleanup

This commit is contained in:
2025-12-19 01:22:45 +01:00
parent d617654928
commit d0fac9e6c1
135 changed files with 5104 additions and 1315 deletions

View File

@@ -0,0 +1,121 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService';
import type { Response } from 'express';
import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
describe('AnalyticsController', () => {
let controller: AnalyticsController;
let service: ReturnType<typeof vi.mocked<AnalyticsService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AnalyticsController],
providers: [
{
provide: AnalyticsService,
useValue: {
recordPageView: vi.fn(),
recordEngagement: vi.fn(),
getDashboardData: vi.fn(),
getAnalyticsMetrics: vi.fn(),
},
},
],
}).compile();
controller = module.get<AnalyticsController>(AnalyticsController);
service = vi.mocked(module.get(AnalyticsService));
});
describe('recordPageView', () => {
it('should record a page view and return 201', async () => {
const input = {
entityType: EntityType.RACE,
entityId: 'race-123',
visitorType: VisitorType.ANONYMOUS,
sessionId: 'session-456',
visitorId: 'visitor-789',
referrer: 'https://example.com',
userAgent: 'Mozilla/5.0',
country: 'US',
};
const output = { pageViewId: 'pv-123' };
service.recordPageView.mockResolvedValue(output);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.recordPageView(input, mockRes);
expect(service.recordPageView).toHaveBeenCalledWith(input);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(output);
});
});
describe('recordEngagement', () => {
it('should record an engagement and return 201', async () => {
const input = {
action: EngagementAction.CLICK_SPONSOR_LOGO,
entityType: EngagementEntityType.RACE,
entityId: 'race-123',
actorType: 'driver' as const,
sessionId: 'session-456',
actorId: 'actor-789',
metadata: { key: 'value' },
};
const output = { eventId: 'event-123', engagementWeight: 10 };
service.recordEngagement.mockResolvedValue(output);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.recordEngagement(input, mockRes);
expect(service.recordEngagement).toHaveBeenCalledWith(input);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(output);
});
});
describe('getDashboardData', () => {
it('should return dashboard data', async () => {
const output = {
totalUsers: 100,
activeUsers: 50,
totalRaces: 20,
totalLeagues: 5,
};
service.getDashboardData.mockResolvedValue(output);
const result = await controller.getDashboardData();
expect(service.getDashboardData).toHaveBeenCalled();
expect(result).toEqual(output);
});
});
describe('getAnalyticsMetrics', () => {
it('should return analytics metrics', async () => {
const output = {
pageViews: 1000,
uniqueVisitors: 500,
averageSessionDuration: 300,
bounceRate: 0.4,
};
service.getAnalyticsMetrics.mockResolvedValue(output);
const result = await controller.getAnalyticsMetrics();
expect(service.getAnalyticsMetrics).toHaveBeenCalled();
expect(result).toEqual(output);
});
});
});

View File

@@ -1,12 +1,12 @@
import { Controller, Get, Post, Body, Res, HttpStatus } from '@nestjs/common'; import { Controller, Get, Post, Body, Res, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
import type { Response } from 'express'; import type { Response } from 'express';
import type { RecordPageViewInputDTO } from './dtos/RecordPageViewInputDTO'; import { RecordPageViewInputDTO } from './dtos/RecordPageViewInputDTO';
import type { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO'; import { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
import type { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO'; import { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO'; import { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
import type { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO'; import { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO';
import type { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO'; import { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO';
import { AnalyticsService } from './AnalyticsService'; import { AnalyticsService } from './AnalyticsService';
type RecordPageViewInput = RecordPageViewInputDTO; type RecordPageViewInput = RecordPageViewInputDTO;

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AnalyticsModule } from './AnalyticsModule';
import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService';
describe('AnalyticsModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [AnalyticsModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide AnalyticsController', () => {
const controller = module.get<AnalyticsController>(AnalyticsController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(AnalyticsController);
});
it('should provide AnalyticsService', () => {
const service = module.get<AnalyticsService>(AnalyticsService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(AnalyticsService);
});
});

View File

@@ -1,26 +1,27 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import { AnalyticsService } from './AnalyticsService'; import { AnalyticsService } from './AnalyticsService';
import { RecordPageViewUseCase } from './use-cases/RecordPageViewUseCase'; import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from './use-cases/RecordEngagementUseCase'; import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
import type { IPageViewRepository } from '@core/analytics/domain/repositories/IPageViewRepository';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { Logger } from '@core/shared/application';
const Logger_TOKEN = 'Logger_TOKEN'; const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN'; const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN'; const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN'; const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN'; const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
const GET_DASHBOARD_DATA_USE_CASE_TOKEN = 'GetDashboardDataUseCase_TOKEN';
const GET_ANALYTICS_METRICS_USE_CASE_TOKEN = 'GetAnalyticsMetricsUseCase_TOKEN';
import type { Logger } from '@core/shared/application';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import { InMemoryEngagementRepository } from '@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository'; import { InMemoryEngagementRepository } from '@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
export const AnalyticsProviders: Provider[] = [ export const AnalyticsProviders: Provider[] = [
AnalyticsService, AnalyticsService,
RecordPageViewUseCase,
RecordEngagementUseCase,
{ {
provide: Logger_TOKEN, provide: Logger_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
@@ -35,10 +36,22 @@ export const AnalyticsProviders: Provider[] = [
}, },
{ {
provide: RECORD_PAGE_VIEW_USE_CASE_TOKEN, provide: RECORD_PAGE_VIEW_USE_CASE_TOKEN,
useClass: RecordPageViewUseCase, useFactory: (repo: IPageViewRepository, logger: Logger) => new RecordPageViewUseCase(repo, logger),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN],
}, },
{ {
provide: RECORD_ENGAGEMENT_USE_CASE_TOKEN, provide: RECORD_ENGAGEMENT_USE_CASE_TOKEN,
useClass: RecordEngagementUseCase, useFactory: (repo: IEngagementRepository, logger: Logger) => new RecordEngagementUseCase(repo, logger),
inject: [IENGAGEMENT_REPO_TOKEN, Logger_TOKEN],
},
{
provide: GET_DASHBOARD_DATA_USE_CASE_TOKEN,
useFactory: (logger: Logger) => new GetDashboardDataUseCase(logger),
inject: [Logger_TOKEN],
},
{
provide: GET_ANALYTICS_METRICS_USE_CASE_TOKEN,
useFactory: (repo: IPageViewRepository, logger: Logger) => new GetAnalyticsMetricsUseCase(repo, logger),
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN],
}, },
]; ];

View File

@@ -5,9 +5,10 @@ import type { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO'; import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
import type { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO'; import type { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO';
import type { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO'; import type { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO';
import type { Logger } from '@core/shared/application'; import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordPageViewUseCase } from './use-cases/RecordPageViewUseCase'; import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
import { RecordEngagementUseCase } from './use-cases/RecordEngagementUseCase'; import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
type RecordPageViewInput = RecordPageViewInputDTO; type RecordPageViewInput = RecordPageViewInputDTO;
type RecordPageViewOutput = RecordPageViewOutputDTO; type RecordPageViewOutput = RecordPageViewOutputDTO;
@@ -16,16 +17,18 @@ type RecordEngagementOutput = RecordEngagementOutputDTO;
type GetDashboardDataOutput = GetDashboardDataOutputDTO; type GetDashboardDataOutput = GetDashboardDataOutputDTO;
type GetAnalyticsMetricsOutput = GetAnalyticsMetricsOutputDTO; type GetAnalyticsMetricsOutput = GetAnalyticsMetricsOutputDTO;
const Logger_TOKEN = 'Logger_TOKEN';
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN'; const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN'; const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
const GET_DASHBOARD_DATA_USE_CASE_TOKEN = 'GetDashboardDataUseCase_TOKEN';
const GET_ANALYTICS_METRICS_USE_CASE_TOKEN = 'GetAnalyticsMetricsUseCase_TOKEN';
@Injectable() @Injectable()
export class AnalyticsService { export class AnalyticsService {
constructor( constructor(
@Inject(RECORD_PAGE_VIEW_USE_CASE_TOKEN) private readonly recordPageViewUseCase: RecordPageViewUseCase, @Inject(RECORD_PAGE_VIEW_USE_CASE_TOKEN) private readonly recordPageViewUseCase: RecordPageViewUseCase,
@Inject(RECORD_ENGAGEMENT_USE_CASE_TOKEN) private readonly recordEngagementUseCase: RecordEngagementUseCase, @Inject(RECORD_ENGAGEMENT_USE_CASE_TOKEN) private readonly recordEngagementUseCase: RecordEngagementUseCase,
@Inject(Logger_TOKEN) private readonly logger: Logger, @Inject(GET_DASHBOARD_DATA_USE_CASE_TOKEN) private readonly getDashboardDataUseCase: GetDashboardDataUseCase,
@Inject(GET_ANALYTICS_METRICS_USE_CASE_TOKEN) private readonly getAnalyticsMetricsUseCase: GetAnalyticsMetricsUseCase,
) {} ) {}
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> { async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
@@ -37,22 +40,10 @@ export class AnalyticsService {
} }
async getDashboardData(): Promise<GetDashboardDataOutput> { async getDashboardData(): Promise<GetDashboardDataOutput> {
// TODO: Implement actual dashboard data retrieval return await this.getDashboardDataUseCase.execute();
return {
totalUsers: 0,
activeUsers: 0,
totalRaces: 0,
totalLeagues: 0,
};
} }
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutput> { async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutput> {
// TODO: Implement actual analytics metrics retrieval return await this.getAnalyticsMetricsUseCase.execute();
return {
pageViews: 0,
uniqueVisitors: 0,
averageSessionDuration: 0,
bounceRate: 0,
};
} }
} }

View File

@@ -1,12 +0,0 @@
// From core/analytics/domain/types/EngagementEvent.ts
export enum EngagementAction {
CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
CLICK_SPONSOR_URL = 'click_sponsor_url',
DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
JOIN_LEAGUE = 'join_league',
REGISTER_RACE = 'register_race',
VIEW_STANDINGS = 'view_standings',
VIEW_SCHEDULE = 'view_schedule',
SHARE_SOCIAL = 'share_social',
CONTACT_SPONSOR = 'contact_sponsor',
}

View File

@@ -1,9 +0,0 @@
// From core/analytics/domain/types/EngagementEvent.ts
export enum EngagementEntityType {
LEAGUE = 'league',
DRIVER = 'driver',
TEAM = 'team',
RACE = 'race',
SPONSOR = 'sponsor',
SPONSORSHIP = 'sponsorship',
}

View File

@@ -1,8 +0,0 @@
// From core/analytics/domain/types/PageView.ts
export enum EntityType {
LEAGUE = 'league',
DRIVER = 'driver',
TEAM = 'team',
RACE = 'race',
SPONSOR = 'sponsor',
}

View File

@@ -1,7 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum, IsObject } from 'class-validator'; import { IsString, IsOptional, IsEnum, IsObject } from 'class-validator';
import { EngagementAction } from './EngagementAction'; import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
import { EngagementEntityType } from './EngagementEntityType';
export class RecordEngagementInputDTO { export class RecordEngagementInputDTO {
@ApiProperty({ enum: EngagementAction }) @ApiProperty({ enum: EngagementAction })

View File

@@ -1,7 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum } from 'class-validator'; import { IsString, IsOptional, IsEnum } from 'class-validator';
import { EntityType } from './EntityType'; import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
import { VisitorType } from './VisitorType';
export class RecordPageViewInputDTO { export class RecordPageViewInputDTO {
@ApiProperty({ enum: EntityType }) @ApiProperty({ enum: EntityType })

View File

@@ -1,6 +0,0 @@
// From core/analytics/domain/types/PageView.ts
export enum VisitorType {
ANONYMOUS = 'anonymous',
DRIVER = 'driver',
SPONSOR = 'sponsor',
}

View File

@@ -1,88 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecordEngagementUseCase } from './RecordEngagementUseCase';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { Logger } from '@core/shared/application';
describe('RecordEngagementUseCase', () => {
let useCase: RecordEngagementUseCase;
let engagementRepository: jest.Mocked<IEngagementRepository>;
let logger: jest.Mocked<Logger>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RecordEngagementUseCase,
{
provide: 'IEngagementRepository_TOKEN',
useValue: {
save: jest.fn(),
},
},
{
provide: 'Logger_TOKEN',
useValue: {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
},
},
],
}).compile();
useCase = module.get<RecordEngagementUseCase>(RecordEngagementUseCase);
engagementRepository = module.get('IEngagementRepository_TOKEN');
logger = module.get('Logger_TOKEN');
});
describe('execute', () => {
it('should save the engagement event and return the eventId and engagementWeight', async () => {
const input = {
action: 'like' as any,
entityType: 'race' as any,
entityId: 'race-123',
actorType: 'driver',
sessionId: 'session-456',
actorId: 'actor-789',
metadata: { some: 'data' },
};
const mockEvent = {
getEngagementWeight: jest.fn().mockReturnValue(10),
};
// Mock the create function to return the mock event
const originalCreate = require('@gridpilot/analytics/domain/entities/EngagementEvent').EngagementEvent.create;
require('@gridpilot/analytics/domain/entities/EngagementEvent').EngagementEvent.create = jest.fn().mockReturnValue(mockEvent);
engagementRepository.save.mockResolvedValue(undefined);
const result = await useCase.execute(input);
expect(logger.debug).toHaveBeenCalledWith('Executing RecordEngagementUseCase', { input });
expect(engagementRepository.save).toHaveBeenCalledWith(mockEvent);
expect(logger.info).toHaveBeenCalledWith('Engagement recorded successfully', expect.objectContaining({ eventId: expect.any(String), input }));
expect(result).toHaveProperty('eventId');
expect(result).toHaveProperty('engagementWeight', 10);
expect(typeof result.eventId).toBe('string');
// Restore original
require('@gridpilot/analytics/domain/entities/EngagementEvent').EngagementEvent.create = originalCreate;
});
it('should handle errors and throw them', async () => {
const input = {
action: 'like' as any,
entityType: 'race' as any,
entityId: 'race-123',
actorType: 'driver',
sessionId: 'session-456',
};
const error = new Error('Save failed');
engagementRepository.save.mockRejectedValue(error);
await expect(useCase.execute(input)).rejects.toThrow('Save failed');
expect(logger.error).toHaveBeenCalledWith('Error recording engagement', error, { input });
});
});
});

View File

@@ -1,53 +0,0 @@
import { Injectable, Inject } from '@nestjs/common';
import type { RecordEngagementInputDTO } from '../dtos/RecordEngagementInputDTO';
import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { Logger } from '@core/shared/application';
import { EngagementEvent } from '@core/analytics/domain/entities/EngagementEvent';
type RecordEngagementInput = RecordEngagementInputDTO;
type RecordEngagementOutput = RecordEngagementOutputDTO;
const Logger_TOKEN = 'Logger_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
@Injectable()
export class RecordEngagementUseCase {
constructor(
@Inject(IENGAGEMENT_REPO_TOKEN) private readonly engagementRepository: IEngagementRepository,
@Inject(Logger_TOKEN) private readonly logger: Logger,
) {}
async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
this.logger.debug('Executing RecordEngagementUseCase', { input });
try {
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
id: eventId,
action: input.action as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
entityId: input.entityId,
actorType: input.actorType,
sessionId: input.sessionId,
};
const event = EngagementEvent.create({
...baseProps,
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
});
await this.engagementRepository.save(event);
this.logger.info('Engagement recorded successfully', { eventId, input });
return {
eventId,
engagementWeight: event.getEngagementWeight(),
};
} catch (error) {
this.logger.error('Error recording engagement', error, { input });
throw error;
}
}
}

View File

@@ -1,76 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecordPageViewUseCase } from './RecordPageViewUseCase';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import type { Logger } from '@core/shared/application';
describe('RecordPageViewUseCase', () => {
let useCase: RecordPageViewUseCase;
let pageViewRepository: jest.Mocked<IPageViewRepository>;
let logger: jest.Mocked<Logger>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RecordPageViewUseCase,
{
provide: 'IPageViewRepository_TOKEN',
useValue: {
save: jest.fn(),
},
},
{
provide: 'Logger_TOKEN',
useValue: {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
},
},
],
}).compile();
useCase = module.get<RecordPageViewUseCase>(RecordPageViewUseCase);
pageViewRepository = module.get('IPageViewRepository_TOKEN');
logger = module.get('Logger_TOKEN');
});
describe('execute', () => {
it('should save the page view and return the pageViewId', async () => {
const input = {
entityType: 'race' as any,
entityId: 'race-123',
visitorType: 'anonymous' as any,
sessionId: 'session-456',
visitorId: 'visitor-789',
referrer: 'https://example.com',
userAgent: 'Mozilla/5.0',
country: 'US',
};
pageViewRepository.save.mockResolvedValue(undefined);
const result = await useCase.execute(input);
expect(logger.debug).toHaveBeenCalledWith('Executing RecordPageViewUseCase', { input });
expect(pageViewRepository.save).toHaveBeenCalledTimes(1);
expect(logger.info).toHaveBeenCalledWith('Page view recorded successfully', expect.objectContaining({ pageViewId: expect.any(String), input }));
expect(result).toHaveProperty('pageViewId');
expect(typeof result.pageViewId).toBe('string');
});
it('should handle errors and throw them', async () => {
const input = {
entityType: 'race' as any,
entityId: 'race-123',
visitorType: 'anonymous' as any,
sessionId: 'session-456',
};
const error = new Error('Save failed');
pageViewRepository.save.mockRejectedValue(error);
await expect(useCase.execute(input)).rejects.toThrow('Save failed');
expect(logger.error).toHaveBeenCalledWith('Error recording page view', error, { input });
});
});
});

View File

@@ -1,50 +0,0 @@
import { Injectable, Inject } from '@nestjs/common';
import type { RecordPageViewInputDTO } from '../dtos/RecordPageViewInputDTO';
import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import type { Logger } from '@core/shared/application';
import { PageView } from '@core/analytics/domain/entities/PageView';
type RecordPageViewInput = RecordPageViewInputDTO;
type RecordPageViewOutput = RecordPageViewOutputDTO;
const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
@Injectable()
export class RecordPageViewUseCase {
constructor(
@Inject(IPAGE_VIEW_REPO_TOKEN) private readonly pageViewRepository: IPageViewRepository,
@Inject(Logger_TOKEN) private readonly logger: Logger,
) {}
async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
this.logger.debug('Executing RecordPageViewUseCase', { input });
try {
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
id: pageViewId,
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
entityId: input.entityId,
visitorType: input.visitorType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
sessionId: input.sessionId,
};
const pageView = PageView.create({
...baseProps,
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
...(input.country !== undefined ? { country: input.country } : {}),
});
await this.pageViewRepository.save(pageView);
this.logger.info('Page view recorded successfully', { pageViewId, input });
return { pageViewId };
} catch (error) {
this.logger.error('Error recording page view', error, { input });
throw error;
}
}
}

View File

@@ -0,0 +1,108 @@
import { vi } from 'vitest';
import { AuthController } from './AuthController';
import { AuthService } from './AuthService';
import { SignupParams, LoginParams, AuthSessionDTO } from './dtos/AuthDto';
describe('AuthController', () => {
let controller: AuthController;
let service: ReturnType<typeof vi.mocked<AuthService>>;
beforeEach(() => {
service = vi.mocked<AuthService>({
signupWithEmail: vi.fn(),
loginWithEmail: vi.fn(),
getCurrentSession: vi.fn(),
logout: vi.fn(),
});
controller = new AuthController(service);
});
describe('signup', () => {
it('should call service.signupWithEmail and return session', async () => {
const params: SignupParams = {
email: 'test@example.com',
password: 'password123',
displayName: 'Test User',
iracingCustomerId: '12345',
primaryDriverId: 'driver1',
avatarUrl: 'http://example.com/avatar.jpg',
};
const session: AuthSessionDTO = {
token: 'token123',
user: {
userId: 'user1',
email: 'test@example.com',
displayName: 'Test User',
},
};
service.signupWithEmail.mockResolvedValue(session);
const result = await controller.signup(params);
expect(service.signupWithEmail).toHaveBeenCalledWith(params);
expect(result).toEqual(session);
});
});
describe('login', () => {
it('should call service.loginWithEmail and return session', async () => {
const params: LoginParams = {
email: 'test@example.com',
password: 'password123',
};
const session: AuthSessionDTO = {
token: 'token123',
user: {
userId: 'user1',
email: 'test@example.com',
displayName: 'Test User',
},
};
service.loginWithEmail.mockResolvedValue(session);
const result = await controller.login(params);
expect(service.loginWithEmail).toHaveBeenCalledWith(params);
expect(result).toEqual(session);
});
});
describe('getSession', () => {
it('should call service.getCurrentSession and return session', async () => {
const session: AuthSessionDTO = {
token: 'token123',
user: {
userId: 'user1',
email: 'test@example.com',
displayName: 'Test User',
},
};
service.getCurrentSession.mockResolvedValue(session);
const result = await controller.getSession();
expect(service.getCurrentSession).toHaveBeenCalled();
expect(result).toEqual(session);
});
it('should return null if no session', async () => {
service.getCurrentSession.mockResolvedValue(null);
const result = await controller.getSession();
expect(result).toBeNull();
});
});
describe('logout', () => {
it('should call service.logout', async () => {
service.logout.mockResolvedValue(undefined);
await controller.logout();
expect(service.logout).toHaveBeenCalled();
});
});
});

View File

@@ -1,7 +1,6 @@
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common'; import { Controller, Get, Post, Body } from '@nestjs/common';
import { Response } from 'express';
import { AuthService } from './AuthService'; import { AuthService } from './AuthService';
import { LoginParams, SignupParams, LoginWithIracingCallbackParams, AuthSessionDTO, IracingAuthRedirectResult } from './dto/AuthDto'; import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
@@ -27,16 +26,4 @@ export class AuthController {
return this.authService.logout(); return this.authService.logout();
} }
@Get('iracing/start')
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response): Promise<void> {
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
// In real application, you might want to store 'state' in a secure cookie or session.
// For this example, we'll just redirect.
res.redirect(HttpStatus.FOUND, redirectUrl);
}
@Get('iracing/callback')
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string): Promise<AuthSessionDTO> {
return this.authService.loginWithIracingCallback({ code, state, returnTo });
}
} }

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthModule } from './AuthModule';
import { AuthController } from './AuthController';
import { AuthService } from './AuthService';
describe('AuthModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [AuthModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide AuthController', () => {
const controller = module.get<AuthController>(AuthController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(AuthController);
});
it('should provide AuthService', () => {
const service = module.get<AuthService>(AuthService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(AuthService);
});
});

View File

@@ -5,7 +5,7 @@ import { AuthProviders } from './AuthProviders';
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
providers: AuthProviders, providers: [AuthService, ...AuthProviders],
exports: [AuthService], exports: [AuthService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -1,9 +1,7 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import { AuthService } from './AuthService';
// Import interfaces and concrete implementations // Import interfaces and concrete implementations
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository'; import { StoredUser } from '@core/identity/domain/repositories/IUserRepository';
import { IUserRepository, StoredUser } from '@core/identity/domain/repositories/IUserRepository';
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService'; import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
@@ -11,7 +9,6 @@ import { InMemoryAuthRepository } from '@adapters/identity/persistence/inmemory/
import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository'; import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository';
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService'; import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter'; import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter';
// Define the tokens for dependency injection // Define the tokens for dependency injection
@@ -22,10 +19,9 @@ export const LOGGER_TOKEN = 'Logger';
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort'; export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
export const AuthProviders: Provider[] = [ export const AuthProviders: Provider[] = [
AuthService, // Provide the service itself
{ {
provide: AUTH_REPOSITORY_TOKEN, provide: AUTH_REPOSITORY_TOKEN,
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: Logger) => { useFactory: (passwordHashingService: IPasswordHashingService, logger: Logger) => {
// Seed initial users for InMemoryUserRepository // Seed initial users for InMemoryUserRepository
const initialUsers: StoredUser[] = [ const initialUsers: StoredUser[] = [
// Example user (replace with actual test users as needed) // Example user (replace with actual test users as needed)
@@ -41,7 +37,7 @@ export const AuthProviders: Provider[] = [
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers); const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger); return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
}, },
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN], inject: [PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: USER_REPOSITORY_TOKEN, provide: USER_REPOSITORY_TOKEN,

View File

@@ -1,33 +1,26 @@
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common'; import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
// Core Use Cases // Core Use Cases
import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase'; import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase';
import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
import { GetCurrentSessionUseCase } from '@core/identity/application/use-cases/GetCurrentSessionUseCase';
import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase'; import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase';
import { StartIracingAuthRedirectUseCase } from '@core/identity/application/use-cases/StartIracingAuthRedirectUseCase'; import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
import { LoginWithIracingCallbackUseCase } from '@core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
// Core Interfaces and Tokens // Core Interfaces and Tokens
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders'; import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '@core/identity/application/dto/AuthenticatedUserDTO';
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
import { User } from '@core/identity/domain/entities/User';
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository'; import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService'; import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
import type { Logger } from "@core/shared/application"; import type { Logger } from "@core/shared/application";
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort'; import { AUTH_REPOSITORY_TOKEN, IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
import { UserId } from '@core/identity/domain/value-objects/UserId'; import { AuthSessionDTO, LoginParams, SignupParams, AuthenticatedUserDTO } from './dtos/AuthDto';
import { User } from '@core/identity/domain/entities/User';
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '@core/identity/application/dto/AuthenticatedUserDTO';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private readonly loginUseCase: LoginUseCase; private readonly loginUseCase: LoginUseCase;
private readonly signupUseCase: SignupUseCase; private readonly signupUseCase: SignupUseCase;
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
private readonly logoutUseCase: LogoutUseCase; private readonly logoutUseCase: LogoutUseCase;
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
constructor( constructor(
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository, @Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
@@ -38,10 +31,7 @@ export class AuthService {
) { ) {
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService); this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService); this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort); this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
} }
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO { private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
@@ -49,10 +39,14 @@ export class AuthService {
userId: user.getId().value, userId: user.getId().value,
email: user.getEmail() ?? '', email: user.getEmail() ?? '',
displayName: user.getDisplayName() ?? '', displayName: user.getDisplayName() ?? '',
// Map other fields as necessary };
iracingCustomerId: user.getIracingCustomerId() ?? undefined, }
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
avatarUrl: user.getAvatarUrl() ?? undefined, private mapToCoreAuthenticatedUserDTO(apiDto: AuthenticatedUserDTO): CoreAuthenticatedUserDTO {
return {
id: apiDto.userId,
displayName: apiDto.displayName,
email: apiDto.email,
}; };
} }
@@ -85,7 +79,8 @@ export class AuthService {
// Create session after successful signup // Create session after successful signup
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user); const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO); const coreDto = this.mapToCoreAuthenticatedUserDTO(authenticatedUserDTO);
const session = await this.identitySessionPort.createSession(coreDto);
return { return {
token: session.token, token: session.token,
@@ -99,7 +94,8 @@ export class AuthService {
const user = await this.loginUseCase.execute(params.email, params.password); const user = await this.loginUseCase.execute(params.email, params.password);
// Create session after successful login // Create session after successful login
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user); const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO); const coreDto = this.mapToCoreAuthenticatedUserDTO(authenticatedUserDTO);
const session = await this.identitySessionPort.createSession(coreDto);
return { return {
token: session.token, token: session.token,
@@ -111,27 +107,6 @@ export class AuthService {
} }
} }
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
return { redirectUrl: result.redirectUrl, state: result.state };
}
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
// Create session after successful iRacing login
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
return {
token: session.token,
user: authenticatedUserDTO,
};
}
async logout(): Promise<void> { async logout(): Promise<void> {
this.logger.debug('[AuthService] Attempting logout.'); this.logger.debug('[AuthService] Attempting logout.');

View File

@@ -0,0 +1,45 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { DashboardController } from './DashboardController';
import { DashboardService } from './DashboardService';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
describe('DashboardController', () => {
let controller: DashboardController;
let mockService: { getDashboardOverview: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockService = {
getDashboardOverview: vi.fn(),
};
controller = new DashboardController(mockService as any);
});
describe('getDashboardOverview', () => {
it('should call service.getDashboardOverview and return overview', async () => {
const driverId = 'driver-123';
const overview: DashboardOverviewDTO = {
currentDriver: null,
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 5,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
mockService.getDashboardOverview.mockResolvedValue(overview);
const result = await controller.getDashboardOverview(driverId);
expect(mockService.getDashboardOverview).toHaveBeenCalledWith(driverId);
expect(result).toEqual(overview);
});
});
});

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DashboardModule } from './DashboardModule';
import { DashboardController } from './DashboardController';
import { DashboardService } from './DashboardService';
describe('DashboardModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [DashboardModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide DashboardController', () => {
const controller = module.get<DashboardController>(DashboardController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(DashboardController);
});
it('should provide DashboardService', () => {
const service = module.get<DashboardService>(DashboardService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(DashboardService);
});
});

View File

@@ -5,7 +5,7 @@ import { DashboardProviders } from './DashboardProviders';
@Module({ @Module({
controllers: [DashboardController], controllers: [DashboardController],
providers: DashboardProviders, providers: [DashboardService, ...DashboardProviders],
exports: [DashboardService], exports: [DashboardService],
}) })
export class DashboardModule {} export class DashboardModule {}

View File

@@ -3,21 +3,114 @@ import { DashboardService } from './DashboardService';
// Import core interfaces // Import core interfaces
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Import concrete implementations // Import concrete implementations
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
// Import use cases // Simple mock implementations for missing adapters
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; class MockFeedRepository implements IFeedRepository {
async getFeedForDriver(driverId: string, limit?: number) {
return [];
}
async getGlobalFeed(limit?: number) {
return [];
}
}
class MockSocialGraphRepository implements ISocialGraphRepository {
async getFriends(driverId: string) {
return [];
}
async getFriendIds(driverId: string) {
return [];
}
async getSuggestedFriends(driverId: string, limit?: number) {
return [];
}
}
// Define injection tokens // Define injection tokens
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const DashboardProviders: Provider[] = [ export const DashboardProviders: Provider[] = [
DashboardService,
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
}, },
DashboardOverviewUseCase, {
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: RACE_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: RESULT_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryResultRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryLeagueRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: STANDING_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger, {}),
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryLeagueMembershipRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryRaceRegistrationRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: FEED_REPOSITORY_TOKEN,
useFactory: () => new MockFeedRepository(),
},
{
provide: SOCIAL_GRAPH_REPOSITORY_TOKEN,
useFactory: () => new MockSocialGraphRepository(),
},
{
provide: IMAGE_SERVICE_TOKEN,
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
inject: [LOGGER_TOKEN],
},
]; ];

View File

@@ -1,28 +1,76 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import type { DashboardOverviewViewModel } from '@core/racing/application/presenters/IDashboardOverviewPresenter';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Tokens // Tokens
import { LOGGER_TOKEN } from './DashboardProviders'; import {
LOGGER_TOKEN,
DRIVER_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN,
} from './DashboardProviders';
@Injectable() @Injectable()
export class DashboardService { export class DashboardService {
constructor( private readonly dashboardOverviewUseCase: DashboardOverviewUseCase;
private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
async getDashboardOverview(driverId: string): Promise<any> { constructor(
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository?: IDriverRepository,
@Inject(RACE_REPOSITORY_TOKEN) private readonly raceRepository?: IRaceRepository,
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository?: IResultRepository,
@Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository?: ILeagueRepository,
@Inject(STANDING_REPOSITORY_TOKEN) private readonly standingRepository?: IStandingRepository,
@Inject(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN) private readonly leagueMembershipRepository?: ILeagueMembershipRepository,
@Inject(RACE_REGISTRATION_REPOSITORY_TOKEN) private readonly raceRegistrationRepository?: IRaceRegistrationRepository,
@Inject(FEED_REPOSITORY_TOKEN) private readonly feedRepository?: IFeedRepository,
@Inject(SOCIAL_GRAPH_REPOSITORY_TOKEN) private readonly socialRepository?: ISocialGraphRepository,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService?: IImageServicePort,
) {
this.dashboardOverviewUseCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
resultRepository,
leagueRepository,
standingRepository,
leagueMembershipRepository,
raceRegistrationRepository,
feedRepository,
socialRepository,
imageService,
() => null, // getDriverStats
);
}
async getDashboardOverview(driverId: string): Promise<DashboardOverviewViewModel> {
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId }); this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
const result = await this.dashboardOverviewUseCase.execute({ driverId }); const result = await this.dashboardOverviewUseCase.execute({ driverId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to get dashboard overview'); throw new Error(result.error?.message || 'Failed to get dashboard overview');
} }
return result.value; return result.value!;
} }
} }

View File

@@ -1,11 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional } from 'class-validator'; import { IsNumber } from 'class-validator';
import { DashboardDriverSummaryDTO } from '../../../race/dtos/DashboardDriverSummaryDTO'; import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO';
import { DashboardRaceSummaryDTO } from '../../../race/dtos/DashboardRaceSummaryDTO'; import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
import { DashboardRecentResultDTO } from '../../../race/dtos/DashboardRecentResultDTO'; import { DashboardRecentResultDTO } from './DashboardRecentResultDTO';
import { DashboardLeagueStandingSummaryDTO } from '../../../race/dtos/DashboardLeagueStandingSummaryDTO'; import { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO';
import { DashboardFeedSummaryDTO } from '../../../race/dtos/DashboardFeedSummaryDTO'; import { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO';
import { DashboardFriendSummaryDTO } from '../../../race/dtos/DashboardFriendSummaryDTO'; import { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO';
export class DashboardOverviewDTO { export class DashboardOverviewDTO {
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })

View File

@@ -0,0 +1,163 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { DriverController } from './DriverController';
import { DriverService } from './DriverService';
import type { Request } from 'express';
interface AuthenticatedRequest extends Request {
user?: { userId: string };
}
import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
import { DriverStatsDTO } from './dtos/DriverStatsDTO';
import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO';
import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO';
import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
describe('DriverController', () => {
let controller: DriverController;
let service: ReturnType<typeof vi.mocked<DriverService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DriverController],
providers: [
{
provide: DriverService,
useValue: {
getDriversLeaderboard: vi.fn(),
getTotalDrivers: vi.fn(),
getCurrentDriver: vi.fn(),
completeOnboarding: vi.fn(),
getDriverRegistrationStatus: vi.fn(),
getDriver: vi.fn(),
getDriverProfile: vi.fn(),
updateDriverProfile: vi.fn(),
},
},
],
}).compile();
controller = module.get<DriverController>(DriverController);
service = vi.mocked(module.get(DriverService));
});
describe('getDriversLeaderboard', () => {
it('should return drivers leaderboard', async () => {
const leaderboard: DriversLeaderboardDTO = { items: [] };
service.getDriversLeaderboard.mockResolvedValue(leaderboard);
const result = await controller.getDriversLeaderboard();
expect(service.getDriversLeaderboard).toHaveBeenCalled();
expect(result).toEqual(leaderboard);
});
});
describe('getTotalDrivers', () => {
it('should return total drivers stats', async () => {
const stats: DriverStatsDTO = { totalDrivers: 100 };
service.getTotalDrivers.mockResolvedValue(stats);
const result = await controller.getTotalDrivers();
expect(service.getTotalDrivers).toHaveBeenCalled();
expect(result).toEqual(stats);
});
});
describe('getCurrentDriver', () => {
it('should return current driver if userId exists', async () => {
const userId = 'user-123';
const driver: GetDriverOutputDTO = { id: 'driver-123', name: 'Driver' };
service.getCurrentDriver.mockResolvedValue(driver);
const mockReq: Partial<AuthenticatedRequest> = { user: { userId } };
const result = await controller.getCurrentDriver(mockReq as AuthenticatedRequest);
expect(service.getCurrentDriver).toHaveBeenCalledWith(userId);
expect(result).toEqual(driver);
});
it('should return null if no userId', async () => {
const mockReq: Partial<AuthenticatedRequest> = {};
const result = await controller.getCurrentDriver(mockReq as AuthenticatedRequest);
expect(service.getCurrentDriver).not.toHaveBeenCalled();
expect(result).toBeNull();
});
});
describe('completeOnboarding', () => {
it('should complete onboarding', async () => {
const userId = 'user-123';
const input: CompleteOnboardingInputDTO = { someField: 'value' };
const output: CompleteOnboardingOutputDTO = { success: true };
service.completeOnboarding.mockResolvedValue(output);
const mockReq: Partial<AuthenticatedRequest> = { user: { userId } };
const result = await controller.completeOnboarding(input, mockReq as AuthenticatedRequest);
expect(service.completeOnboarding).toHaveBeenCalledWith(userId, input);
expect(result).toEqual(output);
});
});
describe('getDriverRegistrationStatus', () => {
it('should return registration status', async () => {
const driverId = 'driver-123';
const raceId = 'race-456';
const status: DriverRegistrationStatusDTO = { registered: true };
service.getDriverRegistrationStatus.mockResolvedValue(status);
const result = await controller.getDriverRegistrationStatus(driverId, raceId);
expect(service.getDriverRegistrationStatus).toHaveBeenCalledWith({ driverId, raceId });
expect(result).toEqual(status);
});
});
describe('getDriver', () => {
it('should return driver by id', async () => {
const driverId = 'driver-123';
const driver: GetDriverOutputDTO = { id: driverId, name: 'Driver' };
service.getDriver.mockResolvedValue(driver);
const result = await controller.getDriver(driverId);
expect(service.getDriver).toHaveBeenCalledWith(driverId);
expect(result).toEqual(driver);
});
});
describe('getDriverProfile', () => {
it('should return driver profile', async () => {
const driverId = 'driver-123';
const profile: GetDriverProfileOutputDTO = { id: driverId, bio: 'Bio' };
service.getDriverProfile.mockResolvedValue(profile);
const result = await controller.getDriverProfile(driverId);
expect(service.getDriverProfile).toHaveBeenCalledWith(driverId);
expect(result).toEqual(profile);
});
});
describe('updateDriverProfile', () => {
it('should update driver profile', async () => {
const driverId = 'driver-123';
const body = { bio: 'New bio', country: 'US' };
const updated: GetDriverOutputDTO = { id: driverId, name: 'Driver' };
service.updateDriverProfile.mockResolvedValue(updated);
const result = await controller.updateDriverProfile(driverId, body);
expect(service.updateDriverProfile).toHaveBeenCalledWith(driverId, body.bio, body.country);
expect(result).toEqual(updated);
});
});
});

View File

@@ -1,6 +1,10 @@
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common'; import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
interface AuthenticatedRequest extends Request {
user?: { userId: string };
}
import { DriverService } from './DriverService'; import { DriverService } from './DriverService';
import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
import { DriverStatsDTO } from './dtos/DriverStatsDTO'; import { DriverStatsDTO } from './dtos/DriverStatsDTO';
@@ -35,9 +39,9 @@ export class DriverController {
@ApiOperation({ summary: 'Get current authenticated driver' }) @ApiOperation({ summary: 'Get current authenticated driver' })
@ApiResponse({ status: 200, description: 'Current driver data', type: GetDriverOutputDTO }) @ApiResponse({ status: 200, description: 'Current driver data', type: GetDriverOutputDTO })
@ApiResponse({ status: 404, description: 'Driver not found' }) @ApiResponse({ status: 404, description: 'Driver not found' })
async getCurrentDriver(@Req() req: Request): Promise<GetDriverOutputDTO | null> { async getCurrentDriver(@Req() req: AuthenticatedRequest): Promise<GetDriverOutputDTO | null> {
// Assuming userId is available from the request (e.g., via auth middleware) // Assuming userId is available from the request (e.g., via auth middleware)
const userId = req['user']?.userId; const userId = req.user?.userId;
if (!userId) { if (!userId) {
return null; return null;
} }
@@ -49,10 +53,10 @@ export class DriverController {
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutputDTO }) @ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutputDTO })
async completeOnboarding( async completeOnboarding(
@Body() input: CompleteOnboardingInputDTO, @Body() input: CompleteOnboardingInputDTO,
@Req() req: Request, @Req() req: AuthenticatedRequest,
): Promise<CompleteOnboardingOutputDTO> { ): Promise<CompleteOnboardingOutputDTO> {
// Assuming userId is available from the request (e.g., via auth middleware) // Assuming userId is available from the request (e.g., via auth middleware)
const userId = req['user'].userId; // Placeholder for actual user extraction const userId = req.user!.userId; // Placeholder for actual user extraction
return this.driverService.completeOnboarding(userId, input); return this.driverService.completeOnboarding(userId, input);
} }

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DriverModule } from './DriverModule';
import { DriverController } from './DriverController';
import { DriverService } from './DriverService';
describe('DriverModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [DriverModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide DriverController', () => {
const controller = module.get<DriverController>(DriverController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(DriverController);
});
it('should provide DriverService', () => {
const service = module.get<DriverService>(DriverService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(DriverService);
});
});

View File

@@ -9,25 +9,24 @@ import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatin
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository'; import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
import type { Logger } from "@gridpilot/core/shared/application"; import type { Logger } from "@core/shared/application";
// Import use cases // Import use cases
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { InMemoryDriverRepository } from '../../..//racing/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryRankingService } from '../../..//racing/services/InMemoryRankingService'; import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankingService';
import { InMemoryDriverStatsService } from '../../..//racing/services/InMemoryDriverStatsService'; import { InMemoryDriverStatsService } from '@adapters/racing/services/InMemoryDriverStatsService';
import { InMemoryDriverRatingProvider } from '../../..//racing/ports/InMemoryDriverRatingProvider'; import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
import { InMemoryImageServiceAdapter } from '../../..//media/ports/InMemoryImageServiceAdapter'; import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { InMemoryRaceRegistrationRepository } from '../../..//racing/persistence/inmemory/InMemoryRaceRegistrationRepository'; import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryNotificationPreferenceRepository } from '../../..//notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository'; import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
import { ConsoleLogger } from '../../..//logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Define injection tokens // Define injection tokens
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository'; export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
@@ -44,8 +43,6 @@ export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseC
export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase'; export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase';
export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase'; export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase';
export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase'; export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase';
export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
export const GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase'; export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
export const DriverProviders: Provider[] = [ export const DriverProviders: Provider[] = [
@@ -92,14 +89,14 @@ export const DriverProviders: Provider[] = [
// Use cases // Use cases
{ {
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort) => useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort, logger: Logger) =>
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService), new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger),
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN, provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository) => new GetTotalDriversUseCase(driverRepo), useFactory: (driverRepo: IDriverRepository, logger: Logger) => new GetTotalDriversUseCase(driverRepo, logger),
inject: [DRIVER_REPOSITORY_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
@@ -108,8 +105,8 @@ export const DriverProviders: Provider[] = [
}, },
{ {
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
useFactory: (registrationRepo: IRaceRegistrationRepository) => new IsDriverRegisteredForRaceUseCase(registrationRepo), useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) => new IsDriverRegisteredForRaceUseCase(registrationRepo, logger),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN], inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,

View File

@@ -1,18 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { DriverService } from './DriverService'; import { DriverService } from './DriverService';
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
describe('DriverService', () => { describe('DriverService', () => {
let service: DriverService; let service: DriverService;
let getDriversLeaderboardUseCase: jest.Mocked<GetDriversLeaderboardUseCase>; let getDriversLeaderboardUseCase: ReturnType<typeof vi.mocked<GetDriversLeaderboardUseCase>>;
let getTotalDriversUseCase: jest.Mocked<GetTotalDriversUseCase>; let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>;
let completeDriverOnboardingUseCase: jest.Mocked<CompleteDriverOnboardingUseCase>; let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>;
let isDriverRegisteredForRaceUseCase: jest.Mocked<IsDriverRegisteredForRaceUseCase>; let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>;
let logger: jest.Mocked<Logger>; let updateDriverProfileUseCase: ReturnType<typeof vi.mocked<UpdateDriverProfileUseCase>>;
let driverRepository: ReturnType<typeof vi.mocked<IDriverRepository>>;
let logger: ReturnType<typeof vi.mocked<Logger>>;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@@ -21,42 +26,57 @@ describe('DriverService', () => {
{ {
provide: 'GetDriversLeaderboardUseCase', provide: 'GetDriversLeaderboardUseCase',
useValue: { useValue: {
execute: jest.fn(), execute: vi.fn(),
}, },
}, },
{ {
provide: 'GetTotalDriversUseCase', provide: 'GetTotalDriversUseCase',
useValue: { useValue: {
execute: jest.fn(), execute: vi.fn(),
}, },
}, },
{ {
provide: 'CompleteDriverOnboardingUseCase', provide: 'CompleteDriverOnboardingUseCase',
useValue: { useValue: {
execute: jest.fn(), execute: vi.fn(),
}, },
}, },
{ {
provide: 'IsDriverRegisteredForRaceUseCase', provide: 'IsDriverRegisteredForRaceUseCase',
useValue: { useValue: {
execute: jest.fn(), execute: vi.fn(),
},
},
{
provide: 'UpdateDriverProfileUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'IDriverRepository',
useValue: {
findById: vi.fn(),
}, },
}, },
{ {
provide: 'Logger', provide: 'Logger',
useValue: { useValue: {
debug: jest.fn(), debug: vi.fn(),
error: vi.fn(),
}, },
}, },
], ],
}).compile(); }).compile();
service = module.get<DriverService>(DriverService); service = module.get<DriverService>(DriverService);
getDriversLeaderboardUseCase = module.get('GetDriversLeaderboardUseCase'); getDriversLeaderboardUseCase = vi.mocked(module.get('GetDriversLeaderboardUseCase'));
getTotalDriversUseCase = module.get('GetTotalDriversUseCase'); getTotalDriversUseCase = vi.mocked(module.get('GetTotalDriversUseCase'));
completeDriverOnboardingUseCase = module.get('CompleteDriverOnboardingUseCase'); completeDriverOnboardingUseCase = vi.mocked(module.get('CompleteDriverOnboardingUseCase'));
isDriverRegisteredForRaceUseCase = module.get('IsDriverRegisteredForRaceUseCase'); isDriverRegisteredForRaceUseCase = vi.mocked(module.get('IsDriverRegisteredForRaceUseCase'));
logger = module.get('Logger'); updateDriverProfileUseCase = vi.mocked(module.get('UpdateDriverProfileUseCase'));
driverRepository = vi.mocked(module.get('IDriverRepository'));
logger = vi.mocked(module.get('Logger'));
}); });
describe('getDriversLeaderboard', () => { describe('getDriversLeaderboard', () => {

View File

@@ -143,7 +143,12 @@ export class DriverProfileSocialSummaryDTO {
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord'; export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
export type DriverProfileAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary'; export enum DriverProfileAchievementRarity {
COMMON = 'common',
RARE = 'rare',
EPIC = 'epic',
LEGENDARY = 'legendary',
}
export class DriverProfileAchievementDTO { export class DriverProfileAchievementDTO {
@ApiProperty() @ApiProperty()

View File

@@ -1,37 +1,34 @@
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common'; import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger'; import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { LeagueService } from './LeagueService'; import { LeagueService } from './LeagueService';
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO'; import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO'; import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO'; import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO'; import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO'; import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO'; import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO'; import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO'; import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO'; import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO'; import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO'; import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
import { GetLeagueJoinRequestsQueryDTO } from './dtos/GetLeagueJoinRequestsQueryDTO';
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO'; import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO'; import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
@ApiTags('leagues') @ApiTags('leagues')
@Controller('leagues') @Controller('leagues')
@@ -104,8 +101,7 @@ export class LeagueController {
async removeLeagueMember( async removeLeagueMember(
@Param('leagueId') leagueId: string, @Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string, @Param('performerDriverId') performerDriverId: string,
@Param('targetDriverId') targetDriverId: string, @Param('targetDriverId') targetDriverId: string, // Body content for a patch often includes IDs
@Body() input: RemoveLeagueMemberInputDTO, // Body content for a patch often includes IDs
): Promise<RemoveLeagueMemberOutputDTO> { ): Promise<RemoveLeagueMemberOutputDTO> {
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId }); return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
} }
@@ -133,27 +129,27 @@ export class LeagueController {
@Param('leagueId') leagueId: string, @Param('leagueId') leagueId: string,
@Param('ownerId') ownerId: string, @Param('ownerId') ownerId: string,
): Promise<LeagueOwnerSummaryDTO | null> { ): Promise<LeagueOwnerSummaryDTO | null> {
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId }; const query: GetLeagueOwnerSummaryQueryDTO = { ownerId, leagueId };
return this.leagueService.getLeagueOwnerSummary(query); return this.leagueService.getLeagueOwnerSummary(query);
} }
@Get(':leagueId/config') @Get(':leagueId/config')
@ApiOperation({ summary: 'Get league full configuration' }) @ApiOperation({ summary: 'Get league full configuration' })
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO }) @ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO })
async getLeagueFullConfig( async getLeagueFullConfig(
@Param('leagueId') leagueId: string, @Param('leagueId') leagueId: string,
): Promise<LeagueConfigFormModelDTO | null> { ): Promise<LeagueConfigFormModelDTO | null> {
const query: GetLeagueAdminConfigQuery = { leagueId }; const query: GetLeagueAdminConfigQueryDTO = { leagueId };
return this.leagueService.getLeagueFullConfig(query); return this.leagueService.getLeagueFullConfig(query);
} }
@Get(':leagueId/protests') @Get(':leagueId/protests')
@ApiOperation({ summary: 'Get protests for a league' }) @ApiOperation({ summary: 'Get protests for a league' })
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsDTO }) @ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsDTO })
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsDTO> { async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsDTO> {
const query: GetLeagueProtestsQuery = { leagueId }; const query: GetLeagueProtestsQueryDTO = { leagueId };
return this.leagueService.getLeagueProtests(query); return this.leagueService.getLeagueProtests(query);
} }
@Get(':leagueId/protests/:protestId') @Get(':leagueId/protests/:protestId')
@ApiOperation({ summary: 'Get a specific protest for a league' }) @ApiOperation({ summary: 'Get a specific protest for a league' })
@@ -162,7 +158,7 @@ export class LeagueController {
@Param('leagueId') leagueId: string, @Param('leagueId') leagueId: string,
@Param('protestId') protestId: string, @Param('protestId') protestId: string,
): Promise<LeagueAdminProtestsDTO> { ): Promise<LeagueAdminProtestsDTO> {
const query: GetLeagueProtestsQuery = { leagueId }; const query: GetLeagueProtestsQueryDTO = { leagueId };
const allProtests = await this.leagueService.getLeagueProtests(query); const allProtests = await this.leagueService.getLeagueProtests(query);
// Filter to only include the specific protest // Filter to only include the specific protest
@@ -187,12 +183,12 @@ export class LeagueController {
} }
@Get(':leagueId/seasons') @Get(':leagueId/seasons')
@ApiOperation({ summary: 'Get seasons for a league' }) @ApiOperation({ summary: 'Get seasons for a league' })
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryDTO] }) @ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryDTO] })
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryDTO[]> { async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
const query: GetLeagueSeasonsQuery = { leagueId }; const query: GetLeagueSeasonsQueryDTO = { leagueId };
return this.leagueService.getLeagueSeasons(query); return this.leagueService.getLeagueSeasons(query);
} }
@Get(':leagueId/memberships') @Get(':leagueId/memberships')
@ApiOperation({ summary: 'Get league memberships' }) @ApiOperation({ summary: 'Get league memberships' })

View File

@@ -19,7 +19,8 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases // Import use cases
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase'; import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase'; import { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStandingsUseCaseImpl } from '@core/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase'; import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase'; import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase'; import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
@@ -49,6 +50,7 @@ export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository'; export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
export const LeagueProviders: Provider[] = [ export const LeagueProviders: Provider[] = [
LeagueService, // Provide the service itself LeagueService, // Provide the service itself
@@ -108,7 +110,10 @@ export const LeagueProviders: Provider[] = [
}, },
// Use cases // Use cases
GetAllLeaguesWithCapacityUseCase, GetAllLeaguesWithCapacityUseCase,
GetLeagueStandingsUseCase, {
provide: GET_LEAGUE_STANDINGS_USE_CASE,
useClass: GetLeagueStandingsUseCaseImpl,
},
GetLeagueStatsUseCase, GetLeagueStatsUseCase,
GetLeagueFullConfigUseCase, GetLeagueFullConfigUseCase,
CreateLeagueWithSeasonAndScoringUseCase, CreateLeagueWithSeasonAndScoringUseCase,

View File

@@ -1,81 +1,111 @@
import { Injectable, Inject } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO'; import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO'; import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO'; import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
import { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
import { TransferLeagueOwnershipOutputDTO } from './dtos/TransferLeagueOwnershipOutputDTO';
import { LeagueJoinRequestWithDriverDTO } from './dtos/LeagueJoinRequestWithDriverDTO';
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO'; import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO'; import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO'; import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO'; import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO'; import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO'; import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO'; // Core imports for entities
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO'; import type { League } from '@core/racing/domain/entities/League';
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO'; // Core imports for view models
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO'; import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO'; import type { LeagueScoringPresetsViewModel } from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO'; import type { AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO'; import type { GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter';
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO'; import type { GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter';
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO'; import type { ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter';
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO'; import type { RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter';
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO'; import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter';
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO'; import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter';
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO'; import type { UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter';
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO'; import type { GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter';
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO'; import type { GetLeagueProtestsViewModel } from '@core/racing/application/presenters/IGetLeagueProtestsPresenter';
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO'; import type { GetLeagueSeasonsViewModel } from '@core/racing/application/presenters/IGetLeagueSeasonsPresenter';
import type { GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter';
import type { LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter';
import type { LeagueScheduleViewModel } from '@core/racing/application/presenters/ILeagueSchedulePresenter';
import type { LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter';
import type { LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter';
import type { CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter';
import type { JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter';
import type { TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
// Use cases // Use cases
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase'; import { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase'; import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase'; import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/RemoveLeagueMemberUseCase'; import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase'; import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/GetLeagueMembershipsUseCase';
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase'; import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase'; import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/GetLeagueMembershipsUseCase';
import { GetLeagueScheduleUseCase } from '@core/racing/application/use-cases/GetLeagueScheduleUseCase'; import { GetLeagueScheduleUseCase } from '@core/racing/application/use-cases/GetLeagueScheduleUseCase';
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase'; import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/RemoveLeagueMemberUseCase';
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
// API Presenters // API Presenters
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter'; import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter'; import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter';
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter'; import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter'; import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter'; import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter'; import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter'; import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter';
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter'; import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter'; import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter'; import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter'; import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter';
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter'; import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter';
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter'; import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter'; import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter';
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter';
import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter';
// Tokens // Tokens
import { LOGGER_TOKEN } from './LeagueProviders'; import { LOGGER_TOKEN } from './LeagueProviders';
@@ -111,127 +141,173 @@ export class LeagueService {
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> { async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
this.logger.debug('[LeagueService] Fetching all leagues with capacity.'); this.logger.debug('[LeagueService] Fetching all leagues with capacity.');
const result = await this.getAllLeaguesWithCapacityUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new AllLeaguesWithCapacityPresenter(); const presenter = new AllLeaguesWithCapacityPresenter();
await this.getAllLeaguesWithCapacityUseCase.execute(undefined, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getTotalLeagues(): Promise<LeagueStatsDto> { async getTotalLeagues(): Promise<GetTotalLeaguesViewModel> {
this.logger.debug('[LeagueService] Fetching total leagues count.'); this.logger.debug('[LeagueService] Fetching total leagues count.');
const result = await this.getTotalLeaguesUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new TotalLeaguesPresenter(); const presenter = new TotalLeaguesPresenter();
await this.getTotalLeaguesUseCase.execute({}, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> { async getLeagueJoinRequests(leagueId: string): Promise<GetLeagueJoinRequestsViewModel> {
this.logger.debug(`[LeagueService] Fetching join requests for league: ${leagueId}.`); this.logger.debug(`[LeagueService] Fetching join requests for league: ${leagueId}.`);
const result = await this.getLeagueJoinRequestsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new LeagueJoinRequestsPresenter(); const presenter = new LeagueJoinRequestsPresenter();
await this.getLeagueJoinRequestsUseCase.execute({ leagueId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!.joinRequests; return presenter.getViewModel();
} }
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> { async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestViewModel> {
this.logger.debug('Approving join request:', input); this.logger.debug('Approving join request:', input);
const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new ApproveLeagueJoinRequestPresenter(); const presenter = new ApproveLeagueJoinRequestPresenter();
await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel();
} }
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> { async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectLeagueJoinRequestViewModel> {
this.logger.debug('Rejecting join request:', input); this.logger.debug('Rejecting join request:', input);
const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new RejectLeagueJoinRequestPresenter(); const presenter = new RejectLeagueJoinRequestPresenter();
await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel();
} }
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> { async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise<GetLeagueAdminPermissionsViewModel> {
this.logger.debug('Getting league admin permissions', { query }); this.logger.debug('Getting league admin permissions', { query });
const result = await this.getLeagueAdminPermissionsUseCase.execute({ leagueId: query.leagueId, performerDriverId: query.performerDriverId });
// This use case never errors
const presenter = new GetLeagueAdminPermissionsPresenter(); const presenter = new GetLeagueAdminPermissionsPresenter();
await this.getLeagueAdminPermissionsUseCase.execute( presenter.present(result.unwrap());
{ leagueId: query.leagueId, performerDriverId: query.performerDriverId },
presenter
);
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> { async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise<RemoveLeagueMemberViewModel> {
this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId }); this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId });
const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new RemoveLeagueMemberPresenter(); const presenter = new RemoveLeagueMemberPresenter();
await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel();
} }
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> { async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise<UpdateLeagueMemberRoleViewModel> {
this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }); this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new UpdateLeagueMemberRolePresenter(); const presenter = new UpdateLeagueMemberRolePresenter();
await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel();
} }
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> { async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<GetLeagueOwnerSummaryViewModel> {
this.logger.debug('Getting league owner summary:', query); this.logger.debug('Getting league owner summary:', query);
const result = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new GetLeagueOwnerSummaryPresenter(); const presenter = new GetLeagueOwnerSummaryPresenter();
await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!.summary; return presenter.getViewModel();
} }
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> { async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormViewModel | null> {
this.logger.debug('Getting league full config', { query }); this.logger.debug('Getting league full config', { query });
const presenter = new LeagueConfigPresenter();
try { try {
await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId }, presenter); const result = await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId });
return presenter.viewModel; if (result.isErr()) {
this.logger.error('Error getting league full config', new Error(result.unwrapErr().code));
return null;
}
return result.unwrap();
} catch (error) { } catch (error) {
this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error))); this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error)));
return null; return null;
} }
} }
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> { async getLeagueProtests(query: GetLeagueProtestsQueryDTO): Promise<LeagueAdminProtestsDTO> {
this.logger.debug('Getting league protests:', query); this.logger.debug('Getting league protests:', query);
const presenter = new GetLeagueProtestsPresenter(); const result = await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId });
await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId }, presenter); if (result.isErr()) {
return presenter.getViewModel()!; throw new Error(result.unwrapErr().code);
}
return result.unwrap();
} }
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> { async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
this.logger.debug('Getting league seasons:', query); this.logger.debug('Getting league seasons:', query);
const presenter = new GetLeagueSeasonsPresenter(); const result = await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId });
await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId }, presenter); if (result.isErr()) {
return presenter.getViewModel()!.seasons; throw new Error(result.unwrapErr().code);
}
return result.unwrap().seasons;
} }
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsViewModel> { async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
this.logger.debug('Getting league memberships', { leagueId }); this.logger.debug('Getting league memberships', { leagueId });
const presenter = new GetLeagueMembershipsPresenter(); const result = await this.getLeagueMembershipsUseCase.execute({ leagueId });
await this.getLeagueMembershipsUseCase.execute({ leagueId }, presenter); if (result.isErr()) {
return presenter.apiViewModel!; throw new Error(result.unwrapErr().code);
}
return result.unwrap();
} }
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> { async getLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
this.logger.debug('Getting league standings', { leagueId }); this.logger.debug('Getting league standings', { leagueId });
const presenter = new LeagueStandingsPresenter(); return await this.getLeagueStandingsUseCase.execute(leagueId);
await this.getLeagueStandingsUseCase.execute({ leagueId }, presenter);
return presenter.getViewModel()!;
} }
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> { async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
this.logger.debug('Getting league schedule', { leagueId }); this.logger.debug('Getting league schedule', { leagueId });
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new LeagueSchedulePresenter(); const presenter = new LeagueSchedulePresenter();
await this.getLeagueScheduleUseCase.execute({ leagueId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getLeagueStats(leagueId: string): Promise<LeagueStatsViewModel> { async getLeagueStats(leagueId: string): Promise<LeagueStatsViewModel> {
this.logger.debug('Getting league stats', { leagueId }); this.logger.debug('Getting league stats', { leagueId });
const result = await this.getLeagueStatsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new LeagueStatsPresenter(); const presenter = new LeagueStatsPresenter();
await this.getLeagueStatsUseCase.execute({ leagueId }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminViewModel> { async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
this.logger.debug('Getting league admin data', { leagueId }); this.logger.debug('Getting league admin data', { leagueId });
// For now, we'll keep the orchestration in the service since it combines multiple use cases // For now, we'll keep the orchestration in the service since it combines multiple use cases
// TODO: Create a composite use case that handles all the admin data fetching // TODO: Create a composite use case that handles all the admin data fetching
@@ -253,7 +329,7 @@ export class LeagueService {
}; };
} }
async createLeague(input: CreateLeagueInput): Promise<CreateLeagueOutput> { async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
this.logger.debug('Creating league', { input }); this.logger.debug('Creating league', { input });
const command = { const command = {
name: input.name, name: input.name,
@@ -268,10 +344,12 @@ export class LeagueService {
enableTrophyChampionship: false, enableTrophyChampionship: false,
}; };
const result = await this.createLeagueWithSeasonAndScoringUseCase.execute(command); const result = await this.createLeagueWithSeasonAndScoringUseCase.execute(command);
return { if (result.isErr()) {
leagueId: result.leagueId, throw new Error(result.unwrapErr().code);
success: true, }
}; const presenter = new CreateLeaguePresenter();
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async getLeagueScoringConfig(leagueId: string): Promise<LeagueScoringConfigViewModel | null> { async getLeagueScoringConfig(leagueId: string): Promise<LeagueScoringConfigViewModel | null> {
@@ -281,10 +359,10 @@ export class LeagueService {
try { try {
const result = await this.getLeagueScoringConfigUseCase.execute({ leagueId }); const result = await this.getLeagueScoringConfigUseCase.execute({ leagueId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error('Error getting league scoring config', result.error); this.logger.error('Error getting league scoring config', new Error(result.unwrapErr().code));
return null; return null;
} }
await presenter.present(result.value); await presenter.present(result.unwrap());
return presenter.getViewModel(); return presenter.getViewModel();
} catch (error) { } catch (error) {
this.logger.error('Error getting league scoring config', error instanceof Error ? error : new Error(String(error))); this.logger.error('Error getting league scoring config', error instanceof Error ? error : new Error(String(error)));
@@ -295,40 +373,46 @@ 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');
const result = await this.listLeagueScoringPresetsUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new LeagueScoringPresetsPresenter(); const presenter = new LeagueScoringPresetsPresenter();
await this.listLeagueScoringPresetsUseCase.execute(undefined, presenter); await presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueOutput> { async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueViewModel> {
this.logger.debug('Joining league', { leagueId, driverId }); this.logger.debug('Joining league', { leagueId, driverId });
const result = await this.joinLeagueUseCase.execute({ leagueId, driverId }); const result = await this.joinLeagueUseCase.execute({ leagueId, driverId });
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr();
return { return {
success: false, success: false,
error: result.error.code, error: error.code,
}; };
} }
return { const presenter = new JoinLeaguePresenter();
success: true, presenter.present(result.unwrap());
membershipId: result.value.id, return presenter.getViewModel();
};
} }
async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipOutput> { async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipViewModel> {
this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId }); this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId });
const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId }); const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr();
return { return {
success: false, success: false,
error: result.error.code, error: error.code,
}; };
} }
return { const presenter = new TransferLeagueOwnershipPresenter();
success: true, presenter.present(result.unwrap());
}; return presenter.getViewModel();
} }
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> { async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {

View File

@@ -0,0 +1,5 @@
export interface JoinLeagueOutputDTO {
success: boolean;
error?: string;
membershipId?: string;
}

View File

@@ -23,9 +23,9 @@ export class LeagueConfigFormModelDTO {
@Type(() => LeagueConfigFormModelStructureDTO) @Type(() => LeagueConfigFormModelStructureDTO)
structure: LeagueConfigFormModelStructureDTO; structure: LeagueConfigFormModelStructureDTO;
@ApiProperty({ type: [Object] }) @ApiProperty({ type: [Object] })
@IsArray() @IsArray()
championships: any[]; championships: Object[];
@ApiProperty({ type: LeagueConfigFormModelScoringDTO }) @ApiProperty({ type: LeagueConfigFormModelScoringDTO })
@ValidateNested() @ValidateNested()

View File

@@ -0,0 +1,11 @@
export interface LeagueJoinRequestWithDriverDTO {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message?: string;
driver: {
id: string;
name: string;
};
}

View File

@@ -0,0 +1,4 @@
export interface TransferLeagueOwnershipOutputDTO {
success: boolean;
error?: string;
}

View File

@@ -0,0 +1,21 @@
import { ICreateLeaguePresenter, CreateLeagueResultDTO, CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter';
export class CreateLeaguePresenter implements ICreateLeaguePresenter {
private result: CreateLeagueViewModel | null = null;
reset() {
this.result = null;
}
present(dto: CreateLeagueResultDTO): void {
this.result = {
leagueId: dto.leagueId,
success: true,
};
}
getViewModel(): CreateLeagueViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,21 @@
import { IJoinLeaguePresenter, JoinLeagueResultDTO, JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter';
export class JoinLeaguePresenter implements IJoinLeaguePresenter {
private result: JoinLeagueViewModel | null = null;
reset() {
this.result = null;
}
present(dto: JoinLeagueResultDTO): void {
this.result = {
success: true,
membershipId: dto.id,
};
}
getViewModel(): JoinLeagueViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -8,11 +8,11 @@ export class LeagueAdminPresenter {
} }
present(data: { present(data: {
joinRequests: any[]; joinRequests: unknown[];
ownerSummary: any; ownerSummary: unknown;
config: any; config: unknown;
protests: any; protests: unknown;
seasons: any[]; seasons: unknown[];
}) { }) {
this.result = { this.result = {
joinRequests: data.joinRequests, joinRequests: data.joinRequests,

View File

@@ -0,0 +1,20 @@
import { ITransferLeagueOwnershipPresenter, TransferLeagueOwnershipResultDTO, TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter';
export class TransferLeagueOwnershipPresenter implements ITransferLeagueOwnershipPresenter {
private result: TransferLeagueOwnershipViewModel | null = null;
reset() {
this.result = null;
}
present(dto: TransferLeagueOwnershipResultDTO): void {
this.result = {
success: dto.success,
};
}
getViewModel(): TransferLeagueOwnershipViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,181 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { MediaController } from './MediaController';
import { MediaService } from './MediaService';
import type { Response } from 'express';
import { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
import { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO';
describe('MediaController', () => {
let controller: MediaController;
let service: ReturnType<typeof vi.mocked<MediaService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MediaController],
providers: [
{
provide: MediaService,
useValue: {
requestAvatarGeneration: vi.fn(),
uploadMedia: vi.fn(),
getMedia: vi.fn(),
deleteMedia: vi.fn(),
getAvatar: vi.fn(),
updateAvatar: vi.fn(),
},
},
],
}).compile();
controller = module.get<MediaController>(MediaController);
service = vi.mocked(module.get(MediaService));
});
describe('requestAvatarGeneration', () => {
it('should request avatar generation and return 201 on success', async () => {
const input: RequestAvatarGenerationInputDTO = { driverId: 'driver-123' };
const result = { success: true, jobId: 'job-123' };
service.requestAvatarGeneration.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.requestAvatarGeneration(input, mockRes);
expect(service.requestAvatarGeneration).toHaveBeenCalledWith(input);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
it('should return 400 on failure', async () => {
const input: RequestAvatarGenerationInputDTO = { driverId: 'driver-123' };
const result = { success: false, error: 'Error' };
service.requestAvatarGeneration.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.requestAvatarGeneration(input, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
});
describe('uploadMedia', () => {
it('should upload media and return 201 on success', async () => {
const file: Express.Multer.File = { filename: 'file.jpg' } as Express.Multer.File;
const input: UploadMediaInputDTO = { type: 'image' };
const result = { success: true, mediaId: 'media-123' };
service.uploadMedia.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.uploadMedia(file, input, mockRes);
expect(service.uploadMedia).toHaveBeenCalledWith({ ...input, file });
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
});
describe('getMedia', () => {
it('should return media if found', async () => {
const mediaId = 'media-123';
const result = { id: mediaId, url: 'url' };
service.getMedia.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.getMedia(mediaId, mockRes);
expect(service.getMedia).toHaveBeenCalledWith(mediaId);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
it('should return 404 if not found', async () => {
const mediaId = 'media-123';
service.getMedia.mockResolvedValue(null);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.getMedia(mediaId, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(404);
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Media not found' });
});
});
describe('deleteMedia', () => {
it('should delete media', async () => {
const mediaId = 'media-123';
const result = { success: true };
service.deleteMedia.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.deleteMedia(mediaId, mockRes);
expect(service.deleteMedia).toHaveBeenCalledWith(mediaId);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
});
describe('getAvatar', () => {
it('should return avatar if found', async () => {
const driverId = 'driver-123';
const result = { url: 'avatar.jpg' };
service.getAvatar.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.getAvatar(driverId, mockRes);
expect(service.getAvatar).toHaveBeenCalledWith(driverId);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
});
describe('updateAvatar', () => {
it('should update avatar', async () => {
const driverId = 'driver-123';
const input = { url: 'new-avatar.jpg' };
const result = { success: true };
service.updateAvatar.mockResolvedValue(result);
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as ReturnType<typeof vi.mocked<Response>>;
await controller.updateAvatar(driverId, input, mockRes);
expect(service.updateAvatar).toHaveBeenCalledWith(driverId, input);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith(result);
});
});
});

View File

@@ -3,25 +3,19 @@ import { ApiTags, ApiResponse, ApiOperation, ApiParam, ApiConsumes } from '@nest
import { Response } from 'express'; import { Response } from 'express';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { MediaService } from './MediaService'; import { MediaService } from './MediaService';
import type { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO'; import { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
import type { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO'; import { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO';
import type { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO'; import { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO';
import type { UploadMediaOutputDTO } from './dtos/UploadMediaOutputDTO'; import { UploadMediaOutputDTO } from './dtos/UploadMediaOutputDTO';
import type { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO'; import { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO';
import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO'; import { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO';
import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO'; import { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO';
import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO'; import { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO';
import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO'; import { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO';
type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO; type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
type UploadMediaInput = UploadMediaInputDTO; type UploadMediaInput = UploadMediaInputDTO;
type UploadMediaOutput = UploadMediaOutputDTO;
type GetMediaOutput = GetMediaOutputDTO;
type DeleteMediaOutput = DeleteMediaOutputDTO;
type GetAvatarOutput = GetAvatarOutputDTO;
type UpdateAvatarInput = UpdateAvatarInputDTO; type UpdateAvatarInput = UpdateAvatarInputDTO;
type UpdateAvatarOutput = UpdateAvatarOutputDTO;
@ApiTags('media') @ApiTags('media')
@Controller('media') @Controller('media')
@@ -30,7 +24,7 @@ export class MediaController {
@Post('avatar/generate') @Post('avatar/generate')
@ApiOperation({ summary: 'Request avatar generation' }) @ApiOperation({ summary: 'Request avatar generation' })
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput }) @ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutputDTO })
async requestAvatarGeneration( async requestAvatarGeneration(
@Body() input: RequestAvatarGenerationInput, @Body() input: RequestAvatarGenerationInput,
@Res() res: Response, @Res() res: Response,
@@ -47,7 +41,7 @@ export class MediaController {
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: 'Upload media file' }) @ApiOperation({ summary: 'Upload media file' })
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiResponse({ status: 201, description: 'Media uploaded successfully', type: UploadMediaOutput }) @ApiResponse({ status: 201, description: 'Media uploaded successfully', type: UploadMediaOutputDTO })
async uploadMedia( async uploadMedia(
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
@Body() input: UploadMediaInput, @Body() input: UploadMediaInput,
@@ -64,7 +58,7 @@ export class MediaController {
@Get(':mediaId') @Get(':mediaId')
@ApiOperation({ summary: 'Get media by ID' }) @ApiOperation({ summary: 'Get media by ID' })
@ApiParam({ name: 'mediaId', description: 'Media ID' }) @ApiParam({ name: 'mediaId', description: 'Media ID' })
@ApiResponse({ status: 200, description: 'Media details', type: GetMediaOutput }) @ApiResponse({ status: 200, description: 'Media details', type: GetMediaOutputDTO })
async getMedia( async getMedia(
@Param('mediaId') mediaId: string, @Param('mediaId') mediaId: string,
@Res() res: Response, @Res() res: Response,
@@ -80,7 +74,7 @@ export class MediaController {
@Delete(':mediaId') @Delete(':mediaId')
@ApiOperation({ summary: 'Delete media by ID' }) @ApiOperation({ summary: 'Delete media by ID' })
@ApiParam({ name: 'mediaId', description: 'Media ID' }) @ApiParam({ name: 'mediaId', description: 'Media ID' })
@ApiResponse({ status: 200, description: 'Media deleted', type: DeleteMediaOutput }) @ApiResponse({ status: 200, description: 'Media deleted', type: DeleteMediaOutputDTO })
async deleteMedia( async deleteMedia(
@Param('mediaId') mediaId: string, @Param('mediaId') mediaId: string,
@Res() res: Response, @Res() res: Response,
@@ -92,7 +86,7 @@ export class MediaController {
@Get('avatar/:driverId') @Get('avatar/:driverId')
@ApiOperation({ summary: 'Get avatar for driver' }) @ApiOperation({ summary: 'Get avatar for driver' })
@ApiParam({ name: 'driverId', description: 'Driver ID' }) @ApiParam({ name: 'driverId', description: 'Driver ID' })
@ApiResponse({ status: 200, description: 'Avatar details', type: GetAvatarOutput }) @ApiResponse({ status: 200, description: 'Avatar details', type: GetAvatarOutputDTO })
async getAvatar( async getAvatar(
@Param('driverId') driverId: string, @Param('driverId') driverId: string,
@Res() res: Response, @Res() res: Response,
@@ -108,7 +102,7 @@ export class MediaController {
@Put('avatar/:driverId') @Put('avatar/:driverId')
@ApiOperation({ summary: 'Update avatar for driver' }) @ApiOperation({ summary: 'Update avatar for driver' })
@ApiParam({ name: 'driverId', description: 'Driver ID' }) @ApiParam({ name: 'driverId', description: 'Driver ID' })
@ApiResponse({ status: 200, description: 'Avatar updated', type: UpdateAvatarOutput }) @ApiResponse({ status: 200, description: 'Avatar updated', type: UpdateAvatarOutputDTO })
async updateAvatar( async updateAvatar(
@Param('driverId') driverId: string, @Param('driverId') driverId: string,
@Body() input: UpdateAvatarInput, @Body() input: UpdateAvatarInput,

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MediaModule } from './MediaModule';
import { MediaController } from './MediaController';
import { MediaService } from './MediaService';
describe('MediaModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [MediaModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide MediaController', () => {
const controller = module.get<MediaController>(MediaController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(MediaController);
});
it('should provide MediaService', () => {
const service = module.get<MediaService>(MediaService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(MediaService);
});
});

View File

@@ -3,39 +3,82 @@ import { MediaService } from './MediaService';
// Import core interfaces // Import core interfaces
import { IAvatarGenerationRepository } from '@core/media/domain/repositories/IAvatarGenerationRepository'; import { IAvatarGenerationRepository } from '@core/media/domain/repositories/IAvatarGenerationRepository';
import { IMediaRepository } from '@core/media/domain/repositories/IMediaRepository';
import { IAvatarRepository } from '@core/media/domain/repositories/IAvatarRepository';
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 type { Logger } 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';
import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase';
import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase';
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Define injection tokens // Define injection tokens
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository'; export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
export const AVATAR_REPOSITORY_TOKEN = 'IAvatarRepository';
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort'; export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
export const AVATAR_GENERATION_PORT_TOKEN = 'AvatarGenerationPort'; export const AVATAR_GENERATION_PORT_TOKEN = 'AvatarGenerationPort';
export const MEDIA_STORAGE_PORT_TOKEN = 'MediaStoragePort';
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
// Use case tokens // Use case tokens
export const REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN = 'RequestAvatarGenerationUseCase'; export const REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN = 'RequestAvatarGenerationUseCase';
export const UPLOAD_MEDIA_USE_CASE_TOKEN = 'UploadMediaUseCase';
export const GET_MEDIA_USE_CASE_TOKEN = 'GetMediaUseCase';
export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase';
export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase';
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase';
import type { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
import type { Media } from '@core/media/domain/entities/Media';
import type { Avatar } from '@core/media/domain/entities/Avatar';
import type { FaceValidationResult } from '@core/media/application/ports/FaceValidationPort';
import type { AvatarGenerationResult } from '@core/media/application/ports/AvatarGenerationPort';
import type { UploadResult } from '@core/media/application/ports/MediaStoragePort';
// Mock implementations // Mock implementations
class MockAvatarGenerationRepository implements IAvatarGenerationRepository { class MockAvatarGenerationRepository implements IAvatarGenerationRepository {
async save(_request: any): Promise<void> {} async save(): Promise<void> {}
async findById(_id: string): Promise<any | null> { return null; } async findById(): Promise<AvatarGenerationRequest | null> { return null; }
async findByUserId(_userId: string): Promise<any[]> { return []; } async findByUserId(): Promise<AvatarGenerationRequest[]> { return []; }
async findLatestByUserId(_userId: string): Promise<any | null> { return null; } async findLatestByUserId(): Promise<AvatarGenerationRequest | null> { return null; }
async delete(_id: string): Promise<void> {} async delete(): Promise<void> {}
}
class MockMediaRepository implements IMediaRepository {
async save(): Promise<void> {}
async findById(): Promise<Media | null> { return null; }
async findByUploadedBy(): Promise<Media[]> { return []; }
async delete(): Promise<void> {}
}
class MockAvatarRepository implements IAvatarRepository {
async save(): Promise<void> {}
async findById(): Promise<Avatar | null> { return null; }
async findActiveByDriverId(): Promise<Avatar | null> { return null; }
async findByDriverId(): Promise<Avatar[]> { return []; }
async delete(): Promise<void> {}
} }
class MockFaceValidationAdapter implements FaceValidationPort { class MockFaceValidationAdapter implements FaceValidationPort {
async validateFacePhoto(data: string): Promise<any> { async validateFacePhoto(): Promise<FaceValidationResult> {
return { isValid: true, hasFace: true, faceCount: 1 }; return {
isValid: true,
hasFace: true,
faceCount: 1,
confidence: 0.95,
};
} }
} }
class MockAvatarGenerationAdapter implements AvatarGenerationPort { class MockAvatarGenerationAdapter implements AvatarGenerationPort {
async generateAvatars(options: any): Promise<any> { async generateAvatars(): Promise<AvatarGenerationResult> {
return { return {
success: true, success: true,
avatars: [ avatars: [
@@ -47,11 +90,22 @@ class MockAvatarGenerationAdapter implements AvatarGenerationPort {
} }
} }
class MockMediaStorageAdapter implements MediaStoragePort {
async uploadMedia(): Promise<UploadResult> {
return {
success: true,
url: 'https://cdn.example.com/media/mock-file.png',
filename: 'mock-file.png',
};
}
async deleteMedia(): Promise<void> {}
}
class MockLogger implements Logger { class MockLogger implements Logger {
debug(message: string, meta?: any): void {} debug(): void {}
info(message: string, meta?: any): void {} info(): void {}
warn(message: string, meta?: any): void {} warn(): void {}
error(message: string, error?: Error): void {} error(): void {}
} }
export const MediaProviders: Provider[] = [ export const MediaProviders: Provider[] = [
@@ -60,6 +114,14 @@ export const MediaProviders: Provider[] = [
provide: AVATAR_GENERATION_REPOSITORY_TOKEN, provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
useClass: MockAvatarGenerationRepository, useClass: MockAvatarGenerationRepository,
}, },
{
provide: MEDIA_REPOSITORY_TOKEN,
useClass: MockMediaRepository,
},
{
provide: AVATAR_REPOSITORY_TOKEN,
useClass: MockAvatarRepository,
},
{ {
provide: FACE_VALIDATION_PORT_TOKEN, provide: FACE_VALIDATION_PORT_TOKEN,
useClass: MockFaceValidationAdapter, useClass: MockFaceValidationAdapter,
@@ -68,6 +130,10 @@ export const MediaProviders: Provider[] = [
provide: AVATAR_GENERATION_PORT_TOKEN, provide: AVATAR_GENERATION_PORT_TOKEN,
useClass: MockAvatarGenerationAdapter, useClass: MockAvatarGenerationAdapter,
}, },
{
provide: MEDIA_STORAGE_PORT_TOKEN,
useClass: MockMediaStorageAdapter,
},
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: MockLogger, useClass: MockLogger,
@@ -79,4 +145,34 @@ export const MediaProviders: Provider[] = [
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger), new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger),
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_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,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
new UploadMediaUseCase(mediaRepo, mediaStorage, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: GET_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, logger: Logger) =>
new GetMediaUseCase(mediaRepo, logger),
inject: [MEDIA_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: DELETE_MEDIA_USE_CASE_TOKEN,
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
new DeleteMediaUseCase(mediaRepo, mediaStorage, logger),
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
},
{
provide: GET_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
new GetAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: UPDATE_AVATAR_USE_CASE_TOKEN,
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
new UpdateAvatarUseCase(avatarRepo, logger),
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
]; ];

View File

@@ -8,6 +8,7 @@ import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO';
import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO'; import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO';
import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO'; import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO';
import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO'; import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO';
import type { RacingSuitColor } from '@core/media/domain/types/AvatarGenerationRequest';
type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO; type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO; type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
@@ -21,18 +22,41 @@ type UpdateAvatarOutput = UpdateAvatarOutputDTO;
// Use cases // Use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase'; import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase';
import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase';
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
// Presenters // Presenters
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter'; import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
import { UploadMediaPresenter } from './presenters/UploadMediaPresenter';
import { GetMediaPresenter } from './presenters/GetMediaPresenter';
import { DeleteMediaPresenter } from './presenters/DeleteMediaPresenter';
import { GetAvatarPresenter } from './presenters/GetAvatarPresenter';
import { UpdateAvatarPresenter } from './presenters/UpdateAvatarPresenter';
// Tokens // Tokens
import { REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, LOGGER_TOKEN } from './MediaProviders'; import {
REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
UPLOAD_MEDIA_USE_CASE_TOKEN,
GET_MEDIA_USE_CASE_TOKEN,
DELETE_MEDIA_USE_CASE_TOKEN,
GET_AVATAR_USE_CASE_TOKEN,
UPDATE_AVATAR_USE_CASE_TOKEN,
LOGGER_TOKEN
} from './MediaProviders';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
@Injectable() @Injectable()
export class MediaService { export class MediaService {
constructor( constructor(
@Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN) private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase, @Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN) private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase,
@Inject(UPLOAD_MEDIA_USE_CASE_TOKEN) private readonly uploadMediaUseCase: UploadMediaUseCase,
@Inject(GET_MEDIA_USE_CASE_TOKEN) private readonly getMediaUseCase: GetMediaUseCase,
@Inject(DELETE_MEDIA_USE_CASE_TOKEN) private readonly deleteMediaUseCase: DeleteMediaUseCase,
@Inject(GET_AVATAR_USE_CASE_TOKEN) private readonly getAvatarUseCase: GetAvatarUseCase,
@Inject(UPDATE_AVATAR_USE_CASE_TOKEN) private readonly updateAvatarUseCase: UpdateAvatarUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {} ) {}
@@ -43,46 +67,118 @@ export class MediaService {
await this.requestAvatarGenerationUseCase.execute({ await this.requestAvatarGenerationUseCase.execute({
userId: input.userId, userId: input.userId,
facePhotoData: input.facePhotoData, facePhotoData: input.facePhotoData,
suitColor: input.suitColor as any, suitColor: input.suitColor as RacingSuitColor,
}, presenter); }, presenter);
return presenter.viewModel; return presenter.viewModel;
} }
async uploadMedia(input: UploadMediaInput & { file: Express.Multer.File }): Promise<UploadMediaOutput> { async uploadMedia(input: UploadMediaInput & { file: Express.Multer.File }): Promise<UploadMediaOutput> {
this.logger.debug('[MediaService] Uploading media.'); this.logger.debug('[MediaService] Uploading media.');
// TODO: Implement media upload logic
return { const presenter = new UploadMediaPresenter();
success: true,
mediaId: 'placeholder-media-id', await this.uploadMediaUseCase.execute({
url: 'placeholder-url', file: input.file,
}; uploadedBy: input.userId, // Assuming userId is the uploader
metadata: input.metadata,
}, presenter);
const result = presenter.viewModel;
if (result.success) {
return {
success: true,
mediaId: result.mediaId!,
url: result.url!,
};
} else {
return {
success: false,
errorMessage: result.errorMessage || 'Upload failed',
};
}
} }
async getMedia(mediaId: string): Promise<GetMediaOutput | null> { async getMedia(mediaId: string): Promise<GetMediaOutput | null> {
this.logger.debug(`[MediaService] Getting media: ${mediaId}`); this.logger.debug(`[MediaService] Getting media: ${mediaId}`);
// TODO: Implement get media logic
const presenter = new GetMediaPresenter();
await this.getMediaUseCase.execute({ mediaId }, presenter);
const result = presenter.viewModel;
if (result.success && result.media) {
return {
success: true,
mediaId: result.media.id,
filename: result.media.filename,
originalName: result.media.originalName,
mimeType: result.media.mimeType,
size: result.media.size,
url: result.media.url,
type: result.media.type,
uploadedBy: result.media.uploadedBy,
uploadedAt: result.media.uploadedAt,
metadata: result.media.metadata,
};
}
return null; return null;
} }
async deleteMedia(mediaId: string): Promise<DeleteMediaOutput> { async deleteMedia(mediaId: string): Promise<DeleteMediaOutput> {
this.logger.debug(`[MediaService] Deleting media: ${mediaId}`); this.logger.debug(`[MediaService] Deleting media: ${mediaId}`);
// TODO: Implement delete media logic
const presenter = new DeleteMediaPresenter();
await this.deleteMediaUseCase.execute({ mediaId }, presenter);
const result = presenter.viewModel;
return { return {
success: true, success: result.success,
errorMessage: result.errorMessage,
}; };
} }
async getAvatar(driverId: string): Promise<GetAvatarOutput | null> { async getAvatar(driverId: string): Promise<GetAvatarOutput | null> {
this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`); this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`);
// TODO: Implement get avatar logic
const presenter = new GetAvatarPresenter();
await this.getAvatarUseCase.execute({ driverId }, presenter);
const result = presenter.viewModel;
if (result.success && result.avatar) {
return {
success: true,
avatarId: result.avatar.id,
driverId: result.avatar.driverId,
mediaUrl: result.avatar.mediaUrl,
selectedAt: result.avatar.selectedAt,
};
}
return null; return null;
} }
async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutput> { async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutput> {
this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`); this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`);
// TODO: Implement update avatar logic
const presenter = new UpdateAvatarPresenter();
await this.updateAvatarUseCase.execute({
driverId,
mediaUrl: input.mediaUrl,
}, presenter);
const result = presenter.viewModel;
return { return {
success: true, success: result.success,
errorMessage: result.errorMessage,
}; };
} }
} }

View File

@@ -3,7 +3,7 @@ import { IsString, IsOptional } from 'class-validator';
export class UploadMediaInputDTO { export class UploadMediaInputDTO {
@ApiProperty({ type: 'string', format: 'binary' }) @ApiProperty({ type: 'string', format: 'binary' })
file: any; // File upload handled by multer file: Express.Multer.File;
@ApiProperty() @ApiProperty()
@IsString() @IsString()

View File

@@ -0,0 +1,14 @@
import type { IDeleteMediaPresenter, DeleteMediaResult } from '@core/media/application/presenters/IDeleteMediaPresenter';
export class DeleteMediaPresenter implements IDeleteMediaPresenter {
private result: DeleteMediaResult | null = null;
present(result: DeleteMediaResult) {
this.result = result;
}
get viewModel(): DeleteMediaResult {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,14 @@
import type { IGetAvatarPresenter, GetAvatarResult } from '@core/media/application/presenters/IGetAvatarPresenter';
export class GetAvatarPresenter implements IGetAvatarPresenter {
private result: GetAvatarResult | null = null;
present(result: GetAvatarResult) {
this.result = result;
}
get viewModel(): GetAvatarResult {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,14 @@
import type { IGetMediaPresenter, GetMediaResult } from '@core/media/application/presenters/IGetMediaPresenter';
export class GetMediaPresenter implements IGetMediaPresenter {
private result: GetMediaResult | null = null;
present(result: GetMediaResult) {
this.result = result;
}
get viewModel(): GetMediaResult {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -1,6 +1,8 @@
import { RequestAvatarGenerationOutput } from '../dto/MediaDto'; import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO';
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '@core/media/application/presenters/IRequestAvatarGenerationPresenter'; import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '@core/media/application/presenters/IRequestAvatarGenerationPresenter';
type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
export class RequestAvatarGenerationPresenter implements IRequestAvatarGenerationPresenter { export class RequestAvatarGenerationPresenter implements IRequestAvatarGenerationPresenter {
private result: RequestAvatarGenerationOutput | null = null; private result: RequestAvatarGenerationOutput | null = null;

View File

@@ -0,0 +1,14 @@
import type { IUpdateAvatarPresenter, UpdateAvatarResult } from '@core/media/application/presenters/IUpdateAvatarPresenter';
export class UpdateAvatarPresenter implements IUpdateAvatarPresenter {
private result: UpdateAvatarResult | null = null;
present(result: UpdateAvatarResult) {
this.result = result;
}
get viewModel(): UpdateAvatarResult {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,14 @@
import type { IUploadMediaPresenter, UploadMediaResult } from '@core/media/application/presenters/IUploadMediaPresenter';
export class UploadMediaPresenter implements IUploadMediaPresenter {
private result: UploadMediaResult | null = null;
present(result: UploadMediaResult) {
this.result = result;
}
get viewModel(): UploadMediaResult {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,194 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { PaymentsController } from './PaymentsController';
import { PaymentsService } from './PaymentsService';
import { GetPaymentsQuery, CreatePaymentInput, UpdatePaymentStatusInput, GetMembershipFeesQuery, UpsertMembershipFeeInput, UpdateMemberPaymentInput, GetPrizesQuery, CreatePrizeInput, AwardPrizeInput, DeletePrizeInput, GetWalletQuery, ProcessWalletTransactionInput } from './dtos/PaymentsDto';
describe('PaymentsController', () => {
let controller: PaymentsController;
let service: ReturnType<typeof vi.mocked<PaymentsService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PaymentsController],
providers: [
{
provide: PaymentsService,
useValue: {
getPayments: vi.fn(),
createPayment: vi.fn(),
updatePaymentStatus: vi.fn(),
getMembershipFees: vi.fn(),
upsertMembershipFee: vi.fn(),
updateMemberPayment: vi.fn(),
getPrizes: vi.fn(),
createPrize: vi.fn(),
awardPrize: vi.fn(),
deletePrize: vi.fn(),
getWallet: vi.fn(),
processWalletTransaction: vi.fn(),
},
},
],
}).compile();
controller = module.get<PaymentsController>(PaymentsController);
service = vi.mocked(module.get(PaymentsService));
});
describe('getPayments', () => {
it('should return payments', async () => {
const query: GetPaymentsQuery = { status: 'pending' };
const result = { payments: [] };
service.getPayments.mockResolvedValue(result);
const response = await controller.getPayments(query);
expect(service.getPayments).toHaveBeenCalledWith(query);
expect(response).toEqual(result);
});
});
describe('createPayment', () => {
it('should create payment', async () => {
const input: CreatePaymentInput = { amount: 100, type: 'membership_fee', payerId: 'payer-123', payerType: 'driver', leagueId: 'league-123' };
const result = { payment: { id: 'pay-123' } };
service.createPayment.mockResolvedValue(result);
const response = await controller.createPayment(input);
expect(service.createPayment).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
describe('updatePaymentStatus', () => {
it('should update payment status', async () => {
const input: UpdatePaymentStatusInput = { paymentId: 'pay-123', status: 'completed' };
const result = { payment: { id: 'pay-123', status: 'completed' } };
service.updatePaymentStatus.mockResolvedValue(result);
const response = await controller.updatePaymentStatus(input);
expect(service.updatePaymentStatus).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
describe('getMembershipFees', () => {
it('should return membership fees', async () => {
const query: GetMembershipFeesQuery = { leagueId: 'league-123' };
const result = { fees: [] };
service.getMembershipFees.mockResolvedValue(result);
const response = await controller.getMembershipFees(query);
expect(service.getMembershipFees).toHaveBeenCalledWith(query);
expect(response).toEqual(result);
});
});
describe('upsertMembershipFee', () => {
it('should upsert membership fee', async () => {
const input: UpsertMembershipFeeInput = { leagueId: 'league-123', amount: 50 };
const result = { feeId: 'fee-123' };
service.upsertMembershipFee.mockResolvedValue(result);
const response = await controller.upsertMembershipFee(input);
expect(service.upsertMembershipFee).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
describe('updateMemberPayment', () => {
it('should update member payment', async () => {
const input: UpdateMemberPaymentInput = { memberId: 'member-123', paymentId: 'pay-123' };
const result = { success: true };
service.updateMemberPayment.mockResolvedValue(result);
const response = await controller.updateMemberPayment(input);
expect(service.updateMemberPayment).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
describe('getPrizes', () => {
it('should return prizes', async () => {
const query: GetPrizesQuery = { leagueId: 'league-123' };
const result = { prizes: [] };
service.getPrizes.mockResolvedValue(result);
const response = await controller.getPrizes(query);
expect(service.getPrizes).toHaveBeenCalledWith(query);
expect(response).toEqual(result);
});
});
describe('createPrize', () => {
it('should create prize', async () => {
const input: CreatePrizeInput = { name: 'Prize', amount: 100 };
const result = { prizeId: 'prize-123' };
service.createPrize.mockResolvedValue(result);
const response = await controller.createPrize(input);
expect(service.createPrize).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
describe('awardPrize', () => {
it('should award prize', async () => {
const input: AwardPrizeInput = { prizeId: 'prize-123', driverId: 'driver-123' };
const result = { success: true };
service.awardPrize.mockResolvedValue(result);
const response = await controller.awardPrize(input);
expect(service.awardPrize).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
describe('deletePrize', () => {
it('should delete prize', async () => {
const query: DeletePrizeInput = { prizeId: 'prize-123' };
const result = { success: true };
service.deletePrize.mockResolvedValue(result);
const response = await controller.deletePrize(query);
expect(service.deletePrize).toHaveBeenCalledWith(query);
expect(response).toEqual(result);
});
});
describe('getWallet', () => {
it('should return wallet', async () => {
const query: GetWalletQuery = { userId: 'user-123' };
const result = { balance: 100 };
service.getWallet.mockResolvedValue(result);
const response = await controller.getWallet(query);
expect(service.getWallet).toHaveBeenCalledWith(query);
expect(response).toEqual(result);
});
});
describe('processWalletTransaction', () => {
it('should process wallet transaction', async () => {
const input: ProcessWalletTransactionInput = { userId: 'user-123', amount: 50, type: 'deposit' };
const result = { transactionId: 'tx-123' };
service.processWalletTransaction.mockResolvedValue(result);
const response = await controller.processWalletTransaction(input);
expect(service.processWalletTransaction).toHaveBeenCalledWith(input);
expect(response).toEqual(result);
});
});
});

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PaymentsModule } from './PaymentsModule';
import { PaymentsController } from './PaymentsController';
import { PaymentsService } from './PaymentsService';
describe('PaymentsModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [PaymentsModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide PaymentsController', () => {
const controller = module.get<PaymentsController>(PaymentsController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(PaymentsController);
});
it('should provide PaymentsService', () => {
const service = module.get<PaymentsService>(PaymentsService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(PaymentsService);
});
});

View File

@@ -23,10 +23,10 @@ import { GetWalletUseCase } from '@core/payments/application/use-cases/GetWallet
import { ProcessWalletTransactionUseCase } from '@core/payments/application/use-cases/ProcessWalletTransactionUseCase'; import { ProcessWalletTransactionUseCase } from '@core/payments/application/use-cases/ProcessWalletTransactionUseCase';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { InMemoryPaymentRepository } from '/payments/persistence/inmemory/InMemoryPaymentRepository'; import { InMemoryPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from '/payments/persistence/inmemory/InMemoryMembershipFeeRepository'; import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
import { InMemoryPrizeRepository } from '/payments/persistence/inmemory/InMemoryPrizeRepository'; import { InMemoryPrizeRepository } from '@adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
import { InMemoryWalletRepository, InMemoryTransactionRepository } from '/payments/persistence/inmemory/InMemoryWalletRepository'; import { InMemoryWalletRepository, InMemoryTransactionRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Repository injection tokens // Repository injection tokens

View File

@@ -0,0 +1,39 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { ProtestsController } from './ProtestsController';
import { RaceService } from '../race/RaceService';
import { ReviewProtestCommandDTO } from '../race/dtos/ReviewProtestCommandDTO';
describe('ProtestsController', () => {
let controller: ProtestsController;
let raceService: ReturnType<typeof vi.mocked<RaceService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProtestsController],
providers: [
{
provide: RaceService,
useValue: {
reviewProtest: vi.fn(),
},
},
],
}).compile();
controller = module.get<ProtestsController>(ProtestsController);
raceService = vi.mocked(module.get(RaceService));
});
describe('reviewProtest', () => {
it('should review protest', async () => {
const protestId = 'protest-123';
const body: Omit<ReviewProtestCommandDTO, 'protestId'> = { decision: 'upheld', reason: 'Reason' };
raceService.reviewProtest.mockResolvedValue(undefined);
await controller.reviewProtest(protestId, body);
expect(raceService.reviewProtest).toHaveBeenCalledWith({ protestId, ...body });
});
});
});

View File

@@ -0,0 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProtestsModule } from './ProtestsModule';
import { ProtestsController } from './ProtestsController';
describe('ProtestsModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [ProtestsModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide ProtestsController', () => {
const controller = module.get<ProtestsController>(ProtestsController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(ProtestsController);
});
});

View File

@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ProtestsController } from './ProtestsController'; import { ProtestsController } from './ProtestsController';
import { RaceModule } from '../race/RaceModule'; import { ProtestsService } from './ProtestsService';
import { ProtestsProviders } from './ProtestsProviders';
@Module({ @Module({
imports: [RaceModule], providers: [ProtestsService, ...ProtestsProviders],
controllers: [ProtestsController], controllers: [ProtestsController],
}) })
export class ProtestsModule {} export class ProtestsModule {}

View File

@@ -0,0 +1,45 @@
import { Provider } from '@nestjs/common';
import { ProtestsService } from './ProtestsService';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
// Import concrete in-memory implementations
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
// Define injection tokens
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LOGGER_TOKEN = 'Logger';
export const ProtestsProviders: Provider[] = [
ProtestsService, // Provide the service itself
{
provide: PROTEST_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryProtestRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: RACE_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryLeagueMembershipRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
ReviewProtestUseCase,
];

View File

@@ -0,0 +1,31 @@
import { Injectable, Inject } from '@nestjs/common';
import type { Logger } from '@core/shared/application/Logger';
// Use cases
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
// Tokens
import { LOGGER_TOKEN } from './ProtestsProviders';
@Injectable()
export class ProtestsService {
constructor(
private readonly reviewProtestUseCase: ReviewProtestUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
async reviewProtest(command: {
protestId: string;
stewardId: string;
decision: 'uphold' | 'dismiss';
decisionNotes: string;
}): Promise<void> {
this.logger.debug('[ProtestsService] Reviewing protest:', command);
const result = await this.reviewProtestUseCase.execute(command);
if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to review protest');
}
}
}

View File

@@ -0,0 +1,74 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RaceController } from './RaceController';
import { RaceService } from './RaceService';
describe('RaceController', () => {
let controller: RaceController;
let service: jest.Mocked<RaceService>;
beforeEach(async () => {
const mockService = {
getAllRaces: jest.fn(),
getTotalRaces: jest.fn(),
getRacesPageData: jest.fn(),
getAllRacesPageData: jest.fn(),
getRaceDetail: jest.fn(),
getRaceResultsDetail: jest.fn(),
getRaceWithSOF: jest.fn(),
getRaceProtests: jest.fn(),
getRacePenalties: jest.fn(),
registerForRace: jest.fn(),
withdrawFromRace: jest.fn(),
cancelRace: jest.fn(),
completeRace: jest.fn(),
importRaceResults: jest.fn(),
fileProtest: jest.fn(),
applyQuickPenalty: jest.fn(),
applyPenalty: jest.fn(),
requestProtestDefense: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [RaceController],
providers: [
{
provide: RaceService,
useValue: mockService,
},
],
}).compile();
controller = module.get<RaceController>(RaceController);
service = module.get(RaceService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('getAllRaces', () => {
it('should return all races', async () => {
const mockResult = { races: [], totalCount: 0 };
service.getAllRaces.mockResolvedValue(mockResult);
const result = await controller.getAllRaces();
expect(service.getAllRaces).toHaveBeenCalled();
expect(result).toEqual(mockResult);
});
});
describe('getTotalRaces', () => {
it('should return total races count', async () => {
const mockResult = { totalRaces: 5 };
service.getTotalRaces.mockResolvedValue(mockResult);
const result = await controller.getTotalRaces();
expect(service.getTotalRaces).toHaveBeenCalled();
expect(result).toEqual(mockResult);
});
});
// Add more tests as needed
});

View File

@@ -9,10 +9,8 @@ import { RaceResultsDetailDTO } from './dtos/RaceResultsDetailDTO';
import { RaceWithSOFDTO } from './dtos/RaceWithSOFDTO'; import { RaceWithSOFDTO } from './dtos/RaceWithSOFDTO';
import { RaceProtestsDTO } from './dtos/RaceProtestsDTO'; import { RaceProtestsDTO } from './dtos/RaceProtestsDTO';
import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO'; import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO';
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO'; import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO'; import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO'; import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
import { ImportRaceResultsSummaryDTO } from './dtos/ImportRaceResultsSummaryDTO'; import { ImportRaceResultsSummaryDTO } from './dtos/ImportRaceResultsSummaryDTO';
import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO'; import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO';
@@ -156,7 +154,7 @@ export class RaceController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'File a protest' }) @ApiOperation({ summary: 'File a protest' })
@ApiResponse({ status: 200, description: 'Protest filed successfully' }) @ApiResponse({ status: 200, description: 'Protest filed successfully' })
async fileProtest(@Body() body: FileProtestCommandDTO): Promise<any> { async fileProtest(@Body() body: FileProtestCommandDTO): Promise<void> {
return this.raceService.fileProtest(body); return this.raceService.fileProtest(body);
} }
@@ -164,7 +162,7 @@ export class RaceController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Apply a quick penalty' }) @ApiOperation({ summary: 'Apply a quick penalty' })
@ApiResponse({ status: 200, description: 'Penalty applied successfully' }) @ApiResponse({ status: 200, description: 'Penalty applied successfully' })
async applyQuickPenalty(@Body() body: QuickPenaltyCommandDTO): Promise<any> { async applyQuickPenalty(@Body() body: QuickPenaltyCommandDTO): Promise<void> {
return this.raceService.applyQuickPenalty(body); return this.raceService.applyQuickPenalty(body);
} }
@@ -172,7 +170,7 @@ export class RaceController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Apply a penalty' }) @ApiOperation({ summary: 'Apply a penalty' })
@ApiResponse({ status: 200, description: 'Penalty applied successfully' }) @ApiResponse({ status: 200, description: 'Penalty applied successfully' })
async applyPenalty(@Body() body: ApplyPenaltyCommandDTO): Promise<any> { async applyPenalty(@Body() body: ApplyPenaltyCommandDTO): Promise<void> {
return this.raceService.applyPenalty(body); return this.raceService.applyPenalty(body);
} }
@@ -180,7 +178,7 @@ export class RaceController {
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Request protest defense' }) @ApiOperation({ summary: 'Request protest defense' })
@ApiResponse({ status: 200, description: 'Defense requested successfully' }) @ApiResponse({ status: 200, description: 'Defense requested successfully' })
async requestProtestDefense(@Body() body: RequestProtestDefenseCommandDTO): Promise<any> { async requestProtestDefense(@Body() body: RequestProtestDefenseCommandDTO): Promise<void> {
return this.raceService.requestProtestDefense(body); return this.raceService.requestProtestDefense(body);
} }
} }

View File

@@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RaceModule } from './RaceModule';
import { RaceController } from './RaceController';
import { RaceService } from './RaceService';
describe('RaceModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [RaceModule],
}).compile();
});
it('should compile the module', async () => {
expect(module).toBeDefined();
});
it('should have RaceController', () => {
const controller = module.get<RaceController>(RaceController);
expect(controller).toBeDefined();
});
it('should have RaceService', () => {
const service = module.get<RaceService>(RaceService);
expect(service).toBeDefined();
});
});

View File

@@ -6,6 +6,7 @@ import type { Logger } from '@core/shared/application/Logger';
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
@@ -23,6 +24,7 @@ import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository'; import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository'; import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository'; import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider'; import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
@@ -58,6 +60,7 @@ export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const PENALTY_REPOSITORY_TOKEN = 'IPenaltyRepository'; export const PENALTY_REPOSITORY_TOKEN = 'IPenaltyRepository';
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository'; export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider'; export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort'; export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
@@ -104,6 +107,11 @@ export const RaceProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryProtestRepository(logger), useFactory: (logger: Logger) => new InMemoryProtestRepository(logger),
inject: [LOGGER_TOKEN], inject: [LOGGER_TOKEN],
}, },
{
provide: STANDING_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger),
inject: [LOGGER_TOKEN],
},
{ {
provide: DRIVER_RATING_PROVIDER_TOKEN, provide: DRIVER_RATING_PROVIDER_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRatingProvider(logger), useFactory: (logger: Logger) => new InMemoryDriverRatingProvider(logger),
@@ -121,13 +129,13 @@ export const RaceProviders: Provider[] = [
// Use cases // Use cases
{ {
provide: GetAllRacesUseCase, provide: GetAllRacesUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => new GetAllRacesUseCase(raceRepo, leagueRepo), useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository, logger: Logger) => new GetAllRacesUseCase(raceRepo, leagueRepo, logger),
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetTotalRacesUseCase, provide: GetTotalRacesUseCase,
useFactory: (raceRepo: IRaceRepository) => new GetTotalRacesUseCase(raceRepo), useFactory: (raceRepo: IRaceRepository, logger: Logger) => new GetTotalRacesUseCase(raceRepo, logger),
inject: [RACE_REPOSITORY_TOKEN], inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetRaceDetailUseCase, provide: GetRaceDetailUseCase,
@@ -173,14 +181,35 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: GetRaceResultsDetailUseCase, provide: GetRaceResultsDetailUseCase,
useFactory: (resultRepo: IResultRepository, driverRepo: IDriverRepository, imageService: IImageServicePort) => useFactory: (
new GetRaceResultsDetailUseCase(resultRepo, driverRepo, imageService), raceRepo: IRaceRepository,
inject: [RESULT_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN], leagueRepo: ILeagueRepository,
resultRepo: IResultRepository,
driverRepo: IDriverRepository,
penaltyRepo: IPenaltyRepository,
) => new GetRaceResultsDetailUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, penaltyRepo),
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN,
PENALTY_REPOSITORY_TOKEN,
],
}, },
{ {
provide: GetRaceWithSOFUseCase, provide: GetRaceWithSOFUseCase,
useFactory: (raceRepo: IRaceRepository) => new GetRaceWithSOFUseCase(raceRepo), useFactory: (
inject: [RACE_REPOSITORY_TOKEN], raceRepo: IRaceRepository,
raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository,
driverRatingProvider: DriverRatingProvider,
) => new GetRaceWithSOFUseCase(raceRepo, raceRegRepo, resultRepo, driverRatingProvider),
inject: [
RACE_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
DRIVER_RATING_PROVIDER_TOKEN,
],
}, },
{ {
provide: GetRaceProtestsUseCase, provide: GetRaceProtestsUseCase,
@@ -200,8 +229,8 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: WithdrawFromRaceUseCase, provide: WithdrawFromRaceUseCase,
useFactory: (raceRegRepo: IRaceRegistrationRepository, logger: Logger) => new WithdrawFromRaceUseCase(raceRegRepo, logger), useFactory: (raceRegRepo: IRaceRegistrationRepository) => new WithdrawFromRaceUseCase(raceRegRepo),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN], inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
}, },
{ {
provide: CancelRaceUseCase, provide: CancelRaceUseCase,
@@ -210,16 +239,78 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: CompleteRaceUseCase, provide: CompleteRaceUseCase,
useFactory: (raceRepo: IRaceRepository, logger: Logger) => new CompleteRaceUseCase(raceRepo, logger), useFactory: (
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN], raceRepo: IRaceRepository,
raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository,
standingRepo: IStandingRepository,
driverRatingProvider: DriverRatingProvider,
) => new CompleteRaceUseCase(raceRepo, raceRegRepo, resultRepo, standingRepo, driverRatingProvider),
inject: [
RACE_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
DRIVER_RATING_PROVIDER_TOKEN,
],
},
{
provide: ImportRaceResultsApiUseCase,
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
resultRepo: IResultRepository,
driverRepo: IDriverRepository,
standingRepo: IStandingRepository,
logger: Logger,
) => new ImportRaceResultsApiUseCase(
raceRepo,
leagueRepo,
resultRepo,
driverRepo,
standingRepo,
logger,
),
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
},
{
provide: ImportRaceResultsUseCase,
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
resultRepo: IResultRepository,
driverRepo: IDriverRepository,
standingRepo: IStandingRepository,
logger: Logger,
) => new ImportRaceResultsUseCase(
raceRepo,
leagueRepo,
resultRepo,
driverRepo,
standingRepo,
logger,
),
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
}, },
ImportRaceResultsApiUseCase,
ImportRaceResultsUseCase,
{ {
provide: FileProtestUseCase, provide: FileProtestUseCase,
useFactory: (protestRepo: IProtestRepository, raceRepo: IRaceRepository, driverRepo: IDriverRepository, logger: Logger) => useFactory: (protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository) =>
new FileProtestUseCase(protestRepo, raceRepo, driverRepo, logger), new FileProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo),
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN], inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
}, },
{ {
provide: QuickPenaltyUseCase, provide: QuickPenaltyUseCase,

View File

@@ -1,20 +1,19 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
AllRacesPageViewModel, import type { GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter';
RaceStatsDto, import type { RaceDetailViewModel } from '@core/racing/application/presenters/IRaceDetailPresenter';
ImportRaceResultsInput, import type { RacesPageViewModel } from '@core/racing/application/presenters/IRacesPagePresenter';
ImportRaceResultsSummaryViewModel, import type { RaceResultsDetailViewModel } from '@core/racing/application/presenters/IRaceResultsDetailPresenter';
RaceDetailViewModelDto, import type { RaceWithSOFViewModel } from '@core/racing/application/presenters/IRaceWithSOFPresenter';
RacesPageDataViewModelDto, import type { RaceProtestsViewModel } from '@core/racing/application/presenters/IRaceProtestsPresenter';
RaceResultsDetailViewModelDto, import type { RacePenaltiesViewModel } from '@core/racing/application/presenters/IRacePenaltiesPresenter';
RaceWithSOFViewModelDto,
RaceProtestsViewModelDto, // DTOs
RacePenaltiesViewModelDto, import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
GetRaceDetailParamsDto, import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
RegisterForRaceParamsDto, import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
WithdrawFromRaceParamsDto, import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
RaceActionParamsDto, import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
} from './dtos/RaceDTO';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
@@ -46,6 +45,13 @@ import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter';
import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter'; import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter';
import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter'; import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter';
// Command DTOs
import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO';
import { QuickPenaltyCommandDTO } from './dtos/QuickPenaltyCommandDTO';
import { ApplyPenaltyCommandDTO } from './dtos/ApplyPenaltyCommandDTO';
import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCommandDTO';
import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO';
// Tokens // Tokens
import { LOGGER_TOKEN } from './RaceProviders'; import { LOGGER_TOKEN } from './RaceProviders';
@@ -66,7 +72,6 @@ export class RaceService {
private readonly withdrawFromRaceUseCase: WithdrawFromRaceUseCase, private readonly withdrawFromRaceUseCase: WithdrawFromRaceUseCase,
private readonly cancelRaceUseCase: CancelRaceUseCase, private readonly cancelRaceUseCase: CancelRaceUseCase,
private readonly completeRaceUseCase: CompleteRaceUseCase, private readonly completeRaceUseCase: CompleteRaceUseCase,
private readonly importRaceResultsUseCase: ImportRaceResultsUseCase,
private readonly fileProtestUseCase: FileProtestUseCase, private readonly fileProtestUseCase: FileProtestUseCase,
private readonly quickPenaltyUseCase: QuickPenaltyUseCase, private readonly quickPenaltyUseCase: QuickPenaltyUseCase,
private readonly applyPenaltyUseCase: ApplyPenaltyUseCase, private readonly applyPenaltyUseCase: ApplyPenaltyUseCase,
@@ -83,34 +88,33 @@ export class RaceService {
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getTotalRaces(): Promise<RaceStatsDto> { async getTotalRaces(): Promise<GetTotalRacesViewModel> {
this.logger.debug('[RaceService] Fetching total races count.'); this.logger.debug('[RaceService] Fetching total races count.');
const presenter = new GetTotalRacesPresenter(); const presenter = new GetTotalRacesPresenter();
await this.getTotalRacesUseCase.execute({}, presenter); await this.getTotalRacesUseCase.execute({}, presenter);
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> { async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsSummaryDTO> {
this.logger.debug('Importing race results:', input); this.logger.debug('Importing race results:', input);
const presenter = new ImportRaceResultsApiPresenter(); const presenter = new ImportRaceResultsApiPresenter();
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter); await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter);
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getRaceDetail(params: GetRaceDetailParamsDto): Promise<RaceDetailViewModelDto> { async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailViewModel> {
this.logger.debug('[RaceService] Fetching race detail:', params); this.logger.debug('[RaceService] Fetching race detail:', params);
const presenter = new RaceDetailPresenter();
const result = await this.getRaceDetailUseCase.execute(params); const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to get race detail'); throw new Error('Failed to get race detail');
} }
return result.value; return result.value;
} }
async getRacesPageData(): Promise<RacesPageDataViewModelDto> { async getRacesPageData(): Promise<RacesPageViewModel> {
this.logger.debug('[RaceService] Fetching races page data.'); this.logger.debug('[RaceService] Fetching races page data.');
const result = await this.getRacesPageDataUseCase.execute(); const result = await this.getRacesPageDataUseCase.execute();
@@ -122,7 +126,7 @@ export class RaceService {
return result.value; return result.value;
} }
async getAllRacesPageData(): Promise<RacesPageDataViewModelDto> { async getAllRacesPageData(): Promise<RacesPageViewModel> {
this.logger.debug('[RaceService] Fetching all races page data.'); this.logger.debug('[RaceService] Fetching all races page data.');
const result = await this.getAllRacesPageDataUseCase.execute(); const result = await this.getAllRacesPageDataUseCase.execute();
@@ -134,165 +138,143 @@ export class RaceService {
return result.value; return result.value;
} }
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailViewModelDto> { async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailViewModel> {
this.logger.debug('[RaceService] Fetching race results detail:', { raceId }); this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
const result = await this.getRaceResultsDetailUseCase.execute({ raceId }); const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to get race results detail'); throw new Error('Failed to get race results detail');
} }
return result.value; return result.value;
} }
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFViewModelDto> { async getRaceWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId }); this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
const result = await this.getRaceWithSOFUseCase.execute({ raceId }); const result = await this.getRaceWithSOFUseCase.execute({ raceId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to get race with SOF'); throw new Error('Failed to get race with SOF');
} }
return result.value; return result.value;
} }
async getRaceProtests(raceId: string): Promise<RaceProtestsViewModelDto> { async getRaceProtests(raceId: string): Promise<RaceProtestsViewModel> {
this.logger.debug('[RaceService] Fetching race protests:', { raceId }); this.logger.debug('[RaceService] Fetching race protests:', { raceId });
const result = await this.getRaceProtestsUseCase.execute({ raceId }); const result = await this.getRaceProtestsUseCase.execute({ raceId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to get race protests'); throw new Error('Failed to get race protests');
} }
return result.value; return result.value;
} }
async getRacePenalties(raceId: string): Promise<RacePenaltiesViewModelDto> { async getRacePenalties(raceId: string): Promise<RacePenaltiesViewModel> {
this.logger.debug('[RaceService] Fetching race penalties:', { raceId }); this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
const result = await this.getRacePenaltiesUseCase.execute({ raceId }); const result = await this.getRacePenaltiesUseCase.execute({ raceId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to get race penalties'); throw new Error('Failed to get race penalties');
} }
return result.value; return result.value;
} }
async registerForRace(params: RegisterForRaceParamsDto): Promise<void> { async registerForRace(params: RegisterForRaceParamsDTO): Promise<void> {
this.logger.debug('[RaceService] Registering for race:', params); this.logger.debug('[RaceService] Registering for race:', params);
const result = await this.registerForRaceUseCase.execute(params); const result = await this.registerForRaceUseCase.execute(params);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to register for race'); throw new Error('Failed to register for race');
} }
} }
async withdrawFromRace(params: WithdrawFromRaceParamsDto): Promise<void> { async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<void> {
this.logger.debug('[RaceService] Withdrawing from race:', params); this.logger.debug('[RaceService] Withdrawing from race:', params);
const result = await this.withdrawFromRaceUseCase.execute(params); const result = await this.withdrawFromRaceUseCase.execute(params);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to withdraw from race'); throw new Error('Failed to withdraw from race');
} }
} }
async cancelRace(params: RaceActionParamsDto): Promise<void> { async cancelRace(params: RaceActionParamsDTO): Promise<void> {
this.logger.debug('[RaceService] Cancelling race:', params); this.logger.debug('[RaceService] Cancelling race:', params);
const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId }); const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to cancel race'); throw new Error('Failed to cancel race');
} }
} }
async completeRace(params: RaceActionParamsDto): Promise<void> { async completeRace(params: RaceActionParamsDTO): Promise<void> {
this.logger.debug('[RaceService] Completing race:', params); this.logger.debug('[RaceService] Completing race:', params);
const result = await this.completeRaceUseCase.execute({ raceId: params.raceId }); const result = await this.completeRaceUseCase.execute({ raceId: params.raceId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to complete race'); throw new Error('Failed to complete race');
}
}
async importRaceResultsAlt(params: { raceId: string; resultsFileContent: string }): Promise<void> {
this.logger.debug('[RaceService] Importing race results (alt):', params);
const result = await this.importRaceResultsUseCase.execute({
raceId: params.raceId,
resultsFileContent: params.resultsFileContent,
});
if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to import race results');
} }
} }
async fileProtest(command: any): Promise<any> {
async fileProtest(command: FileProtestCommandDTO): Promise<void> {
this.logger.debug('[RaceService] Filing protest:', command); this.logger.debug('[RaceService] Filing protest:', command);
const result = await this.fileProtestUseCase.execute(command); const result = await this.fileProtestUseCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to file protest'); throw new Error('Failed to file protest');
} }
return result.value;
} }
async applyQuickPenalty(command: any): Promise<any> { async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<void> {
this.logger.debug('[RaceService] Applying quick penalty:', command); this.logger.debug('[RaceService] Applying quick penalty:', command);
const result = await this.quickPenaltyUseCase.execute(command); const result = await this.quickPenaltyUseCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to apply quick penalty'); throw new Error('Failed to apply quick penalty');
} }
return result.value;
} }
async applyPenalty(command: any): Promise<any> { async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<void> {
this.logger.debug('[RaceService] Applying penalty:', command); this.logger.debug('[RaceService] Applying penalty:', command);
const result = await this.applyPenaltyUseCase.execute(command); const result = await this.applyPenaltyUseCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to apply penalty'); throw new Error('Failed to apply penalty');
} }
return result.value;
} }
async requestProtestDefense(command: any): Promise<any> { async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<void> {
this.logger.debug('[RaceService] Requesting protest defense:', command); this.logger.debug('[RaceService] Requesting protest defense:', command);
const result = await this.requestProtestDefenseUseCase.execute(command); const result = await this.requestProtestDefenseUseCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to request protest defense'); throw new Error('Failed to request protest defense');
} }
return result.value;
} }
async reviewProtest(command: any): Promise<any> { async reviewProtest(command: ReviewProtestCommandDTO): Promise<void> {
this.logger.debug('[RaceService] Reviewing protest:', command); this.logger.debug('[RaceService] Reviewing protest:', command);
const result = await this.reviewProtestUseCase.execute(command); const result = await this.reviewProtestUseCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.error.details.message || 'Failed to review protest'); throw new Error('Failed to review protest');
} }
return result.value;
} }
} }

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { RaceViewModel } from './RaceViewModel'; import { RaceViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
export class AllRacesPageDTO { export class AllRacesPageDTO {
@ApiProperty({ type: [RaceViewModel] }) @ApiProperty({ type: [RaceViewModel] })

View File

@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty } from 'class-validator'; import { IsString, IsNotEmpty } from 'class-validator';
export class GetRaceDetailParamsDTODTO { export class GetRaceDetailParamsDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsString, IsNumber } from 'class-validator'; import { IsBoolean, IsString, IsNumber } from 'class-validator';
export class ImportRaceResultsSummaryDTOViewModel { export class ImportRaceResultsSummaryDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success!: boolean; success!: boolean;

View File

@@ -1,9 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { RaceProtestDto } from './RaceProtestDto'; import { RaceProtestDTO } from './RaceProtestDTO';
export class RaceProtestsDTO { export class RaceProtestsDTO {
@ApiProperty({ type: [RaceProtestDto] }) @ApiProperty({ type: [RaceProtestDTO] })
protests!: RaceProtestDto[]; protests!: RaceProtestDTO[];
@ApiProperty() @ApiProperty()
driverMap!: Record<string, string>; driverMap!: Record<string, string>;

View File

@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { RaceResultDto } from './RaceResultDto'; import { RaceResultDTO } from './RaceResultDTO';
export class RaceResultsDetailDTO { export class RaceResultsDetailDTO {
@ApiProperty() @ApiProperty()
@@ -11,6 +11,6 @@ export class RaceResultsDetailDTO {
@IsString() @IsString()
track!: string; track!: string;
@ApiProperty({ type: [RaceResultDto] }) @ApiProperty({ type: [RaceResultDTO] })
results!: RaceResultDto[]; results!: RaceResultDTO[];
} }

View File

@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { RacesPageDataRaceDto } from './RacesPageDataRaceDto'; import { RacesPageDataRaceDTO } from './RacesPageDataRaceDTO';
export class RacesPageDataDTO { export class RacesPageDataDTO {
@ApiProperty({ type: [RacesPageDataRaceDto] }) @ApiProperty({ type: [RacesPageDataRaceDTO] })
races!: RacesPageDataRaceDto[]; races!: RacesPageDataRaceDTO[];
} }

View File

@@ -1,8 +1,7 @@
import { IGetTotalRacesPresenter, GetTotalRacesResultDTO } from '@core/racing/application/presenters/IGetTotalRacesPresenter'; import { IGetTotalRacesPresenter, GetTotalRacesResultDTO, GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter';
import { RaceStatsDto } from '../dto/RaceDto';
export class GetTotalRacesPresenter implements IGetTotalRacesPresenter { export class GetTotalRacesPresenter implements IGetTotalRacesPresenter {
private result: RaceStatsDto | null = null; private result: GetTotalRacesViewModel | null = null;
reset() { reset() {
this.result = null; this.result = null;
@@ -14,7 +13,7 @@ export class GetTotalRacesPresenter implements IGetTotalRacesPresenter {
}; };
} }
getViewModel(): RaceStatsDto | null { getViewModel(): GetTotalRacesViewModel | null {
return this.result; return this.result;
} }
} }

View File

@@ -0,0 +1,210 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { SponsorController } from './SponsorController';
import { SponsorService } from './SponsorService';
describe('SponsorController', () => {
let controller: SponsorController;
let sponsorService: ReturnType<typeof vi.mocked<SponsorService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SponsorController],
providers: [
{
provide: SponsorService,
useValue: {
getEntitySponsorshipPricing: vi.fn(),
getSponsors: vi.fn(),
createSponsor: vi.fn(),
getSponsorDashboard: vi.fn(),
getSponsorSponsorships: vi.fn(),
getSponsor: vi.fn(),
getPendingSponsorshipRequests: vi.fn(),
acceptSponsorshipRequest: vi.fn(),
rejectSponsorshipRequest: vi.fn(),
},
},
],
}).compile();
controller = module.get<SponsorController>(SponsorController);
sponsorService = vi.mocked(module.get(SponsorService));
});
describe('getEntitySponsorshipPricing', () => {
it('should return sponsorship pricing', async () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
sponsorService.getEntitySponsorshipPricing.mockResolvedValue(mockResult);
const result = await controller.getEntitySponsorshipPricing();
expect(result).toEqual(mockResult);
expect(sponsorService.getEntitySponsorshipPricing).toHaveBeenCalled();
});
});
describe('getSponsors', () => {
it('should return sponsors list', async () => {
const mockResult = { sponsors: [] };
sponsorService.getSponsors.mockResolvedValue(mockResult);
const result = await controller.getSponsors();
expect(result).toEqual(mockResult);
expect(sponsorService.getSponsors).toHaveBeenCalled();
});
});
describe('createSponsor', () => {
it('should create sponsor', async () => {
const input = { name: 'Test Sponsor', contactEmail: 'test@example.com' };
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
sponsorService.createSponsor.mockResolvedValue(mockResult);
const result = await controller.createSponsor(input);
expect(result).toEqual(mockResult);
expect(sponsorService.createSponsor).toHaveBeenCalledWith(input);
});
});
describe('getSponsorDashboard', () => {
it('should return sponsor dashboard', async () => {
const sponsorId = 'sponsor-1';
const mockResult = { sponsorId, metrics: {}, sponsoredLeagues: [] };
sponsorService.getSponsorDashboard.mockResolvedValue(mockResult);
const result = await controller.getSponsorDashboard(sponsorId);
expect(result).toEqual(mockResult);
expect(sponsorService.getSponsorDashboard).toHaveBeenCalledWith({ sponsorId });
});
it('should return null when sponsor not found', async () => {
const sponsorId = 'sponsor-1';
sponsorService.getSponsorDashboard.mockResolvedValue(null);
const result = await controller.getSponsorDashboard(sponsorId);
expect(result).toBeNull();
});
});
describe('getSponsorSponsorships', () => {
it('should return sponsor sponsorships', async () => {
const sponsorId = 'sponsor-1';
const mockResult = { sponsorId, sponsorships: [] };
sponsorService.getSponsorSponsorships.mockResolvedValue(mockResult);
const result = await controller.getSponsorSponsorships(sponsorId);
expect(result).toEqual(mockResult);
expect(sponsorService.getSponsorSponsorships).toHaveBeenCalledWith({ sponsorId });
});
it('should return null when sponsor not found', async () => {
const sponsorId = 'sponsor-1';
sponsorService.getSponsorSponsorships.mockResolvedValue(null);
const result = await controller.getSponsorSponsorships(sponsorId);
expect(result).toBeNull();
});
});
describe('getSponsor', () => {
it('should return sponsor', async () => {
const sponsorId = 'sponsor-1';
const mockResult = { id: sponsorId, name: 'Test Sponsor' };
sponsorService.getSponsor.mockResolvedValue(mockResult);
const result = await controller.getSponsor(sponsorId);
expect(result).toEqual(mockResult);
expect(sponsorService.getSponsor).toHaveBeenCalledWith(sponsorId);
});
it('should return null when sponsor not found', async () => {
const sponsorId = 'sponsor-1';
sponsorService.getSponsor.mockResolvedValue(null);
const result = await controller.getSponsor(sponsorId);
expect(result).toBeNull();
});
});
describe('getPendingSponsorshipRequests', () => {
it('should return pending sponsorship requests', async () => {
const query = { entityType: 'season' as const, entityId: 'season-1' };
const mockResult = { entityType: 'season', entityId: 'season-1', requests: [], totalCount: 0 };
sponsorService.getPendingSponsorshipRequests.mockResolvedValue(mockResult);
const result = await controller.getPendingSponsorshipRequests(query);
expect(result).toEqual(mockResult);
expect(sponsorService.getPendingSponsorshipRequests).toHaveBeenCalledWith(query);
});
});
describe('acceptSponsorshipRequest', () => {
it('should accept sponsorship request', async () => {
const requestId = 'request-1';
const input = { respondedBy: 'user-1' };
const mockResult = {
requestId,
sponsorshipId: 'sponsorship-1',
status: 'accepted' as const,
acceptedAt: new Date(),
platformFee: 10,
netAmount: 90,
};
sponsorService.acceptSponsorshipRequest.mockResolvedValue(mockResult);
const result = await controller.acceptSponsorshipRequest(requestId, input);
expect(result).toEqual(mockResult);
expect(sponsorService.acceptSponsorshipRequest).toHaveBeenCalledWith(requestId, input.respondedBy);
});
it('should return null on error', async () => {
const requestId = 'request-1';
const input = { respondedBy: 'user-1' };
sponsorService.acceptSponsorshipRequest.mockResolvedValue(null);
const result = await controller.acceptSponsorshipRequest(requestId, input);
expect(result).toBeNull();
});
});
describe('rejectSponsorshipRequest', () => {
it('should reject sponsorship request', async () => {
const requestId = 'request-1';
const input = { respondedBy: 'user-1', reason: 'Not interested' };
const mockResult = {
requestId,
status: 'rejected' as const,
rejectedAt: new Date(),
reason: 'Not interested',
};
sponsorService.rejectSponsorshipRequest.mockResolvedValue(mockResult);
const result = await controller.rejectSponsorshipRequest(requestId, input);
expect(result).toEqual(mockResult);
expect(sponsorService.rejectSponsorshipRequest).toHaveBeenCalledWith(requestId, input.respondedBy, input.reason);
});
it('should return null on error', async () => {
const requestId = 'request-1';
const input = { respondedBy: 'user-1' };
sponsorService.rejectSponsorshipRequest.mockResolvedValue(null);
const result = await controller.rejectSponsorshipRequest(requestId, input);
expect(result).toBeNull();
});
});
});

View File

@@ -13,6 +13,8 @@ import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO'; import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO'; import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO';
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO'; import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO';
import type { AcceptSponsorshipRequestResultDTO } from '@core/racing/application/dtos/AcceptSponsorshipRequestResultDTO';
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@ApiTags('sponsors') @ApiTags('sponsors')
@Controller('sponsors') @Controller('sponsors')
@@ -69,26 +71,26 @@ export class SponsorController {
@ApiOperation({ summary: 'Get pending sponsorship requests' }) @ApiOperation({ summary: 'Get pending sponsorship requests' })
@ApiResponse({ status: 200, description: 'List of pending sponsorship requests', type: GetPendingSponsorshipRequestsOutputDTO }) @ApiResponse({ status: 200, description: 'List of pending sponsorship requests', type: GetPendingSponsorshipRequestsOutputDTO })
async getPendingSponsorshipRequests(@Query() query: { entityType: string; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> { async getPendingSponsorshipRequests(@Query() query: { entityType: string; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> {
return this.sponsorService.getPendingSponsorshipRequests(query); return this.sponsorService.getPendingSponsorshipRequests(query as { entityType: import('@core/racing/domain/entities/SponsorshipRequest').SponsorableEntityType; entityId: string });
} }
@Post('requests/:requestId/accept') @Post('requests/:requestId/accept')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Accept a sponsorship request' }) @ApiOperation({ summary: 'Accept a sponsorship request' })
@ApiResponse({ status: 200, description: 'Sponsorship request accepted' }) @ApiResponse({ status: 200, description: 'Sponsorship request accepted' })
@ApiResponse({ status: 400, description: 'Invalid request' }) @ApiResponse({ status: 400, description: 'Invalid request' })
@ApiResponse({ status: 404, description: 'Request not found' }) @ApiResponse({ status: 404, description: 'Request not found' })
async acceptSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO): Promise<any> { async acceptSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO): Promise<AcceptSponsorshipRequestResultDTO | null> {
return this.sponsorService.acceptSponsorshipRequest(requestId, input.respondedBy); return this.sponsorService.acceptSponsorshipRequest(requestId, input.respondedBy);
} }
@Post('requests/:requestId/reject') @Post('requests/:requestId/reject')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Reject a sponsorship request' }) @ApiOperation({ summary: 'Reject a sponsorship request' })
@ApiResponse({ status: 200, description: 'Sponsorship request rejected' }) @ApiResponse({ status: 200, description: 'Sponsorship request rejected' })
@ApiResponse({ status: 400, description: 'Invalid request' }) @ApiResponse({ status: 400, description: 'Invalid request' })
@ApiResponse({ status: 404, description: 'Request not found' }) @ApiResponse({ status: 404, description: 'Request not found' })
async rejectSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: RejectSponsorshipRequestInputDTO): Promise<any> { async rejectSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: RejectSponsorshipRequestInputDTO): Promise<RejectSponsorshipRequestResultDTO | null> {
return this.sponsorService.rejectSponsorshipRequest(requestId, input.respondedBy, input.reason); return this.sponsorService.rejectSponsorshipRequest(requestId, input.respondedBy, input.reason);
} }
} }

View File

@@ -10,6 +10,10 @@ import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/IL
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
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 { INotificationService } from '@core/notifications/application/ports/INotificationService';
import { IPaymentGateway } from '@core/racing/application/ports/IPaymentGateway';
import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
// Import use cases // Import use cases
@@ -152,7 +156,7 @@ export const SponsorProviders: Provider[] = [
}, },
{ {
provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: any, paymentGateway: any, walletRepository: any, leagueWalletRepository: any, logger: Logger) => useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: INotificationService, paymentGateway: IPaymentGateway, walletRepository: IWalletRepository, leagueWalletRepository: ILeagueWalletRepository, logger: Logger) =>
new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepo, seasonSponsorshipRepo, seasonRepo, notificationService, paymentGateway, walletRepository, leagueWalletRepository, logger), new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepo, seasonSponsorshipRepo, seasonRepo, notificationService, paymentGateway, walletRepository, leagueWalletRepository, logger),
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, 'INotificationService', 'IPaymentGateway', 'IWalletRepository', 'ILeagueWalletRepository', LOGGER_TOKEN], inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, 'INotificationService', 'IPaymentGateway', 'IWalletRepository', 'ILeagueWalletRepository', LOGGER_TOKEN],
}, },

View File

@@ -0,0 +1,258 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { SponsorService } from './SponsorService';
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import type { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import type { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import type { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import type { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
describe('SponsorService', () => {
let service: SponsorService;
let getSponsorshipPricingUseCase: { execute: Mock };
let getSponsorsUseCase: { execute: Mock };
let createSponsorUseCase: { execute: Mock };
let getSponsorDashboardUseCase: { execute: Mock };
let getSponsorSponsorshipsUseCase: { execute: Mock };
let getSponsorUseCase: { execute: Mock };
let getPendingSponsorshipRequestsUseCase: { execute: Mock };
let acceptSponsorshipRequestUseCase: { execute: Mock };
let rejectSponsorshipRequestUseCase: { execute: Mock };
let logger: {
debug: Mock;
info: Mock;
warn: Mock;
error: Mock;
};
beforeEach(() => {
getSponsorshipPricingUseCase = { execute: vi.fn() };
getSponsorsUseCase = { execute: vi.fn() };
createSponsorUseCase = { execute: vi.fn() };
getSponsorDashboardUseCase = { execute: vi.fn() };
getSponsorSponsorshipsUseCase = { execute: vi.fn() };
getSponsorUseCase = { execute: vi.fn() };
getPendingSponsorshipRequestsUseCase = { execute: vi.fn() };
acceptSponsorshipRequestUseCase = { execute: vi.fn() };
rejectSponsorshipRequestUseCase = { execute: vi.fn() };
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
service = new SponsorService(
getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase,
getSponsorsUseCase as unknown as GetSponsorsUseCase,
createSponsorUseCase as unknown as CreateSponsorUseCase,
getSponsorDashboardUseCase as unknown as GetSponsorDashboardUseCase,
getSponsorSponsorshipsUseCase as unknown as GetSponsorSponsorshipsUseCase,
getSponsorUseCase as unknown as GetSponsorUseCase,
getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase,
acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase,
rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase,
logger as unknown as Logger,
);
});
describe('getEntitySponsorshipPricing', () => {
it('should return sponsorship pricing', async () => {
const mockPresenter = {
viewModel: { entityType: 'season', entityId: 'season-1', pricing: [] },
};
getSponsorshipPricingUseCase.execute.mockResolvedValue(undefined);
// Mock the presenter
const originalGetSponsorshipPricingPresenter = await import('./presenters/GetSponsorshipPricingPresenter');
const mockPresenterClass = vi.fn().mockImplementation(() => mockPresenter);
vi.doMock('./presenters/GetSponsorshipPricingPresenter', () => ({
GetSponsorshipPricingPresenter: mockPresenterClass,
}));
const result = await service.getEntitySponsorshipPricing();
expect(result).toEqual(mockPresenter.viewModel);
expect(getSponsorshipPricingUseCase.execute).toHaveBeenCalledWith(undefined, mockPresenter);
});
});
describe('getSponsors', () => {
it('should return sponsors list', async () => {
const mockPresenter = {
viewModel: { sponsors: [] },
};
getSponsorsUseCase.execute.mockResolvedValue(undefined);
const result = await service.getSponsors();
expect(result).toEqual(mockPresenter.viewModel);
expect(getSponsorsUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object));
});
});
describe('createSponsor', () => {
it('should create sponsor successfully', async () => {
const input = { name: 'Test Sponsor', contactEmail: 'test@example.com' };
const mockPresenter = {
viewModel: { id: 'sponsor-1', name: 'Test Sponsor' },
};
createSponsorUseCase.execute.mockResolvedValue(undefined);
const result = await service.createSponsor(input);
expect(result).toEqual(mockPresenter.viewModel);
expect(createSponsorUseCase.execute).toHaveBeenCalledWith(input, expect.any(Object));
});
});
describe('getSponsorDashboard', () => {
it('should return sponsor dashboard', async () => {
const params = { sponsorId: 'sponsor-1' };
const mockPresenter = {
viewModel: { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] },
};
getSponsorDashboardUseCase.execute.mockResolvedValue(undefined);
const result = await service.getSponsorDashboard(params);
expect(result).toEqual(mockPresenter.viewModel);
expect(getSponsorDashboardUseCase.execute).toHaveBeenCalledWith(params, expect.any(Object));
});
});
describe('getSponsorSponsorships', () => {
it('should return sponsor sponsorships', async () => {
const params = { sponsorId: 'sponsor-1' };
const mockPresenter = {
viewModel: { sponsorId: 'sponsor-1', sponsorships: [] },
};
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(undefined);
const result = await service.getSponsorSponsorships(params);
expect(result).toEqual(mockPresenter.viewModel);
expect(getSponsorSponsorshipsUseCase.execute).toHaveBeenCalledWith(params, expect.any(Object));
});
});
describe('getSponsor', () => {
it('should return sponsor when found', async () => {
const sponsorId = 'sponsor-1';
const mockSponsor = { id: sponsorId, name: 'Test Sponsor' };
getSponsorUseCase.execute.mockResolvedValue(Result.ok(mockSponsor));
const result = await service.getSponsor(sponsorId);
expect(result).toEqual(mockSponsor);
expect(getSponsorUseCase.execute).toHaveBeenCalledWith({ sponsorId });
});
it('should return null when sponsor not found', async () => {
const sponsorId = 'sponsor-1';
getSponsorUseCase.execute.mockResolvedValue(Result.err({ code: 'NOT_FOUND' }));
const result = await service.getSponsor(sponsorId);
expect(result).toBeNull();
});
});
describe('getPendingSponsorshipRequests', () => {
it('should return pending sponsorship requests', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
const mockResult = {
entityType: 'season',
entityId: 'season-1',
requests: [],
totalCount: 0,
};
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(mockResult));
const result = await service.getPendingSponsorshipRequests(params);
expect(result).toEqual(mockResult);
expect(getPendingSponsorshipRequestsUseCase.execute).toHaveBeenCalledWith(params);
});
it('should return empty result on error', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const result = await service.getPendingSponsorshipRequests(params);
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
requests: [],
totalCount: 0,
});
});
});
describe('acceptSponsorshipRequest', () => {
it('should accept sponsorship request successfully', async () => {
const requestId = 'request-1';
const respondedBy = 'user-1';
const mockResult = {
requestId,
sponsorshipId: 'sponsorship-1',
status: 'accepted' as const,
acceptedAt: new Date(),
platformFee: 10,
netAmount: 90,
};
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(mockResult));
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(result).toEqual(mockResult);
expect(acceptSponsorshipRequestUseCase.execute).toHaveBeenCalledWith({ requestId, respondedBy });
});
it('should return null on error', async () => {
const requestId = 'request-1';
const respondedBy = 'user-1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.err({ code: 'NOT_FOUND' }));
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(result).toBeNull();
});
});
describe('rejectSponsorshipRequest', () => {
it('should reject sponsorship request successfully', async () => {
const requestId = 'request-1';
const respondedBy = 'user-1';
const reason = 'Not interested';
const mockResult = {
requestId,
status: 'rejected' as const,
rejectedAt: new Date(),
reason,
};
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(mockResult));
const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
expect(result).toEqual(mockResult);
expect(rejectSponsorshipRequestUseCase.execute).toHaveBeenCalledWith({ requestId, respondedBy, reason });
});
it('should return null on error', async () => {
const requestId = 'request-1';
const respondedBy = 'user-1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.err({ code: 'NOT_FOUND' }));
const result = await service.rejectSponsorshipRequest(requestId, respondedBy);
expect(result).toBeNull();
});
});
});

View File

@@ -11,11 +11,6 @@ import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO'; import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO'; import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO';
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO'; import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO';
import { SponsorDTO } from './dtos/SponsorDTO';
import { SponsorDashboardMetricsDTO } from './dtos/SponsorDashboardMetricsDTO';
import { SponsoredLeagueDTO } from './dtos/SponsoredLeagueDTO';
import { SponsorDashboardInvestmentDTO } from './dtos/SponsorDashboardInvestmentDTO';
import { SponsorshipDetailDTO } from './dtos/SponsorshipDetailDTO';
// Use cases // Use cases
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
@@ -24,16 +19,13 @@ import { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateS
import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase'; import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; import { GetPendingSponsorshipRequestsUseCase, GetPendingSponsorshipRequestsDTO } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest';
import type { AcceptSponsorshipRequestResultDTO } from '@core/racing/application/dtos/AcceptSponsorshipRequestResultDTO';
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
// Presenters
import { GetSponsorshipPricingPresenter } from './presenters/GetSponsorshipPricingPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
// Tokens // Tokens
import { GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN, GET_SPONSORS_USE_CASE_TOKEN, CREATE_SPONSOR_USE_CASE_TOKEN, GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN, GET_SPONSOR_USE_CASE_TOKEN, GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN, ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, LOGGER_TOKEN } from './SponsorProviders'; import { GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN, GET_SPONSORS_USE_CASE_TOKEN, CREATE_SPONSOR_USE_CASE_TOKEN, GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN, GET_SPONSOR_USE_CASE_TOKEN, GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN, ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, LOGGER_TOKEN } from './SponsorProviders';
@@ -57,41 +49,56 @@ export class SponsorService {
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> { async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> {
this.logger.debug('[SponsorService] Fetching sponsorship pricing.'); this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
const presenter = new GetSponsorshipPricingPresenter(); const result = await this.getSponsorshipPricingUseCase.execute();
await this.getSponsorshipPricingUseCase.execute(undefined, presenter); if (result.isErr()) {
return presenter.viewModel; this.logger.error('[SponsorService] Failed to fetch sponsorship pricing.', result.error);
return { entityType: 'season', entityId: '', pricing: [] };
}
return result.value as GetEntitySponsorshipPricingResultDTO;
} }
async getSponsors(): Promise<GetSponsorsOutputDTO> { async getSponsors(): Promise<GetSponsorsOutputDTO> {
this.logger.debug('[SponsorService] Fetching sponsors.'); this.logger.debug('[SponsorService] Fetching sponsors.');
const presenter = new GetSponsorsPresenter(); const result = await this.getSponsorsUseCase.execute();
await this.getSponsorsUseCase.execute(undefined, presenter); if (result.isErr()) {
return presenter.viewModel; this.logger.error('[SponsorService] Failed to fetch sponsors.', result.error);
return { sponsors: [] };
}
return result.value as GetSponsorsOutputDTO;
} }
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> { async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> {
this.logger.debug('[SponsorService] Creating sponsor.', { input }); this.logger.debug('[SponsorService] Creating sponsor.', { input });
const presenter = new CreateSponsorPresenter(); const result = await this.createSponsorUseCase.execute(input);
await this.createSponsorUseCase.execute(input, presenter); if (result.isErr()) {
return presenter.viewModel; this.logger.error('[SponsorService] Failed to create sponsor.', result.error);
throw new Error(result.error.details?.message || 'Failed to create sponsor');
}
return result.value as CreateSponsorOutputDTO;
} }
async getSponsorDashboard(params: GetSponsorDashboardQueryParamsDTO): Promise<SponsorDashboardDTO | null> { async getSponsorDashboard(params: GetSponsorDashboardQueryParamsDTO): Promise<SponsorDashboardDTO | null> {
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params }); this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
const presenter = new GetSponsorDashboardPresenter(); const result = await this.getSponsorDashboardUseCase.execute(params);
await this.getSponsorDashboardUseCase.execute(params, presenter); if (result.isErr()) {
return presenter.viewModel as SponsorDashboardDTO | null; this.logger.error('[SponsorService] Failed to fetch sponsor dashboard.', result.error);
return null;
}
return result.value as SponsorDashboardDTO | null;
} }
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParamsDTO): Promise<SponsorSponsorshipsDTO | null> { async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParamsDTO): Promise<SponsorSponsorshipsDTO | null> {
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params }); this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
const presenter = new GetSponsorSponsorshipsPresenter(); const result = await this.getSponsorSponsorshipsUseCase.execute(params);
await this.getSponsorSponsorshipsUseCase.execute(params, presenter); if (result.isErr()) {
return presenter.viewModel as SponsorSponsorshipsDTO | null; this.logger.error('[SponsorService] Failed to fetch sponsor sponsorships.', result.error);
return null;
}
return result.value as SponsorSponsorshipsDTO | null;
} }
async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO | null> { async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO | null> {
@@ -105,18 +112,18 @@ export class SponsorService {
return result.value as GetSponsorOutputDTO | null; return result.value as GetSponsorOutputDTO | null;
} }
async getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> { async getPendingSponsorshipRequests(params: { entityType: SponsorableEntityType; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> {
this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params }); this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params });
const result = await this.getPendingSponsorshipRequestsUseCase.execute(params as any); const result = await this.getPendingSponsorshipRequestsUseCase.execute(params as GetPendingSponsorshipRequestsDTO);
if (result.isErr()) { if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch pending sponsorship requests.', result.error); this.logger.error('[SponsorService] Failed to fetch pending sponsorship requests.', result.error);
return { entityType: params.entityType as any, entityId: params.entityId, requests: [], totalCount: 0 }; return { entityType: params.entityType, entityId: params.entityId, requests: [], totalCount: 0 };
} }
return result.value as GetPendingSponsorshipRequestsOutputDTO; return result.value as GetPendingSponsorshipRequestsOutputDTO;
} }
async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<{ requestId: string; sponsorshipId: string; status: string; acceptedAt: Date; platformFee: number; netAmount: number } | null> { async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<AcceptSponsorshipRequestResultDTO | null> {
this.logger.debug('[SponsorService] Accepting sponsorship request.', { requestId, respondedBy }); this.logger.debug('[SponsorService] Accepting sponsorship request.', { requestId, respondedBy });
const result = await this.acceptSponsorshipRequestUseCase.execute({ requestId, respondedBy }); const result = await this.acceptSponsorshipRequestUseCase.execute({ requestId, respondedBy });
@@ -127,7 +134,7 @@ export class SponsorService {
return result.value; return result.value;
} }
async rejectSponsorshipRequest(requestId: string, respondedBy: string, reason?: string): Promise<{ requestId: string; status: string; rejectedAt: Date } | null> { async rejectSponsorshipRequest(requestId: string, respondedBy: string, reason?: string): Promise<RejectSponsorshipRequestResultDTO | null> {
this.logger.debug('[SponsorService] Rejecting sponsorship request.', { requestId, respondedBy, reason }); this.logger.debug('[SponsorService] Rejecting sponsorship request.', { requestId, respondedBy, reason });
const result = await this.rejectSponsorshipRequestUseCase.execute({ requestId, respondedBy, reason }); const result = await this.rejectSponsorshipRequestUseCase.execute({ requestId, respondedBy, reason });

View File

@@ -5,5 +5,5 @@ export class AcceptSponsorshipRequestInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
respondedBy: string; respondedBy!: string;
} }

View File

@@ -5,12 +5,12 @@ export class CreateSponsorInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsEmail() @IsEmail()
@IsNotEmpty() @IsNotEmpty()
contactEmail: string; contactEmail!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CreateSponsorPresenter } from './CreateSponsorPresenter';
describe('CreateSponsorPresenter', () => {
let presenter: CreateSponsorPresenter;
beforeEach(() => {
presenter = new CreateSponsorPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com' };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
});
});
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 = { id: 'sponsor-1', name: 'Test Sponsor' };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetEntitySponsorshipPricingPresenter } from './GetEntitySponsorshipPricingPresenter';
describe('GetEntitySponsorshipPricingPresenter', () => {
let presenter: GetEntitySponsorshipPricingPresenter;
beforeEach(() => {
presenter = new GetEntitySponsorshipPricingPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
});
});
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 = { entityType: 'season', entityId: 'season-1', pricing: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorDashboardPresenter } from './GetSponsorDashboardPresenter';
describe('GetSponsorDashboardPresenter', () => {
let presenter: GetSponsorDashboardPresenter;
beforeEach(() => {
presenter = new GetSponsorDashboardPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
});
});
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 = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorSponsorshipsPresenter } from './GetSponsorSponsorshipsPresenter';
describe('GetSponsorSponsorshipsPresenter', () => {
let presenter: GetSponsorSponsorshipsPresenter;
beforeEach(() => {
presenter = new GetSponsorSponsorshipsPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
});
});
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 = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
});

View File

@@ -0,0 +1,62 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorsPresenter } from './GetSponsorsPresenter';
describe('GetSponsorsPresenter', () => {
let presenter: GetSponsorsPresenter;
beforeEach(() => {
presenter = new GetSponsorsPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { sponsors: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult = {
sponsors: [
{ id: 'sponsor-1', name: 'Sponsor One', contactEmail: 's1@example.com' },
{ id: 'sponsor-2', name: 'Sponsor Two', contactEmail: 's2@example.com' },
],
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult = { sponsors: [] };
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
});
});
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 = { sponsors: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
});

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter';
describe('GetSponsorshipPricingPresenter', () => {
let presenter: GetSponsorshipPricingPresenter;
beforeEach(() => {
presenter = new GetSponsorshipPricingPresenter();
});
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { tiers: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
describe('present', () => {
it('should store the result', () => {
const mockResult = { tiers: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
describe('getViewModel', () => {
it('should return null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('should return the result when presented', () => {
const mockResult = { tiers: [] };
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
});
});
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 = { tiers: [] };
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
});
});
});

View File

@@ -0,0 +1,149 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { TeamController } from './TeamController';
import { TeamService } from './TeamService';
import type { Request } from 'express';
import { CreateTeamInputDTO, UpdateTeamInputDTO } from './dtos/CreateTeamInputDTO';
describe('TeamController', () => {
let controller: TeamController;
let service: ReturnType<typeof vi.mocked<TeamService>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TeamController],
providers: [
{
provide: TeamService,
useValue: {
getAll: vi.fn(),
getDetails: vi.fn(),
getMembers: vi.fn(),
getJoinRequests: vi.fn(),
create: vi.fn(),
update: vi.fn(),
getDriverTeam: vi.fn(),
getMembership: vi.fn(),
},
},
],
}).compile();
controller = module.get<TeamController>(TeamController);
service = vi.mocked(module.get(TeamService));
});
describe('getAll', () => {
it('should return all teams', async () => {
const result = { teams: [] };
service.getAll.mockResolvedValue(result);
const response = await controller.getAll();
expect(service.getAll).toHaveBeenCalled();
expect(response).toEqual(result);
});
});
describe('getDetails', () => {
it('should return team details', async () => {
const teamId = 'team-123';
const userId = 'user-456';
const result = { id: teamId, name: 'Team' };
service.getDetails.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const response = await controller.getDetails(teamId, mockReq as Request);
expect(service.getDetails).toHaveBeenCalledWith(teamId, userId);
expect(response).toEqual(result);
});
});
describe('getMembers', () => {
it('should return team members', async () => {
const teamId = 'team-123';
const result = { members: [] };
service.getMembers.mockResolvedValue(result);
const response = await controller.getMembers(teamId);
expect(service.getMembers).toHaveBeenCalledWith(teamId);
expect(response).toEqual(result);
});
});
describe('getJoinRequests', () => {
it('should return join requests', async () => {
const teamId = 'team-123';
const result = { requests: [] };
service.getJoinRequests.mockResolvedValue(result);
const response = await controller.getJoinRequests(teamId);
expect(service.getJoinRequests).toHaveBeenCalledWith(teamId);
expect(response).toEqual(result);
});
});
describe('create', () => {
it('should create team', async () => {
const input: CreateTeamInputDTO = { name: 'New Team' };
const userId = 'user-123';
const result = { teamId: 'team-456' };
service.create.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const response = await controller.create(input, mockReq as Request);
expect(service.create).toHaveBeenCalledWith(input, userId);
expect(response).toEqual(result);
});
});
describe('update', () => {
it('should update team', async () => {
const teamId = 'team-123';
const input: UpdateTeamInputDTO = { name: 'Updated Team' };
const userId = 'user-456';
const result = { success: true };
service.update.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const response = await controller.update(teamId, input, mockReq as Request);
expect(service.update).toHaveBeenCalledWith(teamId, input, userId);
expect(response).toEqual(result);
});
});
describe('getDriverTeam', () => {
it('should return driver team', async () => {
const driverId = 'driver-123';
const result = { teamId: 'team-456' };
service.getDriverTeam.mockResolvedValue(result);
const response = await controller.getDriverTeam(driverId);
expect(service.getDriverTeam).toHaveBeenCalledWith(driverId);
expect(response).toEqual(result);
});
});
describe('getMembership', () => {
it('should return team membership', async () => {
const teamId = 'team-123';
const driverId = 'driver-456';
const result = { role: 'member' };
service.getMembership.mockResolvedValue(result);
const response = await controller.getMembership(teamId, driverId);
expect(service.getMembership).toHaveBeenCalledWith(teamId, driverId);
expect(response).toEqual(result);
});
});
});

View File

@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TeamModule } from './TeamModule';
import { TeamController } from './TeamController';
import { TeamService } from './TeamService';
describe('TeamModule', () => {
let module: TestingModule;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [TeamModule],
}).compile();
});
it('should compile the module', () => {
expect(module).toBeDefined();
});
it('should provide TeamController', () => {
const controller = module.get<TeamController>(TeamController);
expect(controller).toBeDefined();
expect(controller).toBeInstanceOf(TeamController);
});
it('should provide TeamService', () => {
const service = module.get<TeamService>(TeamService);
expect(service).toBeDefined();
expect(service).toBeInstanceOf(TeamService);
});
});

View File

@@ -1,6 +1,110 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import { TeamService } from './TeamService'; import { TeamService } from './TeamService';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Import concrete in-memory implementations
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
// Define injection tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const LOGGER_TOKEN = 'Logger';
export const TeamProviders: Provider[] = [ export const TeamProviders: Provider[] = [
TeamService, TeamService, // Provide the service itself
{
provide: TEAM_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: IMAGE_SERVICE_TOKEN,
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: GetAllTeamsUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetAllTeamsUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamDetailsUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new GetTeamDetailsUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: GetTeamMembersUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamJoinRequestsUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: CreateTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new CreateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: UpdateTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new UpdateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: GetDriverTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamMembershipUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetTeamMembershipUseCase(membershipRepo, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
]; ];

View File

@@ -5,8 +5,7 @@ import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriv
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter'; import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter'; import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { AllTeamsViewModel, DriverTeamViewModel, GetDriverTeamQuery } from './dto/TeamDto'; import { AllTeamsViewModel, DriverTeamViewModel } from './dtos/TeamDto';
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
describe('TeamService', () => { describe('TeamService', () => {
let service: TeamService; let service: TeamService;
@@ -31,138 +30,72 @@ describe('TeamService', () => {
providers: [ providers: [
TeamService, TeamService,
{ {
provide: TEAM_GET_ALL_USE_CASE_TOKEN, provide: GetAllTeamsUseCase,
useValue: mockGetAllTeamsUseCase, useValue: mockGetAllTeamsUseCase,
}, },
{ {
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, provide: GetDriverTeamUseCase,
useValue: mockGetDriverTeamUseCase, useValue: mockGetDriverTeamUseCase,
}, },
{ {
provide: TEAM_LOGGER_TOKEN, provide: 'Logger',
useValue: mockLogger, useValue: mockLogger,
}, },
], ],
}).compile(); }).compile();
service = module.get<TeamService>(TeamService); service = module.get<TeamService>(TeamService);
getAllTeamsUseCase = module.get(TEAM_GET_ALL_USE_CASE_TOKEN); getAllTeamsUseCase = module.get(GetAllTeamsUseCase);
getDriverTeamUseCase = module.get(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN); getDriverTeamUseCase = module.get(GetDriverTeamUseCase);
logger = module.get(TEAM_LOGGER_TOKEN); logger = module.get('Logger');
}); });
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
describe('getAllTeams', () => { describe('getAll', () => {
it('should create presenter, call use case, and return view model', async () => { it('should call use case and return result', async () => {
const mockViewModel: AllTeamsViewModel = { const mockResult = { isOk: () => true, value: { teams: [], totalCount: 0 } };
teams: [], getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any);
totalCount: 0,
};
const mockPresenter = { const mockPresenter = {
reset: jest.fn(),
present: jest.fn(), present: jest.fn(),
get viewModel(): AllTeamsViewModel { getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }),
return mockViewModel;
},
}; };
// Mock the presenter constructor
const originalConstructor = AllTeamsPresenter;
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter); (AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
// Mock the use case to call the presenter const result = await service.getAll();
getAllTeamsUseCase.execute.mockImplementation(async (input, presenter) => {
presenter.present({ teams: [] });
});
const result = await service.getAllTeams(); expect(getAllTeamsUseCase.execute).toHaveBeenCalled();
expect(result).toEqual({ teams: [], totalCount: 0 });
expect(AllTeamsPresenter).toHaveBeenCalled();
expect(getAllTeamsUseCase.execute).toHaveBeenCalledWith(undefined, mockPresenter);
expect(result).toBe(mockViewModel);
// Restore
AllTeamsPresenter = originalConstructor;
}); });
}); });
describe('getDriverTeam', () => { describe('getDriverTeam', () => {
it('should create presenter, call use case, and return view model', async () => { it('should call use case and return result', async () => {
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' }; const mockResult = { isOk: () => true, value: {} };
const mockViewModel: DriverTeamViewModel = { getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
team: {
id: 'team1',
name: 'Team 1',
tag: 'T1',
description: 'Description',
ownerId: 'driver1',
leagues: [],
},
membership: {
role: 'owner' as any,
joinedAt: new Date(),
isActive: true,
},
isOwner: true,
canManage: true,
};
const mockPresenter = { const mockPresenter = {
reset: jest.fn(),
present: jest.fn(), present: jest.fn(),
get viewModel(): DriverTeamViewModel { getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel),
return mockViewModel;
},
}; };
// Mock the presenter constructor
const originalConstructor = DriverTeamPresenter;
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter); (DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
// Mock the use case to call the presenter const result = await service.getDriverTeam('driver1');
getDriverTeamUseCase.execute.mockImplementation(async (input, presenter) => {
presenter.present({
team: {
id: 'team1',
name: 'Team 1',
tag: 'T1',
description: 'Description',
ownerId: 'driver1',
leagues: [],
},
membership: {
role: 'owner',
status: 'active',
joinedAt: new Date(),
},
driverId: 'driver1',
});
});
const result = await service.getDriverTeam(query); expect(getDriverTeamUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver1' });
expect(result).toEqual({});
expect(DriverTeamPresenter).toHaveBeenCalled();
expect(getDriverTeamUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver1' }, mockPresenter);
expect(result).toBe(mockViewModel);
// Restore
DriverTeamPresenter = originalConstructor;
}); });
it('should return null on error', async () => { it('should return null on error', async () => {
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' }; const mockResult = { isErr: () => true, error: {} };
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
// Mock the use case to throw an error const result = await service.getDriverTeam('driver1');
getDriverTeamUseCase.execute.mockRejectedValue(new Error('Team not found'));
const result = await service.getDriverTeam(query);
expect(result).toBeNull(); expect(result).toBeNull();
expect(logger.error).toHaveBeenCalled();
}); });
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { GetAllTeamsOutputDTO } from './dtos/GetAllTeamsOutputDTO'; import { GetAllTeamsOutputDTO } from './dtos/GetAllTeamsOutputDTO';
import { GetTeamDetailsOutputDTO } from './dtos/GetTeamDetailsOutputDTO'; import { GetTeamDetailsOutputDTO } from './dtos/GetTeamDetailsOutputDTO';
import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO'; import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO';
@@ -10,63 +10,162 @@ import { UpdateTeamOutputDTO } from './dtos/UpdateTeamOutputDTO';
import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO'; import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO';
import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO'; import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO';
// Core imports
import type { Logger } from '@core/shared/application/Logger';
// Use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
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';
// Tokens
import { LOGGER_TOKEN } from './TeamProviders';
@Injectable() @Injectable()
export class TeamService { export class TeamService {
constructor(
private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
private readonly getTeamDetailsUseCase: GetTeamDetailsUseCase,
private readonly getTeamMembersUseCase: GetTeamMembersUseCase,
private readonly getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase,
private readonly createTeamUseCase: CreateTeamUseCase,
private readonly updateTeamUseCase: UpdateTeamUseCase,
private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
private readonly getTeamMembershipUseCase: GetTeamMembershipUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
async getAll(): Promise<GetAllTeamsOutputDTO> { async getAll(): Promise<GetAllTeamsOutputDTO> {
// TODO: Implement getAll teams logic this.logger.debug('[TeamService] Fetching all teams.');
return {
teams: [], const presenter = new AllTeamsPresenter();
totalCount: 0, const result = await this.getAllTeamsUseCase.execute();
}; if (result.isErr()) {
this.logger.error('Error fetching all teams', result.error);
return { teams: [], totalCount: 0 };
}
await presenter.present(result.value);
return presenter.getViewModel()!;
} }
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> { async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
// TODO: Implement get team details logic this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`);
return null;
const presenter = new TeamDetailsPresenter();
const result = await this.getTeamDetailsUseCase.execute({ teamId, driverId: userId || '' });
if (result.isErr()) {
this.logger.error(`Error fetching team details for teamId: ${teamId}`, result.error);
return null;
}
await presenter.present(result.value);
return presenter.getViewModel();
} }
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> { async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
// TODO: Implement get team members logic this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
return {
members: [], const presenter = new TeamMembersPresenter();
totalCount: 0, const result = await this.getTeamMembersUseCase.execute({ teamId });
ownerCount: 0, if (result.isErr()) {
managerCount: 0, this.logger.error(`Error fetching team members for teamId: ${teamId}`, result.error);
memberCount: 0, return {
}; members: [],
totalCount: 0,
ownerCount: 0,
managerCount: 0,
memberCount: 0,
};
}
await presenter.present(result.value);
return presenter.getViewModel()!;
} }
async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> { async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
// TODO: Implement get team join requests logic this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`);
return {
requests: [], const presenter = new TeamJoinRequestsPresenter();
pendingCount: 0, const result = await this.getTeamJoinRequestsUseCase.execute({ teamId });
totalCount: 0, if (result.isErr()) {
}; this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, result.error);
return {
requests: [],
pendingCount: 0,
totalCount: 0,
};
}
await presenter.present(result.value);
return presenter.getViewModel()!;
} }
async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> { async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> {
// TODO: Implement create team logic this.logger.debug('[TeamService] Creating team', { input, userId });
return {
id: 'placeholder-id', const command = {
success: true, name: input.name,
tag: input.tag,
description: input.description,
ownerId: userId || '',
}; };
const result = await this.createTeamUseCase.execute(command);
if (result.isErr()) {
this.logger.error('Error creating team', result.error);
return { id: '', success: false };
}
return { id: result.value.id, success: true };
} }
async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> { async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> {
// TODO: Implement update team logic this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId });
return {
success: true, const command = {
teamId,
name: input.name,
tag: input.tag,
description: input.description,
performerId: userId || '',
}; };
const result = await this.updateTeamUseCase.execute(command);
if (result.isErr()) {
this.logger.error(`Error updating team ${teamId}`, result.error);
return { success: false };
}
return { success: true };
} }
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> { async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
// TODO: Implement get driver team logic this.logger.debug(`[TeamService] Fetching driver team for driverId: ${driverId}`);
return null;
const result = await this.getDriverTeamUseCase.execute({ driverId });
if (result.isErr()) {
this.logger.error(`Error fetching driver team for driverId: ${driverId}`, result.error);
return null;
}
const presenter = new DriverTeamPresenter();
await presenter.present(result.value);
return presenter.getViewModel();
} }
async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> { async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
// TODO: Implement get team membership logic this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`);
return null;
const result = await this.getTeamMembershipUseCase.execute({ teamId, driverId });
if (result.isErr()) {
this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}`, result.error);
return null;
}
return result.value;
} }
} }

View File

@@ -1,4 +1,4 @@
import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter'; import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel, TeamLeaderboardItemViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter { export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
private result: TeamsLeaderboardViewModel | null = null; private result: TeamsLeaderboardViewModel | null = null;
@@ -9,7 +9,7 @@ export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
present(dto: TeamsLeaderboardResultDTO) { present(dto: TeamsLeaderboardResultDTO) {
this.result = { this.result = {
teams: dto.teams as any, // Cast to match the view model teams: dto.teams as TeamLeaderboardItemViewModel[],
recruitingCount: dto.recruitingCount, recruitingCount: dto.recruitingCount,
groupsBySkillLevel: { groupsBySkillLevel: {
beginner: [], beginner: [],
@@ -17,7 +17,7 @@ export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
advanced: [], advanced: [],
pro: [], pro: [],
}, },
topTeams: dto.teams.slice(0, 10) as any, topTeams: (dto.teams as TeamLeaderboardItemViewModel[]).slice(0, 10),
}; };
} }

View File

@@ -0,0 +1,53 @@
import type { Logger } from '@core/shared/application';
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
export interface GetAnalyticsMetricsInput {
startDate?: Date;
endDate?: Date;
}
export interface GetAnalyticsMetricsOutput {
pageViews: number;
uniqueVisitors: number;
averageSessionDuration: number;
bounceRate: number;
}
export class GetAnalyticsMetricsUseCase {
constructor(
private readonly pageViewRepository: IPageViewRepository,
private readonly logger: Logger,
) {}
async execute(input: GetAnalyticsMetricsInput = {}): Promise<GetAnalyticsMetricsOutput> {
try {
const startDate = input.startDate ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
const endDate = input.endDate ?? new Date();
// For now, return placeholder values as actual implementation would require
// aggregating data across all entities or specifying which entity
// This is a simplified version
const pageViews = 0;
const uniqueVisitors = 0;
const averageSessionDuration = 0;
const bounceRate = 0;
this.logger.info('Analytics metrics retrieved', {
startDate,
endDate,
pageViews,
uniqueVisitors,
});
return {
pageViews,
uniqueVisitors,
averageSessionDuration,
bounceRate,
};
} catch (error) {
this.logger.error('Failed to get analytics metrics', { error, input });
throw error;
}
}
}

View File

@@ -0,0 +1,43 @@
import type { Logger } from '@core/shared/application';
export interface GetDashboardDataInput {}
export interface GetDashboardDataOutput {
totalUsers: number;
activeUsers: number;
totalRaces: number;
totalLeagues: number;
}
export class GetDashboardDataUseCase {
constructor(
private readonly logger: Logger,
) {}
async execute(_input: GetDashboardDataInput = {}): Promise<GetDashboardDataOutput> {
try {
// Placeholder implementation - would need repositories from identity and racing domains
const totalUsers = 0;
const activeUsers = 0;
const totalRaces = 0;
const totalLeagues = 0;
this.logger.info('Dashboard data retrieved', {
totalUsers,
activeUsers,
totalRaces,
totalLeagues,
});
return {
totalUsers,
activeUsers,
totalRaces,
totalLeagues,
};
} catch (error) {
this.logger.error('Failed to get dashboard data', { error });
throw error;
}
}
}

View File

@@ -1,13 +1,7 @@
/**
* Use Case: RecordEngagementUseCase
*
* Records an engagement event when a visitor interacts with an entity.
*/
import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent';
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
import { EngagementEvent } from '../../domain/entities/EngagementEvent';
import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent';
export interface RecordEngagementInput { export interface RecordEngagementInput {
action: EngagementAction; action: EngagementAction;
@@ -24,43 +18,41 @@ export interface RecordEngagementOutput {
engagementWeight: number; engagementWeight: number;
} }
export class RecordEngagementUseCase export class RecordEngagementUseCase {
implements AsyncUseCase<RecordEngagementInput, RecordEngagementOutput> {
constructor( constructor(
private readonly engagementRepository: IEngagementRepository, private readonly engagementRepository: IEngagementRepository,
private readonly logger: Logger, private readonly logger: Logger,
) {} ) {}
async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> { async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
this.logger.debug('Executing RecordEngagementUseCase', { input });
try { try {
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const engagementEvent = EngagementEvent.create({
id: crypto.randomUUID(),
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
id: eventId,
action: input.action, action: input.action,
entityType: input.entityType, entityType: input.entityType,
entityId: input.entityId, entityId: input.entityId,
actorId: input.actorId,
actorType: input.actorType, actorType: input.actorType,
sessionId: input.sessionId, sessionId: input.sessionId,
}; metadata: input.metadata,
const event = EngagementEvent.create({
...baseProps,
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
}); });
await this.engagementRepository.save(event); await this.engagementRepository.save(engagementEvent);
this.logger.info('Engagement recorded successfully', { eventId, input });
this.logger.info('Engagement event recorded', {
engagementId: engagementEvent.id,
action: input.action,
entityId: input.entityId,
entityType: input.entityType,
});
return { return {
eventId, eventId: engagementEvent.id,
engagementWeight: event.getEngagementWeight(), engagementWeight: engagementEvent.getEngagementWeight(),
}; };
} catch (error) { } catch (error) {
this.logger.error('Error recording engagement', error instanceof Error ? error : new Error(String(error)), { input }); this.logger.error('Failed to record engagement event', { error: error as Error, input });
throw error; throw error;
} }
} }
} }

View File

@@ -1,14 +1,7 @@
/**
* Use Case: RecordPageViewUseCase
*
* Records a page view event when a visitor accesses an entity page.
*/
import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { IPageViewRepository } from '../../domain/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';
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
export interface RecordPageViewInput { export interface RecordPageViewInput {
entityType: EntityType; entityType: EntityType;
@@ -25,41 +18,40 @@ export interface RecordPageViewOutput {
pageViewId: string; pageViewId: string;
} }
export class RecordPageViewUseCase export class RecordPageViewUseCase {
implements AsyncUseCase<RecordPageViewInput, RecordPageViewOutput> {
constructor( constructor(
private readonly pageViewRepository: IPageViewRepository, private readonly pageViewRepository: IPageViewRepository,
private readonly logger: Logger, private readonly logger: Logger,
) {} ) {}
async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> { async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
this.logger.debug('Executing RecordPageViewUseCase', { input });
try { try {
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const pageView = PageView.create({
id: crypto.randomUUID(),
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
id: pageViewId,
entityType: input.entityType, entityType: input.entityType,
entityId: input.entityId, entityId: input.entityId,
visitorId: input.visitorId,
visitorType: input.visitorType, visitorType: input.visitorType,
sessionId: input.sessionId, sessionId: input.sessionId,
}; referrer: input.referrer,
userAgent: input.userAgent,
const pageView = PageView.create({ country: input.country,
...baseProps,
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
...(input.country !== undefined ? { country: input.country } : {}),
}); });
await this.pageViewRepository.save(pageView); await this.pageViewRepository.save(pageView);
this.logger.info('Page view recorded successfully', { pageViewId, input });
return { pageViewId }; this.logger.info('Page view recorded', {
pageViewId: pageView.id,
entityId: input.entityId,
entityType: input.entityType,
});
return {
pageViewId: pageView.id,
};
} catch (error) { } catch (error) {
const err = error instanceof Error ? error : new Error(String(error)); this.logger.error('Failed to record page view', { error, input });
this.logger.error('Error recording page view', err, { input });
throw error; throw error;
} }
} }
} }

View File

@@ -0,0 +1,12 @@
import type { PageView } from '../entities/PageView';
export interface IPageViewRepository {
save(pageView: PageView): Promise<void>;
findById(id: string): Promise<PageView | null>;
findByEntityId(entityId: string): Promise<PageView[]>;
findBySessionId(sessionId: string): Promise<PageView[]>;
countByEntityId(entityId: string): Promise<number>;
getUniqueVisitorsCount(entityId: string, startDate: Date, endDate: Date): Promise<number>;
getAverageSessionDuration(entityId: string, startDate: Date, endDate: Date): Promise<number>;
getBounceRate(entityId: string, startDate: Date, endDate: Date): Promise<number>;
}

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