league service
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriverController } from './DriverController';
|
||||
import { DriverProviders } from './DriverProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [DriverController],
|
||||
providers: [DriverService],
|
||||
providers: DriverProviders,
|
||||
exports: [DriverService],
|
||||
})
|
||||
export class DriverModule {}
|
||||
|
||||
@@ -11,6 +11,11 @@ import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repo
|
||||
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { Logger } from "@gridpilot/core/shared/application";
|
||||
|
||||
// Import use cases
|
||||
import { GetDriversLeaderboardUseCase } from '../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '../../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService';
|
||||
@@ -31,6 +36,12 @@ export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
// Use case tokens
|
||||
export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseCase';
|
||||
export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase';
|
||||
export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase';
|
||||
export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase';
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
DriverService, // Provide the service itself
|
||||
{
|
||||
@@ -72,4 +83,26 @@ export const DriverProviders: Provider[] = [
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort) =>
|
||||
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository) => new GetTotalDriversUseCase(driverRepo),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository) => new CompleteDriverOnboardingUseCase(driverRepo),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
|
||||
useFactory: (registrationRepo: IRaceRegistrationRepository) => new IsDriverRegisteredForRaceUseCase(registrationRepo),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
|
||||
},
|
||||
];
|
||||
|
||||
187
apps/api/src/modules/driver/DriverService.spec.ts
Normal file
187
apps/api/src/modules/driver/DriverService.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DriverService } from './DriverService';
|
||||
import { GetDriversLeaderboardUseCase } from '../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '../../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '../../../../core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { Logger } from '../../../../core/shared/logging/Logger';
|
||||
|
||||
describe('DriverService', () => {
|
||||
let service: DriverService;
|
||||
let getDriversLeaderboardUseCase: jest.Mocked<GetDriversLeaderboardUseCase>;
|
||||
let getTotalDriversUseCase: jest.Mocked<GetTotalDriversUseCase>;
|
||||
let completeDriverOnboardingUseCase: jest.Mocked<CompleteDriverOnboardingUseCase>;
|
||||
let isDriverRegisteredForRaceUseCase: jest.Mocked<IsDriverRegisteredForRaceUseCase>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DriverService,
|
||||
{
|
||||
provide: 'GetDriversLeaderboardUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'GetTotalDriversUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'CompleteDriverOnboardingUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'IsDriverRegisteredForRaceUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'Logger',
|
||||
useValue: {
|
||||
debug: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DriverService>(DriverService);
|
||||
getDriversLeaderboardUseCase = module.get('GetDriversLeaderboardUseCase');
|
||||
getTotalDriversUseCase = module.get('GetTotalDriversUseCase');
|
||||
completeDriverOnboardingUseCase = module.get('CompleteDriverOnboardingUseCase');
|
||||
isDriverRegisteredForRaceUseCase = module.get('IsDriverRegisteredForRaceUseCase');
|
||||
logger = module.get('Logger');
|
||||
});
|
||||
|
||||
describe('getDriversLeaderboard', () => {
|
||||
it('should call GetDriversLeaderboardUseCase and return the view model', async () => {
|
||||
const mockViewModel = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver 1',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'DE',
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
podiums: 20,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.png',
|
||||
},
|
||||
],
|
||||
totalRaces: 50,
|
||||
totalWins: 10,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
getDriversLeaderboardUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.getDriversLeaderboard();
|
||||
|
||||
expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object));
|
||||
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.');
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalDrivers', () => {
|
||||
it('should call GetTotalDriversUseCase and return the view model', async () => {
|
||||
const mockViewModel = { totalDrivers: 5 };
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
getTotalDriversUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.getTotalDrivers();
|
||||
|
||||
expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object));
|
||||
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.');
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeOnboarding', () => {
|
||||
it('should call CompleteDriverOnboardingUseCase and return the view model', async () => {
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
displayName: 'John Doe',
|
||||
country: 'US',
|
||||
timezone: 'America/New_York',
|
||||
bio: 'Racing enthusiast',
|
||||
};
|
||||
|
||||
const mockViewModel = {
|
||||
success: true,
|
||||
driverId: 'user-123',
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
completeDriverOnboardingUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.completeOnboarding('user-123', input);
|
||||
|
||||
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: 'user-123',
|
||||
...input,
|
||||
},
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123');
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverRegistrationStatus', () => {
|
||||
it('should call IsDriverRegisteredForRaceUseCase and return the view model', async () => {
|
||||
const query = {
|
||||
driverId: 'driver-1',
|
||||
raceId: 'race-1',
|
||||
};
|
||||
|
||||
const mockViewModel = {
|
||||
isRegistered: true,
|
||||
raceId: 'race-1',
|
||||
driverId: 'driver-1',
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
isDriverRegisteredForRaceUseCase.execute.mockImplementation(async (params, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.getDriverRegistrationStatus(query);
|
||||
|
||||
expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query, expect.any(Object));
|
||||
expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query);
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
|
||||
|
||||
// Use cases
|
||||
import { GetDriversLeaderboardUseCase } from '../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '../../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '../../../../core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
|
||||
// Presenters
|
||||
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
|
||||
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
|
||||
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
|
||||
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
|
||||
|
||||
// Tokens
|
||||
import { GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, LOGGER_TOKEN } from './DriverProviders';
|
||||
import { Logger } from '../../../../core/shared/logging/Logger';
|
||||
|
||||
@Injectable()
|
||||
export class DriverService {
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
@Inject(GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN) private readonly getDriversLeaderboardUseCase: GetDriversLeaderboardUseCase,
|
||||
@Inject(GET_TOTAL_DRIVERS_USE_CASE_TOKEN) private readonly getTotalDriversUseCase: GetTotalDriversUseCase,
|
||||
@Inject(COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN) private readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase,
|
||||
@Inject(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) private readonly isDriverRegisteredForRaceUseCase: IsDriverRegisteredForRaceUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
console.log('[DriverService] Returning mock driver leaderboard.');
|
||||
const drivers: DriverLeaderboardItemViewModel[] = [
|
||||
{ id: 'driver-1', name: 'Mock Driver 1', rating: 2500, skillLevel: 'Pro', nationality: 'DE', racesCompleted: 50, wins: 10, podiums: 20, isActive: true, rank: 1, avatarUrl: 'https://cdn.example.com/avatars/driver-1.png' },
|
||||
{ id: 'driver-2', name: 'Mock Driver 2', rating: 2400, skillLevel: 'Amateur', nationality: 'US', racesCompleted: 40, wins: 5, podiums: 15, isActive: true, rank: 2, avatarUrl: 'https://cdn.example.com/avatars/driver-2.png' },
|
||||
];
|
||||
return {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces: drivers.reduce((sum, item) => sum + (item.racesCompleted ?? 0), 0),
|
||||
totalWins: drivers.reduce((sum, item) => sum + (item.wins ?? 0), 0),
|
||||
activeCount: drivers.filter(d => d.isActive).length,
|
||||
};
|
||||
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
|
||||
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
await this.getDriversLeaderboardUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
console.log('[DriverService] Returning mock total drivers.');
|
||||
return {
|
||||
totalDrivers: 2,
|
||||
};
|
||||
this.logger.debug('[DriverService] Fetching total drivers count.');
|
||||
|
||||
const presenter = new DriverStatsPresenter();
|
||||
await this.getTotalDriversUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
|
||||
console.log('Completing onboarding for user:', userId, input);
|
||||
return {
|
||||
success: true,
|
||||
driverId: `driver-${userId}-onboarded`,
|
||||
};
|
||||
this.logger.debug('Completing onboarding for user:', userId);
|
||||
|
||||
const presenter = new CompleteOnboardingPresenter();
|
||||
await this.completeDriverOnboardingUseCase.execute({
|
||||
userId,
|
||||
firstName: input.firstName,
|
||||
lastName: input.lastName,
|
||||
displayName: input.displayName,
|
||||
country: input.country,
|
||||
timezone: input.timezone,
|
||||
bio: input.bio,
|
||||
}, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
|
||||
console.log('Checking driver registration status:', query);
|
||||
return {
|
||||
isRegistered: false, // Mock response
|
||||
raceId: query.raceId,
|
||||
driverId: query.driverId,
|
||||
};
|
||||
this.logger.debug('Checking driver registration status:', query);
|
||||
|
||||
const presenter = new DriverRegistrationStatusPresenter();
|
||||
await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CompleteOnboardingPresenter } from './CompleteOnboardingPresenter';
|
||||
import type { CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
|
||||
|
||||
describe('CompleteOnboardingPresenter', () => {
|
||||
let presenter: CompleteOnboardingPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new CompleteOnboardingPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map successful core DTO to API view model', () => {
|
||||
const dto: CompleteDriverOnboardingResultDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should map failed core DTO to API view model', () => {
|
||||
const dto: CompleteDriverOnboardingResultDTO = {
|
||||
success: false,
|
||||
errorMessage: 'Driver already exists',
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Driver already exists',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: CompleteDriverOnboardingResultDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CompleteOnboardingOutput } from '../dto/DriverDto';
|
||||
import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
|
||||
|
||||
export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPresenter {
|
||||
private result: CompleteOnboardingOutput | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CompleteDriverOnboardingResultDTO) {
|
||||
this.result = {
|
||||
success: dto.success,
|
||||
driverId: dto.driverId,
|
||||
errorMessage: dto.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): CompleteOnboardingOutput {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DriverRegistrationStatusPresenter } from './DriverRegistrationStatusPresenter';
|
||||
|
||||
describe('DriverRegistrationStatusPresenter', () => {
|
||||
let presenter: DriverRegistrationStatusPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DriverRegistrationStatusPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map parameters to view model for registered driver', () => {
|
||||
presenter.present(true, 'race-123', 'driver-456');
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
isRegistered: true,
|
||||
raceId: 'race-123',
|
||||
driverId: 'driver-456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should map parameters to view model for unregistered driver', () => {
|
||||
presenter.present(false, 'race-789', 'driver-101');
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
isRegistered: false,
|
||||
raceId: 'race-789',
|
||||
driverId: 'driver-101',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
presenter.present(true, 'race-123', 'driver-456');
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DriverRegistrationStatusViewModel } from '../dto/DriverDto';
|
||||
import type { IDriverRegistrationStatusPresenter } from '../../../../../core/racing/application/presenters/IDriverRegistrationStatusPresenter';
|
||||
|
||||
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
|
||||
private result: DriverRegistrationStatusViewModel | null = null;
|
||||
|
||||
present(isRegistered: boolean, raceId: string, driverId: string) {
|
||||
this.result = {
|
||||
isRegistered,
|
||||
raceId,
|
||||
driverId,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): DriverRegistrationStatusViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
// For consistency with other presenters
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
get viewModel(): DriverRegistrationStatusViewModel {
|
||||
return this.getViewModel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DriverStatsPresenter } from './DriverStatsPresenter';
|
||||
import type { TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
|
||||
|
||||
describe('DriverStatsPresenter', () => {
|
||||
let presenter: DriverStatsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DriverStatsPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map core DTO to API view model correctly', () => {
|
||||
const dto: TotalDriversResultDTO = {
|
||||
totalDrivers: 42,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
totalDrivers: 42,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: TotalDriversResultDTO = {
|
||||
totalDrivers: 10,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DriverStatsDto } from '../dto/DriverDto';
|
||||
import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
|
||||
|
||||
export class DriverStatsPresenter implements ITotalDriversPresenter {
|
||||
private result: DriverStatsDto | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TotalDriversResultDTO) {
|
||||
this.result = {
|
||||
totalDrivers: dto.totalDrivers,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriverStatsDto {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DriversLeaderboardPresenter } from './DriversLeaderboardPresenter';
|
||||
import type { DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
|
||||
describe('DriversLeaderboardPresenter', () => {
|
||||
let presenter: DriversLeaderboardPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DriversLeaderboardPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map core DTO to API view model correctly', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date('2023-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
country: 'DE',
|
||||
iracingId: '67890',
|
||||
joinedAt: new Date('2023-01-02'),
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
{ driverId: 'driver-2', rating: 2400, overallRank: 2 },
|
||||
],
|
||||
stats: {
|
||||
'driver-1': { racesCompleted: 50, wins: 10, podiums: 20 },
|
||||
'driver-2': { racesCompleted: 40, wins: 5, podiums: 15 },
|
||||
},
|
||||
avatarUrls: {
|
||||
'driver-1': 'https://example.com/avatar1.png',
|
||||
'driver-2': 'https://example.com/avatar2.png',
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0]).toEqual({
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
podiums: 20,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.png',
|
||||
});
|
||||
expect(result.drivers[1]).toEqual({
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
rating: 2400,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'DE',
|
||||
racesCompleted: 40,
|
||||
wins: 5,
|
||||
podiums: 15,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/avatar2.png',
|
||||
});
|
||||
expect(result.totalRaces).toBe(90);
|
||||
expect(result.totalWins).toBe(15);
|
||||
expect(result.activeCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should sort drivers by rating descending', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
country: 'DE',
|
||||
iracingId: '67890',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2400, overallRank: 2 },
|
||||
{ driverId: 'driver-2', rating: 2500, overallRank: 1 },
|
||||
],
|
||||
stats: {},
|
||||
avatarUrls: {},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result.drivers[0].id).toBe('driver-2'); // Higher rating first
|
||||
expect(result.drivers[1].id).toBe('driver-1');
|
||||
});
|
||||
|
||||
it('should handle missing stats gracefully', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
],
|
||||
stats: {}, // No stats
|
||||
avatarUrls: {},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result.drivers[0].racesCompleted).toBe(0);
|
||||
expect(result.drivers[0].wins).toBe(0);
|
||||
expect(result.drivers[0].podiums).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [],
|
||||
rankings: [],
|
||||
stats: {},
|
||||
avatarUrls: {},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DriversLeaderboardViewModel, DriverLeaderboardItemViewModel } from '../dto/DriverDto';
|
||||
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
|
||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||
private result: DriversLeaderboardViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: DriversLeaderboardResultDTO) {
|
||||
const drivers: DriverLeaderboardItemViewModel[] = dto.drivers.map(driver => {
|
||||
const ranking = dto.rankings.find(r => r.driverId === driver.id);
|
||||
const stats = dto.stats[driver.id];
|
||||
const avatarUrl = dto.avatarUrls[driver.id];
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating: ranking?.rating ?? 0,
|
||||
skillLevel: 'Pro', // TODO: map from domain
|
||||
nationality: driver.country,
|
||||
racesCompleted: stats?.racesCompleted ?? 0,
|
||||
wins: stats?.wins ?? 0,
|
||||
podiums: stats?.podiums ?? 0,
|
||||
isActive: true, // TODO: determine from domain
|
||||
rank: ranking?.overallRank ?? 0,
|
||||
avatarUrl,
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate totals
|
||||
const totalRaces = drivers.reduce((sum, d) => sum + (d.racesCompleted ?? 0), 0);
|
||||
const totalWins = drivers.reduce((sum, d) => sum + (d.wins ?? 0), 0);
|
||||
const activeCount = drivers.filter(d => d.isActive).length;
|
||||
|
||||
this.result = {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces,
|
||||
totalWins,
|
||||
activeCount,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriversLeaderboardViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user