league service
This commit is contained in:
@@ -15,9 +15,18 @@
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
debug(message: string, ...args: any[]): void {
|
||||
|
||||
@@ -13,5 +13,6 @@ module.exports = {
|
||||
testRegex: '.*\\.spec\\.ts$',
|
||||
moduleNameMapper: {
|
||||
'^@gridpilot/(.*)$': '<rootDir>/../../core/$1', // Corrected path
|
||||
'^adapters/(.*)$': '<rootDir>/../../adapters/$1',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HelloController } from './presentation/hello.controller';
|
||||
import { HelloService } from './application/hello/hello.service';
|
||||
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
|
||||
import { DatabaseModule } from './infrastructure/database/database.module';
|
||||
import { LoggingModule } from './infrastructure/logging/LoggingModule';
|
||||
import { AuthModule } from './modules/auth/AuthModule';
|
||||
import { LeagueModule } from './modules/league/LeagueModule';
|
||||
import { RaceModule } from './modules/race/RaceModule';
|
||||
@@ -16,6 +17,7 @@ import { PaymentsModule } from './modules/payments/PaymentsModule';
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
LoggingModule,
|
||||
AnalyticsModule,
|
||||
AuthModule,
|
||||
LeagueModule,
|
||||
|
||||
15
apps/api/src/infrastructure/logging/LoggingModule.ts
Normal file
15
apps/api/src/infrastructure/logging/LoggingModule.ts
Normal 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 {}
|
||||
@@ -1,43 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
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';
|
||||
import { AnalyticsProviders } from './AnalyticsProviders';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
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,
|
||||
],
|
||||
providers: AnalyticsProviders,
|
||||
exports: [AnalyticsService],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
|
||||
30
apps/api/src/modules/analytics/AnalyticsProviders.ts
Normal file
30
apps/api/src/modules/analytics/AnalyticsProviders.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
import { AuthController } from './AuthController';
|
||||
import { AuthProviders } from './AuthProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
providers: AuthProviders,
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriverController } from './DriverController';
|
||||
import { DriverProviders } from './DriverProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [DriverController],
|
||||
providers: [DriverService],
|
||||
providers: DriverProviders,
|
||||
exports: [DriverService],
|
||||
})
|
||||
export class DriverModule {}
|
||||
|
||||
@@ -11,6 +11,11 @@ import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repo
|
||||
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { Logger } from "@gridpilot/core/shared/application";
|
||||
|
||||
// Import use cases
|
||||
import { GetDriversLeaderboardUseCase } from '../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '../../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService';
|
||||
@@ -31,6 +36,12 @@ export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
// Use case tokens
|
||||
export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseCase';
|
||||
export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase';
|
||||
export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase';
|
||||
export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase';
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
DriverService, // Provide the service itself
|
||||
{
|
||||
@@ -72,4 +83,26 @@ export const DriverProviders: Provider[] = [
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort) =>
|
||||
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository) => new GetTotalDriversUseCase(driverRepo),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository) => new CompleteDriverOnboardingUseCase(driverRepo),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
|
||||
useFactory: (registrationRepo: IRaceRegistrationRepository) => new IsDriverRegisteredForRaceUseCase(registrationRepo),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
|
||||
},
|
||||
];
|
||||
|
||||
187
apps/api/src/modules/driver/DriverService.spec.ts
Normal file
187
apps/api/src/modules/driver/DriverService.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DriverService } from './DriverService';
|
||||
import { GetDriversLeaderboardUseCase } from '../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '../../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '../../../../core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { Logger } from '../../../../core/shared/logging/Logger';
|
||||
|
||||
describe('DriverService', () => {
|
||||
let service: DriverService;
|
||||
let getDriversLeaderboardUseCase: jest.Mocked<GetDriversLeaderboardUseCase>;
|
||||
let getTotalDriversUseCase: jest.Mocked<GetTotalDriversUseCase>;
|
||||
let completeDriverOnboardingUseCase: jest.Mocked<CompleteDriverOnboardingUseCase>;
|
||||
let isDriverRegisteredForRaceUseCase: jest.Mocked<IsDriverRegisteredForRaceUseCase>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DriverService,
|
||||
{
|
||||
provide: 'GetDriversLeaderboardUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'GetTotalDriversUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'CompleteDriverOnboardingUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'IsDriverRegisteredForRaceUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'Logger',
|
||||
useValue: {
|
||||
debug: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DriverService>(DriverService);
|
||||
getDriversLeaderboardUseCase = module.get('GetDriversLeaderboardUseCase');
|
||||
getTotalDriversUseCase = module.get('GetTotalDriversUseCase');
|
||||
completeDriverOnboardingUseCase = module.get('CompleteDriverOnboardingUseCase');
|
||||
isDriverRegisteredForRaceUseCase = module.get('IsDriverRegisteredForRaceUseCase');
|
||||
logger = module.get('Logger');
|
||||
});
|
||||
|
||||
describe('getDriversLeaderboard', () => {
|
||||
it('should call GetDriversLeaderboardUseCase and return the view model', async () => {
|
||||
const mockViewModel = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver 1',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'DE',
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
podiums: 20,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.png',
|
||||
},
|
||||
],
|
||||
totalRaces: 50,
|
||||
totalWins: 10,
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
getDriversLeaderboardUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.getDriversLeaderboard();
|
||||
|
||||
expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object));
|
||||
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.');
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalDrivers', () => {
|
||||
it('should call GetTotalDriversUseCase and return the view model', async () => {
|
||||
const mockViewModel = { totalDrivers: 5 };
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
getTotalDriversUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.getTotalDrivers();
|
||||
|
||||
expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object));
|
||||
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.');
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeOnboarding', () => {
|
||||
it('should call CompleteDriverOnboardingUseCase and return the view model', async () => {
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
displayName: 'John Doe',
|
||||
country: 'US',
|
||||
timezone: 'America/New_York',
|
||||
bio: 'Racing enthusiast',
|
||||
};
|
||||
|
||||
const mockViewModel = {
|
||||
success: true,
|
||||
driverId: 'user-123',
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
completeDriverOnboardingUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.completeOnboarding('user-123', input);
|
||||
|
||||
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: 'user-123',
|
||||
...input,
|
||||
},
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123');
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverRegistrationStatus', () => {
|
||||
it('should call IsDriverRegisteredForRaceUseCase and return the view model', async () => {
|
||||
const query = {
|
||||
driverId: 'driver-1',
|
||||
raceId: 'race-1',
|
||||
};
|
||||
|
||||
const mockViewModel = {
|
||||
isRegistered: true,
|
||||
raceId: 'race-1',
|
||||
driverId: 'driver-1',
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
isDriverRegisteredForRaceUseCase.execute.mockImplementation(async (params, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
|
||||
const result = await service.getDriverRegistrationStatus(query);
|
||||
|
||||
expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query, expect.any(Object));
|
||||
expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query);
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
|
||||
|
||||
// Use cases
|
||||
import { GetDriversLeaderboardUseCase } from '../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '../../../../core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '../../../../core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
|
||||
// Presenters
|
||||
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
|
||||
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
|
||||
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
|
||||
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
|
||||
|
||||
// Tokens
|
||||
import { GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, LOGGER_TOKEN } from './DriverProviders';
|
||||
import { Logger } from '../../../../core/shared/logging/Logger';
|
||||
|
||||
@Injectable()
|
||||
export class DriverService {
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
@Inject(GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN) private readonly getDriversLeaderboardUseCase: GetDriversLeaderboardUseCase,
|
||||
@Inject(GET_TOTAL_DRIVERS_USE_CASE_TOKEN) private readonly getTotalDriversUseCase: GetTotalDriversUseCase,
|
||||
@Inject(COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN) private readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase,
|
||||
@Inject(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) private readonly isDriverRegisteredForRaceUseCase: IsDriverRegisteredForRaceUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
console.log('[DriverService] Returning mock driver leaderboard.');
|
||||
const drivers: DriverLeaderboardItemViewModel[] = [
|
||||
{ id: 'driver-1', name: 'Mock Driver 1', rating: 2500, skillLevel: 'Pro', nationality: 'DE', racesCompleted: 50, wins: 10, podiums: 20, isActive: true, rank: 1, avatarUrl: 'https://cdn.example.com/avatars/driver-1.png' },
|
||||
{ id: 'driver-2', name: 'Mock Driver 2', rating: 2400, skillLevel: 'Amateur', nationality: 'US', racesCompleted: 40, wins: 5, podiums: 15, isActive: true, rank: 2, avatarUrl: 'https://cdn.example.com/avatars/driver-2.png' },
|
||||
];
|
||||
return {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces: drivers.reduce((sum, item) => sum + (item.racesCompleted ?? 0), 0),
|
||||
totalWins: drivers.reduce((sum, item) => sum + (item.wins ?? 0), 0),
|
||||
activeCount: drivers.filter(d => d.isActive).length,
|
||||
};
|
||||
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
|
||||
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
await this.getDriversLeaderboardUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
console.log('[DriverService] Returning mock total drivers.');
|
||||
return {
|
||||
totalDrivers: 2,
|
||||
};
|
||||
this.logger.debug('[DriverService] Fetching total drivers count.');
|
||||
|
||||
const presenter = new DriverStatsPresenter();
|
||||
await this.getTotalDriversUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
|
||||
console.log('Completing onboarding for user:', userId, input);
|
||||
return {
|
||||
success: true,
|
||||
driverId: `driver-${userId}-onboarded`,
|
||||
};
|
||||
this.logger.debug('Completing onboarding for user:', userId);
|
||||
|
||||
const presenter = new CompleteOnboardingPresenter();
|
||||
await this.completeDriverOnboardingUseCase.execute({
|
||||
userId,
|
||||
firstName: input.firstName,
|
||||
lastName: input.lastName,
|
||||
displayName: input.displayName,
|
||||
country: input.country,
|
||||
timezone: input.timezone,
|
||||
bio: input.bio,
|
||||
}, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
|
||||
console.log('Checking driver registration status:', query);
|
||||
return {
|
||||
isRegistered: false, // Mock response
|
||||
raceId: query.raceId,
|
||||
driverId: query.driverId,
|
||||
};
|
||||
this.logger.debug('Checking driver registration status:', query);
|
||||
|
||||
const presenter = new DriverRegistrationStatusPresenter();
|
||||
await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CompleteOnboardingPresenter } from './CompleteOnboardingPresenter';
|
||||
import type { CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
|
||||
|
||||
describe('CompleteOnboardingPresenter', () => {
|
||||
let presenter: CompleteOnboardingPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new CompleteOnboardingPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map successful core DTO to API view model', () => {
|
||||
const dto: CompleteDriverOnboardingResultDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
errorMessage: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should map failed core DTO to API view model', () => {
|
||||
const dto: CompleteDriverOnboardingResultDTO = {
|
||||
success: false,
|
||||
errorMessage: 'Driver already exists',
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
driverId: undefined,
|
||||
errorMessage: 'Driver already exists',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: CompleteDriverOnboardingResultDTO = {
|
||||
success: true,
|
||||
driverId: 'driver-123',
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CompleteOnboardingOutput } from '../dto/DriverDto';
|
||||
import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
|
||||
|
||||
export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPresenter {
|
||||
private result: CompleteOnboardingOutput | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CompleteDriverOnboardingResultDTO) {
|
||||
this.result = {
|
||||
success: dto.success,
|
||||
driverId: dto.driverId,
|
||||
errorMessage: dto.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): CompleteOnboardingOutput {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DriverRegistrationStatusPresenter } from './DriverRegistrationStatusPresenter';
|
||||
|
||||
describe('DriverRegistrationStatusPresenter', () => {
|
||||
let presenter: DriverRegistrationStatusPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DriverRegistrationStatusPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map parameters to view model for registered driver', () => {
|
||||
presenter.present(true, 'race-123', 'driver-456');
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
isRegistered: true,
|
||||
raceId: 'race-123',
|
||||
driverId: 'driver-456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should map parameters to view model for unregistered driver', () => {
|
||||
presenter.present(false, 'race-789', 'driver-101');
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
isRegistered: false,
|
||||
raceId: 'race-789',
|
||||
driverId: 'driver-101',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
presenter.present(true, 'race-123', 'driver-456');
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DriverRegistrationStatusViewModel } from '../dto/DriverDto';
|
||||
import type { IDriverRegistrationStatusPresenter } from '../../../../../core/racing/application/presenters/IDriverRegistrationStatusPresenter';
|
||||
|
||||
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
|
||||
private result: DriverRegistrationStatusViewModel | null = null;
|
||||
|
||||
present(isRegistered: boolean, raceId: string, driverId: string) {
|
||||
this.result = {
|
||||
isRegistered,
|
||||
raceId,
|
||||
driverId,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): DriverRegistrationStatusViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
// For consistency with other presenters
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
get viewModel(): DriverRegistrationStatusViewModel {
|
||||
return this.getViewModel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DriverStatsPresenter } from './DriverStatsPresenter';
|
||||
import type { TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
|
||||
|
||||
describe('DriverStatsPresenter', () => {
|
||||
let presenter: DriverStatsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DriverStatsPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map core DTO to API view model correctly', () => {
|
||||
const dto: TotalDriversResultDTO = {
|
||||
totalDrivers: 42,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result).toEqual({
|
||||
totalDrivers: 42,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: TotalDriversResultDTO = {
|
||||
totalDrivers: 10,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DriverStatsDto } from '../dto/DriverDto';
|
||||
import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
|
||||
|
||||
export class DriverStatsPresenter implements ITotalDriversPresenter {
|
||||
private result: DriverStatsDto | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TotalDriversResultDTO) {
|
||||
this.result = {
|
||||
totalDrivers: dto.totalDrivers,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriverStatsDto {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DriversLeaderboardPresenter } from './DriversLeaderboardPresenter';
|
||||
import type { DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
|
||||
describe('DriversLeaderboardPresenter', () => {
|
||||
let presenter: DriversLeaderboardPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DriversLeaderboardPresenter();
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map core DTO to API view model correctly', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date('2023-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
country: 'DE',
|
||||
iracingId: '67890',
|
||||
joinedAt: new Date('2023-01-02'),
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
{ driverId: 'driver-2', rating: 2400, overallRank: 2 },
|
||||
],
|
||||
stats: {
|
||||
'driver-1': { racesCompleted: 50, wins: 10, podiums: 20 },
|
||||
'driver-2': { racesCompleted: 40, wins: 5, podiums: 15 },
|
||||
},
|
||||
avatarUrls: {
|
||||
'driver-1': 'https://example.com/avatar1.png',
|
||||
'driver-2': 'https://example.com/avatar2.png',
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0]).toEqual({
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
podiums: 20,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.png',
|
||||
});
|
||||
expect(result.drivers[1]).toEqual({
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
rating: 2400,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'DE',
|
||||
racesCompleted: 40,
|
||||
wins: 5,
|
||||
podiums: 15,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/avatar2.png',
|
||||
});
|
||||
expect(result.totalRaces).toBe(90);
|
||||
expect(result.totalWins).toBe(15);
|
||||
expect(result.activeCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should sort drivers by rating descending', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
country: 'DE',
|
||||
iracingId: '67890',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2400, overallRank: 2 },
|
||||
{ driverId: 'driver-2', rating: 2500, overallRank: 1 },
|
||||
],
|
||||
stats: {},
|
||||
avatarUrls: {},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result.drivers[0].id).toBe('driver-2'); // Higher rating first
|
||||
expect(result.drivers[1].id).toBe('driver-1');
|
||||
});
|
||||
|
||||
it('should handle missing stats gracefully', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
],
|
||||
stats: {}, // No stats
|
||||
avatarUrls: {},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
expect(result.drivers[0].racesCompleted).toBe(0);
|
||||
expect(result.drivers[0].wins).toBe(0);
|
||||
expect(result.drivers[0].podiums).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [],
|
||||
rankings: [],
|
||||
stats: {},
|
||||
avatarUrls: {},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DriversLeaderboardViewModel, DriverLeaderboardItemViewModel } from '../dto/DriverDto';
|
||||
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
|
||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||
private result: DriversLeaderboardViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: DriversLeaderboardResultDTO) {
|
||||
const drivers: DriverLeaderboardItemViewModel[] = dto.drivers.map(driver => {
|
||||
const ranking = dto.rankings.find(r => r.driverId === driver.id);
|
||||
const stats = dto.stats[driver.id];
|
||||
const avatarUrl = dto.avatarUrls[driver.id];
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating: ranking?.rating ?? 0,
|
||||
skillLevel: 'Pro', // TODO: map from domain
|
||||
nationality: driver.country,
|
||||
racesCompleted: stats?.racesCompleted ?? 0,
|
||||
wins: stats?.wins ?? 0,
|
||||
podiums: stats?.podiums ?? 0,
|
||||
isActive: true, // TODO: determine from domain
|
||||
rank: ranking?.overallRank ?? 0,
|
||||
avatarUrl,
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate totals
|
||||
const totalRaces = drivers.reduce((sum, d) => sum + (d.racesCompleted ?? 0), 0);
|
||||
const totalWins = drivers.reduce((sum, d) => sum + (d.wins ?? 0), 0);
|
||||
const activeCount = drivers.filter(d => d.isActive).length;
|
||||
|
||||
this.result = {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces,
|
||||
totalWins,
|
||||
activeCount,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriversLeaderboardViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
43
apps/api/src/modules/league/LeagueController.spec.ts
Normal file
43
apps/api/src/modules/league/LeagueController.spec.ts
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
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
|
||||
|
||||
@ApiTags('leagues')
|
||||
@@ -133,4 +133,47 @@ export class LeagueController {
|
||||
const query: GetLeagueSeasonsQuery = { leagueId };
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeagueController } from './LeagueController';
|
||||
import { LeagueProviders } from './LeagueProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [LeagueController],
|
||||
providers: [LeagueService],
|
||||
providers: LeagueProviders,
|
||||
exports: [LeagueService],
|
||||
})
|
||||
export class LeagueModule {}
|
||||
|
||||
@@ -2,15 +2,7 @@ import { Provider } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
|
||||
// Import core interfaces
|
||||
import { ILeagueRepository } from 'core/racing/domain/repositories/ILeagueRepository';
|
||||
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 { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
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 { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
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 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
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository';
|
||||
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
|
||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
|
||||
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
|
||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
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 LeagueProviders: Provider[] = [
|
||||
@@ -51,6 +67,11 @@ export const LeagueProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: STANDING_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger), // Factory for InMemoryStandingRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SEASON_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
|
||||
@@ -76,8 +97,33 @@ export const LeagueProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
GetAllLeaguesWithCapacityUseCase,
|
||||
GetLeagueStandingsUseCase,
|
||||
GetLeagueStatsUseCase,
|
||||
GetLeagueFullConfigUseCase,
|
||||
CreateLeagueWithSeasonAndScoringUseCase,
|
||||
GetRaceProtestsUseCase,
|
||||
GetTotalLeaguesUseCase,
|
||||
GetLeagueJoinRequestsUseCase,
|
||||
ApproveLeagueJoinRequestUseCase,
|
||||
RejectLeagueJoinRequestUseCase,
|
||||
RemoveLeagueMemberUseCase,
|
||||
UpdateLeagueMemberRoleUseCase,
|
||||
GetLeagueOwnerSummaryUseCase,
|
||||
GetLeagueProtestsUseCase,
|
||||
GetLeagueSeasonsUseCase,
|
||||
GetLeagueMembershipsUseCase,
|
||||
GetLeagueScheduleUseCase,
|
||||
GetLeagueStatsUseCase,
|
||||
GetLeagueAdminPermissionsUseCase,
|
||||
];
|
||||
|
||||
170
apps/api/src/modules/league/LeagueService.spec.ts
Normal file
170
apps/api/src/modules/league/LeagueService.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
@@ -1,125 +1,237 @@
|
||||
import { Injectable } 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 { 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
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
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';
|
||||
|
||||
const mockDriverData: Map<string, DriverDto> = new Map();
|
||||
mockDriverData.set('driver-owner-1', { id: 'driver-owner-1', name: 'Owner Driver' });
|
||||
mockDriverData.set('driver-1', { id: 'driver-1', name: 'Demo Driver 1' });
|
||||
mockDriverData.set('driver-2', { id: 'driver-2', name: 'Demo Driver 2' });
|
||||
// Core imports
|
||||
import { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
const mockRaceData: Map<string, RaceDto> = new Map();
|
||||
mockRaceData.set('race-1', { id: 'race-1', name: 'Test Race 1', date: new Date().toISOString() });
|
||||
mockRaceData.set('race-2', { id: 'race-2', name: 'Test Race 2', date: new Date().toISOString() });
|
||||
// Use cases
|
||||
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 { 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()
|
||||
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> {
|
||||
console.log('[LeagueService] Returning mock leagues with capacity.');
|
||||
return {
|
||||
leagues: [
|
||||
{ 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' } },
|
||||
{ id: 'league-2', name: 'Amateur Series', description: 'Learn the ropes', ownerId: 'owner-2', settings: { maxDrivers: 50 }, createdAt: new Date().toISOString(), usedSlots: 20 },
|
||||
],
|
||||
totalCount: 2,
|
||||
};
|
||||
this.logger.debug('[LeagueService] Fetching all leagues with capacity.');
|
||||
|
||||
const presenter = new AllLeaguesWithCapacityPresenter();
|
||||
await this.getAllLeaguesWithCapacityUseCase.execute(undefined, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
console.log('[LeagueService] Returning mock total leagues.');
|
||||
return { totalLeagues: 2 };
|
||||
this.logger.debug('[LeagueService] Fetching total leagues count.');
|
||||
const presenter = new TotalLeaguesPresenter();
|
||||
await this.getTotalLeaguesUseCase.execute({}, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
console.log(`[LeagueService] Returning mock join requests for league: ${leagueId}.`);
|
||||
return [
|
||||
{
|
||||
id: 'join-req-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: new Date(),
|
||||
message: 'I want to join!',
|
||||
driver: mockDriverData.get('driver-1'),
|
||||
},
|
||||
];
|
||||
this.logger.debug(`[LeagueService] Fetching join requests for league: ${leagueId}.`);
|
||||
const presenter = new LeagueJoinRequestsPresenter();
|
||||
await this.getLeagueJoinRequestsUseCase.execute({ leagueId }, presenter);
|
||||
return presenter.getViewModel()!.joinRequests;
|
||||
}
|
||||
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
|
||||
console.log('Approving join request:', input);
|
||||
return { success: true, message: 'Join request approved.' };
|
||||
this.logger.debug('Approving join request:', input);
|
||||
const presenter = new ApproveLeagueJoinRequestPresenter();
|
||||
await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
|
||||
console.log('Rejecting join request:', input);
|
||||
return { success: true, message: 'Join request rejected.' };
|
||||
this.logger.debug('Rejecting join request:', input);
|
||||
const presenter = new RejectLeagueJoinRequestPresenter();
|
||||
await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
|
||||
console.log('Getting league admin permissions:', query);
|
||||
return { canRemoveMember: true, canUpdateRoles: true };
|
||||
this.logger.debug('Getting league admin permissions', { query });
|
||||
const presenter = new GetLeagueAdminPermissionsPresenter();
|
||||
await this.getLeagueAdminPermissionsUseCase.execute(
|
||||
{ leagueId: query.leagueId, performerDriverId: query.performerDriverId },
|
||||
presenter
|
||||
);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
|
||||
console.log('Removing league member:', input.leagueId, input.targetDriverId);
|
||||
return { success: true };
|
||||
this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId });
|
||||
const presenter = new RemoveLeagueMemberPresenter();
|
||||
await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
console.log('Updating league member role:', input.leagueId, input.targetDriverId, input.newRole);
|
||||
return { success: true };
|
||||
this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
|
||||
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> {
|
||||
console.log('Getting league owner summary:', query);
|
||||
return {
|
||||
driver: mockDriverData.get(query.ownerId)!,
|
||||
rating: 2000,
|
||||
rank: 1,
|
||||
};
|
||||
this.logger.debug('Getting league owner summary:', query);
|
||||
const presenter = new GetLeagueOwnerSummaryPresenter();
|
||||
await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId }, presenter);
|
||||
return presenter.getViewModel()!.summary;
|
||||
}
|
||||
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
|
||||
console.log('Getting league full config:', query);
|
||||
return {
|
||||
leagueId: 'league-1',
|
||||
basics: { name: 'Demo League', description: 'A demo league', visibility: 'public' },
|
||||
structure: { mode: 'solo' },
|
||||
championships: [],
|
||||
scoring: { type: 'standard', points: 10 },
|
||||
dropPolicy: { strategy: 'none' },
|
||||
timings: { raceDayOfWeek: 'Sunday', raceTimeHour: 20, raceTimeMinute: 0 },
|
||||
stewarding: {
|
||||
decisionMode: 'single_steward',
|
||||
requireDefense: false,
|
||||
defenseTimeLimit: 24,
|
||||
voteTimeLimit: 24,
|
||||
protestDeadlineHours: 2,
|
||||
stewardingClosesHours: 24,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
this.logger.debug('Getting league full config', { query });
|
||||
|
||||
const presenter = new LeagueConfigPresenter();
|
||||
try {
|
||||
await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId }, presenter);
|
||||
return presenter.viewModel;
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting league full config', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
|
||||
console.log('Getting league protests:', query);
|
||||
return {
|
||||
protests: [
|
||||
{ id: 'protest-1', raceId: 'race-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', submittedAt: new Date(), description: 'Bad driving!', status: 'pending' },
|
||||
],
|
||||
racesById: { 'race-1': mockRaceData.get('race-1')! },
|
||||
driversById: { 'driver-1': mockDriverData.get('driver-1')!, 'driver-2': mockDriverData.get('driver-2')! },
|
||||
};
|
||||
this.logger.debug('Getting league protests:', query);
|
||||
const presenter = new GetLeagueProtestsPresenter();
|
||||
await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
console.log('Getting league seasons:', query);
|
||||
return [
|
||||
{ seasonId: 'season-1', name: 'Season 1', status: 'active', startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), isPrimary: true, isParallelActive: false },
|
||||
{ seasonId: 'season-2', name: 'Season 2', status: 'upcoming', startDate: new Date('2026-01-01'), endDate: new Date('2026-12-31'), isPrimary: false, isParallelActive: false },
|
||||
];
|
||||
this.logger.debug('Getting league seasons:', query);
|
||||
const presenter = new GetLeagueSeasonsPresenter();
|
||||
await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId }, presenter);
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,3 +559,108 @@ export class LeagueAdminViewModel {
|
||||
@Type(() => 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
109
apps/api/src/modules/league/presenters/LeagueConfigPresenter.ts
Normal file
109
apps/api/src/modules/league/presenters/LeagueConfigPresenter.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
import { MediaController } from './MediaController';
|
||||
import { MediaProviders } from './MediaProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [MediaController],
|
||||
providers: [MediaService],
|
||||
providers: MediaProviders,
|
||||
exports: [MediaService],
|
||||
})
|
||||
export class MediaModule {}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MediaService } from './MediaService';
|
||||
/*
|
||||
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
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 { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { PaymentsController } from './PaymentsController';
|
||||
import { PaymentsProviders } from './PaymentsProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [PaymentsController],
|
||||
providers: [PaymentsService],
|
||||
providers: PaymentsProviders,
|
||||
exports: [PaymentsService],
|
||||
})
|
||||
export class PaymentsModule {}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMem
|
||||
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
|
||||
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
|
||||
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 { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RaceController } from './RaceController';
|
||||
import { RaceProviders } from './RaceProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [RaceController],
|
||||
providers: [RaceService],
|
||||
providers: RaceProviders,
|
||||
exports: [RaceService],
|
||||
})
|
||||
export class RaceModule {}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { SponsorController } from './SponsorController';
|
||||
import { SponsorProviders } from './SponsorProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [SponsorController],
|
||||
providers: [SponsorService],
|
||||
providers: SponsorProviders,
|
||||
exports: [SponsorService],
|
||||
})
|
||||
export class SponsorModule {}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamService } from './TeamService';
|
||||
import { TeamController } from './TeamController';
|
||||
import { TeamProviders } from './TeamProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [TeamController],
|
||||
providers: [TeamService],
|
||||
providers: TeamProviders,
|
||||
exports: [TeamService],
|
||||
})
|
||||
export class TeamModule {}
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { TeamService } from './TeamService';
|
||||
|
||||
export const TeamProviders = [
|
||||
TeamService,
|
||||
// Import core interfaces
|
||||
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],
|
||||
},
|
||||
];
|
||||
|
||||
168
apps/api/src/modules/team/TeamService.spec.ts
Normal file
168
apps/api/src/modules/team/TeamService.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
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()
|
||||
export class TeamService {
|
||||
getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
// TODO: Implement actual logic to fetch all teams
|
||||
return Promise.resolve({
|
||||
teams: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
}
|
||||
constructor(
|
||||
@Inject(TEAM_GET_ALL_USE_CASE_TOKEN) private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
|
||||
@Inject(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN) private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
|
||||
@Inject(TEAM_LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
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> {
|
||||
const { teamId, driverId } = query;
|
||||
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${query.driverId}`);
|
||||
|
||||
const team = this.teams.get(teamId);
|
||||
if (!team) {
|
||||
const presenter = new DriverTeamPresenter();
|
||||
try {
|
||||
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
|
||||
return presenter.viewModel;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching driver team: ${error}`);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate, IsOptional } from 'class-validator';
|
||||
|
||||
export class TeamLeagueDto {
|
||||
@ApiProperty()
|
||||
@@ -37,7 +38,7 @@ export class AllTeamsViewModel {
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator';
|
||||
}
|
||||
|
||||
export class TeamDto {
|
||||
@ApiProperty()
|
||||
|
||||
31
apps/api/src/modules/team/presenters/AllTeamsPresenter.ts
Normal file
31
apps/api/src/modules/team/presenters/AllTeamsPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
42
apps/api/src/modules/team/presenters/DriverTeamPresenter.ts
Normal file
42
apps/api/src/modules/team/presenters/DriverTeamPresenter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,15 @@
|
||||
"@gridpilot/shared/*": [
|
||||
"../../core/shared/*"
|
||||
],
|
||||
"@gridpilot/shared/application/*": [
|
||||
"../../core/shared/application/*"
|
||||
],
|
||||
"@gridpilot/racing/*": [
|
||||
"../../core/racing/*"
|
||||
],
|
||||
"@gridpilot/league/*": [
|
||||
"../../core/league/*"
|
||||
],
|
||||
"@gridpilot/analytics/*": [
|
||||
"../../core/analytics/*"
|
||||
],
|
||||
@@ -52,6 +61,9 @@
|
||||
"@gridpilot/core/shared/logging/*": [
|
||||
"../../core/shared/logging/*"
|
||||
],
|
||||
"adapters/*": [
|
||||
"../../adapters/*"
|
||||
],
|
||||
"@nestjs/testing": [
|
||||
"./node_modules/@nestjs/testing"
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ import AlphaFooter from '@/components/alpha/AlphaFooter';
|
||||
import { AuthProvider } from '@/lib/auth/AuthContext';
|
||||
import NotificationProvider from '@/components/notifications/NotificationProvider';
|
||||
import DevToolbar from '@/components/dev/DevToolbar';
|
||||
import { initializeDIContainer } from '@/lib/di-setup';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -49,6 +50,7 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
await initializeDIContainer();
|
||||
const mode = getAppMode();
|
||||
|
||||
if (mode === 'alpha') {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
import {
|
||||
loadLeagueSchedule,
|
||||
registerForRace,
|
||||
withdrawFromRace,
|
||||
type LeagueScheduleRaceItemViewModel,
|
||||
} from '@/lib/presenters/LeagueSchedulePresenter';
|
||||
import { createLeagueSchedulePresenter } from '@/lib/presenters/factories';
|
||||
import type { LeagueScheduleRaceItemViewModel } from '@/lib/presenters/LeagueSchedulePresenter';
|
||||
|
||||
interface LeagueScheduleProps {
|
||||
leagueId: string;
|
||||
|
||||
@@ -4,12 +4,11 @@ import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
import {
|
||||
loadScheduleRaceFormLeagues,
|
||||
scheduleRaceFromForm,
|
||||
type ScheduleRaceFormData,
|
||||
type ScheduledRaceViewModel,
|
||||
type LeagueOptionViewModel,
|
||||
import { createScheduleRaceFormPresenter } from '@/lib/presenters/factories';
|
||||
import type {
|
||||
ScheduleRaceFormData,
|
||||
ScheduledRaceViewModel,
|
||||
LeagueOptionViewModel,
|
||||
} from '@/lib/presenters/ScheduleRaceFormPresenter';
|
||||
|
||||
interface ScheduleRaceFormProps {
|
||||
|
||||
25
apps/website/lib/app.module.ts
Normal file
25
apps/website/lib/app.module.ts
Normal 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 {}
|
||||
25
apps/website/lib/di-setup.ts
Normal file
25
apps/website/lib/di-setup.ts
Normal 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);
|
||||
}
|
||||
9
apps/website/lib/modules/analytics/AnalyticsModule.ts
Normal file
9
apps/website/lib/modules/analytics/AnalyticsModule.ts
Normal 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 {}
|
||||
30
apps/website/lib/modules/analytics/AnalyticsProviders.ts
Normal file
30
apps/website/lib/modules/analytics/AnalyticsProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/auth/AuthModule.ts
Normal file
9
apps/website/lib/modules/auth/AuthModule.ts
Normal 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 {}
|
||||
30
apps/website/lib/modules/auth/AuthProviders.ts
Normal file
30
apps/website/lib/modules/auth/AuthProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/driver/DriverModule.ts
Normal file
9
apps/website/lib/modules/driver/DriverModule.ts
Normal 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 {}
|
||||
22
apps/website/lib/modules/driver/DriverProviders.ts
Normal file
22
apps/website/lib/modules/driver/DriverProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/league/LeagueModule.ts
Normal file
9
apps/website/lib/modules/league/LeagueModule.ts
Normal 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 {}
|
||||
30
apps/website/lib/modules/league/LeagueProviders.ts
Normal file
30
apps/website/lib/modules/league/LeagueProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
17
apps/website/lib/modules/logging/LoggingModule.ts
Normal file
17
apps/website/lib/modules/logging/LoggingModule.ts
Normal 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 {}
|
||||
9
apps/website/lib/modules/media/MediaModule.ts
Normal file
9
apps/website/lib/modules/media/MediaModule.ts
Normal 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 {}
|
||||
22
apps/website/lib/modules/media/MediaProviders.ts
Normal file
22
apps/website/lib/modules/media/MediaProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/race/RaceModule.ts
Normal file
9
apps/website/lib/modules/race/RaceModule.ts
Normal 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 {}
|
||||
22
apps/website/lib/modules/race/RaceProviders.ts
Normal file
22
apps/website/lib/modules/race/RaceProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/sponsor/SponsorModule.ts
Normal file
9
apps/website/lib/modules/sponsor/SponsorModule.ts
Normal 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 {}
|
||||
22
apps/website/lib/modules/sponsor/SponsorProviders.ts
Normal file
22
apps/website/lib/modules/sponsor/SponsorProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
9
apps/website/lib/modules/team/TeamModule.ts
Normal file
9
apps/website/lib/modules/team/TeamModule.ts
Normal 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 {}
|
||||
22
apps/website/lib/modules/team/TeamProviders.ts
Normal file
22
apps/website/lib/modules/team/TeamProviders.ts
Normal 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],
|
||||
},
|
||||
];
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import {
|
||||
getRaceRepository,
|
||||
getIsDriverRegisteredForRaceQuery,
|
||||
getRegisterForRaceUseCase,
|
||||
getWithdrawFromRaceUseCase,
|
||||
} from '@/lib/di-container';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/application/ports/IRaceRepository';
|
||||
import type { IIsDriverRegisteredForRaceQuery } from '@gridpilot/racing/application/queries/IIsDriverRegisteredForRaceQuery';
|
||||
import type { IRegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/IRegisterForRaceUseCase';
|
||||
import type { IWithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/IWithdrawFromRaceUseCase';
|
||||
|
||||
export interface LeagueScheduleRaceItemViewModel {
|
||||
id: string;
|
||||
@@ -23,85 +21,95 @@ export interface LeagueScheduleViewModel {
|
||||
races: LeagueScheduleRaceItemViewModel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load league schedule with registration status for a given driver.
|
||||
*/
|
||||
export async function loadLeagueSchedule(
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<LeagueScheduleViewModel> {
|
||||
const raceRepo = getRaceRepository();
|
||||
const isRegisteredQuery = getIsDriverRegisteredForRaceQuery();
|
||||
export interface ILeagueSchedulePresenter {
|
||||
loadLeagueSchedule(leagueId: string, driverId: string): Promise<LeagueScheduleViewModel>;
|
||||
registerForRace(raceId: string, leagueId: string, driverId: string): Promise<void>;
|
||||
withdrawFromRace(raceId: string, driverId: string): Promise<void>;
|
||||
}
|
||||
|
||||
const allRaces = await raceRepo.findAll();
|
||||
const leagueRaces = allRaces
|
||||
.filter((race) => race.leagueId === leagueId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
|
||||
export class LeagueSchedulePresenter implements ILeagueSchedulePresenter {
|
||||
constructor(
|
||||
private raceRepository: IRaceRepository,
|
||||
private isDriverRegisteredForRaceQuery: IIsDriverRegisteredForRaceQuery,
|
||||
private registerForRaceUseCase: IRegisterForRaceUseCase,
|
||||
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> = {};
|
||||
await Promise.all(
|
||||
leagueRaces.map(async (race) => {
|
||||
const registered = await isRegisteredQuery.execute({
|
||||
raceId: race.id,
|
||||
driverId,
|
||||
});
|
||||
registrationStates[race.id] = registered;
|
||||
}),
|
||||
);
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
sessionType: race.sessionType,
|
||||
scheduledAt: raceDate,
|
||||
status: race.status,
|
||||
isUpcoming,
|
||||
isPast,
|
||||
isRegistered: registrationStates[race.id] ?? false,
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
return { races };
|
||||
}
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
sessionType: race.sessionType,
|
||||
scheduledAt: raceDate,
|
||||
status: race.status,
|
||||
isUpcoming,
|
||||
isPast,
|
||||
isRegistered: registrationStates[race.id] ?? false,
|
||||
};
|
||||
});
|
||||
/**
|
||||
* Register the driver for a race.
|
||||
*/
|
||||
async registerForRace(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
await this.registerForRaceUseCase.execute({
|
||||
raceId,
|
||||
leagueId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
|
||||
return { races };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the driver for a race.
|
||||
*/
|
||||
export async function registerForRace(
|
||||
raceId: string,
|
||||
leagueId: string,
|
||||
driverId: string,
|
||||
): 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,
|
||||
});
|
||||
/**
|
||||
* Withdraw the driver from a race.
|
||||
*/
|
||||
async withdrawFromRace(
|
||||
raceId: string,
|
||||
driverId: string,
|
||||
): Promise<void> {
|
||||
await this.withdrawFromRaceUseCase.execute({
|
||||
raceId,
|
||||
driverId,
|
||||
});
|
||||
}
|
||||
}
|
||||
101
apps/website/lib/services/LeagueMembershipService.ts
Normal file
101
apps/website/lib/services/LeagueMembershipService.ts
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { OverlaySyncPort, OverlayAction, ActionAck } from '../ports/OverlaySyncPort';
|
||||
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 type { IAsyncApplicationService } from '@gridpilot/shared/application';
|
||||
|
||||
type ConstructorArgs = {
|
||||
lifecycleEmitter: IAutomationLifecycleEmitter
|
||||
lifecycleEmitter: AutomationLifecycleEmitterPort
|
||||
publisher: AutomationEventPublisherPort
|
||||
logger: LoggerPort
|
||||
initialPanelWaitMs?: number
|
||||
@@ -17,7 +17,7 @@ type ConstructorArgs = {
|
||||
export class OverlaySyncService
|
||||
implements OverlaySyncPort, IAsyncApplicationService<OverlayAction, ActionAck>
|
||||
{
|
||||
private lifecycleEmitter: IAutomationLifecycleEmitter
|
||||
private lifecycleEmitter: AutomationLifecycleEmitterPort
|
||||
private publisher: AutomationEventPublisherPort
|
||||
private logger: LoggerPort
|
||||
private initialPanelWaitMs: number
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||
|
||||
export type LeagueScoringPresetPrimaryChampionshipType =
|
||||
| 'driver'
|
||||
| 'team'
|
||||
@@ -23,4 +25,5 @@ export interface LeagueScoringPresetDTO {
|
||||
export interface LeagueScoringPresetProvider {
|
||||
listPresets(): LeagueScoringPresetDTO[];
|
||||
getPresetById(id: string): LeagueScoringPresetDTO | undefined;
|
||||
createScoringConfigFromPreset(presetId: string, seasonId: string): LeagueScoringConfig;
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -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> {}
|
||||
@@ -2,24 +2,19 @@ import type { Standing } from '../../domain/entities/Standing';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface StandingItemViewModel {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
driver: { id: string; name: string };
|
||||
points: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
racesCompleted: number;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface LeagueStandingsViewModel {
|
||||
leagueId: string;
|
||||
standings: StandingItemViewModel[];
|
||||
}
|
||||
|
||||
export interface LeagueStandingsResultDTO {
|
||||
standings: Standing[];
|
||||
drivers: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export interface ILeagueStandingsPresenter
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface LeagueStatsViewModel {
|
||||
leagueId: string;
|
||||
totalMembers: number;
|
||||
totalRaces: number;
|
||||
completedRaces: number;
|
||||
scheduledRaces: number;
|
||||
averageSOF: number | null;
|
||||
highestSOF: number | null;
|
||||
lowestSOF: number | null;
|
||||
averageRating: number;
|
||||
}
|
||||
|
||||
export interface ILeagueStatsPresenter {
|
||||
present(
|
||||
leagueId: string,
|
||||
totalRaces: number,
|
||||
completedRaces: number,
|
||||
scheduledRaces: number,
|
||||
sofValues: number[]
|
||||
): LeagueStatsViewModel;
|
||||
getViewModel(): LeagueStatsViewModel;
|
||||
}
|
||||
export interface LeagueStatsResultDTO {
|
||||
totalMembers: number;
|
||||
totalRaces: number;
|
||||
averageRating: number;
|
||||
}
|
||||
|
||||
export interface ILeagueStatsPresenter
|
||||
extends Presenter<LeagueStatsResultDTO, LeagueStatsViewModel> {}
|
||||
@@ -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> {}
|
||||
@@ -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
Reference in New Issue
Block a user