league service

This commit is contained in:
2025-12-16 00:57:31 +01:00
parent 3b566c973d
commit 775d41e055
130 changed files with 4077 additions and 1036 deletions

View File

@@ -15,9 +15,18 @@
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint"],
"extends": [], "extends": [],
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": "error" "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "all",
"argsIgnorePattern": "^$",
"vars": "all",
"varsIgnorePattern": "^$",
"caughtErrors": "all"
}
]
} }
} }
] ]
} }

View File

@@ -1,4 +1,4 @@
import { Logger } from "@gridpilot/core/shared/application"; import { ILogger as Logger } from "@gridpilot/core/shared/application";
export class ConsoleLogger implements Logger { export class ConsoleLogger implements Logger {
debug(message: string, ...args: any[]): void { debug(message: string, ...args: any[]): void {

View File

@@ -13,5 +13,6 @@ module.exports = {
testRegex: '.*\\.spec\\.ts$', testRegex: '.*\\.spec\\.ts$',
moduleNameMapper: { moduleNameMapper: {
'^@gridpilot/(.*)$': '<rootDir>/../../core/$1', // Corrected path '^@gridpilot/(.*)$': '<rootDir>/../../core/$1', // Corrected path
'^adapters/(.*)$': '<rootDir>/../../adapters/$1',
}, },
}; };

View File

@@ -4,6 +4,7 @@ import { HelloController } from './presentation/hello.controller';
import { HelloService } from './application/hello/hello.service'; import { HelloService } from './application/hello/hello.service';
import { AnalyticsModule } from './modules/analytics/AnalyticsModule'; import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
import { DatabaseModule } from './infrastructure/database/database.module'; import { DatabaseModule } from './infrastructure/database/database.module';
import { LoggingModule } from './infrastructure/logging/LoggingModule';
import { AuthModule } from './modules/auth/AuthModule'; import { AuthModule } from './modules/auth/AuthModule';
import { LeagueModule } from './modules/league/LeagueModule'; import { LeagueModule } from './modules/league/LeagueModule';
import { RaceModule } from './modules/race/RaceModule'; import { RaceModule } from './modules/race/RaceModule';
@@ -16,6 +17,7 @@ import { PaymentsModule } from './modules/payments/PaymentsModule';
@Module({ @Module({
imports: [ imports: [
DatabaseModule, DatabaseModule,
LoggingModule,
AnalyticsModule, AnalyticsModule,
AuthModule, AuthModule,
LeagueModule, LeagueModule,

View File

@@ -0,0 +1,15 @@
import { Global, Module } from '@nestjs/common';
import { Logger } from '@gridpilot/shared/application/Logger';
import { ConsoleLogger } from '@gridpilot/adapters/logging/ConsoleLogger';
@Global()
@Module({
providers: [
{
provide: 'Logger',
useClass: ConsoleLogger,
},
],
exports: ['Logger'],
})
export class LoggingModule {}

View File

@@ -1,43 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AnalyticsController } from './AnalyticsController'; import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService'; import { AnalyticsService } from './AnalyticsService';
import { AnalyticsProviders } from './AnalyticsProviders';
const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
import { Logger } from '@gridpilot/shared/logging/Logger';
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
import { IEngagementRepository } from '@gridpilot/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';
@Module({ @Module({
imports: [], imports: [],
controllers: [AnalyticsController], controllers: [AnalyticsController],
providers: [ providers: AnalyticsProviders,
AnalyticsService, exports: [AnalyticsService],
{
provide: Logger_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IPAGE_VIEW_REPO_TOKEN,
useClass: InMemoryPageViewRepository,
},
{
provide: IENGAGEMENT_REPO_TOKEN,
useExisting: InMemoryEngagementRepository, // Assuming TypeOrmEngagementRepository is not available
},
// No need for useExisting here if the original intent was to inject the concrete class when providing the TOKEN
],
exports: [
AnalyticsService,
Logger_TOKEN,
IPAGE_VIEW_REPO_TOKEN,
IENGAGEMENT_REPO_TOKEN,
],
}) })
export class AnalyticsModule {} export class AnalyticsModule {}

View File

@@ -0,0 +1,30 @@
import { Provider } from '@nestjs/common';
import { AnalyticsService } from './AnalyticsService';
const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
import { Logger } from '@gridpilot/shared/logging/Logger';
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
import { IEngagementRepository } from '@gridpilot/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';
export const AnalyticsProviders: Provider[] = [
AnalyticsService,
{
provide: Logger_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IPAGE_VIEW_REPO_TOKEN,
useClass: InMemoryPageViewRepository,
},
{
provide: IENGAGEMENT_REPO_TOKEN,
useClass: InMemoryEngagementRepository,
},
];

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthService } from './AuthService'; import { AuthService } from './AuthService';
import { AuthController } from './AuthController'; import { AuthController } from './AuthController';
import { AuthProviders } from './AuthProviders';
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService], providers: AuthProviders,
exports: [AuthService], exports: [AuthService],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DriverService } from './DriverService'; import { DriverService } from './DriverService';
import { DriverController } from './DriverController'; import { DriverController } from './DriverController';
import { DriverProviders } from './DriverProviders';
@Module({ @Module({
controllers: [DriverController], controllers: [DriverController],
providers: [DriverService], providers: DriverProviders,
exports: [DriverService], exports: [DriverService],
}) })
export class DriverModule {} export class DriverModule {}

View File

@@ -11,6 +11,11 @@ import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repo
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository'; import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
import { Logger } from "@gridpilot/core/shared/application"; 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 concrete in-memory implementations
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService'; 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 NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
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
// 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[] = [ export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself DriverService, // Provide the service itself
{ {
@@ -72,4 +83,26 @@ export const DriverProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, 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],
},
]; ];

View 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);
});
});
});

View File

@@ -1,46 +1,69 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto'; 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() @Injectable()
export class DriverService { 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> { async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
console.log('[DriverService] Returning mock driver leaderboard.'); this.logger.debug('[DriverService] Fetching drivers 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' }, const presenter = new DriversLeaderboardPresenter();
{ 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' }, await this.getDriversLeaderboardUseCase.execute(undefined, presenter);
]; return presenter.viewModel;
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,
};
} }
async getTotalDrivers(): Promise<DriverStatsDto> { async getTotalDrivers(): Promise<DriverStatsDto> {
console.log('[DriverService] Returning mock total drivers.'); this.logger.debug('[DriverService] Fetching total drivers count.');
return {
totalDrivers: 2, const presenter = new DriverStatsPresenter();
}; await this.getTotalDriversUseCase.execute(undefined, presenter);
return presenter.viewModel;
} }
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> { async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
console.log('Completing onboarding for user:', userId, input); this.logger.debug('Completing onboarding for user:', userId);
return {
success: true, const presenter = new CompleteOnboardingPresenter();
driverId: `driver-${userId}-onboarded`, 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> { async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
console.log('Checking driver registration status:', query); this.logger.debug('Checking driver registration status:', query);
return {
isRegistered: false, // Mock response const presenter = new DriverRegistrationStatusPresenter();
raceId: query.raceId, await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }, presenter);
driverId: query.driverId, return presenter.viewModel;
};
} }
} }

View File

@@ -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');
});
});
});

View File

@@ -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;
}
}

View File

@@ -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');
});
});
});

View File

@@ -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();
}
}

View File

@@ -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');
});
});
});

View File

@@ -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;
}
}

View File

@@ -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');
});
});
});

View File

@@ -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;
}
}

View File

@@ -0,0 +1,43 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LeagueController } from './LeagueController';
import { LeagueService } from './LeagueService';
import { LeagueProviders } from './LeagueProviders';
describe('LeagueController (integration)', () => {
let controller: LeagueController;
let service: LeagueService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LeagueController],
providers: [LeagueService, ...LeagueProviders],
}).compile();
controller = module.get<LeagueController>(LeagueController);
service = module.get<LeagueService>(LeagueService);
});
it('should get total leagues', async () => {
const result = await controller.getTotalLeagues();
expect(result).toHaveProperty('totalLeagues');
expect(typeof result.totalLeagues).toBe('number');
});
it('should get all leagues with capacity', async () => {
const result = await controller.getAllLeaguesWithCapacity();
expect(result).toHaveProperty('leagues');
expect(result).toHaveProperty('totalCount');
expect(Array.isArray(result.leagues)).toBe(true);
});
it('should get league standings', async () => {
try {
const result = await controller.getLeagueStandings('non-existent-league');
expect(result).toHaveProperty('standings');
expect(Array.isArray(result.standings)).toBe(true);
} catch (error) {
// Expected for non-existent league
expect(error.message).toContain('not found');
}
});
});

View File

@@ -1,7 +1,7 @@
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common'; import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger'; import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
import { LeagueService } from './LeagueService'; import { LeagueService } from './LeagueService';
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel } from './dto/LeagueDto'; import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, LeagueMembershipsViewModel, LeagueStandingsViewModel, LeagueScheduleViewModel, LeagueStatsViewModel, LeagueAdminViewModel, CreateLeagueInput, CreateLeagueOutput } from './dto/LeagueDto';
import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries
@ApiTags('leagues') @ApiTags('leagues')
@@ -133,4 +133,47 @@ export class LeagueController {
const query: GetLeagueSeasonsQuery = { leagueId }; const query: GetLeagueSeasonsQuery = { leagueId };
return this.leagueService.getLeagueSeasons(query); return this.leagueService.getLeagueSeasons(query);
} }
@Get(':leagueId/memberships')
@ApiOperation({ summary: 'Get league memberships' })
@ApiResponse({ status: 200, description: 'List of league members', type: LeagueMembershipsViewModel })
async getLeagueMemberships(@Param('leagueId') leagueId: string): Promise<LeagueMembershipsViewModel> {
return this.leagueService.getLeagueMemberships(leagueId);
}
@Get(':leagueId/standings')
@ApiOperation({ summary: 'Get league standings' })
@ApiResponse({ status: 200, description: 'League standings', type: LeagueStandingsViewModel })
async getLeagueStandings(@Param('leagueId') leagueId: string): Promise<LeagueStandingsViewModel> {
return this.leagueService.getLeagueStandings(leagueId);
}
@Get(':leagueId/schedule')
@ApiOperation({ summary: 'Get league schedule' })
@ApiResponse({ status: 200, description: 'League schedule', type: LeagueScheduleViewModel })
async getLeagueSchedule(@Param('leagueId') leagueId: string): Promise<LeagueScheduleViewModel> {
return this.leagueService.getLeagueSchedule(leagueId);
}
@Get(':leagueId/stats')
@ApiOperation({ summary: 'Get league stats' })
@ApiResponse({ status: 200, description: 'League stats', type: LeagueStatsViewModel })
async getLeagueStats(@Param('leagueId') leagueId: string): Promise<LeagueStatsViewModel> {
return this.leagueService.getLeagueStats(leagueId);
}
@Get(':leagueId/admin')
@ApiOperation({ summary: 'Get league admin data' })
@ApiResponse({ status: 200, description: 'League admin data', type: LeagueAdminViewModel })
async getLeagueAdmin(@Param('leagueId') leagueId: string): Promise<LeagueAdminViewModel> {
return this.leagueService.getLeagueAdmin(leagueId);
}
@Post()
@ApiOperation({ summary: 'Create a new league' })
@ApiBody({ type: CreateLeagueInput })
@ApiResponse({ status: 201, description: 'League created successfully', type: CreateLeagueOutput })
async createLeague(@Body() input: CreateLeagueInput): Promise<CreateLeagueOutput> {
return this.leagueService.createLeague(input);
}
} }

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { LeagueService } from './LeagueService'; import { LeagueService } from './LeagueService';
import { LeagueController } from './LeagueController'; import { LeagueController } from './LeagueController';
import { LeagueProviders } from './LeagueProviders';
@Module({ @Module({
controllers: [LeagueController], controllers: [LeagueController],
providers: [LeagueService], providers: LeagueProviders,
exports: [LeagueService], exports: [LeagueService],
}) })
export class LeagueModule {} export class LeagueModule {}

View File

@@ -2,15 +2,7 @@ import { Provider } from '@nestjs/common';
import { LeagueService } from './LeagueService'; import { LeagueService } from './LeagueService';
// Import core interfaces // Import core interfaces
import { ILeagueRepository } from 'core/racing/domain/repositories/ILeagueRepository'; import { Logger } from '@gridpilot/shared/application/Logger';
import { ILeagueMembershipRepository } from 'core/racing/domain/repositories/ILeagueMembershipRepository';
import { ILeagueStandingsRepository } from 'core/league/application/ports/ILeagueStandingsRepository';
import { ISeasonRepository } from 'core/racing/domain/repositories/ISeasonRepository';
import { ILeagueScoringConfigRepository } from 'core/racing/domain/repositories/ILeagueScoringConfigRepository';
import { IGameRepository } from 'core/racing/domain/repositories/IGameRepository';
import { IProtestRepository } from 'core/racing/domain/repositories/IProtestRepository';
import { IRaceRepository } from 'core/racing/domain/repositories/IRaceRepository';
import { Logger } from 'core/shared/logging/Logger';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository'; import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
@@ -21,17 +13,41 @@ import { InMemoryLeagueScoringConfigRepository } from 'adapters/racing/persisten
import { InMemoryGameRepository } from 'adapters/racing/persistence/inmemory/InMemoryGameRepository'; import { InMemoryGameRepository } from 'adapters/racing/persistence/inmemory/InMemoryGameRepository';
import { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository'; import { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository';
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository'; import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryDriverRepository } from 'adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryStandingRepository } from 'adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger'; import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@gridpilot/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@gridpilot/racing/application/use-cases/GetTotalLeaguesUseCase';
import { GetLeagueJoinRequestsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
import { ApproveLeagueJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
import { RejectLeagueJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
import { RemoveLeagueMemberUseCase } from '@gridpilot/racing/application/use-cases/RemoveLeagueMemberUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@gridpilot/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
import { GetLeagueOwnerSummaryUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { GetLeagueProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueProtestsUseCase';
import { GetLeagueSeasonsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueSeasonsUseCase';
import { GetLeagueMembershipsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueMembershipsUseCase';
import { GetLeagueScheduleUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScheduleUseCase';
import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueAdminPermissionsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
// Define injection tokens // Define injection tokens
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository'; export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository'; export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository';
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository'; export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository'; export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
export const GAME_REPOSITORY_TOKEN = 'IGameRepository'; export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository'; 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 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 LeagueProviders: Provider[] = [ export const LeagueProviders: Provider[] = [
@@ -51,6 +67,11 @@ export const LeagueProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
inject: [LOGGER_TOKEN], inject: [LOGGER_TOKEN],
}, },
{
provide: STANDING_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger), // Factory for InMemoryStandingRepository
inject: [LOGGER_TOKEN],
},
{ {
provide: SEASON_REPOSITORY_TOKEN, provide: SEASON_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository useFactory: (logger: Logger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
@@ -76,8 +97,33 @@ export const LeagueProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger), useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
inject: [LOGGER_TOKEN], inject: [LOGGER_TOKEN],
}, },
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
{ {
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
}, },
// Use cases
GetAllLeaguesWithCapacityUseCase,
GetLeagueStandingsUseCase,
GetLeagueStatsUseCase,
GetLeagueFullConfigUseCase,
CreateLeagueWithSeasonAndScoringUseCase,
GetRaceProtestsUseCase,
GetTotalLeaguesUseCase,
GetLeagueJoinRequestsUseCase,
ApproveLeagueJoinRequestUseCase,
RejectLeagueJoinRequestUseCase,
RemoveLeagueMemberUseCase,
UpdateLeagueMemberRoleUseCase,
GetLeagueOwnerSummaryUseCase,
GetLeagueProtestsUseCase,
GetLeagueSeasonsUseCase,
GetLeagueMembershipsUseCase,
GetLeagueScheduleUseCase,
GetLeagueStatsUseCase,
GetLeagueAdminPermissionsUseCase,
]; ];

View File

@@ -0,0 +1,170 @@
import { LeagueService } from './LeagueService';
import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@gridpilot/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@gridpilot/racing/application/use-cases/GetTotalLeaguesUseCase';
import { GetLeagueJoinRequestsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
import { ApproveLeagueJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
import { RejectLeagueJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
import { RemoveLeagueMemberUseCase } from '@gridpilot/racing/application/use-cases/RemoveLeagueMemberUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@gridpilot/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
import { GetLeagueOwnerSummaryUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { GetLeagueProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueProtestsUseCase';
import { Logger } from '@gridpilot/shared/application/Logger';
describe('LeagueService', () => {
let service: LeagueService;
let mockGetTotalLeaguesUseCase: jest.Mocked<GetTotalLeaguesUseCase>;
let mockGetLeagueJoinRequestsUseCase: jest.Mocked<GetLeagueJoinRequestsUseCase>;
let mockApproveLeagueJoinRequestUseCase: jest.Mocked<ApproveLeagueJoinRequestUseCase>;
let mockLogger: jest.Mocked<Logger>;
beforeEach(() => {
mockGetTotalLeaguesUseCase = {
execute: jest.fn(),
} as any;
mockGetLeagueJoinRequestsUseCase = {
execute: jest.fn(),
} as any;
mockApproveLeagueJoinRequestUseCase = {
execute: jest.fn(),
} as any;
mockLogger = {
debug: jest.fn(),
} as any;
service = new LeagueService(
{} as any, // mockGetAllLeaguesWithCapacityUseCase
{} as any, // mockGetLeagueStandingsUseCase
{} as any, // mockGetLeagueStatsUseCase
{} as any, // mockGetLeagueFullConfigUseCase
{} as any, // mockCreateLeagueWithSeasonAndScoringUseCase
{} as any, // mockGetRaceProtestsUseCase
mockGetTotalLeaguesUseCase,
mockGetLeagueJoinRequestsUseCase,
mockApproveLeagueJoinRequestUseCase,
{} as any, // mockRejectLeagueJoinRequestUseCase
{} as any, // mockRemoveLeagueMemberUseCase
{} as any, // mockUpdateLeagueMemberRoleUseCase
{} as any, // mockGetLeagueOwnerSummaryUseCase
{} as any, // mockGetLeagueProtestsUseCase
{} as any, // mockGetLeagueSeasonsUseCase
{} as any, // mockGetLeagueMembershipsUseCase
{} as any, // mockGetLeagueScheduleUseCase
{} as any, // mockGetLeagueAdminPermissionsUseCase
mockLogger,
);
});
it('should get total leagues', async () => {
mockGetTotalLeaguesUseCase.execute.mockImplementation(async (params, presenter) => {
presenter.present({ totalLeagues: 5 });
});
const result = await service.getTotalLeagues();
expect(result).toEqual({ totalLeagues: 5 });
expect(mockLogger.debug).toHaveBeenCalledWith('[LeagueService] Fetching total leagues count.');
});
it('should get league join requests', async () => {
mockGetLeagueJoinRequestsUseCase.execute.mockImplementation(async (params, presenter) => {
presenter.present({
joinRequests: [{ id: 'req-1', leagueId: 'league-1', driverId: 'driver-1', requestedAt: new Date(), message: 'msg' }],
drivers: [{ id: 'driver-1', name: 'Driver 1' }],
});
});
const result = await service.getLeagueJoinRequests('league-1');
expect(result).toEqual([{
id: 'req-1',
leagueId: 'league-1',
driverId: 'driver-1',
requestedAt: expect.any(Date),
message: 'msg',
driver: { id: 'driver-1', name: 'Driver 1' },
}]);
});
it('should approve league join request', async () => {
mockApproveLeagueJoinRequestUseCase.execute.mockImplementation(async (params, presenter) => {
presenter.present({ success: true, message: 'Join request approved.' });
});
const result = await service.approveLeagueJoinRequest({ leagueId: 'league-1', requestId: 'req-1' });
expect(result).toEqual({ success: true, message: 'Join request approved.' });
});
it('should reject league join request', async () => {
const mockRejectUseCase = {
execute: jest.fn().mockImplementation(async (params, presenter) => {
presenter.present({ success: true, message: 'Join request rejected.' });
}),
} as any;
service = new LeagueService(
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
mockRejectUseCase,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
mockLogger,
);
const result = await service.rejectLeagueJoinRequest({ requestId: 'req-1', leagueId: 'league-1' });
expect(result).toEqual({ success: true, message: 'Join request rejected.' });
});
it('should remove league member', async () => {
const mockRemoveUseCase = {
execute: jest.fn().mockImplementation(async (params, presenter) => {
presenter.present({ success: true });
}),
} as any;
service = new LeagueService(
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
mockRemoveUseCase,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
mockLogger,
);
const result = await service.removeLeagueMember({ leagueId: 'league-1', performerDriverId: 'performer-1', targetDriverId: 'driver-1' });
expect(result).toEqual({ success: true });
});
});

View File

@@ -1,125 +1,237 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, GetLeagueAdminPermissionsInput, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery, LeagueMembershipsViewModel, LeagueStandingsViewModel, LeagueScheduleViewModel, LeagueStatsViewModel, LeagueAdminViewModel, CreateLeagueInput, CreateLeagueOutput } from './dto/LeagueDto';
import { DriverDto } from '../driver/dto/DriverDto'; // Using the local DTO for mock data
import { RaceDto } from '../race/dto/RaceDto'; // Using the local DTO for mock data
const mockDriverData: Map<string, DriverDto> = new Map(); // Core imports
mockDriverData.set('driver-owner-1', { id: 'driver-owner-1', name: 'Owner Driver' }); import { Logger } from '@gridpilot/shared/application/Logger';
mockDriverData.set('driver-1', { id: 'driver-1', name: 'Demo Driver 1' });
mockDriverData.set('driver-2', { id: 'driver-2', name: 'Demo Driver 2' });
const mockRaceData: Map<string, RaceDto> = new Map(); // Use cases
mockRaceData.set('race-1', { id: 'race-1', name: 'Test Race 1', date: new Date().toISOString() }); import { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
mockRaceData.set('race-2', { id: 'race-2', name: 'Test Race 2', date: new Date().toISOString() }); import { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@gridpilot/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@gridpilot/racing/application/use-cases/GetTotalLeaguesUseCase';
import { GetLeagueJoinRequestsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
import { ApproveLeagueJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
import { RejectLeagueJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
import { RemoveLeagueMemberUseCase } from '@gridpilot/racing/application/use-cases/RemoveLeagueMemberUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@gridpilot/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
import { GetLeagueOwnerSummaryUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { GetLeagueProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueProtestsUseCase';
import { GetLeagueSeasonsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueSeasonsUseCase';
import { GetLeagueMembershipsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueMembershipsUseCase';
import { GetLeagueScheduleUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScheduleUseCase';
import { GetLeagueAdminPermissionsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
// API Presenters
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter';
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter';
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter';
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter';
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
// Tokens
import { LOGGER_TOKEN } from './LeagueProviders';
@Injectable() @Injectable()
export class LeagueService { export class LeagueService {
constructor(
constructor() {} private readonly getAllLeaguesWithCapacityUseCase: GetAllLeaguesWithCapacityUseCase,
private readonly getLeagueStandingsUseCase: GetLeagueStandingsUseCase,
private readonly getLeagueStatsUseCase: GetLeagueStatsUseCase,
private readonly getLeagueFullConfigUseCase: GetLeagueFullConfigUseCase,
private readonly createLeagueWithSeasonAndScoringUseCase: CreateLeagueWithSeasonAndScoringUseCase,
private readonly getRaceProtestsUseCase: GetRaceProtestsUseCase,
private readonly getTotalLeaguesUseCase: GetTotalLeaguesUseCase,
private readonly getLeagueJoinRequestsUseCase: GetLeagueJoinRequestsUseCase,
private readonly approveLeagueJoinRequestUseCase: ApproveLeagueJoinRequestUseCase,
private readonly rejectLeagueJoinRequestUseCase: RejectLeagueJoinRequestUseCase,
private readonly removeLeagueMemberUseCase: RemoveLeagueMemberUseCase,
private readonly updateLeagueMemberRoleUseCase: UpdateLeagueMemberRoleUseCase,
private readonly getLeagueOwnerSummaryUseCase: GetLeagueOwnerSummaryUseCase,
private readonly getLeagueProtestsUseCase: GetLeagueProtestsUseCase,
private readonly getLeagueSeasonsUseCase: GetLeagueSeasonsUseCase,
private readonly getLeagueMembershipsUseCase: GetLeagueMembershipsUseCase,
private readonly getLeagueScheduleUseCase: GetLeagueScheduleUseCase,
private readonly getLeagueAdminPermissionsUseCase: GetLeagueAdminPermissionsUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> { async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
console.log('[LeagueService] Returning mock leagues with capacity.'); this.logger.debug('[LeagueService] Fetching all leagues with capacity.');
return {
leagues: [ const presenter = new AllLeaguesWithCapacityPresenter();
{ id: 'league-1', name: 'Global Racing', description: 'The premier league', ownerId: 'owner-1', settings: { maxDrivers: 100 }, createdAt: new Date().toISOString(), usedSlots: 50, socialLinks: { discordUrl: 'https://discord.gg/test' } }, await this.getAllLeaguesWithCapacityUseCase.execute(undefined, presenter);
{ id: 'league-2', name: 'Amateur Series', description: 'Learn the ropes', ownerId: 'owner-2', settings: { maxDrivers: 50 }, createdAt: new Date().toISOString(), usedSlots: 20 }, return presenter.getViewModel()!;
],
totalCount: 2,
};
} }
async getTotalLeagues(): Promise<LeagueStatsDto> { async getTotalLeagues(): Promise<LeagueStatsDto> {
console.log('[LeagueService] Returning mock total leagues.'); this.logger.debug('[LeagueService] Fetching total leagues count.');
return { totalLeagues: 2 }; const presenter = new TotalLeaguesPresenter();
await this.getTotalLeaguesUseCase.execute({}, presenter);
return presenter.getViewModel()!;
} }
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> { async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
console.log(`[LeagueService] Returning mock join requests for league: ${leagueId}.`); this.logger.debug(`[LeagueService] Fetching join requests for league: ${leagueId}.`);
return [ const presenter = new LeagueJoinRequestsPresenter();
{ await this.getLeagueJoinRequestsUseCase.execute({ leagueId }, presenter);
id: 'join-req-1', return presenter.getViewModel()!.joinRequests;
leagueId: 'league-1',
driverId: 'driver-1',
requestedAt: new Date(),
message: 'I want to join!',
driver: mockDriverData.get('driver-1'),
},
];
} }
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> { async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
console.log('Approving join request:', input); this.logger.debug('Approving join request:', input);
return { success: true, message: 'Join request approved.' }; const presenter = new ApproveLeagueJoinRequestPresenter();
await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId }, presenter);
return presenter.getViewModel()!;
} }
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> { async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
console.log('Rejecting join request:', input); this.logger.debug('Rejecting join request:', input);
return { success: true, message: 'Join request rejected.' }; const presenter = new RejectLeagueJoinRequestPresenter();
await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId }, presenter);
return presenter.getViewModel()!;
} }
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> { async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
console.log('Getting league admin permissions:', query); this.logger.debug('Getting league admin permissions', { query });
return { canRemoveMember: true, canUpdateRoles: true }; const presenter = new GetLeagueAdminPermissionsPresenter();
await this.getLeagueAdminPermissionsUseCase.execute(
{ leagueId: query.leagueId, performerDriverId: query.performerDriverId },
presenter
);
return presenter.getViewModel()!;
} }
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> { async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
console.log('Removing league member:', input.leagueId, input.targetDriverId); this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId });
return { success: true }; const presenter = new RemoveLeagueMemberPresenter();
await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId }, presenter);
return presenter.getViewModel()!;
} }
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> { async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
console.log('Updating league member role:', input.leagueId, input.targetDriverId, input.newRole); this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
return { success: true }; const presenter = new UpdateLeagueMemberRolePresenter();
await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }, presenter);
return presenter.getViewModel()!;
} }
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> { async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> {
console.log('Getting league owner summary:', query); this.logger.debug('Getting league owner summary:', query);
return { const presenter = new GetLeagueOwnerSummaryPresenter();
driver: mockDriverData.get(query.ownerId)!, await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId }, presenter);
rating: 2000, return presenter.getViewModel()!.summary;
rank: 1,
};
} }
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> { async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
console.log('Getting league full config:', query); this.logger.debug('Getting league full config', { query });
return {
leagueId: 'league-1', const presenter = new LeagueConfigPresenter();
basics: { name: 'Demo League', description: 'A demo league', visibility: 'public' }, try {
structure: { mode: 'solo' }, await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId }, presenter);
championships: [], return presenter.viewModel;
scoring: { type: 'standard', points: 10 }, } catch (error) {
dropPolicy: { strategy: 'none' }, this.logger.error('Error getting league full config', error);
timings: { raceDayOfWeek: 'Sunday', raceTimeHour: 20, raceTimeMinute: 0 }, return null;
stewarding: { }
decisionMode: 'single_steward',
requireDefense: false,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 2,
stewardingClosesHours: 24,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
},
};
} }
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> { async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
console.log('Getting league protests:', query); this.logger.debug('Getting league protests:', query);
return { const presenter = new GetLeagueProtestsPresenter();
protests: [ await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId }, presenter);
{ id: 'protest-1', raceId: 'race-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', submittedAt: new Date(), description: 'Bad driving!', status: 'pending' }, return presenter.getViewModel()!;
],
racesById: { 'race-1': mockRaceData.get('race-1')! },
driversById: { 'driver-1': mockDriverData.get('driver-1')!, 'driver-2': mockDriverData.get('driver-2')! },
};
} }
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> { async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
console.log('Getting league seasons:', query); this.logger.debug('Getting league seasons:', query);
return [ const presenter = new GetLeagueSeasonsPresenter();
{ seasonId: 'season-1', name: 'Season 1', status: 'active', startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), isPrimary: true, isParallelActive: false }, await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId }, presenter);
{ seasonId: 'season-2', name: 'Season 2', status: 'upcoming', startDate: new Date('2026-01-01'), endDate: new Date('2026-12-31'), isPrimary: false, isParallelActive: false }, return presenter.getViewModel()!.seasons;
]; }
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsViewModel> {
this.logger.debug('Getting league memberships', { leagueId });
const presenter = new GetLeagueMembershipsPresenter();
await this.getLeagueMembershipsUseCase.execute({ leagueId }, presenter);
return presenter.apiViewModel!;
}
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
this.logger.debug('Getting league standings', { leagueId });
const presenter = new LeagueStandingsPresenter();
await this.getLeagueStandingsUseCase.execute({ leagueId }, presenter);
return presenter.getViewModel()!;
}
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
this.logger.debug('Getting league schedule', { leagueId });
const presenter = new LeagueSchedulePresenter();
await this.getLeagueScheduleUseCase.execute({ leagueId }, presenter);
return presenter.getViewModel()!;
}
async getLeagueStats(leagueId: string): Promise<LeagueStatsViewModel> {
this.logger.debug('Getting league stats', { leagueId });
const presenter = new LeagueStatsPresenter();
await this.getLeagueStatsUseCase.execute({ leagueId }, presenter);
return presenter.getViewModel()!;
}
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminViewModel> {
this.logger.debug('Getting league admin data', { leagueId });
// 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
const joinRequests = await this.getLeagueJoinRequests(leagueId);
const config = await this.getLeagueFullConfig({ leagueId });
const protests = await this.getLeagueProtests({ leagueId });
const seasons = await this.getLeagueSeasons({ leagueId });
// Get owner summary - we need the ownerId, so we use a simple approach for now
// In a full implementation, we'd have a use case that gets league basic info
const ownerSummary = config ? await this.getLeagueOwnerSummary({ ownerId: 'placeholder', leagueId }) : null;
return {
joinRequests,
ownerSummary,
config: { form: config },
protests,
seasons,
};
}
async createLeague(input: CreateLeagueInput): Promise<CreateLeagueOutput> {
this.logger.debug('Creating league', { input });
const command = {
name: input.name,
description: input.description,
ownerId: input.ownerId,
visibility: 'unranked' as const,
gameId: 'iracing', // Assume default
maxDrivers: 32, // Default value
enableDriverChampionship: true,
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
};
const result = await this.createLeagueWithSeasonAndScoringUseCase.execute(command);
return {
leagueId: result.leagueId,
success: true,
};
} }
} }

View File

@@ -559,3 +559,108 @@ export class LeagueAdminViewModel {
@Type(() => LeagueSeasonSummaryViewModel) @Type(() => LeagueSeasonSummaryViewModel)
seasons: LeagueSeasonSummaryViewModel[]; seasons: LeagueSeasonSummaryViewModel[];
} }
export class LeagueMemberDto {
@ApiProperty()
@IsString()
driverId: string;
@ApiProperty({ type: () => DriverDto })
@ValidateNested()
@Type(() => DriverDto)
driver: DriverDto;
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
@IsEnum(['owner', 'manager', 'member'])
role: 'owner' | 'manager' | 'member';
@ApiProperty()
@IsDate()
@Type(() => Date)
joinedAt: Date;
}
export class LeagueMembershipsViewModel {
@ApiProperty({ type: [LeagueMemberDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => LeagueMemberDto)
members: LeagueMemberDto[];
}
export class LeagueStandingDto {
@ApiProperty()
@IsString()
driverId: string;
@ApiProperty({ type: () => DriverDto })
@ValidateNested()
@Type(() => DriverDto)
driver: DriverDto;
@ApiProperty()
@IsNumber()
points: number;
@ApiProperty()
@IsNumber()
rank: number;
}
export class LeagueStandingsViewModel {
@ApiProperty({ type: [LeagueStandingDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => LeagueStandingDto)
standings: LeagueStandingDto[];
}
export class LeagueScheduleViewModel {
@ApiProperty({ type: [RaceDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => RaceDto)
races: RaceDto[];
}
export class LeagueStatsViewModel {
@ApiProperty()
@IsNumber()
totalMembers: number;
@ApiProperty()
@IsNumber()
totalRaces: number;
@ApiProperty()
@IsNumber()
averageRating: number;
}
export class CreateLeagueInput {
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsString()
description: string;
@ApiProperty({ enum: ['public', 'private'] })
@IsEnum(['public', 'private'])
visibility: 'public' | 'private';
@ApiProperty()
@IsString()
ownerId: string;
}
export class CreateLeagueOutput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsBoolean()
success: boolean;
}

View File

@@ -0,0 +1,30 @@
import { IAllLeaguesWithCapacityPresenter, AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel } from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter {
private result: AllLeaguesWithCapacityViewModel | null = null;
reset() {
this.result = null;
}
present(dto: AllLeaguesWithCapacityResultDTO) {
const leagues = dto.leagues.map(league => ({
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: { maxDrivers: league.settings.maxDrivers || 0 },
createdAt: league.createdAt.toISOString(),
usedSlots: dto.memberCounts.get(league.id) || 0,
socialLinks: league.socialLinks,
}));
this.result = {
leagues,
totalCount: leagues.length,
};
}
getViewModel(): AllLeaguesWithCapacityViewModel | null {
return this.result;
}
}

View File

@@ -0,0 +1,18 @@
import { IApproveLeagueJoinRequestPresenter, ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel } from '@gridpilot/racing/application/presenters/IApproveLeagueJoinRequestPresenter';
export class ApproveLeagueJoinRequestPresenter implements IApproveLeagueJoinRequestPresenter {
private result: ApproveLeagueJoinRequestViewModel | null = null;
reset() {
this.result = null;
}
present(dto: ApproveLeagueJoinRequestResultDTO) {
this.result = dto;
}
getViewModel(): ApproveLeagueJoinRequestViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,17 @@
import { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueAdminPermissionsPresenter';
export class GetLeagueAdminPermissionsPresenter implements IGetLeagueAdminPermissionsPresenter {
private result: GetLeagueAdminPermissionsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueAdminPermissionsResultDTO) {
this.result = dto;
}
getViewModel(): GetLeagueAdminPermissionsViewModel | null {
return this.result;
}
}

View File

@@ -0,0 +1,47 @@
import { IGetLeagueMembershipsPresenter, GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueMembershipsPresenter';
import { LeagueMembershipsViewModel } from '../dto/LeagueDto';
export class GetLeagueMembershipsPresenter implements IGetLeagueMembershipsPresenter {
private result: GetLeagueMembershipsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueMembershipsResultDTO) {
const driverMap = new Map(dto.drivers.map(d => [d.id, d]));
const members = dto.memberships.map(membership => ({
driverId: membership.driverId,
driver: driverMap.get(membership.driverId)!,
role: this.mapRole(membership.role) as 'owner' | 'manager' | 'member',
joinedAt: membership.joinedAt,
}));
this.result = { memberships: { members } };
}
private mapRole(role: string): 'owner' | 'manager' | 'member' {
switch (role) {
case 'owner':
return 'owner';
case 'admin':
return 'manager'; // Map admin to manager for API
case 'steward':
return 'member'; // Map steward to member for API
case 'member':
return 'member';
default:
return 'member';
}
}
getViewModel(): GetLeagueMembershipsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
// API-specific method
get apiViewModel(): LeagueMembershipsViewModel | null {
if (!this.result?.memberships) return null;
return this.result.memberships as LeagueMembershipsViewModel;
}
}

View File

@@ -0,0 +1,18 @@
import { IGetLeagueOwnerSummaryPresenter, GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueOwnerSummaryPresenter';
export class GetLeagueOwnerSummaryPresenter implements IGetLeagueOwnerSummaryPresenter {
private result: GetLeagueOwnerSummaryViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueOwnerSummaryResultDTO) {
this.result = { summary: dto.summary };
}
getViewModel(): GetLeagueOwnerSummaryViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,30 @@
import { IGetLeagueProtestsPresenter, GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueProtestsPresenter';
export class GetLeagueProtestsPresenter implements IGetLeagueProtestsPresenter {
private result: GetLeagueProtestsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueProtestsResultDTO) {
const racesById = {};
dto.races.forEach(race => {
racesById[race.id] = race;
});
const driversById = {};
dto.drivers.forEach(driver => {
driversById[driver.id] = driver;
});
this.result = {
protests: dto.protests,
racesById,
driversById,
};
}
getViewModel(): GetLeagueProtestsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,27 @@
import { IGetLeagueSeasonsPresenter, GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueSeasonsPresenter';
export class GetLeagueSeasonsPresenter implements IGetLeagueSeasonsPresenter {
private result: GetLeagueSeasonsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueSeasonsResultDTO) {
const seasons = dto.seasons.map(season => ({
seasonId: season.id,
name: season.name,
status: season.status,
startDate: season.startDate,
endDate: season.endDate,
isPrimary: season.isPrimary,
isParallelActive: season.isParallelActive,
}));
this.result = { seasons };
}
getViewModel(): GetLeagueSeasonsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,30 @@
import { LeagueAdminViewModel } from '../dto/LeagueDto';
export class LeagueAdminPresenter {
private result: LeagueAdminViewModel | null = null;
reset() {
this.result = null;
}
present(data: {
joinRequests: any[];
ownerSummary: any;
config: any;
protests: any;
seasons: any[];
}) {
this.result = {
joinRequests: data.joinRequests,
ownerSummary: data.ownerSummary,
config: { form: data.config },
protests: data.protests,
seasons: data.seasons,
};
}
getViewModel(): LeagueAdminViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,109 @@
import { ILeagueFullConfigPresenter, LeagueFullConfigData, LeagueConfigFormViewModel } from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter';
import { LeagueConfigFormModelDto } from '../dto/LeagueDto';
export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
private result: LeagueConfigFormViewModel | null = null;
reset() {
this.result = null;
}
present(dto: LeagueFullConfigData) {
// Map from LeagueFullConfigData to LeagueConfigFormViewModel
const league = dto.league;
const settings = league.settings;
const stewarding = settings.stewarding;
this.result = {
leagueId: league.id,
basics: {
name: league.name,
description: league.description,
visibility: 'public', // TODO: Map visibility from league
gameId: 'iracing', // TODO: Map from game
},
structure: {
mode: 'solo', // TODO: Map from league settings
maxDrivers: settings.maxDrivers || 32,
multiClassEnabled: false, // TODO: Map
},
championships: {
enableDriverChampionship: true, // TODO: Map
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
},
scoring: {
customScoringEnabled: false, // TODO: Map
},
dropPolicy: {
strategy: 'none', // TODO: Map
},
timings: {
practiceMinutes: 30, // TODO: Map
qualifyingMinutes: 15,
mainRaceMinutes: 60,
sessionCount: 1,
roundsPlanned: 10, // TODO: Map
},
stewarding: {
decisionMode: stewarding?.decisionMode || 'admin_only',
requireDefense: stewarding?.requireDefense || false,
defenseTimeLimit: stewarding?.defenseTimeLimit || 48,
voteTimeLimit: stewarding?.voteTimeLimit || 72,
protestDeadlineHours: stewarding?.protestDeadlineHours || 48,
stewardingClosesHours: stewarding?.stewardingClosesHours || 168,
notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true,
notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true,
requiredVotes: stewarding?.requiredVotes,
},
};
}
getViewModel(): LeagueConfigFormViewModel | null {
return this.result;
}
// API-specific method to get the DTO
get viewModel(): LeagueConfigFormModelDto | null {
if (!this.result) return null;
// Map from LeagueConfigFormViewModel to LeagueConfigFormModelDto
return {
leagueId: this.result.leagueId,
basics: {
name: this.result.basics.name,
description: this.result.basics.description,
visibility: this.result.basics.visibility as 'public' | 'private',
},
structure: {
mode: this.result.structure.mode as 'solo' | 'team',
},
championships: [], // TODO: Map championships
scoring: {
type: 'standard', // TODO: Map scoring type
points: 25, // TODO: Map points
},
dropPolicy: {
strategy: this.result.dropPolicy.strategy as 'none' | 'worst_n',
n: this.result.dropPolicy.n,
},
timings: {
raceDayOfWeek: 'sunday', // TODO: Map from timings
raceTimeHour: 20,
raceTimeMinute: 0,
},
stewarding: {
decisionMode: this.result.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
requireDefense: this.result.stewarding.requireDefense,
defenseTimeLimit: this.result.stewarding.defenseTimeLimit,
voteTimeLimit: this.result.stewarding.voteTimeLimit,
protestDeadlineHours: this.result.stewarding.protestDeadlineHours,
stewardingClosesHours: this.result.stewarding.stewardingClosesHours,
notifyAccusedOnProtest: this.result.stewarding.notifyAccusedOnProtest,
notifyOnVoteRequired: this.result.stewarding.notifyOnVoteRequired,
requiredVotes: this.result.stewarding.requiredVotes,
},
};
}
}

View File

@@ -0,0 +1,27 @@
import { IGetLeagueJoinRequestsPresenter, GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueJoinRequestsPresenter';
export class LeagueJoinRequestsPresenter implements IGetLeagueJoinRequestsPresenter {
private result: GetLeagueJoinRequestsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueJoinRequestsResultDTO) {
const driverMap = new Map(dto.drivers.map(d => [d.id, d]));
const joinRequests = dto.joinRequests.map(request => ({
id: request.id,
leagueId: request.leagueId,
driverId: request.driverId,
requestedAt: request.requestedAt,
message: request.message,
driver: driverMap.get(request.driverId) || null,
}));
this.result = { joinRequests };
}
getViewModel(): GetLeagueJoinRequestsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,23 @@
import { IGetLeagueSchedulePresenter, GetLeagueScheduleResultDTO, LeagueScheduleViewModel } from '@gridpilot/racing/application/presenters/IGetLeagueSchedulePresenter';
export class LeagueSchedulePresenter implements IGetLeagueSchedulePresenter {
private result: LeagueScheduleViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueScheduleResultDTO) {
this.result = {
races: dto.races.map(race => ({
id: race.id,
name: race.name,
date: race.scheduledAt.toISOString(),
})),
};
}
getViewModel(): LeagueScheduleViewModel | null {
return this.result;
}
}

View File

@@ -0,0 +1,27 @@
import { ILeagueStandingsPresenter, LeagueStandingsResultDTO, LeagueStandingsViewModel } from '@gridpilot/racing/application/presenters/ILeagueStandingsPresenter';
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
private result: LeagueStandingsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: LeagueStandingsResultDTO) {
const driverMap = new Map(dto.drivers.map(d => [d.id, { id: d.id, name: d.name }]));
const standings = dto.standings
.sort((a, b) => a.position - b.position)
.map(standing => ({
driverId: standing.driverId,
driver: driverMap.get(standing.driverId)!,
points: standing.points,
rank: standing.position,
}));
this.result = { standings };
}
getViewModel(): LeagueStandingsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,17 @@
import { ILeagueStatsPresenter, LeagueStatsResultDTO, LeagueStatsViewModel } from '@gridpilot/racing/application/presenters/ILeagueStatsPresenter';
export class LeagueStatsPresenter implements ILeagueStatsPresenter {
private result: LeagueStatsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: LeagueStatsResultDTO) {
this.result = dto;
}
getViewModel(): LeagueStatsViewModel | null {
return this.result;
}
}

View File

@@ -0,0 +1,18 @@
import { IRejectLeagueJoinRequestPresenter, RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel } from '@gridpilot/racing/application/presenters/IRejectLeagueJoinRequestPresenter';
export class RejectLeagueJoinRequestPresenter implements IRejectLeagueJoinRequestPresenter {
private result: RejectLeagueJoinRequestViewModel | null = null;
reset() {
this.result = null;
}
present(dto: RejectLeagueJoinRequestResultDTO) {
this.result = dto;
}
getViewModel(): RejectLeagueJoinRequestViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,18 @@
import { IRemoveLeagueMemberPresenter, RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel } from '@gridpilot/racing/application/presenters/IRemoveLeagueMemberPresenter';
export class RemoveLeagueMemberPresenter implements IRemoveLeagueMemberPresenter {
private result: RemoveLeagueMemberViewModel | null = null;
reset() {
this.result = null;
}
present(dto: RemoveLeagueMemberResultDTO) {
this.result = dto;
}
getViewModel(): RemoveLeagueMemberViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,20 @@
import { IGetTotalLeaguesPresenter, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel } from '@gridpilot/racing/application/presenters/IGetTotalLeaguesPresenter';
import { LeagueStatsDto } from '../dto/LeagueDto';
export class TotalLeaguesPresenter implements IGetTotalLeaguesPresenter {
private result: LeagueStatsDto | null = null;
reset() {
this.result = null;
}
present(dto: GetTotalLeaguesResultDTO) {
this.result = {
totalLeagues: dto.totalLeagues,
};
}
getViewModel(): LeagueStatsDto | null {
return this.result;
}
}

View File

@@ -0,0 +1,18 @@
import { IUpdateLeagueMemberRolePresenter, UpdateLeagueMemberRoleResultDTO, UpdateLeagueMemberRoleViewModel } from '@gridpilot/racing/application/presenters/IUpdateLeagueMemberRolePresenter';
export class UpdateLeagueMemberRolePresenter implements IUpdateLeagueMemberRolePresenter {
private result: UpdateLeagueMemberRoleViewModel | null = null;
reset() {
this.result = null;
}
present(dto: UpdateLeagueMemberRoleResultDTO) {
this.result = dto;
}
getViewModel(): UpdateLeagueMemberRoleViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MediaService } from './MediaService'; import { MediaService } from './MediaService';
import { MediaController } from './MediaController'; import { MediaController } from './MediaController';
import { MediaProviders } from './MediaProviders';
@Module({ @Module({
controllers: [MediaController], controllers: [MediaController],
providers: [MediaService], providers: MediaProviders,
exports: [MediaService], exports: [MediaService],
}) })
export class MediaModule {} export class MediaModule {}

View File

@@ -7,7 +7,7 @@ import { MediaService } from './MediaService';
/* /*
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository'; import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort'; import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
import { Logger } from 'core/shared/logging/Logger'; import { Logger } from '@gridpilot/shared/logging/Logger';
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository'; import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter'; import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PaymentsService } from './PaymentsService'; import { PaymentsService } from './PaymentsService';
import { PaymentsController } from './PaymentsController'; import { PaymentsController } from './PaymentsController';
import { PaymentsProviders } from './PaymentsProviders';
@Module({ @Module({
controllers: [PaymentsController], controllers: [PaymentsController],
providers: [PaymentsService], providers: PaymentsProviders,
exports: [PaymentsService], exports: [PaymentsService],
}) })
export class PaymentsModule {} export class PaymentsModule {}

View File

@@ -11,7 +11,7 @@ import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMem
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository'; import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository'; import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway'; import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
import { Logger } from 'core/shared/logging/Logger'; import { Logger } from '@gridpilot/shared/logging/Logger';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository'; import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { RaceService } from './RaceService'; import { RaceService } from './RaceService';
import { RaceController } from './RaceController'; import { RaceController } from './RaceController';
import { RaceProviders } from './RaceProviders';
@Module({ @Module({
controllers: [RaceController], controllers: [RaceController],
providers: [RaceService], providers: RaceProviders,
exports: [RaceService], exports: [RaceService],
}) })
export class RaceModule {} export class RaceModule {}

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SponsorService } from './SponsorService'; import { SponsorService } from './SponsorService';
import { SponsorController } from './SponsorController'; import { SponsorController } from './SponsorController';
import { SponsorProviders } from './SponsorProviders';
@Module({ @Module({
controllers: [SponsorController], controllers: [SponsorController],
providers: [SponsorService], providers: SponsorProviders,
exports: [SponsorService], exports: [SponsorService],
}) })
export class SponsorModule {} export class SponsorModule {}

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TeamService } from './TeamService'; import { TeamService } from './TeamService';
import { TeamController } from './TeamController'; import { TeamController } from './TeamController';
import { TeamProviders } from './TeamProviders';
@Module({ @Module({
controllers: [TeamController], controllers: [TeamController],
providers: [TeamService], providers: TeamProviders,
exports: [TeamService], exports: [TeamService],
}) })
export class TeamModule {} export class TeamModule {}

View File

@@ -1,5 +1,54 @@
import { Provider } from '@nestjs/common';
import { TeamService } from './TeamService'; import { TeamService } from './TeamService';
export const TeamProviders = [ // Import core interfaces
TeamService, import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
import { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
import { Logger } from '@gridpilot/shared/application/Logger';
// Import concrete in-memory implementations
import { InMemoryTeamRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamRepository';
import { InMemoryTeamMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
// Tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const TEAM_GET_ALL_USE_CASE_TOKEN = 'GetAllTeamsUseCase';
export const TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
export const TEAM_LOGGER_TOKEN = 'Logger';
export const TeamProviders: Provider[] = [
TeamService, // Provide the service itself
{
provide: TEAM_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
inject: [TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
inject: [TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: TEAM_GET_ALL_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetAllTeamsUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
},
{
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
},
]; ];

View File

@@ -0,0 +1,168 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
import { Logger } from '@gridpilot/shared/application/Logger';
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { AllTeamsViewModel, DriverTeamViewModel, GetDriverTeamQuery } from './dto/TeamDto';
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
describe('TeamService', () => {
let service: TeamService;
let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>;
let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>;
let logger: jest.Mocked<Logger>;
beforeEach(async () => {
const mockGetAllTeamsUseCase = {
execute: jest.fn(),
};
const mockGetDriverTeamUseCase = {
execute: jest.fn(),
};
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TeamService,
{
provide: TEAM_GET_ALL_USE_CASE_TOKEN,
useValue: mockGetAllTeamsUseCase,
},
{
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
useValue: mockGetDriverTeamUseCase,
},
{
provide: TEAM_LOGGER_TOKEN,
useValue: mockLogger,
},
],
}).compile();
service = module.get<TeamService>(TeamService);
getAllTeamsUseCase = module.get(TEAM_GET_ALL_USE_CASE_TOKEN);
getDriverTeamUseCase = module.get(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN);
logger = module.get(TEAM_LOGGER_TOKEN);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('getAllTeams', () => {
it('should create presenter, call use case, and return view model', async () => {
const mockViewModel: AllTeamsViewModel = {
teams: [],
totalCount: 0,
};
const mockPresenter = {
reset: jest.fn(),
present: jest.fn(),
get viewModel(): AllTeamsViewModel {
return mockViewModel;
},
};
// Mock the presenter constructor
const originalConstructor = AllTeamsPresenter;
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
// Mock the use case to call the presenter
getAllTeamsUseCase.execute.mockImplementation(async (input, presenter) => {
presenter.present({ teams: [] });
});
const result = await service.getAllTeams();
expect(AllTeamsPresenter).toHaveBeenCalled();
expect(getAllTeamsUseCase.execute).toHaveBeenCalledWith(undefined, mockPresenter);
expect(result).toBe(mockViewModel);
// Restore
AllTeamsPresenter = originalConstructor;
});
});
describe('getDriverTeam', () => {
it('should create presenter, call use case, and return view model', async () => {
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' };
const mockViewModel: DriverTeamViewModel = {
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 = {
reset: jest.fn(),
present: jest.fn(),
get viewModel(): DriverTeamViewModel {
return mockViewModel;
},
};
// Mock the presenter constructor
const originalConstructor = DriverTeamPresenter;
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
// Mock the use case to call the presenter
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(DriverTeamPresenter).toHaveBeenCalled();
expect(getDriverTeamUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver1' }, mockPresenter);
expect(result).toBe(mockViewModel);
// Restore
DriverTeamPresenter = originalConstructor;
});
it('should return null on error', async () => {
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' };
// Mock the use case to throw an error
getDriverTeamUseCase.execute.mockRejectedValue(new Error('Team not found'));
const result = await service.getDriverTeam(query);
expect(result).toBeNull();
expect(logger.error).toHaveBeenCalled();
});
});
});

View File

@@ -1,43 +1,46 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto'; import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel } from './dto/TeamDto';
// Use cases
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
// Presenters
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
// Logger
import { Logger } from '@gridpilot/shared/application/Logger';
// Tokens
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
@Injectable() @Injectable()
export class TeamService { export class TeamService {
getAllTeams(): Promise<AllTeamsViewModel> { constructor(
// TODO: Implement actual logic to fetch all teams @Inject(TEAM_GET_ALL_USE_CASE_TOKEN) private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
return Promise.resolve({ @Inject(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN) private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
teams: [], @Inject(TEAM_LOGGER_TOKEN) private readonly logger: Logger,
totalCount: 0, ) {}
});
}
private teams: Map<string, TeamDto> = new Map(); // In-memory store for teams async getAllTeams(): Promise<AllTeamsViewModel> {
this.logger.debug('[TeamService] Fetching all teams.');
const presenter = new AllTeamsPresenter();
await this.getAllTeamsUseCase.execute(undefined, presenter);
return presenter.viewModel;
}
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> { async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
const { teamId, driverId } = query; this.logger.debug(`[TeamService] Fetching driver team for driverId: ${query.driverId}`);
const team = this.teams.get(teamId); const presenter = new DriverTeamPresenter();
if (!team) { try {
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
return presenter.viewModel;
} catch (error) {
this.logger.error(`Error fetching driver team: ${error}`);
return null; return null;
} }
// Mock membership and roles
const membership: MembershipDto = {
role: driverId === team.ownerId ? MembershipRole.OWNER : MembershipRole.MEMBER,
joinedAt: new Date(Date.now() - 86400000 * 30), // Joined 30 days ago
isActive: true, // Always active for mock
};
const isOwner = team.ownerId === driverId;
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
return {
team: team,
membership,
isOwner,
canManage,
};
} }
// Add other methods related to Team logic here based on other presenters
} }

View File

@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate, IsOptional } from 'class-validator';
export class TeamLeagueDto { export class TeamLeagueDto {
@ApiProperty() @ApiProperty()
@@ -37,7 +38,7 @@ export class AllTeamsViewModel {
@ApiProperty() @ApiProperty()
totalCount: number; totalCount: number;
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator'; }
export class TeamDto { export class TeamDto {
@ApiProperty() @ApiProperty()

View File

@@ -0,0 +1,31 @@
import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel } from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
import { TeamListItemViewModel } from '../dto/TeamDto';
export class AllTeamsPresenter implements IAllTeamsPresenter {
private result: AllTeamsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: AllTeamsResultDTO) {
const teams: TeamListItemViewModel[] = dto.teams.map(team => ({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount,
leagues: team.leagues || [],
}));
this.result = {
teams,
totalCount: teams.length,
};
}
get viewModel(): AllTeamsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,42 @@
import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
import { TeamDto, MembershipDto, MembershipRole } from '../dto/TeamDto';
export class DriverTeamPresenter implements IDriverTeamPresenter {
private result: DriverTeamViewModel | null = null;
reset() {
this.result = null;
}
present(dto: DriverTeamResultDTO) {
const team: TeamDto = {
id: dto.team.id,
name: dto.team.name,
tag: dto.team.tag,
description: dto.team.description,
ownerId: dto.team.ownerId,
leagues: dto.team.leagues || [],
};
const membership: MembershipDto = {
role: dto.membership.role as MembershipRole,
joinedAt: dto.membership.joinedAt,
isActive: dto.membership.status === 'active',
};
const isOwner = dto.team.ownerId === dto.driverId;
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
this.result = {
team,
membership,
isOwner,
canManage,
};
}
get viewModel(): DriverTeamViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -31,6 +31,15 @@
"@gridpilot/shared/*": [ "@gridpilot/shared/*": [
"../../core/shared/*" "../../core/shared/*"
], ],
"@gridpilot/shared/application/*": [
"../../core/shared/application/*"
],
"@gridpilot/racing/*": [
"../../core/racing/*"
],
"@gridpilot/league/*": [
"../../core/league/*"
],
"@gridpilot/analytics/*": [ "@gridpilot/analytics/*": [
"../../core/analytics/*" "../../core/analytics/*"
], ],
@@ -52,6 +61,9 @@
"@gridpilot/core/shared/logging/*": [ "@gridpilot/core/shared/logging/*": [
"../../core/shared/logging/*" "../../core/shared/logging/*"
], ],
"adapters/*": [
"../../adapters/*"
],
"@nestjs/testing": [ "@nestjs/testing": [
"./node_modules/@nestjs/testing" "./node_modules/@nestjs/testing"
] ]

View File

@@ -11,6 +11,7 @@ import AlphaFooter from '@/components/alpha/AlphaFooter';
import { AuthProvider } from '@/lib/auth/AuthContext'; import { AuthProvider } from '@/lib/auth/AuthContext';
import NotificationProvider from '@/components/notifications/NotificationProvider'; import NotificationProvider from '@/components/notifications/NotificationProvider';
import DevToolbar from '@/components/dev/DevToolbar'; import DevToolbar from '@/components/dev/DevToolbar';
import { initializeDIContainer } from '@/lib/di-setup';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -49,6 +50,7 @@ export default async function RootLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
await initializeDIContainer();
const mode = getAppMode(); const mode = getAppMode();
if (mode === 'alpha') { if (mode === 'alpha') {

View File

@@ -3,12 +3,8 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffectiveDriverId } from '@/lib/currentDriver'; import { useEffectiveDriverId } from '@/lib/currentDriver';
import { import { createLeagueSchedulePresenter } from '@/lib/presenters/factories';
loadLeagueSchedule, import type { LeagueScheduleRaceItemViewModel } from '@/lib/presenters/LeagueSchedulePresenter';
registerForRace,
withdrawFromRace,
type LeagueScheduleRaceItemViewModel,
} from '@/lib/presenters/LeagueSchedulePresenter';
interface LeagueScheduleProps { interface LeagueScheduleProps {
leagueId: string; leagueId: string;

View File

@@ -4,12 +4,11 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Input from '../ui/Input'; import Input from '../ui/Input';
import { import { createScheduleRaceFormPresenter } from '@/lib/presenters/factories';
loadScheduleRaceFormLeagues, import type {
scheduleRaceFromForm, ScheduleRaceFormData,
type ScheduleRaceFormData, ScheduledRaceViewModel,
type ScheduledRaceViewModel, LeagueOptionViewModel,
type LeagueOptionViewModel,
} from '@/lib/presenters/ScheduleRaceFormPresenter'; } from '@/lib/presenters/ScheduleRaceFormPresenter';
interface ScheduleRaceFormProps { interface ScheduleRaceFormProps {

View File

@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { LeagueModule } from './modules/league/LeagueModule';
import { DriverModule } from './modules/driver/DriverModule';
import { TeamModule } from './modules/team/TeamModule';
import { RaceModule } from './modules/race/RaceModule';
import { SponsorModule } from './modules/sponsor/SponsorModule';
import { AuthModule } from './modules/auth/AuthModule';
import { MediaModule } from './modules/media/MediaModule';
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
import { LoggingModule } from './modules/logging/LoggingModule';
@Module({
imports: [
LoggingModule,
LeagueModule,
DriverModule,
TeamModule,
RaceModule,
SponsorModule,
AuthModule,
MediaModule,
AnalyticsModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,25 @@
import { NestFactory } from '@nestjs/core';
import { INestApplicationContext } from '@nestjs/common';
import { AppModule } from './app.module';
let appContext: INestApplicationContext | null = null;
export async function initializeDIContainer(): Promise<void> {
if (appContext) {
return; // Already initialized
}
appContext = await NestFactory.createApplicationContext(AppModule);
}
export function getDIContainer(): INestApplicationContext {
if (!appContext) {
throw new Error('DI container not initialized. Call initializeDIContainer() first.');
}
return appContext;
}
export async function getService<T>(token: string | symbol): Promise<T> {
const container = getDIContainer();
return container.get<T>(token);
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AnalyticsProviders, PAGE_VIEW_REPOSITORY_TOKEN, ENGAGEMENT_REPOSITORY_TOKEN } from './AnalyticsProviders';
@Module({
imports: [],
providers: AnalyticsProviders,
exports: [PAGE_VIEW_REPOSITORY_TOKEN, ENGAGEMENT_REPOSITORY_TOKEN],
})
export class AnalyticsModule {}

View File

@@ -0,0 +1,30 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemoryPageViewRepository } from '@gridpilot/adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import { InMemoryEngagementRepository } from '@gridpilot/adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const PAGE_VIEW_REPOSITORY_TOKEN = Symbol('IPageViewRepository');
export const ENGAGEMENT_REPOSITORY_TOKEN = Symbol('IEngagementRepository');
export const AnalyticsProviders: Provider[] = [
{
provide: PAGE_VIEW_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryPageViewRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: ENGAGEMENT_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryEngagementRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AuthProviders, AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
@Module({
imports: [],
providers: AuthProviders,
exports: [AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN],
})
export class AuthModule {}

View File

@@ -0,0 +1,30 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { IAuthRepository } from '@gridpilot/identity/domain/repositories/IAuthRepository';
import { IUserRepository } from '@gridpilot/identity/domain/repositories/IUserRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemoryAuthRepository } from '@gridpilot/adapters/identity/persistence/inmemory/InMemoryAuthRepository';
import { InMemoryUserRepository } from '@gridpilot/adapters/identity/persistence/inmemory/InMemoryUserRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const AUTH_REPOSITORY_TOKEN = Symbol('IAuthRepository');
export const USER_REPOSITORY_TOKEN = Symbol('IUserRepository');
export const AuthProviders: Provider[] = [
{
provide: AUTH_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryAuthRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: USER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryUserRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { DriverProviders, DRIVER_REPOSITORY_TOKEN } from './DriverProviders';
@Module({
imports: [],
providers: DriverProviders,
exports: [DRIVER_REPOSITORY_TOKEN],
})
export class DriverModule {}

View File

@@ -0,0 +1,22 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemoryDriverRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryDriverRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const DRIVER_REPOSITORY_TOKEN = Symbol('IDriverRepository');
export const DriverProviders: Provider[] = [
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LeagueProviders, GET_LEAGUE_STANDINGS_USE_CASE_TOKEN } from './LeagueProviders';
@Module({
imports: [],
providers: LeagueProviders,
exports: [GET_LEAGUE_STANDINGS_USE_CASE_TOKEN],
})
export class LeagueModule {}

View File

@@ -0,0 +1,30 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { GetLeagueStandingsUseCase } from '@gridpilot/league/application/use-cases/GetLeagueStandingsUseCase';
import { ILeagueStandingsRepository } from '@gridpilot/league/application/ports/ILeagueStandingsRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { GetLeagueStandingsUseCaseImpl } from '@gridpilot/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
import { InMemoryLeagueStandingsRepository } from '@gridpilot/adapters/league/persistence/inmemory/InMemoryLeagueStandingsRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const GET_LEAGUE_STANDINGS_USE_CASE_TOKEN = Symbol('GetLeagueStandingsUseCase');
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = Symbol('ILeagueStandingsRepository');
export const LeagueProviders: Provider[] = [
{
provide: GET_LEAGUE_STANDINGS_USE_CASE_TOKEN,
useFactory: (repository: ILeagueStandingsRepository, logger: Logger) => new GetLeagueStandingsUseCaseImpl(repository),
inject: [LEAGUE_STANDINGS_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { Logger } from '@gridpilot/shared/logging/Logger';
import { ConsoleLogger } from '@gridpilot/adapters/logging/ConsoleLogger';
export const LOGGER_TOKEN = Symbol('Logger');
@Global()
@Module({
providers: [
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
],
exports: [LOGGER_TOKEN],
})
export class LoggingModule {}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MediaProviders, AVATAR_GENERATION_REPOSITORY_TOKEN } from './MediaProviders';
@Module({
imports: [],
providers: MediaProviders,
exports: [AVATAR_GENERATION_REPOSITORY_TOKEN],
})
export class MediaModule {}

View File

@@ -0,0 +1,22 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { IAvatarGenerationRepository } from '@gridpilot/media/domain/repositories/IAvatarGenerationRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemoryAvatarGenerationRepository } from '@gridpilot/adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const AVATAR_GENERATION_REPOSITORY_TOKEN = Symbol('IAvatarGenerationRepository');
export const MediaProviders: Provider[] = [
{
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryAvatarGenerationRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RaceProviders, RACE_REPOSITORY_TOKEN } from './RaceProviders';
@Module({
imports: [],
providers: RaceProviders,
exports: [RACE_REPOSITORY_TOKEN],
})
export class RaceModule {}

View File

@@ -0,0 +1,22 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemoryRaceRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryRaceRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const RACE_REPOSITORY_TOKEN = Symbol('IRaceRepository');
export const RaceProviders: Provider[] = [
{
provide: RACE_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SponsorProviders, SPONSOR_REPOSITORY_TOKEN } from './SponsorProviders';
@Module({
imports: [],
providers: SponsorProviders,
exports: [SPONSOR_REPOSITORY_TOKEN],
})
export class SponsorModule {}

View File

@@ -0,0 +1,22 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemorySponsorRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemorySponsorRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const SPONSOR_REPOSITORY_TOKEN = Symbol('ISponsorRepository');
export const SponsorProviders: Provider[] = [
{
provide: SPONSOR_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemorySponsorRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TeamProviders, TEAM_REPOSITORY_TOKEN } from './TeamProviders';
@Module({
imports: [],
providers: TeamProviders,
exports: [TEAM_REPOSITORY_TOKEN],
})
export class TeamModule {}

View File

@@ -0,0 +1,22 @@
import { Provider } from '@nestjs/common';
// Import core interfaces
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
import { Logger } from '@gridpilot/shared/logging/Logger';
// Import implementations
import { InMemoryTeamRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryTeamRepository';
// Import tokens
import { LOGGER_TOKEN } from '../logging/LoggingModule';
// Define injection tokens
export const TEAM_REPOSITORY_TOKEN = Symbol('ITeamRepository');
export const TeamProviders: Provider[] = [
{
provide: TEAM_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -1,10 +1,8 @@
import type { Race } from '@gridpilot/racing/domain/entities/Race'; import type { Race } from '@gridpilot/racing/domain/entities/Race';
import { import type { IRaceRepository } from '@gridpilot/racing/application/ports/IRaceRepository';
getRaceRepository, import type { IIsDriverRegisteredForRaceQuery } from '@gridpilot/racing/application/queries/IIsDriverRegisteredForRaceQuery';
getIsDriverRegisteredForRaceQuery, import type { IRegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/IRegisterForRaceUseCase';
getRegisterForRaceUseCase, import type { IWithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/IWithdrawFromRaceUseCase';
getWithdrawFromRaceUseCase,
} from '@/lib/di-container';
export interface LeagueScheduleRaceItemViewModel { export interface LeagueScheduleRaceItemViewModel {
id: string; id: string;
@@ -23,85 +21,95 @@ export interface LeagueScheduleViewModel {
races: LeagueScheduleRaceItemViewModel[]; races: LeagueScheduleRaceItemViewModel[];
} }
/** export interface ILeagueSchedulePresenter {
* Load league schedule with registration status for a given driver. loadLeagueSchedule(leagueId: string, driverId: string): Promise<LeagueScheduleViewModel>;
*/ registerForRace(raceId: string, leagueId: string, driverId: string): Promise<void>;
export async function loadLeagueSchedule( withdrawFromRace(raceId: string, driverId: string): Promise<void>;
leagueId: string, }
driverId: string,
): Promise<LeagueScheduleViewModel> {
const raceRepo = getRaceRepository();
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
const allRaces = await raceRepo.findAll(); export class LeagueSchedulePresenter implements ILeagueSchedulePresenter {
const leagueRaces = allRaces constructor(
.filter((race) => race.leagueId === leagueId) private raceRepository: IRaceRepository,
.sort( private isDriverRegisteredForRaceQuery: IIsDriverRegisteredForRaceQuery,
(a, b) => private registerForRaceUseCase: IRegisterForRaceUseCase,
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(), private withdrawFromRaceUseCase: IWithdrawFromRaceUseCase,
) {}
/**
* Load league schedule with registration status for a given driver.
*/
async loadLeagueSchedule(
leagueId: string,
driverId: string,
): Promise<LeagueScheduleViewModel> {
const allRaces = await this.raceRepository.findAll();
const leagueRaces = allRaces
.filter((race) => race.leagueId === leagueId)
.sort(
(a, b) =>
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
);
const now = new Date();
const registrationStates: Record<string, boolean> = {};
await Promise.all(
leagueRaces.map(async (race) => {
const registered = await this.isDriverRegisteredForRaceQuery.execute({
raceId: race.id,
driverId,
});
registrationStates[race.id] = registered;
}),
); );
const now = new Date(); const races: LeagueScheduleRaceItemViewModel[] = leagueRaces.map((race) => {
const raceDate = new Date(race.scheduledAt);
const isPast = race.status === 'completed' || raceDate <= now;
const isUpcoming = race.status === 'scheduled' && raceDate > now;
const registrationStates: Record<string, boolean> = {}; return {
await Promise.all( id: race.id,
leagueRaces.map(async (race) => { leagueId: race.leagueId,
const registered = await isRegisteredQuery.execute({ track: race.track,
raceId: race.id, car: race.car,
driverId, sessionType: race.sessionType,
}); scheduledAt: raceDate,
registrationStates[race.id] = registered; status: race.status,
}), isUpcoming,
); isPast,
isRegistered: registrationStates[race.id] ?? false,
};
});
const races: LeagueScheduleRaceItemViewModel[] = leagueRaces.map((race) => { return { races };
const raceDate = new Date(race.scheduledAt); }
const isPast = race.status === 'completed' || raceDate <= now;
const isUpcoming = race.status === 'scheduled' && raceDate > now;
return { /**
id: race.id, * Register the driver for a race.
leagueId: race.leagueId, */
track: race.track, async registerForRace(
car: race.car, raceId: string,
sessionType: race.sessionType, leagueId: string,
scheduledAt: raceDate, driverId: string,
status: race.status, ): Promise<void> {
isUpcoming, await this.registerForRaceUseCase.execute({
isPast, raceId,
isRegistered: registrationStates[race.id] ?? false, leagueId,
}; driverId,
}); });
}
return { races }; /**
} * Withdraw the driver from a race.
*/
/** async withdrawFromRace(
* Register the driver for a race. raceId: string,
*/ driverId: string,
export async function registerForRace( ): Promise<void> {
raceId: string, await this.withdrawFromRaceUseCase.execute({
leagueId: string, raceId,
driverId: string, driverId,
): Promise<void> { });
const useCase = getRegisterForRaceUseCase(); }
await useCase.execute({
raceId,
leagueId,
driverId,
});
}
/**
* Withdraw the driver from a race.
*/
export async function withdrawFromRace(
raceId: string,
driverId: string,
): Promise<void> {
const useCase = getWithdrawFromRaceUseCase();
await useCase.execute({
raceId,
driverId,
});
} }

View File

@@ -0,0 +1,101 @@
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
import type {
LeagueMembership as DomainLeagueMembership,
MembershipRole,
MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
/**
* Lightweight league membership model mirroring the domain type but with
* a stringified joinedAt for easier UI formatting.
*/
export interface LeagueMembership extends Omit<DomainLeagueMembership, 'joinedAt'> {
joinedAt: string;
}
export class LeagueMembershipService {
private leagueMemberships = new Map<string, LeagueMembership[]>();
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly membershipRepository: ILeagueMembershipRepository,
) {
this.initializeLeagueMembershipsFromRepository();
}
/**
* Initialize league memberships once from the in-memory league membership repository
* that is seeded via the static racing seed in the DI container.
*
* This avoids depending on raw testing-support seed exports and keeps all demo
* membership data flowing through the same in-memory repositories used elsewhere.
*/
private async initializeLeagueMembershipsFromRepository() {
if (this.leagueMemberships.size > 0) {
return;
}
try {
const allLeagues = await this.leagueRepository.findAll();
const byLeague = new Map<string, LeagueMembership[]>();
for (const league of allLeagues) {
const memberships = await this.membershipRepository.getLeagueMembers(league.id);
const mapped: LeagueMembership[] = memberships.map((membership) => ({
id: membership.id,
leagueId: membership.leagueId,
driverId: membership.driverId,
role: membership.role,
status: membership.status,
joinedAt:
membership.joinedAt instanceof Date
? membership.joinedAt.toISOString()
: new Date().toISOString(),
}));
byLeague.set(league.id, mapped);
}
for (const [leagueId, list] of byLeague.entries()) {
this.leagueMemberships.set(leagueId, list);
}
} catch (error) {
// In alpha/demo mode we tolerate failures here; callers will see empty memberships.
// eslint-disable-next-line no-console
console.error('Failed to initialize league memberships from repository', error);
}
}
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
const list = this.leagueMemberships.get(leagueId);
if (!list) return null;
return list.find((m) => m.driverId === driverId) ?? null;
}
getLeagueMembers(leagueId: string): LeagueMembership[] {
return [...(this.leagueMemberships.get(leagueId) ?? [])];
}
/**
* Derive a driver's primary league from in-memory league memberships.
* Prefers any active membership and returns the first matching league.
*/
getPrimaryLeagueIdForDriver(driverId: string): string | null {
for (const [leagueId, members] of this.leagueMemberships.entries()) {
if (members.some((m) => m.driverId === driverId && m.status === 'active')) {
return leagueId;
}
}
return null;
}
isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
const membership = this.getMembership(leagueId, driverId);
if (!membership) return false;
return membership.role === 'owner' || membership.role === 'admin';
}
}
export type { MembershipRole, MembershipStatus };

View File

@@ -0,0 +1,8 @@
import { AutomationEvent } from './AutomationEventPublisherPort';
export type LifecycleCallback = (event: AutomationEvent) => Promise<void> | void;
export interface AutomationLifecycleEmitterPort {
onLifecycle(cb: LifecycleCallback): void;
offLifecycle(cb: LifecycleCallback): void;
}

View File

@@ -1,11 +1,11 @@
import { OverlaySyncPort, OverlayAction, ActionAck } from '../ports/OverlaySyncPort'; import { OverlaySyncPort, OverlayAction, ActionAck } from '../ports/OverlaySyncPort';
import { AutomationEventPublisherPort, AutomationEvent } from '../ports/AutomationEventPublisherPort'; import { AutomationEventPublisherPort, AutomationEvent } from '../ports/AutomationEventPublisherPort';
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../infrastructure/adapters/IAutomationLifecycleEmitter'; import { AutomationLifecycleEmitterPort, LifecycleCallback } from '../ports/AutomationLifecycleEmitterPort';
import { LoggerPort } from '../ports/LoggerPort'; import { LoggerPort } from '../ports/LoggerPort';
import type { IAsyncApplicationService } from '@gridpilot/shared/application'; import type { IAsyncApplicationService } from '@gridpilot/shared/application';
type ConstructorArgs = { type ConstructorArgs = {
lifecycleEmitter: IAutomationLifecycleEmitter lifecycleEmitter: AutomationLifecycleEmitterPort
publisher: AutomationEventPublisherPort publisher: AutomationEventPublisherPort
logger: LoggerPort logger: LoggerPort
initialPanelWaitMs?: number initialPanelWaitMs?: number
@@ -17,7 +17,7 @@ type ConstructorArgs = {
export class OverlaySyncService export class OverlaySyncService
implements OverlaySyncPort, IAsyncApplicationService<OverlayAction, ActionAck> implements OverlaySyncPort, IAsyncApplicationService<OverlayAction, ActionAck>
{ {
private lifecycleEmitter: IAutomationLifecycleEmitter private lifecycleEmitter: AutomationLifecycleEmitterPort
private publisher: AutomationEventPublisherPort private publisher: AutomationEventPublisherPort
private logger: LoggerPort private logger: LoggerPort
private initialPanelWaitMs: number private initialPanelWaitMs: number

View File

@@ -1,3 +1,5 @@
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
export type LeagueScoringPresetPrimaryChampionshipType = export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver' | 'driver'
| 'team' | 'team'
@@ -23,4 +25,5 @@ export interface LeagueScoringPresetDTO {
export interface LeagueScoringPresetProvider { export interface LeagueScoringPresetProvider {
listPresets(): LeagueScoringPresetDTO[]; listPresets(): LeagueScoringPresetDTO[];
getPresetById(id: string): LeagueScoringPresetDTO | undefined; getPresetById(id: string): LeagueScoringPresetDTO | undefined;
createScoringConfigFromPreset(presetId: string, seasonId: string): LeagueScoringConfig;
} }

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface ApproveLeagueJoinRequestViewModel {
success: boolean;
message: string;
}
export interface ApproveLeagueJoinRequestResultDTO {
success: boolean;
message: string;
}
export interface IApproveLeagueJoinRequestPresenter extends Presenter<ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel> {}

View File

@@ -0,0 +1,17 @@
export interface CompleteDriverOnboardingViewModel {
success: boolean;
driverId?: string;
errorMessage?: string;
}
export interface CompleteDriverOnboardingResultDTO {
success: boolean;
driverId?: string;
errorMessage?: string;
}
export interface ICompleteDriverOnboardingPresenter {
present(dto: CompleteDriverOnboardingResultDTO): void;
get viewModel(): CompleteDriverOnboardingViewModel;
reset(): void;
}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetLeagueAdminPermissionsViewModel {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}
export interface GetLeagueAdminPermissionsResultDTO {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}
export interface IGetLeagueAdminPermissionsPresenter extends Presenter<GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel> {}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueAdminViewModel {
leagueId: string;
ownerId: string;
}
export interface GetLeagueAdminResultDTO {
leagueId: string;
ownerId: string;
}
export interface IGetLeagueAdminPresenter extends Presenter<GetLeagueAdminResultDTO, LeagueAdminViewModel> {}

View File

@@ -0,0 +1,21 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueJoinRequestViewModel {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message: string;
driver: { id: string; name: string } | null;
}
export interface GetLeagueJoinRequestsViewModel {
joinRequests: LeagueJoinRequestViewModel[];
}
export interface GetLeagueJoinRequestsResultDTO {
joinRequests: any[];
drivers: { id: string; name: string }[];
}
export interface IGetLeagueJoinRequestsPresenter extends Presenter<GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel> {}

View File

@@ -0,0 +1,21 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueMembershipsViewModel {
members: {
driverId: string;
driver: { id: string; name: string };
role: string;
joinedAt: Date;
}[];
}
export interface GetLeagueMembershipsViewModel {
memberships: LeagueMembershipsViewModel;
}
export interface GetLeagueMembershipsResultDTO {
memberships: any[];
drivers: { id: string; name: string }[];
}
export interface IGetLeagueMembershipsPresenter extends Presenter<GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel> {}

View File

@@ -0,0 +1,17 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueOwnerSummaryViewModel {
driver: { id: string; name: string };
rating: number;
rank: number;
}
export interface GetLeagueOwnerSummaryViewModel {
summary: LeagueOwnerSummaryViewModel | null;
}
export interface GetLeagueOwnerSummaryResultDTO {
summary: LeagueOwnerSummaryViewModel | null;
}
export interface IGetLeagueOwnerSummaryPresenter extends Presenter<GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel> {}

View File

@@ -0,0 +1,15 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetLeagueProtestsViewModel {
protests: any[];
racesById: Record<string, any>;
driversById: Record<string, any>;
}
export interface GetLeagueProtestsResultDTO {
protests: any[];
races: any[];
drivers: { id: string; name: string }[];
}
export interface IGetLeagueProtestsPresenter extends Presenter<GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel> {}

View File

@@ -0,0 +1,19 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueScheduleViewModel {
races: Array<{
id: string;
name: string;
date: string;
}>;
}
export interface GetLeagueScheduleResultDTO {
races: Array<{
id: string;
name: string;
scheduledAt: Date;
}>;
}
export interface IGetLeagueSchedulePresenter extends Presenter<GetLeagueScheduleResultDTO, LeagueScheduleViewModel> {}

View File

@@ -0,0 +1,21 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueSeasonSummaryViewModel {
seasonId: string;
name: string;
status: string;
startDate: Date;
endDate: Date;
isPrimary: boolean;
isParallelActive: boolean;
}
export interface GetLeagueSeasonsViewModel {
seasons: LeagueSeasonSummaryViewModel[];
}
export interface GetLeagueSeasonsResultDTO {
seasons: any[];
}
export interface IGetLeagueSeasonsPresenter extends Presenter<GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel> {}

View File

@@ -0,0 +1,11 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetTotalLeaguesViewModel {
totalLeagues: number;
}
export interface GetTotalLeaguesResultDTO {
totalLeagues: number;
}
export interface IGetTotalLeaguesPresenter extends Presenter<GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel> {}

View File

@@ -2,24 +2,19 @@ import type { Standing } from '../../domain/entities/Standing';
import type { Presenter } from '@gridpilot/shared/presentation/Presenter'; import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface StandingItemViewModel { export interface StandingItemViewModel {
id: string;
leagueId: string;
seasonId: string;
driverId: string; driverId: string;
position: number; driver: { id: string; name: string };
points: number; points: number;
wins: number; rank: number;
podiums: number;
racesCompleted: number;
} }
export interface LeagueStandingsViewModel { export interface LeagueStandingsViewModel {
leagueId: string;
standings: StandingItemViewModel[]; standings: StandingItemViewModel[];
} }
export interface LeagueStandingsResultDTO { export interface LeagueStandingsResultDTO {
standings: Standing[]; standings: Standing[];
drivers: { id: string; name: string }[];
} }
export interface ILeagueStandingsPresenter export interface ILeagueStandingsPresenter

View File

@@ -1,20 +1,16 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface LeagueStatsViewModel { export interface LeagueStatsViewModel {
leagueId: string; totalMembers: number;
totalRaces: number; totalRaces: number;
completedRaces: number; averageRating: number;
scheduledRaces: number;
averageSOF: number | null;
highestSOF: number | null;
lowestSOF: number | null;
} }
export interface ILeagueStatsPresenter { export interface LeagueStatsResultDTO {
present( totalMembers: number;
leagueId: string, totalRaces: number;
totalRaces: number, averageRating: number;
completedRaces: number, }
scheduledRaces: number,
sofValues: number[] export interface ILeagueStatsPresenter
): LeagueStatsViewModel; extends Presenter<LeagueStatsResultDTO, LeagueStatsViewModel> {}
getViewModel(): LeagueStatsViewModel;
}

View File

@@ -0,0 +1,13 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RejectLeagueJoinRequestViewModel {
success: boolean;
message: string;
}
export interface RejectLeagueJoinRequestResultDTO {
success: boolean;
message: string;
}
export interface IRejectLeagueJoinRequestPresenter extends Presenter<RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel> {}

View File

@@ -0,0 +1,11 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RemoveLeagueMemberViewModel {
success: boolean;
}
export interface RemoveLeagueMemberResultDTO {
success: boolean;
}
export interface IRemoveLeagueMemberPresenter extends Presenter<RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel> {}

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