diff --git a/apps/api/src/domain/analytics/AnalyticsController.ts b/apps/api/src/domain/analytics/AnalyticsController.ts
index 40bb06383..c4a07da91 100644
--- a/apps/api/src/domain/analytics/AnalyticsController.ts
+++ b/apps/api/src/domain/analytics/AnalyticsController.ts
@@ -1,8 +1,16 @@
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
import type { Response } from 'express';
-import type { RecordPageViewInput, RecordPageViewOutput, RecordEngagementInput, RecordEngagementOutput } from './dto/AnalyticsDto';
+import type { RecordPageViewInputDTO } from './dtos/RecordPageViewInputDTO';
+import type { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
+import type { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
+import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
import { AnalyticsService } from './AnalyticsService';
+type RecordPageViewInput = RecordPageViewInputDTO;
+type RecordPageViewOutput = RecordPageViewOutputDTO;
+type RecordEngagementInput = RecordEngagementInputDTO;
+type RecordEngagementOutput = RecordEngagementOutputDTO;
+
@Controller('analytics')
export class AnalyticsController {
constructor(
diff --git a/apps/api/src/domain/analytics/AnalyticsService.ts b/apps/api/src/domain/analytics/AnalyticsService.ts
index 29baefc68..1b3bb4e16 100644
--- a/apps/api/src/domain/analytics/AnalyticsService.ts
+++ b/apps/api/src/domain/analytics/AnalyticsService.ts
@@ -1,9 +1,17 @@
import { Injectable, Inject } from '@nestjs/common';
-import { RecordEngagementInput, RecordEngagementOutput, RecordPageViewInput, RecordPageViewOutput } from './dto/AnalyticsDto';
+import type { RecordPageViewInputDTO } from './dtos/RecordPageViewInputDTO';
+import type { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
+import type { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
+import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
import type { Logger } from '@core/shared/application';
import { RecordPageViewUseCase } from './use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from './use-cases/RecordEngagementUseCase';
+type RecordPageViewInput = RecordPageViewInputDTO;
+type RecordPageViewOutput = RecordPageViewOutputDTO;
+type RecordEngagementInput = RecordEngagementInputDTO;
+type RecordEngagementOutput = RecordEngagementOutputDTO;
+
const Logger_TOKEN = 'Logger_TOKEN';
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
diff --git a/apps/api/src/domain/analytics/dto/AnalyticsDto.ts b/apps/api/src/domain/analytics/dto/AnalyticsDto.ts
deleted file mode 100644
index 83f29dbaf..000000000
--- a/apps/api/src/domain/analytics/dto/AnalyticsDto.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString, IsOptional, IsEnum, IsBoolean, IsNumber, IsObject } from 'class-validator';
-
-// From core/analytics/domain/types/PageView.ts
-export enum EntityType {
- LEAGUE = 'league',
- DRIVER = 'driver',
- TEAM = 'team',
- RACE = 'race',
- SPONSOR = 'sponsor',
-}
-
-// From core/analytics/domain/types/PageView.ts
-export enum VisitorType {
- ANONYMOUS = 'anonymous',
- DRIVER = 'driver',
- SPONSOR = 'sponsor',
-}
-
-export class RecordPageViewInput {
- @ApiProperty({ enum: EntityType })
- @IsEnum(EntityType)
- entityType!: EntityType;
-
- @ApiProperty()
- @IsString()
- entityId!: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- visitorId?: string;
-
- @ApiProperty({ enum: VisitorType })
- @IsEnum(VisitorType)
- visitorType!: VisitorType;
-
- @ApiProperty()
- @IsString()
- sessionId!: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- referrer?: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- userAgent?: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- country?: string;
-}
-
-export class RecordPageViewOutput {
- @ApiProperty()
- @IsString()
- pageViewId!: string;
-}
-
-// From core/analytics/domain/types/EngagementEvent.ts
-export enum EngagementAction {
- CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
- CLICK_SPONSOR_URL = 'click_sponsor_url',
- DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
- JOIN_LEAGUE = 'join_league',
- REGISTER_RACE = 'register_race',
- VIEW_STANDINGS = 'view_standings',
- VIEW_SCHEDULE = 'view_schedule',
- SHARE_SOCIAL = 'share_social',
- CONTACT_SPONSOR = 'contact_sponsor',
-}
-
-// From core/analytics/domain/types/EngagementEvent.ts
-export enum EngagementEntityType {
- LEAGUE = 'league',
- DRIVER = 'driver',
- TEAM = 'team',
- RACE = 'race',
- SPONSOR = 'sponsor',
- SPONSORSHIP = 'sponsorship',
-}
-
-export class RecordEngagementInput {
- @ApiProperty({ enum: EngagementAction })
- @IsEnum(EngagementAction)
- action!: EngagementAction;
-
- @ApiProperty({ enum: EngagementEntityType })
- @IsEnum(EngagementEntityType)
- entityType!: EngagementEntityType;
-
- @ApiProperty()
- @IsString()
- entityId!: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- actorId?: string;
-
- @ApiProperty({ enum: ['anonymous', 'driver', 'sponsor'] })
- @IsEnum(['anonymous', 'driver', 'sponsor'])
- actorType!: 'anonymous' | 'driver' | 'sponsor';
-
- @ApiProperty()
- @IsString()
- sessionId!: string;
-
- @ApiProperty({ required: false, type: Object })
- @IsOptional()
- @IsObject()
- metadata?: Record;
-}
-
-export class RecordEngagementOutput {
- @ApiProperty()
- @IsString()
- eventId!: string;
-
- @ApiProperty()
- @IsNumber()
- engagementWeight!: number;
-}
diff --git a/apps/api/src/domain/analytics/dtos/EngagementAction.ts b/apps/api/src/domain/analytics/dtos/EngagementAction.ts
new file mode 100644
index 000000000..cd2798344
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/EngagementAction.ts
@@ -0,0 +1,12 @@
+// From core/analytics/domain/types/EngagementEvent.ts
+export enum EngagementAction {
+ CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
+ CLICK_SPONSOR_URL = 'click_sponsor_url',
+ DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
+ JOIN_LEAGUE = 'join_league',
+ REGISTER_RACE = 'register_race',
+ VIEW_STANDINGS = 'view_standings',
+ VIEW_SCHEDULE = 'view_schedule',
+ SHARE_SOCIAL = 'share_social',
+ CONTACT_SPONSOR = 'contact_sponsor',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/EngagementEntityType.ts b/apps/api/src/domain/analytics/dtos/EngagementEntityType.ts
new file mode 100644
index 000000000..711401ba6
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/EngagementEntityType.ts
@@ -0,0 +1,9 @@
+// From core/analytics/domain/types/EngagementEvent.ts
+export enum EngagementEntityType {
+ LEAGUE = 'league',
+ DRIVER = 'driver',
+ TEAM = 'team',
+ RACE = 'race',
+ SPONSOR = 'sponsor',
+ SPONSORSHIP = 'sponsorship',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/EntityType.ts b/apps/api/src/domain/analytics/dtos/EntityType.ts
new file mode 100644
index 000000000..6c29322bd
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/EntityType.ts
@@ -0,0 +1,8 @@
+// From core/analytics/domain/types/PageView.ts
+export enum EntityType {
+ LEAGUE = 'league',
+ DRIVER = 'driver',
+ TEAM = 'team',
+ RACE = 'race',
+ SPONSOR = 'sponsor',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/RecordEngagementInputDTO.ts b/apps/api/src/domain/analytics/dtos/RecordEngagementInputDTO.ts
new file mode 100644
index 000000000..e2e60aa67
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/RecordEngagementInputDTO.ts
@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsOptional, IsEnum, IsObject } from 'class-validator';
+import { EngagementAction } from './EngagementAction';
+import { EngagementEntityType } from './EngagementEntityType';
+
+export class RecordEngagementInputDTO {
+ @ApiProperty({ enum: EngagementAction })
+ @IsEnum(EngagementAction)
+ action!: EngagementAction;
+
+ @ApiProperty({ enum: EngagementEntityType })
+ @IsEnum(EngagementEntityType)
+ entityType!: EngagementEntityType;
+
+ @ApiProperty()
+ @IsString()
+ entityId!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ actorId?: string;
+
+ @ApiProperty({ enum: ['anonymous', 'driver', 'sponsor'] })
+ @IsEnum(['anonymous', 'driver', 'sponsor'])
+ actorType!: 'anonymous' | 'driver' | 'sponsor';
+
+ @ApiProperty()
+ @IsString()
+ sessionId!: string;
+
+ @ApiProperty({ required: false, type: Object })
+ @IsOptional()
+ @IsObject()
+ metadata?: Record;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/RecordEngagementOutputDTO.ts b/apps/api/src/domain/analytics/dtos/RecordEngagementOutputDTO.ts
new file mode 100644
index 000000000..e5f6596a4
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/RecordEngagementOutputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class RecordEngagementOutputDTO {
+ @ApiProperty()
+ @IsString()
+ eventId!: string;
+
+ @ApiProperty()
+ @IsNumber()
+ engagementWeight!: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/RecordPageViewInputDTO.ts b/apps/api/src/domain/analytics/dtos/RecordPageViewInputDTO.ts
new file mode 100644
index 000000000..89c51ac28
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/RecordPageViewInputDTO.ts
@@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsOptional, IsEnum } from 'class-validator';
+import { EntityType } from './EntityType';
+import { VisitorType } from './VisitorType';
+
+export class RecordPageViewInputDTO {
+ @ApiProperty({ enum: EntityType })
+ @IsEnum(EntityType)
+ entityType!: EntityType;
+
+ @ApiProperty()
+ @IsString()
+ entityId!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ visitorId?: string;
+
+ @ApiProperty({ enum: VisitorType })
+ @IsEnum(VisitorType)
+ visitorType!: VisitorType;
+
+ @ApiProperty()
+ @IsString()
+ sessionId!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ referrer?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ userAgent?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ country?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/RecordPageViewOutputDTO.ts b/apps/api/src/domain/analytics/dtos/RecordPageViewOutputDTO.ts
new file mode 100644
index 000000000..f48a54ba5
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/RecordPageViewOutputDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class RecordPageViewOutputDTO {
+ @ApiProperty()
+ @IsString()
+ pageViewId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/dtos/VisitorType.ts b/apps/api/src/domain/analytics/dtos/VisitorType.ts
new file mode 100644
index 000000000..6add7a7e8
--- /dev/null
+++ b/apps/api/src/domain/analytics/dtos/VisitorType.ts
@@ -0,0 +1,6 @@
+// From core/analytics/domain/types/PageView.ts
+export enum VisitorType {
+ ANONYMOUS = 'anonymous',
+ DRIVER = 'driver',
+ SPONSOR = 'sponsor',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/analytics/use-cases/RecordEngagementUseCase.ts b/apps/api/src/domain/analytics/use-cases/RecordEngagementUseCase.ts
index f42fcb849..cc1205200 100644
--- a/apps/api/src/domain/analytics/use-cases/RecordEngagementUseCase.ts
+++ b/apps/api/src/domain/analytics/use-cases/RecordEngagementUseCase.ts
@@ -1,9 +1,13 @@
import { Injectable, Inject } from '@nestjs/common';
-import { RecordEngagementInput, RecordEngagementOutput } from '../dto/AnalyticsDto';
+import type { RecordEngagementInputDTO } from '../dtos/RecordEngagementInputDTO';
+import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
import type { Logger } from '@core/shared/application';
import { EngagementEvent } from '@core/analytics/domain/entities/EngagementEvent';
+type RecordEngagementInput = RecordEngagementInputDTO;
+type RecordEngagementOutput = RecordEngagementOutputDTO;
+
const Logger_TOKEN = 'Logger_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
diff --git a/apps/api/src/domain/analytics/use-cases/RecordPageViewUseCase.ts b/apps/api/src/domain/analytics/use-cases/RecordPageViewUseCase.ts
index 9df664b4a..f39927e32 100644
--- a/apps/api/src/domain/analytics/use-cases/RecordPageViewUseCase.ts
+++ b/apps/api/src/domain/analytics/use-cases/RecordPageViewUseCase.ts
@@ -1,9 +1,13 @@
import { Injectable, Inject } from '@nestjs/common';
-import { RecordPageViewInput, RecordPageViewOutput } from '../dto/AnalyticsDto';
+import type { RecordPageViewInputDTO } from '../dtos/RecordPageViewInputDTO';
+import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
import type { Logger } from '@core/shared/application';
import { PageView } from '@core/analytics/domain/entities/PageView';
+type RecordPageViewInput = RecordPageViewInputDTO;
+type RecordPageViewOutput = RecordPageViewOutputDTO;
+
const Logger_TOKEN = 'Logger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
diff --git a/apps/api/src/domain/auth/dto/AuthDto.ts b/apps/api/src/domain/auth/dtos/AuthDto.ts
similarity index 100%
rename from apps/api/src/domain/auth/dto/AuthDto.ts
rename to apps/api/src/domain/auth/dtos/AuthDto.ts
diff --git a/apps/api/src/domain/driver/DriverController.ts b/apps/api/src/domain/driver/DriverController.ts
index 3c04c74f2..1d71c645b 100644
--- a/apps/api/src/domain/driver/DriverController.ts
+++ b/apps/api/src/domain/driver/DriverController.ts
@@ -2,7 +2,13 @@ import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
import { Request } from 'express';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { DriverService } from './DriverService';
-import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
+import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
+import { DriverStatsDTO } from './dtos/DriverStatsDTO';
+import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
+import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
+import { GetDriverRegistrationStatusQueryDTO } from './dtos/GetDriverRegistrationStatusQueryDTO';
+import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
+import { DriverDTO } from './dtos/DriverDTO';
@ApiTags('drivers')
@Controller('drivers')
@@ -11,15 +17,15 @@ export class DriverController {
@Get('leaderboard')
@ApiOperation({ summary: 'Get drivers leaderboard' })
- @ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardViewModel })
- async getDriversLeaderboard(): Promise {
+ @ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardDTO })
+ async getDriversLeaderboard(): Promise {
return this.driverService.getDriversLeaderboard();
}
@Get('total-drivers')
@ApiOperation({ summary: 'Get the total number of drivers' })
- @ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDto })
- async getTotalDrivers(): Promise {
+ @ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDTO })
+ async getTotalDrivers(): Promise {
return this.driverService.getTotalDrivers();
}
@@ -38,11 +44,11 @@ export class DriverController {
@Post('complete-onboarding')
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
- @ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
+ @ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutputDTO })
async completeOnboarding(
- @Body() input: CompleteOnboardingInput,
+ @Body() input: CompleteOnboardingInputDTO,
@Req() req: Request,
- ): Promise {
+ ): Promise {
// Assuming userId is available from the request (e.g., via auth middleware)
const userId = req['user'].userId; // Placeholder for actual user extraction
return this.driverService.completeOnboarding(userId, input);
@@ -50,13 +56,23 @@ export class DriverController {
@Get(':driverId/races/:raceId/registration-status')
@ApiOperation({ summary: 'Get driver registration status for a specific race' })
- @ApiResponse({ status: 200, description: 'Driver registration status', type: DriverRegistrationStatusViewModel })
+ @ApiResponse({ status: 200, description: 'Driver registration status', type: DriverRegistrationStatusDTO })
async getDriverRegistrationStatus(
@Param('driverId') driverId: string,
@Param('raceId') raceId: string,
- ): Promise {
+ ): Promise {
return this.driverService.getDriverRegistrationStatus({ driverId, raceId });
}
+ @Put(':driverId/profile')
+ @ApiOperation({ summary: 'Update driver profile' })
+ @ApiResponse({ status: 200, description: 'Driver profile updated', type: DriverDTO })
+ async updateDriverProfile(
+ @Param('driverId') driverId: string,
+ @Body() body: { bio?: string; country?: string },
+ ): Promise {
+ return this.driverService.updateDriverProfile(driverId, body.bio, body.country);
+ }
+
// Add other Driver endpoints here based on other presenters
}
diff --git a/apps/api/src/domain/driver/DriverProviders.ts b/apps/api/src/domain/driver/DriverProviders.ts
index 6fed1a7dc..6e62598b5 100644
--- a/apps/api/src/domain/driver/DriverProviders.ts
+++ b/apps/api/src/domain/driver/DriverProviders.ts
@@ -15,6 +15,9 @@ import type { Logger } from "@gridpilot/core/shared/application";
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 { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
+import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
+import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
// Import concrete in-memory implementations
import { InMemoryDriverRepository } from '../../..//racing/persistence/inmemory/InMemoryDriverRepository';
@@ -41,6 +44,9 @@ export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseC
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 GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
+export const GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
+export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself
@@ -105,4 +111,9 @@ export const DriverProviders: Provider[] = [
useFactory: (registrationRepo: IRaceRegistrationRepository) => new IsDriverRegisteredForRaceUseCase(registrationRepo),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
},
+ {
+ provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
+ useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo),
+ inject: [DRIVER_REPOSITORY_TOKEN],
+ },
];
diff --git a/apps/api/src/domain/driver/DriverService.ts b/apps/api/src/domain/driver/DriverService.ts
index eeca2d0a0..00a9eeb8e 100644
--- a/apps/api/src/domain/driver/DriverService.ts
+++ b/apps/api/src/domain/driver/DriverService.ts
@@ -1,5 +1,10 @@
import { Injectable, Inject } from '@nestjs/common';
-import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
+import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
+import { DriverStatsDTO } from './dtos/DriverStatsDTO';
+import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
+import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
+import { GetDriverRegistrationStatusQueryDTO } from './dtos/GetDriverRegistrationStatusQueryDTO';
+import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
// Use cases
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
@@ -14,8 +19,10 @@ import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPres
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 { 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, UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, LOGGER_TOKEN, DRIVER_REPOSITORY_TOKEN } from './DriverProviders';
import type { Logger } from '@core/shared/application';
+import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
+import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
@Injectable()
export class DriverService {
@@ -24,10 +31,12 @@ export class DriverService {
@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(UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN) private readonly updateDriverProfileUseCase: UpdateDriverProfileUseCase,
+ @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
- async getDriversLeaderboard(): Promise {
+ async getDriversLeaderboard(): Promise {
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
const presenter = new DriversLeaderboardPresenter();
@@ -35,7 +44,7 @@ export class DriverService {
return presenter.viewModel;
}
- async getTotalDrivers(): Promise {
+ async getTotalDrivers(): Promise {
this.logger.debug('[DriverService] Fetching total drivers count.');
const presenter = new DriverStatsPresenter();
@@ -43,7 +52,7 @@ export class DriverService {
return presenter.viewModel;
}
- async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise {
+ async completeOnboarding(userId: string, input: CompleteOnboardingInputDTO): Promise {
this.logger.debug('Completing onboarding for user:', userId);
const presenter = new CompleteOnboardingPresenter();
@@ -59,11 +68,37 @@ export class DriverService {
return presenter.viewModel;
}
- async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise {
+ async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQueryDTO): Promise {
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;
}
+
+ async getCurrentDriver(userId: string): Promise {
+ this.logger.debug(`[DriverService] Fetching current driver for userId: ${userId}`);
+
+ const driver = await this.driverRepository.findById(userId);
+ if (!driver) {
+ return null;
+ }
+
+ return {
+ id: driver.id,
+ name: driver.name.value,
+ };
+ }
+
+ async updateDriverProfile(driverId: string, bio?: string, country?: string): Promise {
+ this.logger.debug(`[DriverService] Updating driver profile for driverId: ${driverId}`);
+
+ const result = await this.updateDriverProfileUseCase.execute({ driverId, bio, country });
+ if (result.isErr()) {
+ this.logger.error(`Failed to update driver profile: ${result.error.code}`);
+ return null;
+ }
+
+ return result.value;
+ }
}
diff --git a/apps/api/src/domain/driver/dto/DriverDto.ts b/apps/api/src/domain/driver/dto/DriverDto.ts
deleted file mode 100644
index 0ee9d630a..000000000
--- a/apps/api/src/domain/driver/dto/DriverDto.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
-
-export class DriverLeaderboardItemViewModel {
- @ApiProperty()
- id: string;
-
- @ApiProperty()
- name: string;
-
- @ApiProperty()
- rating: number;
-
- @ApiProperty()
- skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
-
- @ApiProperty()
- nationality: string;
-
- @ApiProperty()
- racesCompleted: number;
-
- @ApiProperty()
- wins: number;
-
- @ApiProperty()
- podiums: number;
-
- @ApiProperty()
- isActive: boolean;
-
- @ApiProperty()
- rank: number;
-
- @ApiProperty({ nullable: true })
- avatarUrl?: string;
-}
-
-export class DriversLeaderboardViewModel {
- @ApiProperty({ type: [DriverLeaderboardItemViewModel] })
- drivers: DriverLeaderboardItemViewModel[];
-
- @ApiProperty()
- totalRaces: number;
-
- @ApiProperty()
- totalWins: number;
-
- @ApiProperty()
- activeCount: number;
-}
-
-export class DriverStatsDto {
- @ApiProperty()
- totalDrivers: number;
-}
-
-export class CompleteOnboardingInput {
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- firstName: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- lastName: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- displayName: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- country: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- timezone?: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- bio?: string;
-}
-
-export class CompleteOnboardingOutput {
- @ApiProperty()
- @IsBoolean()
- success: boolean;
-
- @ApiProperty({ required: false })
- @IsString()
- driverId?: string;
-
- @ApiProperty({ required: false })
- @IsString()
- errorMessage?: string;
-}
-
-export class GetDriverRegistrationStatusQuery {
- @ApiProperty()
- @IsString()
- raceId: string;
-
- @ApiProperty()
- @IsString()
- driverId: string;
-}
-
-export class DriverRegistrationStatusViewModel {
- @ApiProperty()
- @IsBoolean()
- isRegistered: boolean;
-
- @ApiProperty()
- @IsString()
- raceId: string;
-
- @ApiProperty()
- @IsString()
- driverId: string;
-}
-
-export class DriverDto {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- name: string; // Display name or full name
-}
-
-// Add other DTOs for driver-related logic as needed
diff --git a/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts b/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts
new file mode 100644
index 000000000..5462b0d76
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts
@@ -0,0 +1,34 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
+
+export class CompleteOnboardingInputDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ firstName: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ lastName: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ displayName: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ country: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ timezone?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ bio?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts b/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts
new file mode 100644
index 000000000..b425fa9df
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsString } from 'class-validator';
+
+export class CompleteOnboardingOutputDTO {
+ @ApiProperty()
+ @IsBoolean()
+ success: boolean;
+
+ @ApiProperty({ required: false })
+ @IsString()
+ driverId?: string;
+
+ @ApiProperty({ required: false })
+ @IsString()
+ errorMessage?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/dtos/DriverLeaderboardItemDTO.ts b/apps/api/src/domain/driver/dtos/DriverLeaderboardItemDTO.ts
new file mode 100644
index 000000000..ad715c419
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/DriverLeaderboardItemDTO.ts
@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class DriverLeaderboardItemDTO {
+ @ApiProperty()
+ id: string;
+
+ @ApiProperty()
+ name: string;
+
+ @ApiProperty()
+ rating: number;
+
+ @ApiProperty()
+ skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
+
+ @ApiProperty()
+ nationality: string;
+
+ @ApiProperty()
+ racesCompleted: number;
+
+ @ApiProperty()
+ wins: number;
+
+ @ApiProperty()
+ podiums: number;
+
+ @ApiProperty()
+ isActive: boolean;
+
+ @ApiProperty()
+ rank: number;
+
+ @ApiProperty({ nullable: true })
+ avatarUrl?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/dtos/DriverRegistrationStatusDTO.ts b/apps/api/src/domain/driver/dtos/DriverRegistrationStatusDTO.ts
new file mode 100644
index 000000000..28dcb609a
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/DriverRegistrationStatusDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsString } from 'class-validator';
+
+export class DriverRegistrationStatusDTO {
+ @ApiProperty()
+ @IsBoolean()
+ isRegistered!: boolean;
+
+ @ApiProperty()
+ @IsString()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ driverId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts b/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts
new file mode 100644
index 000000000..14b001e56
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts
@@ -0,0 +1,6 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class DriverStatsDTO {
+ @ApiProperty()
+ totalDrivers: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/dtos/DriversLeaderboardDTO.ts b/apps/api/src/domain/driver/dtos/DriversLeaderboardDTO.ts
new file mode 100644
index 000000000..31aa83112
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/DriversLeaderboardDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { DriverLeaderboardItemDTO } from './DriverLeaderboardItemDTO';
+
+export class DriversLeaderboardDTO {
+ @ApiProperty({ type: [DriverLeaderboardItemDTO] })
+ drivers: DriverLeaderboardItemDTO[];
+
+ @ApiProperty()
+ totalRaces: number;
+
+ @ApiProperty()
+ totalWins: number;
+
+ @ApiProperty()
+ activeCount: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts b/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts
new file mode 100644
index 000000000..32cff73a5
--- /dev/null
+++ b/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetDriverRegistrationStatusQueryDTO {
+ @ApiProperty()
+ @IsString()
+ raceId: string;
+
+ @ApiProperty()
+ @IsString()
+ driverId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts
index 7c862af2a..335bcaba6 100644
--- a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts
+++ b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts
@@ -1,8 +1,8 @@
-import { CompleteOnboardingOutput } from '../dto/DriverDto';
+import { CompleteOnboardingOutputDTO } from '../dtos/CompleteOnboardingOutputDTO';
import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPresenter {
- private result: CompleteOnboardingOutput | null = null;
+ private result: CompleteOnboardingOutputDTO | null = null;
reset() {
this.result = null;
@@ -16,7 +16,7 @@ export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPre
};
}
- get viewModel(): CompleteOnboardingOutput {
+ get viewModel(): CompleteOnboardingOutputDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
diff --git a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts
index 2c1d5abf3..af8ee956e 100644
--- a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts
+++ b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts
@@ -1,8 +1,8 @@
-import { DriverRegistrationStatusViewModel } from '../dto/DriverDto';
+import { DriverRegistrationStatusDTO } from '../dtos/DriverRegistrationStatusDTO';
import type { IDriverRegistrationStatusPresenter } from '../../../../../core/racing/application/presenters/IDriverRegistrationStatusPresenter';
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
- private result: DriverRegistrationStatusViewModel | null = null;
+ private result: DriverRegistrationStatusDTO | null = null;
present(isRegistered: boolean, raceId: string, driverId: string) {
this.result = {
@@ -12,7 +12,7 @@ export class DriverRegistrationStatusPresenter implements IDriverRegistrationSta
};
}
- getViewModel(): DriverRegistrationStatusViewModel {
+ getViewModel(): DriverRegistrationStatusDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
@@ -22,7 +22,7 @@ export class DriverRegistrationStatusPresenter implements IDriverRegistrationSta
this.result = null;
}
- get viewModel(): DriverRegistrationStatusViewModel {
+ get viewModel(): DriverRegistrationStatusDTO {
return this.getViewModel();
}
}
\ No newline at end of file
diff --git a/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts b/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts
index 3057ec178..d1b27dabe 100644
--- a/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts
+++ b/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts
@@ -1,8 +1,8 @@
-import { DriverStatsDto } from '../dto/DriverDto';
+import { DriverStatsDTO } from '../dtos/DriverStatsDTO';
import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
export class DriverStatsPresenter implements ITotalDriversPresenter {
- private result: DriverStatsDto | null = null;
+ private result: DriverStatsDTO | null = null;
reset() {
this.result = null;
@@ -14,7 +14,7 @@ export class DriverStatsPresenter implements ITotalDriversPresenter {
};
}
- get viewModel(): DriverStatsDto {
+ get viewModel(): DriverStatsDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
diff --git a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts
index 2aed681fe..85d6c04a8 100644
--- a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts
+++ b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts
@@ -1,15 +1,16 @@
-import { DriversLeaderboardViewModel, DriverLeaderboardItemViewModel } from '../dto/DriverDto';
+import { DriversLeaderboardDTO, DriverLeaderboardItemDTO } from '../dtos/DriversLeaderboardDTO';
+import { DriverLeaderboardItemDTO } from '../dtos/DriverLeaderboardItemDTO';
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
- private result: DriversLeaderboardViewModel | null = null;
+ private result: DriversLeaderboardDTO | null = null;
reset() {
this.result = null;
}
present(dto: DriversLeaderboardResultDTO) {
- const drivers: DriverLeaderboardItemViewModel[] = dto.drivers.map(driver => {
+ const drivers: DriverLeaderboardItemDTO[] = 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];
@@ -42,7 +43,7 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
};
}
- get viewModel(): DriversLeaderboardViewModel {
+ get viewModel(): DriversLeaderboardDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
diff --git a/apps/api/src/domain/league/LeagueController.ts b/apps/api/src/domain/league/LeagueController.ts
index b7eb3ba12..be4fc44ab 100644
--- a/apps/api/src/domain/league/LeagueController.ts
+++ b/apps/api/src/domain/league/LeagueController.ts
@@ -1,8 +1,35 @@
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, LeagueMembershipsViewModel, LeagueStandingsViewModel, LeagueScheduleViewModel, LeagueStatsViewModel, LeagueAdminViewModel, CreateLeagueInput, CreateLeagueOutput } from './dto/LeagueDto';
-import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries
+import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
+import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
+import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
+import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
+import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
+import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
+import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
+import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
+import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
+import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
+import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
+import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
+import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
+import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
+import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
+import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
+import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
+import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
+import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
+import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
+import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
+import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
+import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
+import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
+import { GetLeagueJoinRequestsQueryDTO } from './dtos/GetLeagueJoinRequestsQueryDTO';
+import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
+import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
+import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
+import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
@ApiTags('leagues')
@Controller('leagues')
@@ -11,169 +38,197 @@ export class LeagueController {
@Get('all-with-capacity')
@ApiOperation({ summary: 'Get all leagues with their capacity information' })
- @ApiResponse({ status: 200, description: 'List of leagues with capacity', type: AllLeaguesWithCapacityViewModel })
- async getAllLeaguesWithCapacity(): Promise {
+ @ApiResponse({ status: 200, description: 'List of leagues with capacity', type: AllLeaguesWithCapacityDTO })
+ async getAllLeaguesWithCapacity(): Promise {
return this.leagueService.getAllLeaguesWithCapacity();
}
@Get('total-leagues')
@ApiOperation({ summary: 'Get the total number of leagues' })
- @ApiResponse({ status: 200, description: 'Total number of leagues', type: LeagueStatsDto })
- async getTotalLeagues(): Promise {
+ @ApiResponse({ status: 200, description: 'Total number of leagues', type: LeagueStatsDTO })
+ async getTotalLeagues(): Promise {
return this.leagueService.getTotalLeagues();
}
@Get(':leagueId/join-requests')
@ApiOperation({ summary: 'Get all outstanding join requests for a league' })
- @ApiResponse({ status: 200, description: 'List of join requests', type: [LeagueJoinRequestViewModel] })
- async getJoinRequests(@Param('leagueId') leagueId: string): Promise {
+ @ApiResponse({ status: 200, description: 'List of join requests', type: [LeagueJoinRequestDTO] })
+ async getJoinRequests(@Param('leagueId') leagueId: string): Promise {
// No specific query DTO needed for GET, leagueId from param
return this.leagueService.getLeagueJoinRequests(leagueId);
}
@Post(':leagueId/join-requests/approve')
@ApiOperation({ summary: 'Approve a league join request' })
- @ApiBody({ type: ApproveJoinRequestInput }) // Explicitly define body type for Swagger
- @ApiResponse({ status: 200, description: 'Join request approved', type: ApproveJoinRequestOutput })
+ @ApiBody({ type: ApproveJoinRequestInputDTO }) // Explicitly define body type for Swagger
+ @ApiResponse({ status: 200, description: 'Join request approved', type: ApproveJoinRequestOutputDTO })
@ApiResponse({ status: 404, description: 'Join request not found' })
async approveJoinRequest(
@Param('leagueId') leagueId: string,
- @Body() input: ApproveJoinRequestInput,
- ): Promise {
+ @Body() input: ApproveJoinRequestInputDTO,
+ ): Promise {
return this.leagueService.approveLeagueJoinRequest({ ...input, leagueId });
}
@Post(':leagueId/join-requests/reject')
@ApiOperation({ summary: 'Reject a league join request' })
- @ApiBody({ type: RejectJoinRequestInput })
- @ApiResponse({ status: 200, description: 'Join request rejected', type: RejectJoinRequestOutput })
+ @ApiBody({ type: RejectJoinRequestInputDTO })
+ @ApiResponse({ status: 200, description: 'Join request rejected', type: RejectJoinRequestOutputDTO })
@ApiResponse({ status: 404, description: 'Join request not found' })
async rejectJoinRequest(
@Param('leagueId') leagueId: string,
- @Body() input: RejectJoinRequestInput,
- ): Promise {
+ @Body() input: RejectJoinRequestInputDTO,
+ ): Promise {
return this.leagueService.rejectLeagueJoinRequest({ ...input, leagueId });
}
@Get(':leagueId/permissions/:performerDriverId')
@ApiOperation({ summary: 'Get league admin permissions for a performer' })
- @ApiResponse({ status: 200, description: 'League admin permissions', type: LeagueAdminPermissionsViewModel })
+ @ApiResponse({ status: 200, description: 'League admin permissions', type: LeagueAdminPermissionsDTO })
async getLeagueAdminPermissions(
@Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string,
- ): Promise {
+ ): Promise {
// No specific input DTO needed for Get, parameters from path
return this.leagueService.getLeagueAdminPermissions({ leagueId, performerDriverId });
}
@Patch(':leagueId/members/:targetDriverId/remove')
@ApiOperation({ summary: 'Remove a member from the league' })
- @ApiBody({ type: RemoveLeagueMemberInput }) // Explicitly define body type for Swagger
- @ApiResponse({ status: 200, description: 'Member removed successfully', type: RemoveLeagueMemberOutput })
+ @ApiBody({ type: RemoveLeagueMemberInputDTO }) // Explicitly define body type for Swagger
+ @ApiResponse({ status: 200, description: 'Member removed successfully', type: RemoveLeagueMemberOutputDTO })
@ApiResponse({ status: 400, description: 'Cannot remove member' })
@ApiResponse({ status: 404, description: 'Member not found' })
async removeLeagueMember(
@Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string,
@Param('targetDriverId') targetDriverId: string,
- @Body() input: RemoveLeagueMemberInput, // Body content for a patch often includes IDs
- ): Promise {
+ @Body() input: RemoveLeagueMemberInputDTO, // Body content for a patch often includes IDs
+ ): Promise {
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
}
@Patch(':leagueId/members/:targetDriverId/role')
@ApiOperation({ summary: "Update a member's role in the league" })
- @ApiBody({ type: UpdateLeagueMemberRoleInput }) // Explicitly define body type for Swagger
- @ApiResponse({ status: 200, description: 'Member role updated successfully', type: UpdateLeagueMemberRoleOutput })
+ @ApiBody({ type: UpdateLeagueMemberRoleInputDTO }) // Explicitly define body type for Swagger
+ @ApiResponse({ status: 200, description: 'Member role updated successfully', type: UpdateLeagueMemberRoleOutputDTO })
@ApiResponse({ status: 400, description: 'Cannot update role' })
@ApiResponse({ status: 404, description: 'Member not found' })
async updateLeagueMemberRole(
@Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string,
@Param('targetDriverId') targetDriverId: string,
- @Body() input: UpdateLeagueMemberRoleInput, // Body includes newRole, other for swagger
- ): Promise {
+ @Body() input: UpdateLeagueMemberRoleInputDTO, // Body includes newRole, other for swagger
+ ): Promise {
return this.leagueService.updateLeagueMemberRole({ leagueId, performerDriverId, targetDriverId, newRole: input.newRole });
}
@Get(':leagueId/owner-summary/:ownerId')
@ApiOperation({ summary: 'Get owner summary for a league' })
- @ApiResponse({ status: 200, description: 'League owner summary', type: LeagueOwnerSummaryViewModel })
+ @ApiResponse({ status: 200, description: 'League owner summary', type: LeagueOwnerSummaryDTO })
@ApiResponse({ status: 404, description: 'Owner or league not found' })
async getLeagueOwnerSummary(
@Param('leagueId') leagueId: string,
@Param('ownerId') ownerId: string,
- ): Promise {
+ ): Promise {
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId };
return this.leagueService.getLeagueOwnerSummary(query);
}
@Get(':leagueId/config')
@ApiOperation({ summary: 'Get league full configuration' })
- @ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDto })
+ @ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO })
async getLeagueFullConfig(
@Param('leagueId') leagueId: string,
- ): Promise {
+ ): Promise {
const query: GetLeagueAdminConfigQuery = { leagueId };
return this.leagueService.getLeagueFullConfig(query);
}
@Get(':leagueId/protests')
@ApiOperation({ summary: 'Get protests for a league' })
- @ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsViewModel })
- async getLeagueProtests(@Param('leagueId') leagueId: string): Promise {
+ @ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsDTO })
+ async getLeagueProtests(@Param('leagueId') leagueId: string): Promise {
const query: GetLeagueProtestsQuery = { leagueId };
return this.leagueService.getLeagueProtests(query);
}
@Get(':leagueId/seasons')
@ApiOperation({ summary: 'Get seasons for a league' })
- @ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryViewModel] })
- async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise {
+ @ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryDTO] })
+ async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise {
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 {
+ @ApiResponse({ status: 200, description: 'List of league members', type: LeagueMembershipsDTO })
+ async getLeagueMemberships(@Param('leagueId') leagueId: string): Promise {
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 {
+ @ApiResponse({ status: 200, description: 'League standings', type: LeagueStandingsDTO })
+ async getLeagueStandings(@Param('leagueId') leagueId: string): Promise {
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 {
+ @ApiResponse({ status: 200, description: 'League schedule', type: LeagueScheduleDTO })
+ async getLeagueSchedule(@Param('leagueId') leagueId: string): Promise {
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 {
+ @ApiResponse({ status: 200, description: 'League stats', type: LeagueStatsDTO })
+ async getLeagueStats(@Param('leagueId') leagueId: string): Promise {
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 {
+ @ApiResponse({ status: 200, description: 'League admin data', type: LeagueAdminDTO })
+ async getLeagueAdmin(@Param('leagueId') leagueId: string): Promise {
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 {
+ @ApiBody({ type: CreateLeagueInputDTO })
+ @ApiResponse({ status: 201, description: 'League created successfully', type: CreateLeagueOutputDTO })
+ async createLeague(@Body() input: CreateLeagueInputDTO): Promise {
return this.leagueService.createLeague(input);
}
+
+ @Get('scoring-presets')
+ @ApiOperation({ summary: 'Get league scoring presets' })
+ @ApiResponse({ status: 200, description: 'List of scoring presets' })
+ async getLeagueScoringPresets() {
+ return this.leagueService.listLeagueScoringPresets();
+ }
+
+ @Get(':leagueId/scoring-config')
+ @ApiOperation({ summary: 'Get league scoring config' })
+ @ApiResponse({ status: 200, description: 'League scoring config' })
+ async getLeagueScoringConfig(@Param('leagueId') leagueId: string) {
+ return this.leagueService.getLeagueScoringConfig(leagueId);
+ }
+
+ @Post(':leagueId/join')
+ @ApiOperation({ summary: 'Join a league' })
+ @ApiResponse({ status: 200, description: 'Joined league successfully' })
+ async joinLeague(@Param('leagueId') leagueId: string, @Body() body: { driverId: string }) {
+ return this.leagueService.joinLeague(leagueId, body.driverId);
+ }
+
+ @Post(':leagueId/transfer-ownership')
+ @ApiOperation({ summary: 'Transfer league ownership' })
+ @ApiResponse({ status: 200, description: 'Ownership transferred successfully' })
+ async transferLeagueOwnership(@Param('leagueId') leagueId: string, @Body() body: { currentOwnerId: string, newOwnerId: string }) {
+ return this.leagueService.transferLeagueOwnership(leagueId, body.currentOwnerId, body.newOwnerId);
+ }
}
diff --git a/apps/api/src/domain/league/LeagueService.ts b/apps/api/src/domain/league/LeagueService.ts
index fff2f5348..a09179648 100644
--- a/apps/api/src/domain/league/LeagueService.ts
+++ b/apps/api/src/domain/league/LeagueService.ts
@@ -1,5 +1,32 @@
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';
+import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
+import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
+import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
+import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
+import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
+import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
+import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
+import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
+import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
+import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
+import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
+import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
+import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
+import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
+import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
+import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
+import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
+import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
+import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
+import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
+import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
+import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
+import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
+import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
+import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
+import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
+import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
+import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
// Core imports
import type { Logger } from '@core/shared/application/Logger';
@@ -9,6 +36,10 @@ import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-c
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
+import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
+import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
+import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
+import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
@@ -27,6 +58,8 @@ import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-c
// API Presenters
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
+import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter';
+import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
@@ -52,6 +85,10 @@ export class LeagueService {
private readonly getLeagueStandingsUseCase: GetLeagueStandingsUseCase,
private readonly getLeagueStatsUseCase: GetLeagueStatsUseCase,
private readonly getLeagueFullConfigUseCase: GetLeagueFullConfigUseCase,
+ private readonly getLeagueScoringConfigUseCase: GetLeagueScoringConfigUseCase,
+ private readonly listLeagueScoringPresetsUseCase: ListLeagueScoringPresetsUseCase,
+ private readonly joinLeagueUseCase: JoinLeagueUseCase,
+ private readonly transferLeagueOwnershipUseCase: TransferLeagueOwnershipUseCase,
private readonly createLeagueWithSeasonAndScoringUseCase: CreateLeagueWithSeasonAndScoringUseCase,
private readonly getRaceProtestsUseCase: GetRaceProtestsUseCase,
private readonly getTotalLeaguesUseCase: GetTotalLeaguesUseCase,
@@ -234,4 +271,61 @@ export class LeagueService {
success: true,
};
}
+
+ async getLeagueScoringConfig(leagueId: string): Promise {
+ this.logger.debug('Getting league scoring config', { leagueId });
+
+ const presenter = new LeagueScoringConfigPresenter();
+ try {
+ const result = await this.getLeagueScoringConfigUseCase.execute({ leagueId });
+ if (result.isErr()) {
+ this.logger.error('Error getting league scoring config', result.error);
+ return null;
+ }
+ await presenter.present(result.value);
+ return presenter.getViewModel();
+ } catch (error) {
+ this.logger.error('Error getting league scoring config', error instanceof Error ? error : new Error(String(error)));
+ return null;
+ }
+ }
+
+ async listLeagueScoringPresets(): Promise {
+ this.logger.debug('Listing league scoring presets');
+
+ const presenter = new LeagueScoringPresetsPresenter();
+ await this.listLeagueScoringPresetsUseCase.execute(undefined, presenter);
+ return presenter.getViewModel()!;
+ }
+
+ async joinLeague(leagueId: string, driverId: string): Promise {
+ this.logger.debug('Joining league', { leagueId, driverId });
+
+ const result = await this.joinLeagueUseCase.execute({ leagueId, driverId });
+ if (result.isErr()) {
+ return {
+ success: false,
+ error: result.error.code,
+ };
+ }
+ return {
+ success: true,
+ membershipId: result.value.id,
+ };
+ }
+
+ async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise {
+ this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId });
+
+ const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
+ if (result.isErr()) {
+ return {
+ success: false,
+ error: result.error.code,
+ };
+ }
+ return {
+ success: true,
+ };
+ }
}
diff --git a/apps/api/src/domain/league/dto/LeagueDto.ts b/apps/api/src/domain/league/dto/LeagueDto.ts
deleted file mode 100644
index f96ddb7ba..000000000
--- a/apps/api/src/domain/league/dto/LeagueDto.ts
+++ /dev/null
@@ -1,666 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString, IsNumber, IsBoolean, IsDate, IsOptional, IsEnum, IsArray, ValidateNested } from 'class-validator';
-import { Type } from 'class-transformer';
-import { DriverDto } from '../../driver/dto/DriverDto';
-import { RaceDto } from '../../race/dto/RaceDto';
-
-
-export class LeagueSettingsDto {
- @ApiProperty({ nullable: true })
- @IsOptional()
- @IsNumber()
- maxDrivers?: number;
- // Add other league settings properties as needed
-}
-
-export class LeagueWithCapacityViewModel {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- name: string;
-
- // ... other properties of LeagueWithCapacityViewModel
- @ApiProperty({ nullable: true })
- @IsOptional()
- @IsString()
- description?: string;
-
- @ApiProperty()
- @IsString()
- ownerId: string;
-
- @ApiProperty({ type: () => LeagueSettingsDto })
- @ValidateNested()
- @Type(() => LeagueSettingsDto)
- settings: LeagueSettingsDto;
-
- @ApiProperty()
- @IsString()
- createdAt: string;
-
- @ApiProperty()
- @IsNumber()
- usedSlots: number;
-
- @ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
- @IsOptional()
- socialLinks?: {
- discordUrl?: string;
- youtubeUrl?: string;
- websiteUrl?: string;
- };
-}
-
-export class AllLeaguesWithCapacityViewModel {
- @ApiProperty({ type: [LeagueWithCapacityViewModel] })
- @IsArray()
- @ValidateNested({ each: true })
- @Type(() => LeagueWithCapacityViewModel)
- leagues: LeagueWithCapacityViewModel[];
-
- @ApiProperty()
- @IsNumber()
- totalCount: number;
-}
-
-export class LeagueStatsDto {
- @ApiProperty()
- @IsNumber()
- totalLeagues: number;
-}
-
-export class ProtestDto {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- raceId: string;
-
- @ApiProperty()
- @IsString()
- protestingDriverId: string;
-
- @ApiProperty()
- @IsString()
- accusedDriverId: string;
-
- @ApiProperty()
- @IsDate()
- @Type(() => Date)
- submittedAt: Date;
-
- @ApiProperty()
- @IsString()
- description: string;
-
- @ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
- @IsEnum(['pending', 'accepted', 'rejected'])
- status: 'pending' | 'accepted' | 'rejected';
-}
-
-export class SeasonDto {
- @ApiProperty()
- @IsString()
- seasonId: string;
-
- @ApiProperty()
- @IsString()
- name: string;
-
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- @Type(() => Date)
- startDate?: Date;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- @Type(() => Date)
- endDate?: Date;
-
- @ApiProperty({ enum: ['planned', 'active', 'completed'] })
- @IsEnum(['planned', 'active', 'completed'])
- status: 'planned' | 'active' | 'completed';
-
- @ApiProperty()
- @IsBoolean()
- isPrimary: boolean;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- seasonGroupId?: string;
-}
-
-export class LeagueJoinRequestViewModel {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty()
- @IsString()
- driverId: string;
-
- @ApiProperty()
- @IsDate()
- @Type(() => Date)
- requestedAt: Date;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- message?: string;
-
- @ApiProperty({ type: () => DriverDto, required: false })
- @IsOptional()
- @ValidateNested()
- @Type(() => DriverDto)
- driver?: DriverDto;
-}
-
-export class GetLeagueJoinRequestsQuery {
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class ApproveJoinRequestInput {
- @ApiProperty()
- @IsString()
- requestId: string;
-
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class ApproveJoinRequestOutput {
- @ApiProperty()
- @IsBoolean()
- success: boolean;
-
- @ApiProperty({ required: false })
- @IsString()
- message?: string;
-}
-
-export class RejectJoinRequestInput {
- @ApiProperty()
- @IsString()
- requestId: string;
-
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class RejectJoinRequestOutput {
- @ApiProperty()
- @IsBoolean()
- success: boolean;
-
- @ApiProperty({ required: false })
- @IsString()
- message?: string;
-}
-
-export class GetLeagueAdminPermissionsInput {
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty()
- @IsString()
- performerDriverId: string;
-}
-
-export class LeagueAdminPermissionsViewModel {
- @ApiProperty()
- @IsBoolean()
- canRemoveMember: boolean;
-
- @ApiProperty()
- @IsBoolean()
- canUpdateRoles: boolean;
-}
-
-export class RemoveLeagueMemberInput {
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty()
- @IsString()
- performerDriverId: string;
-
- @ApiProperty()
- @IsString()
- targetDriverId: string;
-}
-
-export class RemoveLeagueMemberOutput {
- @ApiProperty()
- @IsBoolean()
- success: boolean;
-}
-
-export class UpdateLeagueMemberRoleInput {
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty()
- @IsString()
- performerDriverId: string;
-
- @ApiProperty()
- @IsString()
- targetDriverId: string;
-
- @ApiProperty({ enum: ['owner', 'manager', 'member'] })
- @IsEnum(['owner', 'manager', 'member'])
- newRole: 'owner' | 'manager' | 'member';
-}
-
-export class UpdateLeagueMemberRoleOutput {
- @ApiProperty()
- @IsBoolean()
- success: boolean;
-}
-
-export class GetLeagueOwnerSummaryQuery {
- @ApiProperty()
- @IsString()
- ownerId: string;
-
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class LeagueOwnerSummaryViewModel {
- @ApiProperty({ type: () => DriverDto })
- @ValidateNested()
- @Type(() => DriverDto)
- driver: DriverDto;
-
- @ApiProperty({ nullable: true })
- @IsOptional()
- @IsNumber()
- rating: number | null;
-
- @ApiProperty({ nullable: true })
- @IsOptional()
- @IsNumber()
- rank: number | null;
-}
-
-export class LeagueConfigFormModelBasicsDto {
- @ApiProperty()
- @IsString()
- name: string;
-
- @ApiProperty()
- @IsString()
- description: string;
-
- @ApiProperty({ enum: ['public', 'private'] })
- @IsEnum(['public', 'private'])
- visibility: 'public' | 'private';
-}
-
-export class LeagueConfigFormModelStructureDto {
- @ApiProperty()
- @IsString()
- @IsEnum(['solo', 'team'])
- mode: 'solo' | 'team';
-}
-
-export class LeagueConfigFormModelScoringDto {
- @ApiProperty()
- @IsString()
- type: string;
-
- @ApiProperty()
- @IsNumber()
- points: number;
-}
-
-export class LeagueConfigFormModelDropPolicyDto {
- @ApiProperty({ enum: ['none', 'worst_n'] })
- @IsEnum(['none', 'worst_n'])
- strategy: 'none' | 'worst_n';
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsNumber()
- n?: number;
-}
-
-export class LeagueConfigFormModelStewardingDto {
- @ApiProperty({ enum: ['single_steward', 'committee_vote'] })
- @IsEnum(['single_steward', 'committee_vote'])
- decisionMode: 'single_steward' | 'committee_vote';
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsNumber()
- requiredVotes?: number;
-
- @ApiProperty()
- @IsBoolean()
- requireDefense: boolean;
-
- @ApiProperty()
- @IsNumber()
- defenseTimeLimit: number;
-
- @ApiProperty()
- @IsNumber()
- voteTimeLimit: number;
-
- @ApiProperty()
- @IsNumber()
- protestDeadlineHours: number;
-
- @ApiProperty()
- @IsNumber()
- stewardingClosesHours: number;
-
- @ApiProperty()
- @IsBoolean()
- notifyAccusedOnProtest: boolean;
-
- @ApiProperty()
- @IsBoolean()
- notifyOnVoteRequired: boolean;
-}
-
-export class LeagueConfigFormModelTimingsDto {
- @ApiProperty()
- @IsString()
- raceDayOfWeek: string;
-
- @ApiProperty()
- @IsNumber()
- raceTimeHour: number;
-
- @ApiProperty()
- @IsNumber()
- raceTimeMinute: number;
-}
-
-export class LeagueConfigFormModelDto {
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty({ type: LeagueConfigFormModelBasicsDto })
- @ValidateNested()
- @Type(() => LeagueConfigFormModelBasicsDto)
- basics: LeagueConfigFormModelBasicsDto;
-
- @ApiProperty({ type: LeagueConfigFormModelStructureDto })
- @ValidateNested()
- @Type(() => LeagueConfigFormModelStructureDto)
- structure: LeagueConfigFormModelStructureDto;
-
- @ApiProperty({ type: [Object] })
- @IsArray()
- championships: any[];
-
- @ApiProperty({ type: LeagueConfigFormModelScoringDto })
- @ValidateNested()
- @Type(() => LeagueConfigFormModelScoringDto)
- scoring: LeagueConfigFormModelScoringDto;
-
- @ApiProperty({ type: LeagueConfigFormModelDropPolicyDto })
- @ValidateNested()
- @Type(() => LeagueConfigFormModelDropPolicyDto)
- dropPolicy: LeagueConfigFormModelDropPolicyDto;
-
- @ApiProperty({ type: LeagueConfigFormModelTimingsDto })
- @ValidateNested()
- @Type(() => LeagueConfigFormModelTimingsDto)
- timings: LeagueConfigFormModelTimingsDto;
-
- @ApiProperty({ type: LeagueConfigFormModelStewardingDto })
- @ValidateNested()
- @Type(() => LeagueConfigFormModelStewardingDto)
- stewarding: LeagueConfigFormModelStewardingDto;
-}
-
-export class GetLeagueAdminConfigQuery {
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class GetLeagueAdminConfigOutput {
- @ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
- @IsOptional()
- @ValidateNested()
- @Type(() => LeagueConfigFormModelDto)
- form: LeagueConfigFormModelDto | null;
-}
-
-export class LeagueAdminConfigViewModel {
- @ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
- @IsOptional()
- @ValidateNested()
- @Type(() => LeagueConfigFormModelDto)
- form: LeagueConfigFormModelDto | null;
-}
-
-export class GetLeagueProtestsQuery {
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class LeagueAdminProtestsViewModel {
- @ApiProperty({ type: [ProtestDto] })
- @IsArray()
- @ValidateNested({ each: true })
- @Type(() => ProtestDto)
- protests: ProtestDto[];
-
- @ApiProperty({ type: () => RaceDto })
- @ValidateNested()
- @Type(() => RaceDto)
- racesById: { [raceId: string]: RaceDto };
-
- @ApiProperty({ type: () => DriverDto })
- @ValidateNested()
- @Type(() => DriverDto)
- driversById: { [driverId: string]: DriverDto };
-}
-
-export class GetLeagueSeasonsQuery {
- @ApiProperty()
- @IsString()
- leagueId: string;
-}
-
-export class LeagueSeasonSummaryViewModel {
- @ApiProperty()
- @IsString()
- seasonId: string;
-
- @ApiProperty()
- @IsString()
- name: string;
-
- @ApiProperty()
- @IsString()
- status: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- @Type(() => Date)
- startDate?: Date;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- @Type(() => Date)
- endDate?: Date;
-
- @ApiProperty()
- @IsBoolean()
- isPrimary: boolean;
-
- @ApiProperty()
- @IsBoolean()
- isParallelActive: boolean;
-}
-
-export class LeagueAdminViewModel {
- @ApiProperty({ type: [LeagueJoinRequestViewModel] })
- @IsArray()
- @ValidateNested({ each: true })
- @Type(() => LeagueJoinRequestViewModel)
- joinRequests: LeagueJoinRequestViewModel[];
-
- @ApiProperty({ type: () => LeagueOwnerSummaryViewModel, nullable: true })
- @IsOptional()
- @ValidateNested()
- @Type(() => LeagueOwnerSummaryViewModel)
- ownerSummary: LeagueOwnerSummaryViewModel | null;
-
- @ApiProperty({ type: () => LeagueAdminConfigViewModel })
- @ValidateNested()
- @Type(() => LeagueAdminConfigViewModel)
- config: LeagueAdminConfigViewModel;
-
- @ApiProperty({ type: () => LeagueAdminProtestsViewModel })
- @ValidateNested()
- @Type(() => LeagueAdminProtestsViewModel)
- protests: LeagueAdminProtestsViewModel;
-
- @ApiProperty({ type: [LeagueSeasonSummaryViewModel] })
- @IsArray()
- @ValidateNested({ each: true })
- @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;
-}
diff --git a/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityAndScoringDTO.ts b/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityAndScoringDTO.ts
new file mode 100644
index 000000000..6a95b5120
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityAndScoringDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueSummaryDTO } from './LeagueSummaryDTO';
+
+export class AllLeaguesWithCapacityAndScoringDTO {
+ @ApiProperty({ type: [LeagueSummaryDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => LeagueSummaryDTO)
+ leagues: LeagueSummaryDTO[];
+
+ @ApiProperty()
+ @IsNumber()
+ totalCount: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts b/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts
new file mode 100644
index 000000000..9d9b9a64a
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/AllLeaguesWithCapacityDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueWithCapacityDTO } from './LeagueWithCapacityDTO';
+
+export class AllLeaguesWithCapacityDTO {
+ @ApiProperty({ type: [LeagueWithCapacityDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => LeagueWithCapacityDTO)
+ leagues: LeagueWithCapacityDTO[];
+
+ @ApiProperty()
+ @IsNumber()
+ totalCount: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/ApproveJoinRequestInputDTO.ts b/apps/api/src/domain/league/dtos/ApproveJoinRequestInputDTO.ts
new file mode 100644
index 000000000..5249b7b67
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/ApproveJoinRequestInputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class ApproveJoinRequestInputDTO {
+ @ApiProperty()
+ @IsString()
+ requestId: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/ApproveJoinRequestOutputDTO.ts b/apps/api/src/domain/league/dtos/ApproveJoinRequestOutputDTO.ts
new file mode 100644
index 000000000..b29262b26
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/ApproveJoinRequestOutputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean } from 'class-validator';
+
+export class ApproveJoinRequestOutputDTO {
+ @ApiProperty()
+ @IsBoolean()
+ success: boolean;
+
+ @ApiProperty({ required: false })
+ @IsString()
+ message?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/CreateLeagueInputDTO.ts b/apps/api/src/domain/league/dtos/CreateLeagueInputDTO.ts
new file mode 100644
index 000000000..2795b7d34
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/CreateLeagueInputDTO.ts
@@ -0,0 +1,20 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum } from 'class-validator';
+
+export class CreateLeagueInputDTO {
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ @ApiProperty()
+ @IsString()
+ description: string;
+
+ @ApiProperty({ enum: ['public', 'private'] })
+ @IsEnum(['public', 'private'])
+ visibility: 'public' | 'private';
+
+ @ApiProperty()
+ @IsString()
+ ownerId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/CreateLeagueOutputDTO.ts b/apps/api/src/domain/league/dtos/CreateLeagueOutputDTO.ts
new file mode 100644
index 000000000..914e92293
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/CreateLeagueOutputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean } from 'class-validator';
+
+export class CreateLeagueOutputDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty()
+ @IsBoolean()
+ success: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueAdminConfigOutputDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueAdminConfigOutputDTO.ts
new file mode 100644
index 000000000..22a2bad70
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueAdminConfigOutputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsOptional, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueConfigFormModelDTO } from './LeagueConfigFormModelDTO';
+
+export class GetLeagueAdminConfigOutputDTO {
+ @ApiProperty({ type: () => LeagueConfigFormModelDTO, nullable: true })
+ @IsOptional()
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelDTO)
+ form: LeagueConfigFormModelDTO | null;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueAdminConfigQueryDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueAdminConfigQueryDTO.ts
new file mode 100644
index 000000000..a8aadccba
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueAdminConfigQueryDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetLeagueAdminConfigQueryDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueAdminPermissionsInputDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueAdminPermissionsInputDTO.ts
new file mode 100644
index 000000000..1927fd508
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueAdminPermissionsInputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetLeagueAdminPermissionsInputDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty()
+ @IsString()
+ performerDriverId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueJoinRequestsQueryDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueJoinRequestsQueryDTO.ts
new file mode 100644
index 000000000..ae2357a15
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueJoinRequestsQueryDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetLeagueJoinRequestsQueryDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueOwnerSummaryQueryDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueOwnerSummaryQueryDTO.ts
new file mode 100644
index 000000000..0c31ae3ff
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueOwnerSummaryQueryDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetLeagueOwnerSummaryQueryDTO {
+ @ApiProperty()
+ @IsString()
+ ownerId: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueProtestsQueryDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueProtestsQueryDTO.ts
new file mode 100644
index 000000000..2ef063d4e
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueProtestsQueryDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetLeagueProtestsQueryDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/GetLeagueSeasonsQueryDTO.ts b/apps/api/src/domain/league/dtos/GetLeagueSeasonsQueryDTO.ts
new file mode 100644
index 000000000..c352e1a14
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/GetLeagueSeasonsQueryDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetLeagueSeasonsQueryDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueAdminConfigDTO.ts b/apps/api/src/domain/league/dtos/LeagueAdminConfigDTO.ts
new file mode 100644
index 000000000..9832a0b8c
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueAdminConfigDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsOptional, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueConfigFormModelDTO } from './LeagueConfigFormModelDTO';
+
+export class LeagueAdminConfigDTO {
+ @ApiProperty({ type: () => LeagueConfigFormModelDTO, nullable: true })
+ @IsOptional()
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelDTO)
+ form: LeagueConfigFormModelDTO | null;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueAdminDTO.ts b/apps/api/src/domain/league/dtos/LeagueAdminDTO.ts
new file mode 100644
index 000000000..e9811d442
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueAdminDTO.ts
@@ -0,0 +1,38 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsOptional, IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueJoinRequestDTO } from './LeagueJoinRequestDTO';
+import { LeagueOwnerSummaryDTO } from './LeagueOwnerSummaryDTO';
+import { LeagueAdminConfigDTO } from './LeagueAdminConfigDTO';
+import { LeagueAdminProtestsDTO } from './LeagueAdminProtestsDTO';
+import { LeagueSeasonSummaryDTO } from './LeagueSeasonSummaryDTO';
+
+export class LeagueAdminDTO {
+ @ApiProperty({ type: [LeagueJoinRequestDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => LeagueJoinRequestDTO)
+ joinRequests: LeagueJoinRequestDTO[];
+
+ @ApiProperty({ type: () => LeagueOwnerSummaryDTO, nullable: true })
+ @IsOptional()
+ @ValidateNested()
+ @Type(() => LeagueOwnerSummaryDTO)
+ ownerSummary: LeagueOwnerSummaryDTO | null;
+
+ @ApiProperty({ type: () => LeagueAdminConfigDTO })
+ @ValidateNested()
+ @Type(() => LeagueAdminConfigDTO)
+ config: LeagueAdminConfigDTO;
+
+ @ApiProperty({ type: () => LeagueAdminProtestsDTO })
+ @ValidateNested()
+ @Type(() => LeagueAdminProtestsDTO)
+ protests: LeagueAdminProtestsDTO;
+
+ @ApiProperty({ type: [LeagueSeasonSummaryDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => LeagueSeasonSummaryDTO)
+ seasons: LeagueSeasonSummaryDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueAdminPermissionsDTO.ts b/apps/api/src/domain/league/dtos/LeagueAdminPermissionsDTO.ts
new file mode 100644
index 000000000..1bc0c5c87
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueAdminPermissionsDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean } from 'class-validator';
+
+export class LeagueAdminPermissionsDTO {
+ @ApiProperty()
+ @IsBoolean()
+ canRemoveMember: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ canUpdateRoles: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueAdminProtestsDTO.ts b/apps/api/src/domain/league/dtos/LeagueAdminProtestsDTO.ts
new file mode 100644
index 000000000..cb2f766f9
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueAdminProtestsDTO.ts
@@ -0,0 +1,24 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { DriverDto } from '../../driver/dto/DriverDto';
+import { RaceDto } from '../../race/dto/RaceDto';
+import { ProtestDTO } from './ProtestDTO';
+
+export class LeagueAdminProtestsDTO {
+ @ApiProperty({ type: [ProtestDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => ProtestDTO)
+ protests: ProtestDTO[];
+
+ @ApiProperty({ type: () => RaceDto })
+ @ValidateNested()
+ @Type(() => RaceDto)
+ racesById: { [raceId: string]: RaceDto };
+
+ @ApiProperty({ type: () => DriverDto })
+ @ValidateNested()
+ @Type(() => DriverDto)
+ driversById: { [driverId: string]: DriverDto };
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelBasicsDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelBasicsDTO.ts
new file mode 100644
index 000000000..1bacccbb5
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelBasicsDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum } from 'class-validator';
+
+export class LeagueConfigFormModelBasicsDTO {
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ @ApiProperty()
+ @IsString()
+ description: string;
+
+ @ApiProperty({ enum: ['public', 'private'] })
+ @IsEnum(['public', 'private'])
+ visibility: 'public' | 'private';
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelDTO.ts
new file mode 100644
index 000000000..c7e72d21a
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelDTO.ts
@@ -0,0 +1,49 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueConfigFormModelBasicsDTO } from './LeagueConfigFormModelBasicsDTO';
+import { LeagueConfigFormModelStructureDTO } from './LeagueConfigFormModelStructureDTO';
+import { LeagueConfigFormModelScoringDTO } from './LeagueConfigFormModelScoringDTO';
+import { LeagueConfigFormModelDropPolicyDTO } from './LeagueConfigFormModelDropPolicyDTO';
+import { LeagueConfigFormModelStewardingDTO } from './LeagueConfigFormModelStewardingDTO';
+import { LeagueConfigFormModelTimingsDTO } from './LeagueConfigFormModelTimingsDTO';
+
+export class LeagueConfigFormModelDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty({ type: LeagueConfigFormModelBasicsDTO })
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelBasicsDTO)
+ basics: LeagueConfigFormModelBasicsDTO;
+
+ @ApiProperty({ type: LeagueConfigFormModelStructureDTO })
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelStructureDTO)
+ structure: LeagueConfigFormModelStructureDTO;
+
+ @ApiProperty({ type: [Object] })
+ @IsArray()
+ championships: any[];
+
+ @ApiProperty({ type: LeagueConfigFormModelScoringDTO })
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelScoringDTO)
+ scoring: LeagueConfigFormModelScoringDTO;
+
+ @ApiProperty({ type: LeagueConfigFormModelDropPolicyDTO })
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelDropPolicyDTO)
+ dropPolicy: LeagueConfigFormModelDropPolicyDTO;
+
+ @ApiProperty({ type: LeagueConfigFormModelTimingsDTO })
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelTimingsDTO)
+ timings: LeagueConfigFormModelTimingsDTO;
+
+ @ApiProperty({ type: LeagueConfigFormModelStewardingDTO })
+ @ValidateNested()
+ @Type(() => LeagueConfigFormModelStewardingDTO)
+ stewarding: LeagueConfigFormModelStewardingDTO;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelDropPolicyDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelDropPolicyDTO.ts
new file mode 100644
index 000000000..a91948475
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelDropPolicyDTO.ts
@@ -0,0 +1,13 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsOptional, IsEnum } from 'class-validator';
+
+export class LeagueConfigFormModelDropPolicyDTO {
+ @ApiProperty({ enum: ['none', 'worst_n'] })
+ @IsEnum(['none', 'worst_n'])
+ strategy: 'none' | 'worst_n';
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsNumber()
+ n?: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelScoringDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelScoringDTO.ts
new file mode 100644
index 000000000..911fc763c
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelScoringDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class LeagueConfigFormModelScoringDTO {
+ @ApiProperty()
+ @IsString()
+ type: string;
+
+ @ApiProperty()
+ @IsNumber()
+ points: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelStewardingDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelStewardingDTO.ts
new file mode 100644
index 000000000..d710012a9
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelStewardingDTO.ts
@@ -0,0 +1,41 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsBoolean, IsOptional, IsEnum } from 'class-validator';
+
+export class LeagueConfigFormModelStewardingDTO {
+ @ApiProperty({ enum: ['single_steward', 'committee_vote'] })
+ @IsEnum(['single_steward', 'committee_vote'])
+ decisionMode: 'single_steward' | 'committee_vote';
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsNumber()
+ requiredVotes?: number;
+
+ @ApiProperty()
+ @IsBoolean()
+ requireDefense: boolean;
+
+ @ApiProperty()
+ @IsNumber()
+ defenseTimeLimit: number;
+
+ @ApiProperty()
+ @IsNumber()
+ voteTimeLimit: number;
+
+ @ApiProperty()
+ @IsNumber()
+ protestDeadlineHours: number;
+
+ @ApiProperty()
+ @IsNumber()
+ stewardingClosesHours: number;
+
+ @ApiProperty()
+ @IsBoolean()
+ notifyAccusedOnProtest: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ notifyOnVoteRequired: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelStructureDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelStructureDTO.ts
new file mode 100644
index 000000000..03025442c
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelStructureDTO.ts
@@ -0,0 +1,9 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum } from 'class-validator';
+
+export class LeagueConfigFormModelStructureDTO {
+ @ApiProperty()
+ @IsString()
+ @IsEnum(['solo', 'team'])
+ mode: 'solo' | 'team';
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueConfigFormModelTimingsDTO.ts b/apps/api/src/domain/league/dtos/LeagueConfigFormModelTimingsDTO.ts
new file mode 100644
index 000000000..e3e06ef70
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueConfigFormModelTimingsDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class LeagueConfigFormModelTimingsDTO {
+ @ApiProperty()
+ @IsString()
+ raceDayOfWeek: string;
+
+ @ApiProperty()
+ @IsNumber()
+ raceTimeHour: number;
+
+ @ApiProperty()
+ @IsNumber()
+ raceTimeMinute: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueJoinRequestDTO.ts b/apps/api/src/domain/league/dtos/LeagueJoinRequestDTO.ts
new file mode 100644
index 000000000..46b97af06
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueJoinRequestDTO.ts
@@ -0,0 +1,34 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsDate, IsOptional, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { DriverDto } from '../../driver/dto/DriverDto';
+
+export class LeagueJoinRequestDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty()
+ @IsString()
+ driverId: string;
+
+ @ApiProperty()
+ @IsDate()
+ @Type(() => Date)
+ requestedAt: Date;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ message?: string;
+
+ @ApiProperty({ type: () => DriverDto, required: false })
+ @IsOptional()
+ @ValidateNested()
+ @Type(() => DriverDto)
+ driver?: DriverDto;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueMemberDTO.ts b/apps/api/src/domain/league/dtos/LeagueMemberDTO.ts
new file mode 100644
index 000000000..57421047a
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueMemberDTO.ts
@@ -0,0 +1,24 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsDate, IsEnum, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { DriverDto } from '../../driver/dto/DriverDto';
+
+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;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueMembershipsDTO.ts b/apps/api/src/domain/league/dtos/LeagueMembershipsDTO.ts
new file mode 100644
index 000000000..090ea7e34
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueMembershipsDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueMemberDTO } from './LeagueMemberDTO';
+
+export class LeagueMembershipsDTO {
+ @ApiProperty({ type: [LeagueMemberDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => LeagueMemberDTO)
+ members: LeagueMemberDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueOwnerSummaryDTO.ts b/apps/api/src/domain/league/dtos/LeagueOwnerSummaryDTO.ts
new file mode 100644
index 000000000..62ad5a8e2
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueOwnerSummaryDTO.ts
@@ -0,0 +1,21 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsOptional, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { DriverDto } from '../../driver/dto/DriverDto';
+
+export class LeagueOwnerSummaryDTO {
+ @ApiProperty({ type: () => DriverDto })
+ @ValidateNested()
+ @Type(() => DriverDto)
+ driver: DriverDto;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsNumber()
+ rating: number | null;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsNumber()
+ rank: number | null;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueScheduleDTO.ts b/apps/api/src/domain/league/dtos/LeagueScheduleDTO.ts
new file mode 100644
index 000000000..07a420a12
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueScheduleDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { RaceDto } from '../../race/dto/RaceDto';
+
+export class LeagueScheduleDTO {
+ @ApiProperty({ type: [RaceDto] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => RaceDto)
+ races: RaceDto[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueSeasonSummaryDTO.ts b/apps/api/src/domain/league/dtos/LeagueSeasonSummaryDTO.ts
new file mode 100644
index 000000000..fb91dd8e0
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueSeasonSummaryDTO.ts
@@ -0,0 +1,37 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean, IsDate, IsOptional } from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class LeagueSeasonSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ seasonId: string;
+
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ @ApiProperty()
+ @IsString()
+ status: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ @Type(() => Date)
+ startDate?: Date;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ @Type(() => Date)
+ endDate?: Date;
+
+ @ApiProperty()
+ @IsBoolean()
+ isPrimary: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ isParallelActive: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueSettingsDTO.ts b/apps/api/src/domain/league/dtos/LeagueSettingsDTO.ts
new file mode 100644
index 000000000..95df9b514
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueSettingsDTO.ts
@@ -0,0 +1,10 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsOptional } from 'class-validator';
+
+export class LeagueSettingsDTO {
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsNumber()
+ maxDrivers?: number;
+ // Add other league settings properties as needed
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts b/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts
new file mode 100644
index 000000000..2045b4501
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts
@@ -0,0 +1,23 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { DriverDto } from '../../driver/dto/DriverDto';
+
+export class LeagueStandingDTO {
+ @ApiProperty()
+ @IsString()
+ driverId: string;
+
+ @ApiProperty({ type: () => DriverDto })
+ @ValidateNested()
+ @Type(() => DriverDto)
+ driver: DriverDto;
+
+ @ApiProperty()
+ @IsNumber()
+ points: number;
+
+ @ApiProperty()
+ @IsNumber()
+ rank: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueStandingsDTO.ts b/apps/api/src/domain/league/dtos/LeagueStandingsDTO.ts
new file mode 100644
index 000000000..9d8700316
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueStandingsDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueStandingDTO } from './LeagueStandingDTO';
+
+export class LeagueStandingsDTO {
+ @ApiProperty({ type: [LeagueStandingDTO] })
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => LeagueStandingDTO)
+ standings: LeagueStandingDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueStatsDTO.ts b/apps/api/src/domain/league/dtos/LeagueStatsDTO.ts
new file mode 100644
index 000000000..66489b843
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueStatsDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber } from 'class-validator';
+
+export class LeagueStatsDTO {
+ @ApiProperty()
+ @IsNumber()
+ totalMembers: number;
+
+ @ApiProperty()
+ @IsNumber()
+ totalRaces: number;
+
+ @ApiProperty()
+ @IsNumber()
+ averageRating: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueSummaryDTO.ts b/apps/api/src/domain/league/dtos/LeagueSummaryDTO.ts
new file mode 100644
index 000000000..55e5fccb2
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueSummaryDTO.ts
@@ -0,0 +1,58 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber, IsBoolean, IsOptional } from 'class-validator';
+
+export class LeagueSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ description?: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ logoUrl?: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ coverImage?: string;
+
+ @ApiProperty()
+ @IsNumber()
+ memberCount: number;
+
+ @ApiProperty()
+ @IsNumber()
+ maxMembers: number;
+
+ @ApiProperty()
+ @IsBoolean()
+ isPublic: boolean;
+
+ @ApiProperty()
+ @IsString()
+ ownerId: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ ownerName?: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ scoringType?: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ status?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/LeagueWithCapacityDTO.ts b/apps/api/src/domain/league/dtos/LeagueWithCapacityDTO.ts
new file mode 100644
index 000000000..bf95af5b2
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/LeagueWithCapacityDTO.ts
@@ -0,0 +1,45 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber, IsOptional, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+import { LeagueSettingsDTO } from './LeagueSettingsDTO';
+
+export class LeagueWithCapacityDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ // ... other properties of LeagueWithCapacityDTO
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsString()
+ description?: string;
+
+ @ApiProperty()
+ @IsString()
+ ownerId: string;
+
+ @ApiProperty({ type: () => LeagueSettingsDTO })
+ @ValidateNested()
+ @Type(() => LeagueSettingsDTO)
+ settings: LeagueSettingsDTO;
+
+ @ApiProperty()
+ @IsString()
+ createdAt: string;
+
+ @ApiProperty()
+ @IsNumber()
+ usedSlots: number;
+
+ @ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
+ @IsOptional()
+ socialLinks?: {
+ discordUrl?: string;
+ youtubeUrl?: string;
+ websiteUrl?: string;
+ };
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/ProtestDTO.ts b/apps/api/src/domain/league/dtos/ProtestDTO.ts
new file mode 100644
index 000000000..76fc53d8a
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/ProtestDTO.ts
@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsDate, IsEnum } from 'class-validator';
+import { Type } from 'class-transformer';
+
+// TODO: protests are filed at race level but also managed on league level
+
+export class ProtestDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ raceId: string;
+
+ @ApiProperty()
+ @IsString()
+ protestingDriverId: string;
+
+ @ApiProperty()
+ @IsString()
+ accusedDriverId: string;
+
+ @ApiProperty()
+ @IsDate()
+ @Type(() => Date)
+ submittedAt: Date;
+
+ @ApiProperty()
+ @IsString()
+ description: string;
+
+ @ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
+ @IsEnum(['pending', 'accepted', 'rejected'])
+ status: 'pending' | 'accepted' | 'rejected';
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/RejectJoinRequestInputDTO.ts b/apps/api/src/domain/league/dtos/RejectJoinRequestInputDTO.ts
new file mode 100644
index 000000000..4110afb74
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/RejectJoinRequestInputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class RejectJoinRequestInputDTO {
+ @ApiProperty()
+ @IsString()
+ requestId: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/RejectJoinRequestOutputDTO.ts b/apps/api/src/domain/league/dtos/RejectJoinRequestOutputDTO.ts
new file mode 100644
index 000000000..c873dc91a
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/RejectJoinRequestOutputDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean } from 'class-validator';
+
+export class RejectJoinRequestOutputDTO {
+ @ApiProperty()
+ @IsBoolean()
+ success: boolean;
+
+ @ApiProperty({ required: false })
+ @IsString()
+ message?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/RemoveLeagueMemberInputDTO.ts b/apps/api/src/domain/league/dtos/RemoveLeagueMemberInputDTO.ts
new file mode 100644
index 000000000..b1350b40a
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/RemoveLeagueMemberInputDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class RemoveLeagueMemberInputDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty()
+ @IsString()
+ performerDriverId: string;
+
+ @ApiProperty()
+ @IsString()
+ targetDriverId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/RemoveLeagueMemberOutputDTO.ts b/apps/api/src/domain/league/dtos/RemoveLeagueMemberOutputDTO.ts
new file mode 100644
index 000000000..c9cca5bee
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/RemoveLeagueMemberOutputDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean } from 'class-validator';
+
+export class RemoveLeagueMemberOutputDTO {
+ @ApiProperty()
+ @IsBoolean()
+ success: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/SeasonDTO.ts b/apps/api/src/domain/league/dtos/SeasonDTO.ts
new file mode 100644
index 000000000..ab0d7fe96
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/SeasonDTO.ts
@@ -0,0 +1,42 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean, IsDate, IsOptional, IsEnum } from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class SeasonDTO {
+ @ApiProperty()
+ @IsString()
+ seasonId: string;
+
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ @Type(() => Date)
+ startDate?: Date;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ @Type(() => Date)
+ endDate?: Date;
+
+ @ApiProperty({ enum: ['planned', 'active', 'completed'] })
+ @IsEnum(['planned', 'active', 'completed'])
+ status: 'planned' | 'active' | 'completed';
+
+ @ApiProperty()
+ @IsBoolean()
+ isPrimary: boolean;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ seasonGroupId?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/UpdateLeagueMemberRoleInputDTO.ts b/apps/api/src/domain/league/dtos/UpdateLeagueMemberRoleInputDTO.ts
new file mode 100644
index 000000000..2b55ef26e
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/UpdateLeagueMemberRoleInputDTO.ts
@@ -0,0 +1,20 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum } from 'class-validator';
+
+export class UpdateLeagueMemberRoleInputDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty()
+ @IsString()
+ performerDriverId: string;
+
+ @ApiProperty()
+ @IsString()
+ targetDriverId: string;
+
+ @ApiProperty({ enum: ['owner', 'manager', 'member'] })
+ @IsEnum(['owner', 'manager', 'member'])
+ newRole: 'owner' | 'manager' | 'member';
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/dtos/UpdateLeagueMemberRoleOutputDTO.ts b/apps/api/src/domain/league/dtos/UpdateLeagueMemberRoleOutputDTO.ts
new file mode 100644
index 000000000..d2e34ba07
--- /dev/null
+++ b/apps/api/src/domain/league/dtos/UpdateLeagueMemberRoleOutputDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean } from 'class-validator';
+
+export class UpdateLeagueMemberRoleOutputDTO {
+ @ApiProperty()
+ @IsBoolean()
+ success: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts b/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts
index 0d1573ade..de4f6747f 100644
--- a/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts
+++ b/apps/api/src/domain/league/presenters/GetLeagueMembershipsPresenter.ts
@@ -1,5 +1,5 @@
import { IGetLeagueMembershipsPresenter, GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter';
-import { LeagueMembershipsViewModel } from '../dto/LeagueDto';
+import { LeagueMembershipsDTO } from '../dtos/LeagueMembershipsDTO';
export class GetLeagueMembershipsPresenter implements IGetLeagueMembershipsPresenter {
private result: GetLeagueMembershipsViewModel | null = null;
@@ -40,7 +40,7 @@ export class GetLeagueMembershipsPresenter implements IGetLeagueMembershipsPrese
}
// API-specific method
- get apiViewModel(): LeagueMembershipsViewModel | null {
+ get apiViewModel(): LeagueMembershipsDTO | null {
if (!this.result?.memberships) return null;
return this.result.memberships as LeagueMembershipsViewModel;
}
diff --git a/apps/api/src/domain/league/presenters/LeagueAdminPresenter.ts b/apps/api/src/domain/league/presenters/LeagueAdminPresenter.ts
index 9b0b6c4fa..afd52a0e6 100644
--- a/apps/api/src/domain/league/presenters/LeagueAdminPresenter.ts
+++ b/apps/api/src/domain/league/presenters/LeagueAdminPresenter.ts
@@ -1,7 +1,7 @@
-import { LeagueAdminViewModel } from '../dto/LeagueDto';
+import { LeagueAdminDTO } from '../dtos/LeagueAdminDTO';
export class LeagueAdminPresenter {
- private result: LeagueAdminViewModel | null = null;
+ private result: LeagueAdminDTO | null = null;
reset() {
this.result = null;
@@ -23,7 +23,7 @@ export class LeagueAdminPresenter {
};
}
- getViewModel(): LeagueAdminViewModel {
+ getViewModel(): LeagueAdminDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
diff --git a/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts b/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts
index 207b1aa4f..19fa7f4e0 100644
--- a/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts
+++ b/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts
@@ -1,5 +1,5 @@
import { ILeagueFullConfigPresenter, LeagueFullConfigData, LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter';
-import { LeagueConfigFormModelDto } from '../dto/LeagueDto';
+import { LeagueConfigFormModelDTO } from '../dtos/LeagueConfigFormModelDTO';
export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
private result: LeagueConfigFormViewModel | null = null;
@@ -65,7 +65,7 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
}
// API-specific method to get the DTO
- get viewModel(): LeagueConfigFormModelDto | null {
+ get viewModel(): LeagueConfigFormModelDTO | null {
if (!this.result) return null;
// Map from LeagueConfigFormViewModel to LeagueConfigFormModelDto
diff --git a/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts b/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts
new file mode 100644
index 000000000..b2bbb9a01
--- /dev/null
+++ b/apps/api/src/domain/league/presenters/LeagueScoringConfigPresenter.ts
@@ -0,0 +1,150 @@
+import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
+import type { BonusRule } from '@core/racing/domain/types/BonusRule';
+import type {
+ ILeagueScoringConfigPresenter,
+ LeagueScoringConfigData,
+ LeagueScoringConfigViewModel,
+ LeagueScoringChampionshipViewModel,
+} from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
+
+export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter {
+ private viewModel: LeagueScoringConfigViewModel | null = null;
+
+ reset(): void {
+ this.viewModel = null;
+ }
+
+ present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
+ const championships: LeagueScoringChampionshipViewModel[] =
+ data.championships.map((champ) => this.mapChampionship(champ));
+
+ const dropPolicySummary =
+ data.preset?.dropPolicySummary ??
+ this.deriveDropPolicyDescriptionFromChampionships(data.championships);
+
+ this.viewModel = {
+ leagueId: data.leagueId,
+ seasonId: data.seasonId,
+ gameId: data.gameId,
+ gameName: data.gameName,
+ scoringPresetId: data.scoringPresetId ?? 'custom',
+ scoringPresetName: data.preset?.name ?? 'Custom',
+ dropPolicySummary,
+ championships,
+ };
+
+ return this.viewModel;
+ }
+
+ getViewModel(): LeagueScoringConfigViewModel | null {
+ return this.viewModel;
+ }
+
+ private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel {
+ const sessionTypes = championship.sessionTypes.map((s) => s.toString());
+ const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType);
+ const bonusSummary = this.buildBonusSummary(
+ championship.bonusRulesBySessionType ?? {},
+ );
+ const dropPolicyDescription = this.deriveDropPolicyDescription(
+ championship.dropScorePolicy,
+ );
+
+ return {
+ id: championship.id,
+ name: championship.name,
+ type: championship.type,
+ sessionTypes,
+ pointsPreview,
+ bonusSummary,
+ dropPolicyDescription,
+ };
+ }
+
+ private buildPointsPreview(
+ tables: Record number }>,
+ ): Array<{ sessionType: string; position: number; points: number }> {
+ const preview: Array<{
+ sessionType: string;
+ position: number;
+ points: number;
+ }> = [];
+
+ const maxPositions = 10;
+
+ for (const [sessionType, table] of Object.entries(tables)) {
+ for (let pos = 1; pos <= maxPositions; pos++) {
+ const points = table.getPointsForPosition(pos);
+ if (points && points !== 0) {
+ preview.push({
+ sessionType,
+ position: pos,
+ points,
+ });
+ }
+ }
+ }
+
+ return preview;
+ }
+
+ private buildBonusSummary(
+ bonusRulesBySessionType: Record,
+ ): string[] {
+ const summaries: string[] = [];
+
+ for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) {
+ for (const rule of rules) {
+ if (rule.type === 'fastestLap') {
+ const base = `Fastest lap in ${sessionType}`;
+ if (rule.requiresFinishInTopN) {
+ summaries.push(
+ `${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`,
+ );
+ } else {
+ summaries.push(`${base} +${rule.points} points`);
+ }
+ } else {
+ summaries.push(
+ `${rule.type} bonus in ${sessionType} worth ${rule.points} points`,
+ );
+ }
+ }
+ }
+
+ return summaries;
+ }
+
+ private deriveDropPolicyDescriptionFromChampionships(
+ championships: ChampionshipConfig[],
+ ): string {
+ const first = championships[0];
+ if (!first) {
+ return 'All results count';
+ }
+ return this.deriveDropPolicyDescription(first.dropScorePolicy);
+ }
+
+ private deriveDropPolicyDescription(policy: {
+ strategy: string;
+ count?: number;
+ dropCount?: number;
+ }): string {
+ if (!policy || policy.strategy === 'none') {
+ return 'All results count';
+ }
+
+ if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
+ return `Best ${policy.count} results count towards the championship`;
+ }
+
+ if (
+ policy.strategy === 'dropWorstN' &&
+ typeof policy.dropCount === 'number'
+ ) {
+ return `Worst ${policy.dropCount} results are dropped from the championship total`;
+ }
+
+ return 'Custom drop score rules apply';
+ }
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts b/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts
new file mode 100644
index 000000000..95893504c
--- /dev/null
+++ b/apps/api/src/domain/league/presenters/LeagueScoringPresetsPresenter.ts
@@ -0,0 +1,23 @@
+import type {
+ ILeagueScoringPresetsPresenter,
+ LeagueScoringPresetsResultDTO,
+ LeagueScoringPresetsViewModel,
+} from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
+
+export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter {
+ private viewModel: LeagueScoringPresetsViewModel | null = null;
+
+ reset(): void {
+ this.viewModel = null;
+ }
+
+ present(dto: LeagueScoringPresetsResultDTO): void {
+ this.viewModel = {
+ presets: dto.presets,
+ };
+ }
+
+ getViewModel(): LeagueScoringPresetsViewModel | null {
+ return this.viewModel;
+ }
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts b/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts
index 6e831ad7b..3b9d5acc7 100644
--- a/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts
+++ b/apps/api/src/domain/league/presenters/TotalLeaguesPresenter.ts
@@ -1,5 +1,5 @@
import { IGetTotalLeaguesPresenter, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter';
-import { LeagueStatsDto } from '../dto/LeagueDto';
+import { LeagueStatsDTO } from '../dtos/LeagueStatsDTO';
export class TotalLeaguesPresenter implements IGetTotalLeaguesPresenter {
private result: LeagueStatsDto | null = null;
diff --git a/apps/api/src/domain/media/MediaController.ts b/apps/api/src/domain/media/MediaController.ts
index 3378cc6f5..372476859 100644
--- a/apps/api/src/domain/media/MediaController.ts
+++ b/apps/api/src/domain/media/MediaController.ts
@@ -2,7 +2,11 @@ import { Controller, Post, Body, HttpStatus, Res } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { Response } from 'express';
import { MediaService } from './MediaService';
-import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
+import type { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
+import type { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO';
+
+type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
+type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
@ApiTags('media')
@Controller('media')
diff --git a/apps/api/src/domain/media/MediaService.ts b/apps/api/src/domain/media/MediaService.ts
index 512d5dc74..013b14d5e 100644
--- a/apps/api/src/domain/media/MediaService.ts
+++ b/apps/api/src/domain/media/MediaService.ts
@@ -1,5 +1,9 @@
import { Injectable, Inject } from '@nestjs/common';
-import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto';
+import type { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
+import type { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO';
+
+type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
+type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
// Use cases
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
diff --git a/apps/api/src/domain/media/dto/MediaDto.ts b/apps/api/src/domain/media/dto/MediaDto.ts
deleted file mode 100644
index 9b1f79456..000000000
--- a/apps/api/src/domain/media/dto/MediaDto.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString, IsNotEmpty, IsBoolean } from 'class-validator';
-
-export class RequestAvatarGenerationInput {
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- userId: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- facePhotoData: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- suitColor: string;
-}
-
-export class RequestAvatarGenerationOutput {
- @ApiProperty({ type: Boolean })
- @IsBoolean()
- success: boolean;
-
- @ApiProperty({ required: false })
- @IsString()
- requestId?: string;
-
- @ApiProperty({ type: [String], required: false })
- avatarUrls?: string[];
-
- @ApiProperty({ required: false })
- @IsString()
- errorMessage?: string;
-}
-
-// Assuming FacePhotoData and SuitColor are simple string types for DTO purposes
-export type FacePhotoData = string;
-export type SuitColor = string;
diff --git a/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts b/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts
new file mode 100644
index 000000000..9a7f7e92c
--- /dev/null
+++ b/apps/api/src/domain/media/dtos/RequestAvatarGenerationInputDTO.ts
@@ -0,0 +1,19 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class RequestAvatarGenerationInputDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ userId: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ facePhotoData: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ suitColor: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts b/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts
new file mode 100644
index 000000000..9f65917e3
--- /dev/null
+++ b/apps/api/src/domain/media/dtos/RequestAvatarGenerationOutputDTO.ts
@@ -0,0 +1,19 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsString } from 'class-validator';
+
+export class RequestAvatarGenerationOutputDTO {
+ @ApiProperty({ type: Boolean })
+ @IsBoolean()
+ success: boolean;
+
+ @ApiProperty({ required: false })
+ @IsString()
+ requestId?: string;
+
+ @ApiProperty({ type: [String], required: false })
+ avatarUrls?: string[];
+
+ @ApiProperty({ required: false })
+ @IsString()
+ errorMessage?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/media/types/FacePhotoData.ts b/apps/api/src/domain/media/types/FacePhotoData.ts
new file mode 100644
index 000000000..26bbde5c1
--- /dev/null
+++ b/apps/api/src/domain/media/types/FacePhotoData.ts
@@ -0,0 +1,2 @@
+// Assuming FacePhotoData is a simple string type for DTO purposes
+export type FacePhotoData = string;
\ No newline at end of file
diff --git a/apps/api/src/domain/media/types/SuitColor.ts b/apps/api/src/domain/media/types/SuitColor.ts
new file mode 100644
index 000000000..aa65f12a3
--- /dev/null
+++ b/apps/api/src/domain/media/types/SuitColor.ts
@@ -0,0 +1,2 @@
+// Assuming SuitColor is a simple string type for DTO purposes
+export type SuitColor = string;
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/CreatePaymentInputDTO.ts b/apps/api/src/domain/payments/dtos/CreatePaymentInputDTO.ts
new file mode 100644
index 000000000..aa1cce7ce
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/CreatePaymentInputDTO.ts
@@ -0,0 +1,31 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsEnum, IsNumber, IsString, IsOptional } from 'class-validator';
+import { PaymentType } from './PaymentType';
+import { PayerType } from './PayerType';
+
+export class CreatePaymentInputDTO {
+ @ApiProperty({ enum: PaymentType })
+ @IsEnum(PaymentType)
+ type: PaymentType;
+
+ @ApiProperty()
+ @IsNumber()
+ amount: number;
+
+ @ApiProperty()
+ @IsString()
+ payerId: string;
+
+ @ApiProperty({ enum: PayerType })
+ @IsEnum(PayerType)
+ payerType: PayerType;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ seasonId?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/CreatePaymentOutputDTO.ts b/apps/api/src/domain/payments/dtos/CreatePaymentOutputDTO.ts
new file mode 100644
index 000000000..a6aa94490
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/CreatePaymentOutputDTO.ts
@@ -0,0 +1,7 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { PaymentDTO } from './PaymentDTO';
+
+export class CreatePaymentOutputDTO {
+ @ApiProperty({ type: PaymentDTO })
+ payment: PaymentDTO;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/MemberPaymentStatus.ts b/apps/api/src/domain/payments/dtos/MemberPaymentStatus.ts
new file mode 100644
index 000000000..88db6cb90
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/MemberPaymentStatus.ts
@@ -0,0 +1,5 @@
+export enum MemberPaymentStatus {
+ PENDING = 'pending',
+ PAID = 'paid',
+ OVERDUE = 'overdue',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/MembershipFeeType.ts b/apps/api/src/domain/payments/dtos/MembershipFeeType.ts
new file mode 100644
index 000000000..1e4a63597
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/MembershipFeeType.ts
@@ -0,0 +1,5 @@
+export enum MembershipFeeType {
+ SEASON = 'season',
+ MONTHLY = 'monthly',
+ PER_RACE = 'per_race',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/PayerType.ts b/apps/api/src/domain/payments/dtos/PayerType.ts
new file mode 100644
index 000000000..9e98c7b4e
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/PayerType.ts
@@ -0,0 +1,4 @@
+export enum PayerType {
+ SPONSOR = 'sponsor',
+ DRIVER = 'driver',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/PaymentDTO.ts b/apps/api/src/domain/payments/dtos/PaymentDTO.ts
new file mode 100644
index 000000000..805fb8b51
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/PaymentDTO.ts
@@ -0,0 +1,57 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber, IsEnum, IsOptional, IsDate } from 'class-validator';
+import { PaymentType } from './PaymentType';
+import { PayerType } from './PayerType';
+import { PaymentStatus } from './PaymentStatus';
+
+export class PaymentDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty({ enum: PaymentType })
+ @IsEnum(PaymentType)
+ type: PaymentType;
+
+ @ApiProperty()
+ @IsNumber()
+ amount: number;
+
+ @ApiProperty()
+ @IsNumber()
+ platformFee: number;
+
+ @ApiProperty()
+ @IsNumber()
+ netAmount: number;
+
+ @ApiProperty()
+ @IsString()
+ payerId: string;
+
+ @ApiProperty({ enum: PayerType })
+ @IsEnum(PayerType)
+ payerType: PayerType;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ seasonId?: string;
+
+ @ApiProperty({ enum: PaymentStatus })
+ @IsEnum(PaymentStatus)
+ status: PaymentStatus;
+
+ @ApiProperty()
+ @IsDate()
+ createdAt: Date;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ completedAt?: Date;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/PaymentStatus.ts b/apps/api/src/domain/payments/dtos/PaymentStatus.ts
new file mode 100644
index 000000000..8ef27933f
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/PaymentStatus.ts
@@ -0,0 +1,6 @@
+export enum PaymentStatus {
+ PENDING = 'pending',
+ COMPLETED = 'completed',
+ FAILED = 'failed',
+ REFUNDED = 'refunded',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/PaymentType.ts b/apps/api/src/domain/payments/dtos/PaymentType.ts
new file mode 100644
index 000000000..afe4d1406
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/PaymentType.ts
@@ -0,0 +1,4 @@
+export enum PaymentType {
+ SPONSORSHIP = 'sponsorship',
+ MEMBERSHIP_FEE = 'membership_fee',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dto/PaymentsDto.ts b/apps/api/src/domain/payments/dtos/PaymentsDto.ts
similarity index 100%
rename from apps/api/src/domain/payments/dto/PaymentsDto.ts
rename to apps/api/src/domain/payments/dtos/PaymentsDto.ts
diff --git a/apps/api/src/domain/payments/dtos/PrizeType.ts b/apps/api/src/domain/payments/dtos/PrizeType.ts
new file mode 100644
index 000000000..13fb68441
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/PrizeType.ts
@@ -0,0 +1,5 @@
+export enum PrizeType {
+ CASH = 'cash',
+ MERCHANDISE = 'merchandise',
+ OTHER = 'other',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/ReferenceType.ts b/apps/api/src/domain/payments/dtos/ReferenceType.ts
new file mode 100644
index 000000000..907d3f60d
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/ReferenceType.ts
@@ -0,0 +1,5 @@
+export enum ReferenceType {
+ SPONSORSHIP = 'sponsorship',
+ MEMBERSHIP_FEE = 'membership_fee',
+ PRIZE = 'prize',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/TransactionType.ts b/apps/api/src/domain/payments/dtos/TransactionType.ts
new file mode 100644
index 000000000..d127b2c08
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/TransactionType.ts
@@ -0,0 +1,5 @@
+export enum TransactionType {
+ DEPOSIT = 'deposit',
+ WITHDRAWAL = 'withdrawal',
+ PLATFORM_FEE = 'platform_fee',
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/UpdatePaymentStatusInputDTO.ts b/apps/api/src/domain/payments/dtos/UpdatePaymentStatusInputDTO.ts
new file mode 100644
index 000000000..9b8af4737
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/UpdatePaymentStatusInputDTO.ts
@@ -0,0 +1,13 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum } from 'class-validator';
+import { PaymentStatus } from './PaymentStatus';
+
+export class UpdatePaymentStatusInputDTO {
+ @ApiProperty()
+ @IsString()
+ paymentId: string;
+
+ @ApiProperty({ enum: PaymentStatus })
+ @IsEnum(PaymentStatus)
+ status: PaymentStatus;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/payments/dtos/UpdatePaymentStatusOutputDTO.ts b/apps/api/src/domain/payments/dtos/UpdatePaymentStatusOutputDTO.ts
new file mode 100644
index 000000000..d0bb63c3a
--- /dev/null
+++ b/apps/api/src/domain/payments/dtos/UpdatePaymentStatusOutputDTO.ts
@@ -0,0 +1,7 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { PaymentDTO } from './PaymentDTO';
+
+export class UpdatePaymentStatusOutputDTO {
+ @ApiProperty({ type: PaymentDTO })
+ payment: PaymentDTO;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/RaceController.ts b/apps/api/src/domain/race/RaceController.ts
index 9db692f7c..3ab9c70de 100644
--- a/apps/api/src/domain/race/RaceController.ts
+++ b/apps/api/src/domain/race/RaceController.ts
@@ -1,7 +1,25 @@
-import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
-import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
+import { Controller, Get, Post, Body, HttpCode, HttpStatus, Param, Query } from '@nestjs/common';
+import { ApiTags, ApiResponse, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
import { RaceService } from './RaceService';
-import { AllRacesPageViewModel, RaceStatsDto } from './dto/RaceDto';
+import { AllRacesPageDTO } from './dtos/AllRacesPageDTO';
+import { RaceStatsDTO } from './dtos/RaceStatsDTO';
+import { RaceDetailDTO } from './dtos/RaceDetailDTO';
+import { RacesPageDataDTO } from './dtos/RacesPageDataDTO';
+import { RaceResultsDetailDTO } from './dtos/RaceResultsDetailDTO';
+import { RaceWithSOFDTO } from './dtos/RaceWithSOFDTO';
+import { RaceProtestsDTO } from './dtos/RaceProtestsDTO';
+import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO';
+import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
+import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
+import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
+import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
+import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
+import { ImportRaceResultsSummaryDTO } from './dtos/ImportRaceResultsSummaryDTO';
+import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
+import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO';
+import { QuickPenaltyCommandDTO } from './dtos/QuickPenaltyCommandDTO';
+import { ApplyPenaltyCommandDTO } from './dtos/ApplyPenaltyCommandDTO';
+import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCommandDTO';
@ApiTags('races')
@Controller('races')
@@ -10,17 +28,167 @@ export class RaceController {
@Get('all')
@ApiOperation({ summary: 'Get all races' })
- @ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageViewModel })
- async getAllRaces(): Promise {
+ @ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageDTO })
+ async getAllRaces(): Promise {
return this.raceService.getAllRaces();
}
@Get('total-races')
@ApiOperation({ summary: 'Get the total number of races' })
- @ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDto })
- async getTotalRaces(): Promise {
+ @ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDTO })
+ async getTotalRaces(): Promise {
return this.raceService.getTotalRaces();
}
- // Add other Race endpoints here based on other presenters
+ @Get('page-data')
+ @ApiOperation({ summary: 'Get races page data' })
+ @ApiResponse({ status: 200, description: 'Races page data', type: RacesPageDataDTO })
+ async getRacesPageData(): Promise {
+ return this.raceService.getRacesPageData();
+ }
+
+ @Get('all/page-data')
+ @ApiOperation({ summary: 'Get all races page data' })
+ @ApiResponse({ status: 200, description: 'All races page data', type: RacesPageDataDTO })
+ async getAllRacesPageData(): Promise {
+ return this.raceService.getAllRacesPageData();
+ }
+
+ @Get(':raceId')
+ @ApiOperation({ summary: 'Get race detail' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiQuery({ name: 'driverId', description: 'Driver ID' })
+ @ApiResponse({ status: 200, description: 'Race detail', type: RaceDetailDTO })
+ async getRaceDetail(
+ @Param('raceId') raceId: string,
+ @Query('driverId') driverId: string,
+ ): Promise {
+ return this.raceService.getRaceDetail({ raceId, driverId });
+ }
+
+ @Get(':raceId/results')
+ @ApiOperation({ summary: 'Get race results detail' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Race results detail', type: RaceResultsDetailDTO })
+ async getRaceResultsDetail(@Param('raceId') raceId: string): Promise {
+ return this.raceService.getRaceResultsDetail(raceId);
+ }
+
+ @Get(':raceId/sof')
+ @ApiOperation({ summary: 'Get race with strength of field' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Race with SOF', type: RaceWithSOFDTO })
+ async getRaceWithSOF(@Param('raceId') raceId: string): Promise {
+ return this.raceService.getRaceWithSOF(raceId);
+ }
+
+ @Get(':raceId/protests')
+ @ApiOperation({ summary: 'Get race protests' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Race protests', type: RaceProtestsDTO })
+ async getRaceProtests(@Param('raceId') raceId: string): Promise {
+ return this.raceService.getRaceProtests(raceId);
+ }
+
+ @Get(':raceId/penalties')
+ @ApiOperation({ summary: 'Get race penalties' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Race penalties', type: RacePenaltiesDTO })
+ async getRacePenalties(@Param('raceId') raceId: string): Promise {
+ return this.raceService.getRacePenalties(raceId);
+ }
+
+ @Post(':raceId/register')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Register for race' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Successfully registered for race' })
+ async registerForRace(
+ @Param('raceId') raceId: string,
+ @Body() body: Omit,
+ ): Promise {
+ return this.raceService.registerForRace({ raceId, ...body });
+ }
+
+ @Post(':raceId/withdraw')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Withdraw from race' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Successfully withdrew from race' })
+ async withdrawFromRace(
+ @Param('raceId') raceId: string,
+ @Body() body: Omit,
+ ): Promise {
+ return this.raceService.withdrawFromRace({ raceId, ...body });
+ }
+
+ @Post(':raceId/cancel')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Cancel race' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Successfully cancelled race' })
+ async cancelRace(@Param('raceId') raceId: string): Promise {
+ return this.raceService.cancelRace({ raceId });
+ }
+
+ @Post(':raceId/complete')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Complete race' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Successfully completed race' })
+ async completeRace(@Param('raceId') raceId: string): Promise {
+ return this.raceService.completeRace({ raceId });
+ }
+
+ @Post(':raceId/import-results')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Import race results' })
+ @ApiParam({ name: 'raceId', description: 'Race ID' })
+ @ApiResponse({ status: 200, description: 'Successfully imported race results', type: ImportRaceResultsSummaryDTO })
+ async importRaceResults(
+ @Param('raceId') raceId: string,
+ @Body() body: Omit,
+ ): Promise {
+ return this.raceService.importRaceResults({ raceId, ...body });
+ }
+
+ @Get('dashboard/overview')
+ @ApiOperation({ summary: 'Get dashboard overview' })
+ @ApiQuery({ name: 'driverId', description: 'Driver ID' })
+ @ApiResponse({ status: 200, description: 'Dashboard overview', type: DashboardOverviewDTO })
+ async getDashboardOverview(@Query('driverId') driverId: string): Promise {
+ return this.raceService.getDashboardOverview(driverId);
+ }
+
+ @Post('protests/file')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'File a protest' })
+ @ApiResponse({ status: 200, description: 'Protest filed successfully' })
+ async fileProtest(@Body() body: FileProtestCommandDTO): Promise {
+ return this.raceService.fileProtest(body);
+ }
+
+ @Post('penalties/quick')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Apply a quick penalty' })
+ @ApiResponse({ status: 200, description: 'Penalty applied successfully' })
+ async applyQuickPenalty(@Body() body: QuickPenaltyCommandDTO): Promise {
+ return this.raceService.applyQuickPenalty(body);
+ }
+
+ @Post('penalties/apply')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Apply a penalty' })
+ @ApiResponse({ status: 200, description: 'Penalty applied successfully' })
+ async applyPenalty(@Body() body: ApplyPenaltyCommandDTO): Promise {
+ return this.raceService.applyPenalty(body);
+ }
+
+ @Post('protests/defense/request')
+ @HttpCode(HttpStatus.OK)
+ @ApiOperation({ summary: 'Request protest defense' })
+ @ApiResponse({ status: 200, description: 'Defense requested successfully' })
+ async requestProtestDefense(@Body() body: RequestProtestDefenseCommandDTO): Promise {
+ return this.raceService.requestProtestDefense(body);
+ }
}
diff --git a/apps/api/src/domain/race/RaceProviders.ts b/apps/api/src/domain/race/RaceProviders.ts
index ba9cc2714..0a7a747ce 100644
--- a/apps/api/src/domain/race/RaceProviders.ts
+++ b/apps/api/src/domain/race/RaceProviders.ts
@@ -5,20 +5,61 @@ import { RaceService } from './RaceService';
import type { Logger } from '@core/shared/application/Logger';
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
+import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
+import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
+import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
+import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
+import { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
+import { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
+import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
+import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Import concrete in-memory implementations
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
+import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
+import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
+import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository';
+import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
+import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
+import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
+import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
+import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
+import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
+import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
+import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
+import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
+import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
+import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
+import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
+import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
+import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
+import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
+import { CompleteRaceUseCase } from '@core/racing/application/use-cases/CompleteRaceUseCase';
+import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
+import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
+import { FileProtestUseCase } from '@core/racing/application/use-cases/FileProtestUseCase';
+import { QuickPenaltyUseCase } from '@core/racing/application/use-cases/QuickPenaltyUseCase';
+import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
+import { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase';
// Define injection tokens
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
+export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
+export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
+export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
+export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
+export const PENALTY_REPOSITORY_TOKEN = 'IPenaltyRepository';
+export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
+export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
+export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const LOGGER_TOKEN = 'Logger';
export const RaceProviders: Provider[] = [
@@ -33,6 +74,46 @@ export const RaceProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryLeagueRepository(logger),
inject: [LOGGER_TOKEN],
},
+ {
+ provide: DRIVER_REPOSITORY_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryRaceRegistrationRepository(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: RESULT_REPOSITORY_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryResultRepository(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryLeagueMembershipRepository(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: PENALTY_REPOSITORY_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryPenaltyRepository(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: PROTEST_REPOSITORY_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryProtestRepository(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: DRIVER_RATING_PROVIDER_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryDriverRatingProvider(logger),
+ inject: [LOGGER_TOKEN],
+ },
+ {
+ provide: IMAGE_SERVICE_TOKEN,
+ useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
+ inject: [LOGGER_TOKEN],
+ },
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
@@ -48,5 +129,114 @@ export const RaceProviders: Provider[] = [
useFactory: (raceRepo: IRaceRepository) => new GetTotalRacesUseCase(raceRepo),
inject: [RACE_REPOSITORY_TOKEN],
},
+ {
+ provide: GetRaceDetailUseCase,
+ useFactory: (
+ raceRepo: IRaceRepository,
+ leagueRepo: ILeagueRepository,
+ driverRepo: IDriverRepository,
+ raceRegRepo: IRaceRegistrationRepository,
+ resultRepo: IResultRepository,
+ leagueMembershipRepo: ILeagueMembershipRepository,
+ driverRatingProvider: DriverRatingProvider,
+ imageService: IImageServicePort,
+ ) => new GetRaceDetailUseCase(
+ raceRepo,
+ leagueRepo,
+ driverRepo,
+ raceRegRepo,
+ resultRepo,
+ leagueMembershipRepo,
+ driverRatingProvider,
+ imageService,
+ ),
+ inject: [
+ RACE_REPOSITORY_TOKEN,
+ LEAGUE_REPOSITORY_TOKEN,
+ DRIVER_REPOSITORY_TOKEN,
+ RACE_REGISTRATION_REPOSITORY_TOKEN,
+ RESULT_REPOSITORY_TOKEN,
+ LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
+ DRIVER_RATING_PROVIDER_TOKEN,
+ IMAGE_SERVICE_TOKEN,
+ ],
+ },
+ {
+ provide: GetRacesPageDataUseCase,
+ useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => new GetRacesPageDataUseCase(raceRepo, leagueRepo),
+ inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
+ },
+ {
+ provide: GetAllRacesPageDataUseCase,
+ useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => new GetAllRacesPageDataUseCase(raceRepo, leagueRepo),
+ inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
+ },
+ {
+ provide: GetRaceResultsDetailUseCase,
+ useFactory: (resultRepo: IResultRepository, driverRepo: IDriverRepository, imageService: IImageServicePort) =>
+ new GetRaceResultsDetailUseCase(resultRepo, driverRepo, imageService),
+ inject: [RESULT_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN],
+ },
+ {
+ provide: GetRaceWithSOFUseCase,
+ useFactory: (raceRepo: IRaceRepository) => new GetRaceWithSOFUseCase(raceRepo),
+ inject: [RACE_REPOSITORY_TOKEN],
+ },
+ {
+ provide: GetRaceProtestsUseCase,
+ useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) => new GetRaceProtestsUseCase(protestRepo, driverRepo),
+ inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
+ },
+ {
+ provide: GetRacePenaltiesUseCase,
+ useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) => new GetRacePenaltiesUseCase(penaltyRepo, driverRepo),
+ inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
+ },
+ {
+ provide: RegisterForRaceUseCase,
+ useFactory: (raceRegRepo: IRaceRegistrationRepository, leagueMembershipRepo: ILeagueMembershipRepository, logger: Logger) =>
+ new RegisterForRaceUseCase(raceRegRepo, leagueMembershipRepo, logger),
+ inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
+ {
+ provide: WithdrawFromRaceUseCase,
+ useFactory: (raceRegRepo: IRaceRegistrationRepository, logger: Logger) => new WithdrawFromRaceUseCase(raceRegRepo, logger),
+ inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
+ {
+ provide: CancelRaceUseCase,
+ useFactory: (raceRepo: IRaceRepository, logger: Logger) => new CancelRaceUseCase(raceRepo, logger),
+ inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
+ {
+ provide: CompleteRaceUseCase,
+ useFactory: (raceRepo: IRaceRepository, logger: Logger) => new CompleteRaceUseCase(raceRepo, logger),
+ inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
ImportRaceResultsApiUseCase,
+ ImportRaceResultsUseCase,
+ {
+ provide: FileProtestUseCase,
+ useFactory: (protestRepo: IProtestRepository, raceRepo: IRaceRepository, driverRepo: IDriverRepository, logger: Logger) =>
+ new FileProtestUseCase(protestRepo, raceRepo, driverRepo, logger),
+ inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
+ {
+ provide: QuickPenaltyUseCase,
+ useFactory: (penaltyRepo: IPenaltyRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, logger: Logger) =>
+ new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger),
+ inject: [PENALTY_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
+ {
+ provide: ApplyPenaltyUseCase,
+ useFactory: (penaltyRepo: IPenaltyRepository, protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, logger: Logger) =>
+ new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger),
+ inject: [PENALTY_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
+ },
+ {
+ provide: RequestProtestDefenseUseCase,
+ useFactory: (protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository) =>
+ new RequestProtestDefenseUseCase(protestRepo, raceRepo, leagueMembershipRepo),
+ inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
+ },
];
diff --git a/apps/api/src/domain/race/RaceService.ts b/apps/api/src/domain/race/RaceService.ts
index cd73a0ed8..f91c1b62e 100644
--- a/apps/api/src/domain/race/RaceService.ts
+++ b/apps/api/src/domain/race/RaceService.ts
@@ -1,5 +1,20 @@
import { Injectable, Inject } from '@nestjs/common';
-import { AllRacesPageViewModel, RaceStatsDto, ImportRaceResultsInput, ImportRaceResultsSummaryViewModel } from './dto/RaceDto';
+import {
+ AllRacesPageViewModel,
+ RaceStatsDto,
+ ImportRaceResultsInput,
+ ImportRaceResultsSummaryViewModel,
+ RaceDetailViewModelDto,
+ RacesPageDataViewModelDto,
+ RaceResultsDetailViewModelDto,
+ RaceWithSOFViewModelDto,
+ RaceProtestsViewModelDto,
+ RacePenaltiesViewModelDto,
+ GetRaceDetailParamsDto,
+ RegisterForRaceParamsDto,
+ WithdrawFromRaceParamsDto,
+ RaceActionParamsDto,
+} from './dtos/RaceDTO';
// Core imports
import type { Logger } from '@core/shared/application/Logger';
@@ -8,6 +23,23 @@ import type { Logger } from '@core/shared/application/Logger';
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
+import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
+import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
+import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
+import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
+import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
+import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
+import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
+import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
+import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
+import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
+import { CompleteRaceUseCase } from '@core/racing/application/use-cases/CompleteRaceUseCase';
+import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
+import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
+import { FileProtestUseCase } from '@core/racing/application/use-cases/FileProtestUseCase';
+import { QuickPenaltyUseCase } from '@core/racing/application/use-cases/QuickPenaltyUseCase';
+import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
+import { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase';
// Presenters
import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter';
@@ -23,6 +55,23 @@ export class RaceService {
private readonly getAllRacesUseCase: GetAllRacesUseCase,
private readonly getTotalRacesUseCase: GetTotalRacesUseCase,
private readonly importRaceResultsApiUseCase: ImportRaceResultsApiUseCase,
+ private readonly getRaceDetailUseCase: GetRaceDetailUseCase,
+ private readonly getRacesPageDataUseCase: GetRacesPageDataUseCase,
+ private readonly getAllRacesPageDataUseCase: GetAllRacesPageDataUseCase,
+ private readonly getRaceResultsDetailUseCase: GetRaceResultsDetailUseCase,
+ private readonly getRaceWithSOFUseCase: GetRaceWithSOFUseCase,
+ private readonly getRaceProtestsUseCase: GetRaceProtestsUseCase,
+ private readonly getRacePenaltiesUseCase: GetRacePenaltiesUseCase,
+ private readonly registerForRaceUseCase: RegisterForRaceUseCase,
+ private readonly withdrawFromRaceUseCase: WithdrawFromRaceUseCase,
+ private readonly cancelRaceUseCase: CancelRaceUseCase,
+ private readonly completeRaceUseCase: CompleteRaceUseCase,
+ private readonly importRaceResultsUseCase: ImportRaceResultsUseCase,
+ private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
+ private readonly fileProtestUseCase: FileProtestUseCase,
+ private readonly quickPenaltyUseCase: QuickPenaltyUseCase,
+ private readonly applyPenaltyUseCase: ApplyPenaltyUseCase,
+ private readonly requestProtestDefenseUseCase: RequestProtestDefenseUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -47,4 +96,202 @@ export class RaceService {
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter);
return presenter.getViewModel()!;
}
+
+ async getRaceDetail(params: GetRaceDetailParamsDto): Promise {
+ this.logger.debug('[RaceService] Fetching race detail:', params);
+
+ const presenter = new RaceDetailPresenter();
+ const result = await this.getRaceDetailUseCase.execute(params);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to get race detail');
+ }
+
+ return result.value;
+ }
+
+ async getRacesPageData(): Promise {
+ this.logger.debug('[RaceService] Fetching races page data.');
+
+ const result = await this.getRacesPageDataUseCase.execute();
+
+ if (result.isErr()) {
+ throw new Error('Failed to get races page data');
+ }
+
+ return result.value;
+ }
+
+ async getAllRacesPageData(): Promise {
+ this.logger.debug('[RaceService] Fetching all races page data.');
+
+ const result = await this.getAllRacesPageDataUseCase.execute();
+
+ if (result.isErr()) {
+ throw new Error('Failed to get all races page data');
+ }
+
+ return result.value;
+ }
+
+ async getRaceResultsDetail(raceId: string): Promise {
+ this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
+
+ const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to get race results detail');
+ }
+
+ return result.value;
+ }
+
+ async getRaceWithSOF(raceId: string): Promise {
+ this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
+
+ const result = await this.getRaceWithSOFUseCase.execute({ raceId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to get race with SOF');
+ }
+
+ return result.value;
+ }
+
+ async getRaceProtests(raceId: string): Promise {
+ this.logger.debug('[RaceService] Fetching race protests:', { raceId });
+
+ const result = await this.getRaceProtestsUseCase.execute({ raceId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to get race protests');
+ }
+
+ return result.value;
+ }
+
+ async getRacePenalties(raceId: string): Promise {
+ this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
+
+ const result = await this.getRacePenaltiesUseCase.execute({ raceId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to get race penalties');
+ }
+
+ return result.value;
+ }
+
+ async registerForRace(params: RegisterForRaceParamsDto): Promise {
+ this.logger.debug('[RaceService] Registering for race:', params);
+
+ const result = await this.registerForRaceUseCase.execute(params);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to register for race');
+ }
+ }
+
+ async withdrawFromRace(params: WithdrawFromRaceParamsDto): Promise {
+ this.logger.debug('[RaceService] Withdrawing from race:', params);
+
+ const result = await this.withdrawFromRaceUseCase.execute(params);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to withdraw from race');
+ }
+ }
+
+ async cancelRace(params: RaceActionParamsDto): Promise {
+ this.logger.debug('[RaceService] Cancelling race:', params);
+
+ const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to cancel race');
+ }
+ }
+
+ async completeRace(params: RaceActionParamsDto): Promise {
+ this.logger.debug('[RaceService] Completing race:', params);
+
+ const result = await this.completeRaceUseCase.execute({ raceId: params.raceId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to complete race');
+ }
+ }
+
+ async importRaceResultsAlt(params: { raceId: string; resultsFileContent: string }): Promise {
+ this.logger.debug('[RaceService] Importing race results (alt):', params);
+
+ const result = await this.importRaceResultsUseCase.execute({
+ raceId: params.raceId,
+ resultsFileContent: params.resultsFileContent,
+ });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to import race results');
+ }
+ }
+
+ async getDashboardOverview(driverId: string): Promise {
+ this.logger.debug('[RaceService] Getting dashboard overview:', { driverId });
+
+ const result = await this.dashboardOverviewUseCase.execute({ driverId });
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to get dashboard overview');
+ }
+
+ return result.value;
+ }
+
+ async fileProtest(command: any): Promise {
+ this.logger.debug('[RaceService] Filing protest:', command);
+
+ const result = await this.fileProtestUseCase.execute(command);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to file protest');
+ }
+
+ return result.value;
+ }
+
+ async applyQuickPenalty(command: any): Promise {
+ this.logger.debug('[RaceService] Applying quick penalty:', command);
+
+ const result = await this.quickPenaltyUseCase.execute(command);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to apply quick penalty');
+ }
+
+ return result.value;
+ }
+
+ async applyPenalty(command: any): Promise {
+ this.logger.debug('[RaceService] Applying penalty:', command);
+
+ const result = await this.applyPenaltyUseCase.execute(command);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to apply penalty');
+ }
+
+ return result.value;
+ }
+
+ async requestProtestDefense(command: any): Promise {
+ this.logger.debug('[RaceService] Requesting protest defense:', command);
+
+ const result = await this.requestProtestDefenseUseCase.execute(command);
+
+ if (result.isErr()) {
+ throw new Error(result.error.details.message || 'Failed to request protest defense');
+ }
+
+ return result.value;
+ }
}
diff --git a/apps/api/src/domain/race/dto/RaceDto.ts b/apps/api/src/domain/race/dto/RaceDto.ts
deleted file mode 100644
index e1eb4f68a..000000000
--- a/apps/api/src/domain/race/dto/RaceDto.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString, IsNotEmpty, IsBoolean, IsNumber } from 'class-validator';
-
-export class RaceViewModel {
- @ApiProperty()
- id: string; // Assuming a race has an ID
-
- @ApiProperty()
- name: string; // Assuming a race has a name
-
- @ApiProperty()
- date: string; // Assuming a race has a date
-
- @ApiProperty({ nullable: true })
- leagueName?: string; // Assuming a race might belong to a league
-
- // Add more race-related properties as needed based on the DTO from the application layer
-}
-
-export class AllRacesPageViewModel {
- @ApiProperty({ type: [RaceViewModel] })
- races: RaceViewModel[];
-
- @ApiProperty()
- totalCount: number;
-}
-
-export class RaceStatsDto {
- @ApiProperty()
- totalRaces: number;
-}
-
-export class ImportRaceResultsInput {
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- raceId: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- resultsFileContent: string;
-}
-
-export class ImportRaceResultsSummaryViewModel {
- @ApiProperty()
- @IsBoolean()
- success: boolean;
-
- @ApiProperty()
- @IsString()
- raceId: string;
-
- @ApiProperty()
- @IsNumber()
- driversProcessed: number;
-
- @ApiProperty()
- @IsNumber()
- resultsRecorded: number;
-
- @ApiProperty({ type: [String], required: false })
- errors?: string[];
-}
-
-export class RaceDto {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- name: string;
-
- @ApiProperty()
- @IsString()
- date: string;
-}
diff --git a/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts b/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts
new file mode 100644
index 000000000..bc0abfce2
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts
@@ -0,0 +1,10 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { RaceViewModel } from './RaceViewModel';
+
+export class AllRacesPageDTO {
+ @ApiProperty({ type: [RaceViewModel] })
+ races!: RaceViewModel[];
+
+ @ApiProperty()
+ totalCount!: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/ApplyPenaltyCommandDTO.ts b/apps/api/src/domain/race/dtos/ApplyPenaltyCommandDTO.ts
new file mode 100644
index 000000000..0263dacad
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/ApplyPenaltyCommandDTO.ts
@@ -0,0 +1,65 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty, IsOptional, IsNumber, IsEnum } from 'class-validator';
+
+export class ApplyPenaltyCommandDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ driverId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ stewardId!: string;
+
+ @ApiProperty({
+ enum: [
+ 'time_penalty',
+ 'grid_penalty',
+ 'points_deduction',
+ 'disqualification',
+ 'warning',
+ 'license_points',
+ 'probation',
+ 'fine',
+ 'race_ban',
+ ],
+ })
+ @IsEnum([
+ 'time_penalty',
+ 'grid_penalty',
+ 'points_deduction',
+ 'disqualification',
+ 'warning',
+ 'license_points',
+ 'probation',
+ 'fine',
+ 'race_ban',
+ ])
+ type!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsNumber()
+ value?: number;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ reason!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ protestId?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ notes?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardDriverSummaryDTO.ts b/apps/api/src/domain/race/dtos/DashboardDriverSummaryDTO.ts
new file mode 100644
index 000000000..ced95cee9
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardDriverSummaryDTO.ts
@@ -0,0 +1,47 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber, IsOptional } from 'class-validator';
+
+export class DashboardDriverSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ name!: string;
+
+ @ApiProperty()
+ @IsString()
+ country!: string;
+
+ @ApiProperty()
+ @IsString()
+ avatarUrl!: string;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsNumber()
+ rating?: number | null;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsNumber()
+ globalRank?: number | null;
+
+ @ApiProperty()
+ @IsNumber()
+ totalRaces!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ wins!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ podiums!: number;
+
+ @ApiProperty({ nullable: true })
+ @IsOptional()
+ @IsNumber()
+ consistency?: number | null;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardFeedItemSummaryDTO.ts b/apps/api/src/domain/race/dtos/DashboardFeedItemSummaryDTO.ts
new file mode 100644
index 000000000..2df53f238
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardFeedItemSummaryDTO.ts
@@ -0,0 +1,53 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsOptional } from 'class-validator';
+
+export type DashboardFeedItemType =
+ | 'friend-joined-league'
+ | 'friend-joined-team'
+ | 'friend-finished-race'
+ | 'friend-new-personal-best'
+ | 'new-race-scheduled'
+ | 'new-result-posted'
+ | 'league-highlight';
+
+export class DashboardFeedItemSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty({
+ enum: [
+ 'friend-joined-league',
+ 'friend-joined-team',
+ 'friend-finished-race',
+ 'friend-new-personal-best',
+ 'new-race-scheduled',
+ 'new-result-posted',
+ 'league-highlight',
+ ],
+ })
+ type!: DashboardFeedItemType;
+
+ @ApiProperty()
+ @IsString()
+ headline!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ body?: string;
+
+ @ApiProperty()
+ @IsString()
+ timestamp!: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ ctaLabel?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ ctaHref?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardFeedSummaryDTO.ts b/apps/api/src/domain/race/dtos/DashboardFeedSummaryDTO.ts
new file mode 100644
index 000000000..0ef464782
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardFeedSummaryDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber } from 'class-validator';
+import { DashboardFeedItemSummaryDTO } from './DashboardFeedItemSummaryDTO';
+
+export class DashboardFeedSummaryDTO {
+ @ApiProperty()
+ @IsNumber()
+ notificationCount!: number;
+
+ @ApiProperty({ type: [DashboardFeedItemSummaryDTO] })
+ items!: DashboardFeedItemSummaryDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardFriendSummaryDTO.ts b/apps/api/src/domain/race/dtos/DashboardFriendSummaryDTO.ts
new file mode 100644
index 000000000..02cdb8b14
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardFriendSummaryDTO.ts
@@ -0,0 +1,20 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class DashboardFriendSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ name!: string;
+
+ @ApiProperty()
+ @IsString()
+ country!: string;
+
+ @ApiProperty()
+ @IsString()
+ avatarUrl!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardLeagueStandingSummaryDTO.ts b/apps/api/src/domain/race/dtos/DashboardLeagueStandingSummaryDTO.ts
new file mode 100644
index 000000000..99966f01c
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardLeagueStandingSummaryDTO.ts
@@ -0,0 +1,24 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class DashboardLeagueStandingSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ leagueId!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueName!: string;
+
+ @ApiProperty()
+ @IsNumber()
+ position!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ totalDrivers!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ points!: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardOverviewDTO.ts b/apps/api/src/domain/race/dtos/DashboardOverviewDTO.ts
new file mode 100644
index 000000000..9ad92bf97
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardOverviewDTO.ts
@@ -0,0 +1,41 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsOptional } from 'class-validator';
+import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO';
+import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
+import { DashboardRecentResultDTO } from './DashboardRecentResultDTO';
+import { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO';
+import { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO';
+import { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO';
+
+export class DashboardOverviewDTO {
+ @ApiProperty({ nullable: true })
+ currentDriver!: DashboardDriverSummaryDTO | null;
+
+ @ApiProperty({ type: [DashboardRaceSummaryDTO] })
+ myUpcomingRaces!: DashboardRaceSummaryDTO[];
+
+ @ApiProperty({ type: [DashboardRaceSummaryDTO] })
+ otherUpcomingRaces!: DashboardRaceSummaryDTO[];
+
+ @ApiProperty({ type: [DashboardRaceSummaryDTO] })
+ upcomingRaces!: DashboardRaceSummaryDTO[];
+
+ @ApiProperty()
+ @IsNumber()
+ activeLeaguesCount!: number;
+
+ @ApiProperty({ nullable: true })
+ nextRace!: DashboardRaceSummaryDTO | null;
+
+ @ApiProperty({ type: [DashboardRecentResultDTO] })
+ recentResults!: DashboardRecentResultDTO[];
+
+ @ApiProperty({ type: [DashboardLeagueStandingSummaryDTO] })
+ leagueStandingsSummaries!: DashboardLeagueStandingSummaryDTO[];
+
+ @ApiProperty()
+ feedSummary!: DashboardFeedSummaryDTO;
+
+ @ApiProperty({ type: [DashboardFriendSummaryDTO] })
+ friends!: DashboardFriendSummaryDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardRaceSummaryDTO.ts b/apps/api/src/domain/race/dtos/DashboardRaceSummaryDTO.ts
new file mode 100644
index 000000000..4c870af7c
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardRaceSummaryDTO.ts
@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean } from 'class-validator';
+
+export class DashboardRaceSummaryDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueName!: string;
+
+ @ApiProperty()
+ @IsString()
+ track!: string;
+
+ @ApiProperty()
+ @IsString()
+ car!: string;
+
+ @ApiProperty()
+ @IsString()
+ scheduledAt!: string;
+
+ @ApiProperty({ enum: ['scheduled', 'running', 'completed', 'cancelled'] })
+ @IsString()
+ status!: 'scheduled' | 'running' | 'completed' | 'cancelled';
+
+ @ApiProperty()
+ @IsBoolean()
+ isMyLeague!: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/DashboardRecentResultDTO.ts b/apps/api/src/domain/race/dtos/DashboardRecentResultDTO.ts
new file mode 100644
index 000000000..5cec87cd8
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/DashboardRecentResultDTO.ts
@@ -0,0 +1,32 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class DashboardRecentResultDTO {
+ @ApiProperty()
+ @IsString()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ raceName!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueName!: string;
+
+ @ApiProperty()
+ @IsString()
+ finishedAt!: string;
+
+ @ApiProperty()
+ @IsNumber()
+ position!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ incidents!: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/FileProtestCommandDTO.ts b/apps/api/src/domain/race/dtos/FileProtestCommandDTO.ts
new file mode 100644
index 000000000..5d92e0a5e
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/FileProtestCommandDTO.ts
@@ -0,0 +1,37 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty, IsOptional, IsNumber, IsUrl } from 'class-validator';
+
+export class FileProtestCommandDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ protestingDriverId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ accusedDriverId!: string;
+
+ @ApiProperty()
+ incident!: {
+ lap: number;
+ description: string;
+ timeInRace?: number;
+ };
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ comment?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ @IsUrl()
+ proofVideoUrl?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/GetRaceDetailParamsDTO.ts b/apps/api/src/domain/race/dtos/GetRaceDetailParamsDTO.ts
new file mode 100644
index 000000000..b979fca25
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/GetRaceDetailParamsDTO.ts
@@ -0,0 +1,14 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class GetRaceDetailParamsDTODTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ driverId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/ImportRaceResultsDTO.ts b/apps/api/src/domain/race/dtos/ImportRaceResultsDTO.ts
new file mode 100644
index 000000000..6f5a62a2b
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/ImportRaceResultsDTO.ts
@@ -0,0 +1,14 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class ImportRaceResultsDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ resultsFileContent!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/ImportRaceResultsSummaryDTO.ts b/apps/api/src/domain/race/dtos/ImportRaceResultsSummaryDTO.ts
new file mode 100644
index 000000000..7a89b4727
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/ImportRaceResultsSummaryDTO.ts
@@ -0,0 +1,23 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsString, IsNumber } from 'class-validator';
+
+export class ImportRaceResultsSummaryDTOViewModel {
+ @ApiProperty()
+ @IsBoolean()
+ success!: boolean;
+
+ @ApiProperty()
+ @IsString()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsNumber()
+ driversProcessed!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ resultsRecorded!: number;
+
+ @ApiProperty({ type: [String], required: false })
+ errors?: string[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/QuickPenaltyCommandDTO.ts b/apps/api/src/domain/race/dtos/QuickPenaltyCommandDTO.ts
new file mode 100644
index 000000000..fbac2c5d1
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/QuickPenaltyCommandDTO.ts
@@ -0,0 +1,32 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty, IsOptional, IsEnum } from 'class-validator';
+
+export class QuickPenaltyCommandDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ driverId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ adminId!: string;
+
+ @ApiProperty({ enum: ['track_limits', 'unsafe_rejoin', 'aggressive_driving', 'false_start', 'other'] })
+ @IsEnum(['track_limits', 'unsafe_rejoin', 'aggressive_driving', 'false_start', 'other'])
+ infractionType!: 'track_limits' | 'unsafe_rejoin' | 'aggressive_driving' | 'false_start' | 'other';
+
+ @ApiProperty({ enum: ['warning', 'minor', 'major', 'severe'] })
+ @IsEnum(['warning', 'minor', 'major', 'severe'])
+ severity!: 'warning' | 'minor' | 'major' | 'severe';
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ notes?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceActionParamsDTO.ts b/apps/api/src/domain/race/dtos/RaceActionParamsDTO.ts
new file mode 100644
index 000000000..5bcbc3998
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceActionParamsDTO.ts
@@ -0,0 +1,9 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class RaceActionParamsDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDTO.ts b/apps/api/src/domain/race/dtos/RaceDTO.ts
new file mode 100644
index 000000000..be02d5efe
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDTO.ts
@@ -0,0 +1,15 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class RaceDTO {
+ @ApiProperty()
+ id!: string;
+
+ @ApiProperty()
+ name!: string;
+
+ @ApiProperty()
+ date!: string;
+
+ @ApiProperty({ nullable: true })
+ leagueName?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDetailDTO.ts b/apps/api/src/domain/race/dtos/RaceDetailDTO.ts
new file mode 100644
index 000000000..d73f99367
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDetailDTO.ts
@@ -0,0 +1,29 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsOptional, IsString } from 'class-validator';
+import { RaceDetailRaceDTO } from './RaceDetailRaceDTO';
+import { RaceDetailLeagueDTO } from './RaceDetailLeagueDTO';
+import { RaceDetailEntryDTO } from './RaceDetailEntryDTO';
+import { RaceDetailRegistrationDTO } from './RaceDetailRegistrationDTO';
+import { RaceDetailUserResultDTO } from './RaceDetailUserResultDTO';
+
+export class RaceDetailDTO {
+ @ApiProperty({ nullable: true })
+ race!: RaceDetailRaceDTO | null;
+
+ @ApiProperty({ nullable: true })
+ league!: RaceDetailLeagueDTO | null;
+
+ @ApiProperty({ type: [RaceDetailEntryDTO] })
+ entryList!: RaceDetailEntryDTO[];
+
+ @ApiProperty()
+ registration!: RaceDetailRegistrationDTO;
+
+ @ApiProperty({ nullable: true })
+ userResult!: RaceDetailUserResultDTO | null;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ error?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDetailEntryDTO.ts b/apps/api/src/domain/race/dtos/RaceDetailEntryDTO.ts
new file mode 100644
index 000000000..64a809aa6
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDetailEntryDTO.ts
@@ -0,0 +1,27 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean, IsNumber } from 'class-validator';
+
+export class RaceDetailEntryDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ name!: string;
+
+ @ApiProperty()
+ @IsString()
+ country!: string;
+
+ @ApiProperty()
+ @IsString()
+ avatarUrl!: string;
+
+ @ApiProperty({ nullable: true })
+ rating!: number | null;
+
+ @ApiProperty()
+ @IsBoolean()
+ isCurrentUser!: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDetailLeagueDTO.ts b/apps/api/src/domain/race/dtos/RaceDetailLeagueDTO.ts
new file mode 100644
index 000000000..d4ed75a04
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDetailLeagueDTO.ts
@@ -0,0 +1,22 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class RaceDetailLeagueDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ name!: string;
+
+ @ApiProperty()
+ @IsString()
+ description!: string;
+
+ @ApiProperty()
+ settings!: {
+ maxDrivers?: number;
+ qualifyingFormat?: string;
+ };
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDetailRaceDTO.ts b/apps/api/src/domain/race/dtos/RaceDetailRaceDTO.ts
new file mode 100644
index 000000000..6d7dd710c
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDetailRaceDTO.ts
@@ -0,0 +1,45 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsOptional, IsNumber } from 'class-validator';
+
+export class RaceDetailRaceDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId!: string;
+
+ @ApiProperty()
+ @IsString()
+ track!: string;
+
+ @ApiProperty()
+ @IsString()
+ car!: string;
+
+ @ApiProperty()
+ @IsString()
+ scheduledAt!: string;
+
+ @ApiProperty()
+ @IsString()
+ sessionType!: string;
+
+ @ApiProperty()
+ @IsString()
+ status!: string;
+
+ @ApiProperty({ nullable: true })
+ strengthOfField!: number | null;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsNumber()
+ registeredCount?: number;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsNumber()
+ maxParticipants?: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDetailRegistrationDTO.ts b/apps/api/src/domain/race/dtos/RaceDetailRegistrationDTO.ts
new file mode 100644
index 000000000..891ffef66
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDetailRegistrationDTO.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean } from 'class-validator';
+
+export class RaceDetailRegistrationDTO {
+ @ApiProperty()
+ @IsBoolean()
+ isUserRegistered!: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ canRegister!: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceDetailUserResultDTO.ts b/apps/api/src/domain/race/dtos/RaceDetailUserResultDTO.ts
new file mode 100644
index 000000000..d8b5419a3
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceDetailUserResultDTO.ts
@@ -0,0 +1,35 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber, IsBoolean } from 'class-validator';
+
+export class RaceDetailUserResultDTO {
+ @ApiProperty()
+ @IsNumber()
+ position!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ startPosition!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ incidents!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ fastestLap!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ positionChange!: number;
+
+ @ApiProperty()
+ @IsBoolean()
+ isPodium!: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ isClean!: boolean;
+
+ @ApiProperty({ nullable: true })
+ ratingChange!: number | null;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RacePenaltiesDTO.ts b/apps/api/src/domain/race/dtos/RacePenaltiesDTO.ts
new file mode 100644
index 000000000..89d0b71db
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RacePenaltiesDTO.ts
@@ -0,0 +1,10 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { RacePenaltyDTO } from './RacePenaltyDTO';
+
+export class RacePenaltiesDTO {
+ @ApiProperty({ type: [RacePenaltyDTO] })
+ penalties!: RacePenaltyDTO[];
+
+ @ApiProperty()
+ driverMap!: Record;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RacePenaltyDTO.ts b/apps/api/src/domain/race/dtos/RacePenaltyDTO.ts
new file mode 100644
index 000000000..ccb34c680
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RacePenaltyDTO.ts
@@ -0,0 +1,35 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class RacePenaltyDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ driverId!: string;
+
+ @ApiProperty()
+ @IsString()
+ type!: string;
+
+ @ApiProperty()
+ @IsNumber()
+ value!: number;
+
+ @ApiProperty()
+ @IsString()
+ reason!: string;
+
+ @ApiProperty()
+ @IsString()
+ issuedBy!: string;
+
+ @ApiProperty()
+ @IsString()
+ issuedAt!: string;
+
+ @ApiProperty({ nullable: true })
+ notes?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceProtestDTO.ts b/apps/api/src/domain/race/dtos/RaceProtestDTO.ts
new file mode 100644
index 000000000..0bd0223a1
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceProtestDTO.ts
@@ -0,0 +1,30 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class RaceProtestDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ protestingDriverId!: string;
+
+ @ApiProperty()
+ @IsString()
+ accusedDriverId!: string;
+
+ @ApiProperty()
+ incident!: {
+ lap: number;
+ description: string;
+ };
+
+ @ApiProperty()
+ @IsString()
+ status!: string;
+
+ @ApiProperty()
+ @IsString()
+ filedAt!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceProtestsDTO.ts b/apps/api/src/domain/race/dtos/RaceProtestsDTO.ts
new file mode 100644
index 000000000..934b15fc1
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceProtestsDTO.ts
@@ -0,0 +1,10 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { RaceProtestDto } from './RaceProtestDto';
+
+export class RaceProtestsDTO {
+ @ApiProperty({ type: [RaceProtestDto] })
+ protests!: RaceProtestDto[];
+
+ @ApiProperty()
+ driverMap!: Record;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceResultDTO.ts b/apps/api/src/domain/race/dtos/RaceResultDTO.ts
new file mode 100644
index 000000000..da97db10c
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceResultDTO.ts
@@ -0,0 +1,44 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber, IsBoolean } from 'class-validator';
+
+export class RaceResultDTO {
+ @ApiProperty()
+ @IsString()
+ driverId!: string;
+
+ @ApiProperty()
+ @IsString()
+ driverName!: string;
+
+ @ApiProperty()
+ @IsString()
+ avatarUrl!: string;
+
+ @ApiProperty()
+ @IsNumber()
+ position!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ startPosition!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ incidents!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ fastestLap!: number;
+
+ @ApiProperty()
+ @IsNumber()
+ positionChange!: number;
+
+ @ApiProperty()
+ @IsBoolean()
+ isPodium!: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ isClean!: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceResultsDetailDTO.ts b/apps/api/src/domain/race/dtos/RaceResultsDetailDTO.ts
new file mode 100644
index 000000000..25ea932f7
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceResultsDetailDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+import { RaceResultDto } from './RaceResultDto';
+
+export class RaceResultsDetailDTO {
+ @ApiProperty()
+ @IsString()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ track!: string;
+
+ @ApiProperty({ type: [RaceResultDto] })
+ results!: RaceResultDto[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceStatsDTO.ts b/apps/api/src/domain/race/dtos/RaceStatsDTO.ts
new file mode 100644
index 000000000..1befe2efc
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceStatsDTO.ts
@@ -0,0 +1,6 @@
+import { ApiProperty } from '@nestjs/swagger';
+
+export class RaceStatsDTO {
+ @ApiProperty()
+ totalRaces!: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RaceWithSOFDTO.ts b/apps/api/src/domain/race/dtos/RaceWithSOFDTO.ts
new file mode 100644
index 000000000..31c1d47b5
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RaceWithSOFDTO.ts
@@ -0,0 +1,15 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class RaceWithSOFDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ track!: string;
+
+ @ApiProperty({ nullable: true })
+ strengthOfField!: number | null;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RacesPageDataDTO.ts b/apps/api/src/domain/race/dtos/RacesPageDataDTO.ts
new file mode 100644
index 000000000..1e5fa5d6b
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RacesPageDataDTO.ts
@@ -0,0 +1,7 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { RacesPageDataRaceDto } from './RacesPageDataRaceDto';
+
+export class RacesPageDataDTO {
+ @ApiProperty({ type: [RacesPageDataRaceDto] })
+ races!: RacesPageDataRaceDto[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RacesPageDataRaceDTO.ts b/apps/api/src/domain/race/dtos/RacesPageDataRaceDTO.ts
new file mode 100644
index 000000000..62fe929bd
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RacesPageDataRaceDTO.ts
@@ -0,0 +1,47 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsBoolean } from 'class-validator';
+
+export class RacesPageDataRaceDTO {
+ @ApiProperty()
+ @IsString()
+ id!: string;
+
+ @ApiProperty()
+ @IsString()
+ track!: string;
+
+ @ApiProperty()
+ @IsString()
+ car!: string;
+
+ @ApiProperty()
+ @IsString()
+ scheduledAt!: string;
+
+ @ApiProperty()
+ @IsString()
+ status!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId!: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueName!: string;
+
+ @ApiProperty({ nullable: true })
+ strengthOfField!: number | null;
+
+ @ApiProperty()
+ @IsBoolean()
+ isUpcoming!: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ isLive!: boolean;
+
+ @ApiProperty()
+ @IsBoolean()
+ isPast!: boolean;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RegisterForRaceParamsDTO.ts b/apps/api/src/domain/race/dtos/RegisterForRaceParamsDTO.ts
new file mode 100644
index 000000000..ea96d6967
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RegisterForRaceParamsDTO.ts
@@ -0,0 +1,19 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class RegisterForRaceParamsDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ leagueId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ driverId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/RequestProtestDefenseCommandDTO.ts b/apps/api/src/domain/race/dtos/RequestProtestDefenseCommandDTO.ts
new file mode 100644
index 000000000..d0f4e1cef
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/RequestProtestDefenseCommandDTO.ts
@@ -0,0 +1,14 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class RequestProtestDefenseCommandDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ protestId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ stewardId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/race/dtos/WithdrawFromRaceParamsDTO.ts b/apps/api/src/domain/race/dtos/WithdrawFromRaceParamsDTO.ts
new file mode 100644
index 000000000..35c2edd31
--- /dev/null
+++ b/apps/api/src/domain/race/dtos/WithdrawFromRaceParamsDTO.ts
@@ -0,0 +1,14 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNotEmpty } from 'class-validator';
+
+export class WithdrawFromRaceParamsDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ raceId!: string;
+
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ driverId!: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/SponsorController.ts b/apps/api/src/domain/sponsor/SponsorController.ts
index c967bf247..5ea567597 100644
--- a/apps/api/src/domain/sponsor/SponsorController.ts
+++ b/apps/api/src/domain/sponsor/SponsorController.ts
@@ -1,7 +1,14 @@
import { Controller, Get, Post, Body, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { SponsorService } from './SponsorService';
-import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO } from './dto/SponsorDto';
+import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshipPricingResultDTO';
+import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO';
+import { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
+import { CreateSponsorOutputDTO } from './dtos/CreateSponsorOutputDTO';
+import { GetSponsorDashboardQueryParamsDTO } from './dtos/GetSponsorDashboardQueryParamsDTO';
+import { SponsorDashboardDTO } from './dtos/SponsorDashboardDTO';
+import { GetSponsorSponsorshipsQueryParamsDTO } from './dtos/GetSponsorSponsorshipsQueryParamsDTO';
+import { SponsorSponsorshipsDTO } from './dtos/SponsorSponsorshipsDTO';
@ApiTags('sponsors')
@Controller('sponsors')
@@ -10,23 +17,23 @@ export class SponsorController {
@Get('pricing')
@ApiOperation({ summary: 'Get sponsorship pricing for an entity' })
- @ApiResponse({ status: 200, description: 'Sponsorship pricing', type: GetEntitySponsorshipPricingResultDto })
- async getEntitySponsorshipPricing(): Promise {
+ @ApiResponse({ status: 200, description: 'Sponsorship pricing', type: GetEntitySponsorshipPricingResultDTO })
+ async getEntitySponsorshipPricing(): Promise {
return this.sponsorService.getEntitySponsorshipPricing();
}
@Get()
@ApiOperation({ summary: 'Get all sponsors' })
- @ApiResponse({ status: 200, description: 'List of sponsors', type: GetSponsorsOutput })
- async getSponsors(): Promise {
+ @ApiResponse({ status: 200, description: 'List of sponsors', type: GetSponsorsOutputDTO })
+ async getSponsors(): Promise {
return this.sponsorService.getSponsors();
}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new sponsor' })
- @ApiResponse({ status: 201, description: 'Sponsor created', type: CreateSponsorOutput })
- async createSponsor(@Body() input: CreateSponsorInput): Promise {
+ @ApiResponse({ status: 201, description: 'Sponsor created', type: CreateSponsorOutputDTO })
+ async createSponsor(@Body() input: CreateSponsorInputDTO): Promise {
return this.sponsorService.createSponsor(input);
}
@@ -36,13 +43,13 @@ export class SponsorController {
@ApiResponse({ status: 200, description: 'Sponsor dashboard data', type: SponsorDashboardDTO })
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsorDashboard(@Param('sponsorId') sponsorId: string): Promise {
- return this.sponsorService.getSponsorDashboard({ sponsorId });
+ return this.sponsorService.getSponsorDashboard({ sponsorId } as GetSponsorDashboardQueryParamsDTO);
}
@Get(':sponsorId/sponsorships')
@ApiOperation({ summary: 'Get all sponsorships for a given sponsor' })
@ApiResponse({ status: 200, description: 'List of sponsorships', type: SponsorSponsorshipsDTO })
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsorSponsorships(@Param('sponsorId') sponsorId: string): Promise {
- return this.sponsorService.getSponsorSponsorships({ sponsorId });
+ return this.sponsorService.getSponsorSponsorships({ sponsorId } as GetSponsorSponsorshipsQueryParamsDTO);
}
}
diff --git a/apps/api/src/domain/sponsor/SponsorService.ts b/apps/api/src/domain/sponsor/SponsorService.ts
index 6a4568bcf..0458dace1 100644
--- a/apps/api/src/domain/sponsor/SponsorService.ts
+++ b/apps/api/src/domain/sponsor/SponsorService.ts
@@ -1,5 +1,17 @@
import { Injectable, Inject } from '@nestjs/common';
-import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO, SponsorDto, SponsorDashboardMetricsDTO, SponsoredLeagueDTO, SponsorDashboardInvestmentDTO, SponsorshipDetailDTO } from './dto/SponsorDto';
+import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshipPricingResultDTO';
+import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO';
+import { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
+import { CreateSponsorOutputDTO } from './dtos/CreateSponsorOutputDTO';
+import { GetSponsorDashboardQueryParamsDTO } from './dtos/GetSponsorDashboardQueryParamsDTO';
+import { SponsorDashboardDTO } from './dtos/SponsorDashboardDTO';
+import { GetSponsorSponsorshipsQueryParamsDTO } from './dtos/GetSponsorSponsorshipsQueryParamsDTO';
+import { SponsorSponsorshipsDTO } from './dtos/SponsorSponsorshipsDTO';
+import { SponsorDTO } from './dtos/SponsorDTO';
+import { SponsorDashboardMetricsDTO } from './dtos/SponsorDashboardMetricsDTO';
+import { SponsoredLeagueDTO } from './dtos/SponsoredLeagueDTO';
+import { SponsorDashboardInvestmentDTO } from './dtos/SponsorDashboardInvestmentDTO';
+import { SponsorshipDetailDTO } from './dtos/SponsorshipDetailDTO';
// Use cases
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
@@ -30,7 +42,7 @@ export class SponsorService {
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
- async getEntitySponsorshipPricing(): Promise {
+ async getEntitySponsorshipPricing(): Promise {
this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
const presenter = new GetSponsorshipPricingPresenter();
@@ -38,7 +50,7 @@ export class SponsorService {
return presenter.viewModel;
}
- async getSponsors(): Promise {
+ async getSponsors(): Promise {
this.logger.debug('[SponsorService] Fetching sponsors.');
const presenter = new GetSponsorsPresenter();
@@ -46,7 +58,7 @@ export class SponsorService {
return presenter.viewModel;
}
- async createSponsor(input: CreateSponsorInput): Promise {
+ async createSponsor(input: CreateSponsorInputDTO): Promise {
this.logger.debug('[SponsorService] Creating sponsor.', { input });
const presenter = new CreateSponsorPresenter();
@@ -54,7 +66,7 @@ export class SponsorService {
return presenter.viewModel;
}
- async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise {
+ async getSponsorDashboard(params: GetSponsorDashboardQueryParamsDTO): Promise {
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
const presenter = new GetSponsorDashboardPresenter();
@@ -62,7 +74,7 @@ export class SponsorService {
return presenter.viewModel as SponsorDashboardDTO | null;
}
- async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise {
+ async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParamsDTO): Promise {
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
const presenter = new GetSponsorSponsorshipsPresenter();
diff --git a/apps/api/src/domain/sponsor/dto/SponsorDto.ts b/apps/api/src/domain/sponsor/dto/SponsorDto.ts
deleted file mode 100644
index 482c6638d..000000000
--- a/apps/api/src/domain/sponsor/dto/SponsorDto.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean, IsUrl, IsEmail } from 'class-validator';
-
-export class SponsorshipPricingItemDto {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- level: string;
-
- @ApiProperty()
- @IsNumber()
- price: number;
-
- @ApiProperty()
- @IsString()
- currency: string;
-}
-
-export class GetEntitySponsorshipPricingResultDto {
- @ApiProperty({ type: [SponsorshipPricingItemDto] })
- pricing: SponsorshipPricingItemDto[];
-}
-
-export class SponsorDto {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- name: string;
-
- @ApiProperty()
- @IsString()
- @IsEmail()
- @IsNotEmpty()
- contactEmail: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- @IsUrl()
- websiteUrl?: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- @IsUrl()
- logoUrl?: string;
-
- @ApiProperty()
- @IsDate()
- createdAt: Date;
-}
-
-export class GetSponsorsOutput {
- @ApiProperty({ type: [SponsorDto] })
- sponsors: SponsorDto[];
-}
-
-export class CreateSponsorInput {
- @ApiProperty()
- @IsString()
- @IsNotEmpty()
- name: string;
-
- @ApiProperty()
- @IsEmail()
- @IsNotEmpty()
- contactEmail: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- @IsUrl()
- websiteUrl?: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsString()
- @IsUrl()
- logoUrl?: string;
-}
-
-export class CreateSponsorOutput {
- @ApiProperty({ type: SponsorDto })
- sponsor: SponsorDto;
-}
-
-export class GetSponsorDashboardQueryParams {
- @ApiProperty()
- @IsString()
- sponsorId: string;
-}
-
-export class SponsoredLeagueDTO {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- name: string;
-
- @ApiProperty({ enum: ['main', 'secondary'] })
- @IsEnum(['main', 'secondary'])
- tier: 'main' | 'secondary';
-
- @ApiProperty()
- @IsNumber()
- drivers: number;
-
- @ApiProperty()
- @IsNumber()
- races: number;
-
- @ApiProperty()
- @IsNumber()
- impressions: number;
-
- @ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
- @IsEnum(['active', 'upcoming', 'completed'])
- status: 'active' | 'upcoming' | 'completed';
-}
-
-export class SponsorDashboardMetricsDTO {
- @ApiProperty()
- @IsNumber()
- impressions: number;
-
- @ApiProperty()
- @IsNumber()
- impressionsChange: number;
-
- @ApiProperty()
- @IsNumber()
- uniqueViewers: number;
-
- @ApiProperty()
- @IsNumber()
- viewersChange: number;
-
- @ApiProperty()
- @IsNumber()
- races: number;
-
- @ApiProperty()
- @IsNumber()
- drivers: number;
-
- @ApiProperty()
- @IsNumber()
- exposure: number;
-
- @ApiProperty()
- @IsNumber()
- exposureChange: number;
-}
-
-export class SponsorDashboardInvestmentDTO {
- @ApiProperty()
- @IsNumber()
- activeSponsorships: number;
-
- @ApiProperty()
- @IsNumber()
- totalInvestment: number;
-
- @ApiProperty()
- @IsNumber()
- costPerThousandViews: number;
-}
-
-export class SponsorDashboardDTO {
- @ApiProperty()
- @IsString()
- sponsorId: string;
-
- @ApiProperty()
- @IsString()
- sponsorName: string;
-
- @ApiProperty({ type: SponsorDashboardMetricsDTO })
- metrics: SponsorDashboardMetricsDTO;
-
- @ApiProperty({ type: [SponsoredLeagueDTO] })
- sponsoredLeagues: SponsoredLeagueDTO[];
-
- @ApiProperty({ type: SponsorDashboardInvestmentDTO })
- investment: SponsorDashboardInvestmentDTO;
-}
-
-export class GetSponsorSponsorshipsQueryParams {
- @ApiProperty()
- @IsString()
- sponsorId: string;
-}
-
-export class SponsorshipDetailDTO {
- @ApiProperty()
- @IsString()
- id: string;
-
- @ApiProperty()
- @IsString()
- leagueId: string;
-
- @ApiProperty()
- @IsString()
- leagueName: string;
-
- @ApiProperty()
- @IsString()
- seasonId: string;
-
- @ApiProperty()
- @IsString()
- seasonName: string;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- seasonStartDate?: Date;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- seasonEndDate?: Date;
-
- @ApiProperty({ enum: ['main', 'secondary'] })
- @IsEnum(['main', 'secondary'])
- tier: 'main' | 'secondary';
-
- @ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
- @IsEnum(['pending', 'active', 'expired', 'cancelled'])
- status: 'pending' | 'active' | 'expired' | 'cancelled';
-
- @ApiProperty()
- pricing: {
- amount: number;
- currency: string;
- };
-
- @ApiProperty()
- platformFee: {
- amount: number;
- currency: string;
- };
-
- @ApiProperty()
- netAmount: {
- amount: number;
- currency: string;
- };
-
- @ApiProperty()
- metrics: {
- drivers: number;
- races: number;
- completedRaces: number;
- impressions: number;
- };
-
- @ApiProperty()
- createdAt: Date;
-
- @ApiProperty({ required: false })
- @IsOptional()
- @IsDate()
- activatedAt?: Date;
-}
-
-export class SponsorSponsorshipsDTO {
- @ApiProperty()
- @IsString()
- sponsorId: string;
-
- @ApiProperty()
- @IsString()
- sponsorName: string;
-
- @ApiProperty({ type: [SponsorshipDetailDTO] })
- sponsorships: SponsorshipDetailDTO[];
-
- @ApiProperty()
- summary: {
- totalSponsorships: number;
- activeSponsorships: number;
- totalInvestment: number;
- totalPlatformFees: number;
- currency: string;
- };
-}
-
-// Add other DTOs for sponsor-related logic as needed
diff --git a/apps/api/src/domain/sponsor/dtos/CreateSponsorInputDTO.ts b/apps/api/src/domain/sponsor/dtos/CreateSponsorInputDTO.ts
new file mode 100644
index 000000000..1a0b5cdd7
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/CreateSponsorInputDTO.ts
@@ -0,0 +1,26 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEmail, IsNotEmpty, IsOptional, IsUrl } from 'class-validator';
+
+export class CreateSponsorInputDTO {
+ @ApiProperty()
+ @IsString()
+ @IsNotEmpty()
+ name: string;
+
+ @ApiProperty()
+ @IsEmail()
+ @IsNotEmpty()
+ contactEmail: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ @IsUrl()
+ websiteUrl?: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsString()
+ @IsUrl()
+ logoUrl?: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts b/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts
new file mode 100644
index 000000000..66b8b838d
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts
@@ -0,0 +1,7 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { SponsorDTO } from './SponsorDTO';
+
+export class CreateSponsorOutputDTO {
+ @ApiProperty({ type: SponsorDTO })
+ sponsor: SponsorDTO;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts b/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts
new file mode 100644
index 000000000..a23ae91b3
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts
@@ -0,0 +1,7 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO';
+
+export class GetEntitySponsorshipPricingResultDTO {
+ @ApiProperty({ type: [SponsorshipPricingItemDTO] })
+ pricing: SponsorshipPricingItemDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts
new file mode 100644
index 000000000..9b9ce93a5
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetSponsorDashboardQueryParamsDTO {
+ @ApiProperty()
+ @IsString()
+ sponsorId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts
new file mode 100644
index 000000000..1e9c66e3b
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+
+export class GetSponsorSponsorshipsQueryParamsDTO {
+ @ApiProperty()
+ @IsString()
+ sponsorId: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts
new file mode 100644
index 000000000..82f4f748e
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts
@@ -0,0 +1,7 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { SponsorDTO } from './SponsorDTO';
+
+export class GetSponsorsOutputDTO {
+ @ApiProperty({ type: [SponsorDTO] })
+ sponsors: SponsorDTO[];
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts
new file mode 100644
index 000000000..cea1eab56
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts
@@ -0,0 +1,24 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+import { SponsorDashboardMetricsDTO } from './SponsorDashboardMetricsDTO';
+import { SponsoredLeagueDTO } from './SponsoredLeagueDTO';
+import { SponsorDashboardInvestmentDTO } from './SponsorDashboardInvestmentDTO';
+
+export class SponsorDashboardDTO {
+ @ApiProperty()
+ @IsString()
+ sponsorId: string;
+
+ @ApiProperty()
+ @IsString()
+ sponsorName: string;
+
+ @ApiProperty({ type: SponsorDashboardMetricsDTO })
+ metrics: SponsorDashboardMetricsDTO;
+
+ @ApiProperty({ type: [SponsoredLeagueDTO] })
+ sponsoredLeagues: SponsoredLeagueDTO[];
+
+ @ApiProperty({ type: SponsorDashboardInvestmentDTO })
+ investment: SponsorDashboardInvestmentDTO;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts
new file mode 100644
index 000000000..c2ad67210
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts
@@ -0,0 +1,16 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber } from 'class-validator';
+
+export class SponsorDashboardInvestmentDTO {
+ @ApiProperty()
+ @IsNumber()
+ activeSponsorships: number;
+
+ @ApiProperty()
+ @IsNumber()
+ totalInvestment: number;
+
+ @ApiProperty()
+ @IsNumber()
+ costPerThousandViews: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts
new file mode 100644
index 000000000..60aabc0b0
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts
@@ -0,0 +1,36 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNumber } from 'class-validator';
+
+export class SponsorDashboardMetricsDTO {
+ @ApiProperty()
+ @IsNumber()
+ impressions: number;
+
+ @ApiProperty()
+ @IsNumber()
+ impressionsChange: number;
+
+ @ApiProperty()
+ @IsNumber()
+ uniqueViewers: number;
+
+ @ApiProperty()
+ @IsNumber()
+ viewersChange: number;
+
+ @ApiProperty()
+ @IsNumber()
+ races: number;
+
+ @ApiProperty()
+ @IsNumber()
+ drivers: number;
+
+ @ApiProperty()
+ @IsNumber()
+ exposure: number;
+
+ @ApiProperty()
+ @IsNumber()
+ exposureChange: number;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts
new file mode 100644
index 000000000..8b45bca7f
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts
@@ -0,0 +1,25 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString } from 'class-validator';
+import { SponsorshipDetailDTO } from './SponsorshipDetailDTO';
+
+export class SponsorSponsorshipsDTO {
+ @ApiProperty()
+ @IsString()
+ sponsorId: string;
+
+ @ApiProperty()
+ @IsString()
+ sponsorName: string;
+
+ @ApiProperty({ type: [SponsorshipDetailDTO] })
+ sponsorships: SponsorshipDetailDTO[];
+
+ @ApiProperty()
+ summary: {
+ totalSponsorships: number;
+ activeSponsorships: number;
+ totalInvestment: number;
+ totalPlatformFees: number;
+ currency: string;
+ };
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts
new file mode 100644
index 000000000..081adbe49
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts
@@ -0,0 +1,32 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum, IsNumber } from 'class-validator';
+
+export class SponsoredLeagueDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ name: string;
+
+ @ApiProperty({ enum: ['main', 'secondary'] })
+ @IsEnum(['main', 'secondary'])
+ tier: 'main' | 'secondary';
+
+ @ApiProperty()
+ @IsNumber()
+ drivers: number;
+
+ @ApiProperty()
+ @IsNumber()
+ races: number;
+
+ @ApiProperty()
+ @IsNumber()
+ impressions: number;
+
+ @ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
+ @IsEnum(['active', 'upcoming', 'completed'])
+ status: 'active' | 'upcoming' | 'completed';
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts
new file mode 100644
index 000000000..b37a92cb4
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts
@@ -0,0 +1,76 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsEnum, IsOptional, IsDate } from 'class-validator';
+
+export class SponsorshipDetailDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueId: string;
+
+ @ApiProperty()
+ @IsString()
+ leagueName: string;
+
+ @ApiProperty()
+ @IsString()
+ seasonId: string;
+
+ @ApiProperty()
+ @IsString()
+ seasonName: string;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ seasonStartDate?: Date;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ seasonEndDate?: Date;
+
+ @ApiProperty({ enum: ['main', 'secondary'] })
+ @IsEnum(['main', 'secondary'])
+ tier: 'main' | 'secondary';
+
+ @ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
+ @IsEnum(['pending', 'active', 'expired', 'cancelled'])
+ status: 'pending' | 'active' | 'expired' | 'cancelled';
+
+ @ApiProperty()
+ pricing: {
+ amount: number;
+ currency: string;
+ };
+
+ @ApiProperty()
+ platformFee: {
+ amount: number;
+ currency: string;
+ };
+
+ @ApiProperty()
+ netAmount: {
+ amount: number;
+ currency: string;
+ };
+
+ @ApiProperty()
+ metrics: {
+ drivers: number;
+ races: number;
+ completedRaces: number;
+ impressions: number;
+ };
+
+ @ApiProperty()
+ createdAt: Date;
+
+ @ApiProperty({ required: false })
+ @IsOptional()
+ @IsDate()
+ activatedAt?: Date;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts
new file mode 100644
index 000000000..cce455adc
--- /dev/null
+++ b/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts
@@ -0,0 +1,20 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsString, IsNumber } from 'class-validator';
+
+export class SponsorshipPricingItemDTO {
+ @ApiProperty()
+ @IsString()
+ id: string;
+
+ @ApiProperty()
+ @IsString()
+ level: string;
+
+ @ApiProperty()
+ @IsNumber()
+ price: number;
+
+ @ApiProperty()
+ @IsString()
+ currency: string;
+}
\ No newline at end of file
diff --git a/apps/api/src/domain/team/TeamController.ts b/apps/api/src/domain/team/TeamController.ts
index dd009473d..e07453be8 100644
--- a/apps/api/src/domain/team/TeamController.ts
+++ b/apps/api/src/domain/team/TeamController.ts
@@ -90,4 +90,11 @@ export class TeamController {
async getDriverTeam(@Param('driverId') driverId: string): Promise {
return this.teamService.getDriverTeam({ teamId: '', driverId });
}
+
+ @Get('leaderboard')
+ @ApiOperation({ summary: 'Get teams leaderboard' })
+ @ApiResponse({ status: 200, description: 'Teams leaderboard' })
+ async getTeamsLeaderboard(): Promise {
+ return this.teamService.getTeamsLeaderboard();
+ }
}
diff --git a/apps/api/src/domain/team/TeamProviders.ts b/apps/api/src/domain/team/TeamProviders.ts
index 1af41f624..28f1365e3 100644
--- a/apps/api/src/domain/team/TeamProviders.ts
+++ b/apps/api/src/domain/team/TeamProviders.ts
@@ -24,11 +24,13 @@ import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeam
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { ApproveTeamJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
import { RejectTeamJoinRequestUseCase } from '@core/racing/application/use-cases/RejectTeamJoinRequestUseCase';
+import { GetTeamsLeaderboardUseCase } from '@core/racing/application/use-cases/GetTeamsLeaderboardUseCase';
// Import presenters for use case initialization
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
+import { TeamsLeaderboardPresenter } from './presenters/TeamsLeaderboardPresenter';
// Tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
@@ -44,6 +46,7 @@ export const TEAM_CREATE_USE_CASE_TOKEN = 'CreateTeamUseCase';
export const TEAM_UPDATE_USE_CASE_TOKEN = 'UpdateTeamUseCase';
export const TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN = 'ApproveTeamJoinRequestUseCase';
export const TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN = 'RejectTeamJoinRequestUseCase';
+export const TEAM_GET_LEADERBOARD_USE_CASE_TOKEN = 'GetTeamsLeaderboardUseCase';
export const TEAM_LOGGER_TOKEN = 'Logger';
// Simple image service implementation for team module
@@ -150,4 +153,14 @@ export const TeamProviders: Provider[] = [
new RejectTeamJoinRequestUseCase(membershipRepo),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
+ {
+ provide: TEAM_GET_LEADERBOARD_USE_CASE_TOKEN,
+ useFactory: (
+ teamRepo: ITeamRepository,
+ membershipRepo: ITeamMembershipRepository,
+ driverRepo: IDriverRepository,
+ logger: Logger,
+ ) => new GetTeamsLeaderboardUseCase(teamRepo, membershipRepo, driverRepo, () => null, logger),
+ inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
+ },
];
diff --git a/apps/api/src/domain/team/TeamService.ts b/apps/api/src/domain/team/TeamService.ts
index 8d01bb57b..05a8ff6f7 100644
--- a/apps/api/src/domain/team/TeamService.ts
+++ b/apps/api/src/domain/team/TeamService.ts
@@ -33,6 +33,7 @@ import {
TEAM_UPDATE_USE_CASE_TOKEN,
TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN,
TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN,
+ TEAM_GET_LEADERBOARD_USE_CASE_TOKEN,
TEAM_LOGGER_TOKEN
} from './TeamProviders';
@@ -48,6 +49,7 @@ export class TeamService {
@Inject(TEAM_UPDATE_USE_CASE_TOKEN) private readonly updateTeamUseCase: UpdateTeamUseCase,
@Inject(TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN) private readonly approveTeamJoinRequestUseCase: ApproveTeamJoinRequestUseCase,
@Inject(TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN) private readonly rejectTeamJoinRequestUseCase: RejectTeamJoinRequestUseCase,
+ @Inject(TEAM_GET_LEADERBOARD_USE_CASE_TOKEN) private readonly getTeamsLeaderboardUseCase: GetTeamsLeaderboardUseCase,
@Inject(TEAM_LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -165,4 +167,15 @@ export class TeamService {
throw error;
}
}
+
+ async getTeamsLeaderboard(): Promise {
+ this.logger.debug('[TeamService] Fetching teams leaderboard');
+
+ const result = await this.getTeamsLeaderboardUseCase.execute();
+ if (result.isErr()) {
+ this.logger.error(`Error fetching teams leaderboard: ${result.error}`);
+ throw new Error('Failed to fetch teams leaderboard');
+ }
+ return result.value;
+ }
}
diff --git a/apps/api/src/domain/team/dto/TeamDto.ts b/apps/api/src/domain/team/dtos/TeamDto.ts
similarity index 100%
rename from apps/api/src/domain/team/dto/TeamDto.ts
rename to apps/api/src/domain/team/dtos/TeamDto.ts
diff --git a/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts b/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts
new file mode 100644
index 000000000..7d468d78b
--- /dev/null
+++ b/apps/api/src/domain/team/presenters/TeamsLeaderboardPresenter.ts
@@ -0,0 +1,32 @@
+import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
+
+export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
+ private result: TeamsLeaderboardViewModel | null = null;
+
+ reset() {
+ this.result = null;
+ }
+
+ present(dto: TeamsLeaderboardResultDTO) {
+ this.result = {
+ teams: dto.teams as any, // Cast to match the view model
+ recruitingCount: dto.recruitingCount,
+ groupsBySkillLevel: {
+ beginner: [],
+ intermediate: [],
+ advanced: [],
+ pro: [],
+ },
+ topTeams: dto.teams.slice(0, 10) as any,
+ };
+ }
+
+ getViewModel(): TeamsLeaderboardViewModel | null {
+ return this.result;
+ }
+
+ get viewModel(): TeamsLeaderboardViewModel {
+ if (!this.result) throw new Error('Presenter not presented');
+ return this.result;
+ }
+}
\ No newline at end of file
diff --git a/apps/website/app/dashboard/page.tsx b/apps/website/app/dashboard/page.tsx
index e39cbfd08..8f2d176d0 100644
--- a/apps/website/app/dashboard/page.tsx
+++ b/apps/website/app/dashboard/page.tsx
@@ -26,8 +26,6 @@ import {
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { getAuthService } from '@/lib/auth';
-import { getGetDashboardOverviewUseCase } from '@/lib/di-container';
-import { DashboardOverviewPresenter } from '@/lib/presenters/DashboardOverviewPresenter';
import type {
DashboardOverviewViewModel,
DashboardFeedItemSummaryViewModel,
@@ -94,10 +92,11 @@ export default async function DashboardPage() {
const currentDriverId = session.user.primaryDriverId ?? '';
- const useCase = getGetDashboardOverviewUseCase();
- const presenter = new DashboardOverviewPresenter();
- await useCase.execute({ driverId: currentDriverId }, presenter);
- const viewModel = presenter.getViewModel();
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/races/dashboard/overview?driverId=${currentDriverId}`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch dashboard overview');
+ }
+ const viewModel: DashboardOverviewViewModel = await response.json();
if (!viewModel) {
return null;
diff --git a/apps/website/app/drivers/[id]/page.tsx b/apps/website/app/drivers/[id]/page.tsx
index ff54c64f2..17882b022 100644
--- a/apps/website/app/drivers/[id]/page.tsx
+++ b/apps/website/app/drivers/[id]/page.tsx
@@ -51,7 +51,7 @@ import type { ProfileOverviewViewModel } from '@core/racing/application/presente
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
-import type { GetDriverTeamQueryResultDTO } from '@core/racing/application/dto/TeamCommandAndQueryDTO';
+import type { GetDriverTeamQueryResultDTO } from '@core/racing/application/dtos/GetDriverTeamQueryResultDTO';
import type { TeamMemberViewModel } from '@core/racing/application/presenters/ITeamMembersPresenter';
// ============================================================================
diff --git a/apps/website/app/races/[id]/page.tsx b/apps/website/app/races/[id]/page.tsx
index a9f969d2f..3bf767bee 100644
--- a/apps/website/app/races/[id]/page.tsx
+++ b/apps/website/app/races/[id]/page.tsx
@@ -10,7 +10,7 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs';
import FileProtestModal from '@/components/races/FileProtestModal';
import EndRaceModal from '@/components/leagues/EndRaceModal';
import SponsorInsightsCard, { useSponsorMode, MetricBuilders, SlotTemplates } from '@/components/sponsors/SponsorInsightsCard';
-import { getGetRaceDetailUseCase, getRegisterForRaceUseCase, getWithdrawFromRaceUseCase, getCancelRaceUseCase, getCompleteRaceUseCase } from '@/lib/di-container';
+import { apiClient } from '@/lib/apiClient';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { getMembership, isOwnerOrAdmin } from '@/lib/leagueMembership';
import type {
@@ -61,10 +61,7 @@ export default function RaceDetailPage() {
setLoading(true);
setError(null);
try {
- const useCase = getGetRaceDetailUseCase();
- const presenter = new RaceDetailPresenter();
- await useCase.execute({ raceId, driverId: currentDriverId }, presenter);
- const vm = presenter.getViewModel();
+ const vm = await apiClient.races.getDetail(raceId, currentDriverId);
if (!vm) {
throw new Error('Race detail not available');
}
@@ -130,8 +127,7 @@ export default function RaceDetailPage() {
setCancelling(true);
try {
- const useCase = getCancelRaceUseCase();
- await useCase.execute({ raceId: race.id });
+ await apiClient.races.cancel(race.id);
await loadRaceData();
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to cancel race');
@@ -153,9 +149,7 @@ export default function RaceDetailPage() {
setRegistering(true);
try {
- const useCase = getRegisterForRaceUseCase();
- await useCase.execute({
- raceId: race.id,
+ await apiClient.races.register(race.id, {
leagueId: league.id,
driverId: currentDriverId,
});
@@ -180,9 +174,7 @@ export default function RaceDetailPage() {
setRegistering(true);
try {
- const useCase = getWithdrawFromRaceUseCase();
- await useCase.execute({
- raceId: race.id,
+ await apiClient.races.withdraw(race.id, {
driverId: currentDriverId,
});
await loadRaceData();
@@ -984,8 +976,7 @@ export default function RaceDetailPage() {
raceName={race.track}
onConfirm={async () => {
try {
- const completeRace = getCompleteRaceUseCase();
- await completeRace.execute({ raceId: race.id });
+ await apiClient.races.complete(race.id);
await loadRaceData();
setShowEndRaceModal(false);
} catch (err) {
diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx
index b0b7dc455..f7e9a7adb 100644
--- a/apps/website/app/races/[id]/results/page.tsx
+++ b/apps/website/app/races/[id]/results/page.tsx
@@ -9,20 +9,10 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs';
import ResultsTable from '@/components/races/ResultsTable';
import ImportResultsForm from '@/components/races/ImportResultsForm';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
-import {
- getGetRaceWithSOFUseCase,
- getGetRaceResultsDetailUseCase,
- getImportRaceResultsUseCase,
- getLeagueMembershipRepository,
-} from '@/lib/di-container';
+import { apiClient } from '@/lib/apiClient';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
-import { RaceWithSOFPresenter } from '@/lib/presenters/RaceWithSOFPresenter';
-import { RaceResultsDetailPresenter } from '@/lib/presenters/RaceResultsDetailPresenter';
-import type {
- RaceResultsHeaderViewModel,
- RaceResultsLeagueViewModel,
-} from '@core/racing/application/presenters/IRaceResultsDetailPresenter';
+import type { RaceResultsDetailViewModel, RaceWithSOFViewModel } from '@/lib/apiClient';
type PenaltyTypeDTO =
| 'time_penalty'
@@ -71,14 +61,8 @@ export default function RaceResultsPage() {
const raceId = params.id as string;
const currentDriverId = useEffectiveDriverId();
- const [race, setRace] = useState(null);
- const [league, setLeague] = useState(null);
- const [results, setResults] = useState([]);
- const [drivers, setDrivers] = useState([]);
+ const [raceData, setRaceData] = useState(null);
const [raceSOF, setRaceSOF] = useState(null);
- const [penalties, setPenalties] = useState([]);
- const [pointsSystem, setPointsSystem] = useState | undefined>(undefined);
- const [fastestLapTime, setFastestLapTime] = useState(undefined);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [importing, setImporting] = useState(false);
@@ -89,66 +73,19 @@ export default function RaceResultsPage() {
const loadData = async () => {
try {
- const raceResultsUseCase = getGetRaceResultsDetailUseCase();
- const raceResultsPresenter = new RaceResultsDetailPresenter();
- await raceResultsUseCase.execute({ raceId }, raceResultsPresenter);
-
- const viewModel = raceResultsPresenter.getViewModel();
-
- if (!viewModel) {
- setError('Failed to load race data');
- setLoading(false);
- return;
- }
-
- if (viewModel.error && !viewModel.race) {
- setError(viewModel.error);
- setRace(null);
- setLeague(null);
- setResults([]);
- setDrivers([]);
- setPenalties([]);
- setPointsSystem({});
- setFastestLapTime(undefined);
- } else {
- setError(null);
- setRace(viewModel.race);
- setLeague(viewModel.league);
- setResults(viewModel.results as unknown as RaceResultRowDTO[]);
- setDrivers(
- viewModel.drivers.map((d) => ({
- id: d.id,
- name: d.name,
- })),
- );
- setPointsSystem(viewModel.pointsSystem);
- setFastestLapTime(viewModel.fastestLapTime);
- const mappedPenalties: PenaltyData[] = viewModel.penalties.map((p) => {
- const base: PenaltyData = {
- driverId: p.driverId,
- type: p.type as PenaltyTypeDTO,
- };
- if (typeof p.value === 'number') {
- return { ...base, value: p.value };
- }
- return base;
- });
- setPenalties(mappedPenalties);
- }
+ const raceData = await apiClient.races.getResultsDetail(raceId);
+ setRaceData(raceData);
+ setError(null);
try {
- const raceWithSOFUseCase = getGetRaceWithSOFUseCase();
- const sofPresenter = new RaceWithSOFPresenter();
- await raceWithSOFUseCase.execute({ raceId }, sofPresenter);
- const raceViewModel = sofPresenter.getViewModel();
- if (raceViewModel) {
- setRaceSOF(raceViewModel.strengthOfField);
- }
+ const sofData = await apiClient.races.getWithSOF(raceId);
+ setRaceSOF(sofData.strengthOfField);
} catch (sofErr) {
console.error('Failed to load SOF:', sofErr);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load race data');
+ setRaceData(null);
} finally {
setLoading(false);
}
@@ -160,25 +97,22 @@ export default function RaceResultsPage() {
}, [raceId]);
useEffect(() => {
- if (league?.id && currentDriverId) {
+ if (raceData?.league?.id && currentDriverId) {
const checkAdmin = async () => {
- const membershipRepo = getLeagueMembershipRepository();
- const membership = await membershipRepo.getMembership(league.id, currentDriverId);
- setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
+ // For now, assume admin check - this might need to be updated based on API
+ setIsAdmin(true); // TODO: Implement proper admin check via API
};
checkAdmin();
}
- }, [league?.id, currentDriverId]);
+ }, [raceData?.league?.id, currentDriverId]);
const handleImportSuccess = async (importedResults: ImportResultRowDTO[]) => {
setImporting(true);
setError(null);
try {
- const importUseCase = getImportRaceResultsUseCase();
- await importUseCase.execute({
- raceId,
- results: importedResults,
+ await apiClient.races.importResults(raceId, {
+ resultsFileContent: JSON.stringify(importedResults), // Assuming the API expects JSON string
});
setImportSuccess(true);
@@ -214,7 +148,7 @@ export default function RaceResultsPage() {
);
}
- if (error && !race) {
+ if (error && !raceData) {
return (
@@ -229,12 +163,12 @@ export default function RaceResultsPage() {
Back to Races
-
+
{showQuickPenaltyModal && (
0;
+ const hasResults = raceData?.results.length ?? 0 > 0;
const breadcrumbItems = [
{ label: 'Races', href: '/races' },
- ...(league ? [{ label: league.name, href: `/leagues/${league.id}` }] : []),
- ...(race ? [{ label: race.track, href: `/races/${race.id}` }] : []),
+ ...(raceData?.league ? [{ label: raceData.league.name, href: `/leagues/${raceData.league.id}` }] : []),
+ ...(raceData?.race ? [{ label: raceData.race.track, href: `/races/${raceData.race.id}` }] : []),
{ label: 'Results' },
];
@@ -291,15 +225,15 @@ export default function RaceResultsPage() {
- {race?.track ?? 'Race'} Results
+ {raceData?.race?.track ?? 'Race'} Results
- {race && (
+ {raceData?.race && (
<>
- {new Date(race.scheduledAt).toLocaleDateString('en-US', {
+ {new Date(raceData.race.scheduledAt).toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
@@ -307,11 +241,11 @@ export default function RaceResultsPage() {
- {results.length} drivers classified
+ {raceData.results.length} drivers classified
>
)}
- {league && {league.name}}
+ {raceData?.league && {raceData.league.name}}
@@ -329,14 +263,14 @@ export default function RaceResultsPage() {
)}
- {hasResults ? (
+ {hasResults && raceData ? (
diff --git a/apps/website/app/races/[id]/stewarding/page.tsx b/apps/website/app/races/[id]/stewarding/page.tsx
index 2616cfe86..fed69e5f5 100644
--- a/apps/website/app/races/[id]/stewarding/page.tsx
+++ b/apps/website/app/races/[id]/stewarding/page.tsx
@@ -22,21 +22,10 @@ import {
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
-import {
- getGetRaceProtestsUseCase,
- getGetRacePenaltiesUseCase,
- getRaceRepository,
- getLeagueRepository,
- getLeagueMembershipRepository,
-} from '@/lib/di-container';
+import { apiClient } from '@/lib/apiClient';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles';
-import { RaceProtestsPresenter } from '@/lib/presenters/RaceProtestsPresenter';
-import { RacePenaltiesPresenter } from '@/lib/presenters/RacePenaltiesPresenter';
-import type { RaceProtestViewModel } from '@core/racing/application/presenters/IRaceProtestsPresenter';
-import type { RacePenaltyViewModel } from '@core/racing/application/presenters/IRacePenaltiesPresenter';
-import type { League } from '@core/racing/domain/entities/League';
-import type { Race } from '@core/racing/domain/entities/Race';
+import type { RaceProtestsViewModel, RacePenaltiesViewModel } from '@/lib/apiClient';
export default function RaceStewardingPage() {
const params = useParams();
@@ -44,12 +33,10 @@ export default function RaceStewardingPage() {
const raceId = params.id as string;
const currentDriverId = useEffectiveDriverId();
- const driversById: Record = {};
-
- const [race, setRace] = useState(null);
- const [league, setLeague] = useState(null);
- const [protests, setProtests] = useState([]);
- const [penalties, setPenalties] = useState([]);
+ const [race, setRace] = useState(null); // TODO: Define proper race type
+ const [league, setLeague] = useState(null); // TODO: Define proper league type
+ const [protestsData, setProtestsData] = useState(null);
+ const [penaltiesData, setPenaltiesData] = useState(null);
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [activeTab, setActiveTab] = useState<'pending' | 'resolved' | 'penalties'>('pending');
@@ -58,39 +45,24 @@ export default function RaceStewardingPage() {
async function loadData() {
setLoading(true);
try {
- const raceRepo = getRaceRepository();
- const leagueRepo = getLeagueRepository();
- const membershipRepo = getLeagueMembershipRepository();
- const protestsUseCase = getGetRaceProtestsUseCase();
- const penaltiesUseCase = getGetRacePenaltiesUseCase();
+ // Get race detail for basic info
+ const raceDetail = await apiClient.races.getDetail(raceId, currentDriverId);
+ setRace(raceDetail.race);
+ setLeague(raceDetail.league);
- const raceData = await raceRepo.findById(raceId);
- if (!raceData) {
- setLoading(false);
- return;
- }
- setRace(raceData);
-
- const leagueData = await leagueRepo.findById(raceData.leagueId);
- setLeague(leagueData);
-
- if (leagueData) {
- const membership = await membershipRepo.getMembership(
- leagueData.id,
- currentDriverId,
- );
- setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false);
+ if (raceDetail.league) {
+ // TODO: Implement admin check via API
+ setIsAdmin(true);
}
- const protestsPresenter = new RaceProtestsPresenter();
- await protestsUseCase.execute({ raceId }, protestsPresenter);
- const protestsViewModel = protestsPresenter.getViewModel();
- setProtests(protestsViewModel?.protests ?? []);
+ // Get protests and penalties
+ const [protestsData, penaltiesData] = await Promise.all([
+ apiClient.races.getProtests(raceId),
+ apiClient.races.getPenalties(raceId),
+ ]);
- const penaltiesPresenter = new RacePenaltiesPresenter();
- await penaltiesUseCase.execute({ raceId }, penaltiesPresenter);
- const penaltiesViewModel = penaltiesPresenter.getViewModel();
- setPenalties(penaltiesViewModel?.penalties ?? []);
+ setProtestsData(protestsData);
+ setPenaltiesData(penaltiesData);
} catch (err) {
console.error('Failed to load data:', err);
} finally {
@@ -101,15 +73,15 @@ export default function RaceStewardingPage() {
loadData();
}, [raceId, currentDriverId]);
- const pendingProtests = protests.filter(
+ const pendingProtests = protestsData?.protests.filter(
(p) => p.status === 'pending' || p.status === 'under_review',
- );
- const resolvedProtests = protests.filter(
+ ) ?? [];
+ const resolvedProtests = protestsData?.protests.filter(
(p) =>
p.status === 'upheld' ||
p.status === 'dismissed' ||
p.status === 'withdrawn',
- );
+ ) ?? [];
const getStatusBadge = (status: string) => {
switch (status) {
@@ -247,7 +219,7 @@ export default function RaceStewardingPage() {
Penalties
- {penalties.length}
+ {penaltiesData?.penalties.length ?? 0}
@@ -306,8 +278,8 @@ export default function RaceStewardingPage() {
) : (
pendingProtests.map((protest) => {
- const protester = driversById[protest.protestingDriverId];
- const accused = driversById[protest.accusedDriverId];
+ const protester = protestsData?.driverMap[protest.protestingDriverId];
+ const accused = protestsData?.driverMap[protest.accusedDriverId];
const daysSinceFiled = Math.floor(
(Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24)
);
@@ -393,8 +365,8 @@ export default function RaceStewardingPage() {
) : (
resolvedProtests.map((protest) => {
- const protester = driversById[protest.protestingDriverId];
- const accused = driversById[protest.accusedDriverId];
+ const protester = protestsData?.driverMap[protest.protestingDriverId];
+ const accused = protestsData?.driverMap[protest.accusedDriverId];
return (
@@ -455,8 +427,8 @@ export default function RaceStewardingPage() {
) : (
- penalties.map((penalty) => {
- const driver = driversById[penalty.driverId];
+ penaltiesData?.penalties.map((penalty) => {
+ const driver = penaltiesData?.driverMap[penalty.driverId];
return (
diff --git a/apps/website/app/races/all/page.tsx b/apps/website/app/races/all/page.tsx
index 50725f48e..299c0c48f 100644
--- a/apps/website/app/races/all/page.tsx
+++ b/apps/website/app/races/all/page.tsx
@@ -7,12 +7,8 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
-import { getGetAllRacesPageDataUseCase } from '@/lib/di-container';
-import { AllRacesPagePresenter } from '@/lib/presenters/AllRacesPagePresenter';
-import type {
- AllRacesPageViewModel,
- AllRacesListItemViewModel,
-} from '@core/racing/application/presenters/IAllRacesPagePresenter';
+import { apiClient } from '@/lib/apiClient';
+import type { RacesPageDataViewModel, RacesPageDataRaceViewModel } from '@/lib/apiClient';
import {
Calendar,
Clock,
@@ -39,7 +35,7 @@ export default function AllRacesPage() {
const router = useRouter();
const searchParams = useSearchParams();
- const [pageData, setPageData] = useState
(null);
+ const [pageData, setPageData] = useState(null);
const [loading, setLoading] = useState(true);
// Pagination
@@ -53,10 +49,7 @@ export default function AllRacesPage() {
const loadRaces = async () => {
try {
- const useCase = getGetAllRacesPageDataUseCase();
- const presenter = new AllRacesPagePresenter();
- await useCase.execute(undefined, presenter);
- const viewModel = presenter.getViewModel();
+ const viewModel = await apiClient.races.getAllPageData();
setPageData(viewModel);
} catch (err) {
console.error('Failed to load races:', err);
@@ -69,7 +62,7 @@ export default function AllRacesPage() {
void loadRaces();
}, []);
- const races: AllRacesListItemViewModel[] = pageData?.races ?? [];
+ const races: RacesPageDataRaceViewModel[] = pageData?.races ?? [];
const filteredRaces = useMemo(() => {
return races.filter(race => {
@@ -244,11 +237,14 @@ export default function AllRacesPage() {
className="px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
>
- {pageData?.filters.leagues.map((league) => (
-
- ))}
+ {pageData && [...new Set(pageData.races.map(r => r.leagueId))].map(leagueId => {
+ const race = pageData.races.find(r => r.leagueId === leagueId);
+ return race ? (
+
+ ) : null;
+ })}
{/* Clear Filters */}
diff --git a/apps/website/app/races/page.tsx b/apps/website/app/races/page.tsx
index f43995558..9eb2a8d22 100644
--- a/apps/website/app/races/page.tsx
+++ b/apps/website/app/races/page.tsx
@@ -6,12 +6,8 @@ import Link from 'next/link';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
-import { getGetRacesPageDataUseCase } from '@/lib/di-container';
-import { RacesPagePresenter } from '@/lib/presenters/RacesPagePresenter';
-import type {
- RacesPageViewModel,
- RaceListItemViewModel,
-} from '@core/racing/application/presenters/IRacesPagePresenter';
+import { apiClient } from '@/lib/apiClient';
+import type { RacesPageDataViewModel, RacesPageDataRaceViewModel } from '@/lib/apiClient';
import {
Calendar,
Clock,
@@ -31,14 +27,14 @@ import {
} from 'lucide-react';
type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
-type RaceStatusFilter = RaceListItemViewModel['status'];
+type RaceStatusFilter = RacesPageDataRaceViewModel['status'];
export default function RacesPage() {
const router = useRouter();
-
- const [pageData, setPageData] = useState(null);
+
+ const [pageData, setPageData] = useState(null);
const [loading, setLoading] = useState(true);
-
+
// Filters
const [statusFilter, setStatusFilter] = useState('all');
const [leagueFilter, setLeagueFilter] = useState('all');
@@ -46,10 +42,7 @@ export default function RacesPage() {
const loadRaces = async () => {
try {
- const useCase = getGetRacesPageDataUseCase();
- const presenter = new RacesPagePresenter();
- await useCase.execute(undefined, presenter);
- const data = presenter.getViewModel();
+ const data = await apiClient.races.getPageData();
setPageData(data);
} catch (err) {
console.error('Failed to load races:', err);
@@ -65,7 +58,7 @@ export default function RacesPage() {
// Filter races
const filteredRaces = useMemo(() => {
if (!pageData) return [];
-
+
return pageData.races.filter((race) => {
// Status filter
if (statusFilter !== 'all' && race.status !== statusFilter) {
@@ -94,7 +87,7 @@ export default function RacesPage() {
// Group races by date for calendar view
const racesByDate = useMemo(() => {
- const grouped = new Map();
+ const grouped = new Map();
filteredRaces.forEach((race) => {
if (typeof race.scheduledAt !== 'string') {
return;
@@ -107,11 +100,16 @@ export default function RacesPage() {
});
return grouped;
}, [filteredRaces]);
-
- const upcomingRaces = pageData?.upcomingThisWeek ?? [];
- const liveRaces = pageData?.liveRaces ?? [];
- const recentResults = pageData?.recentResults ?? [];
- const stats = pageData?.stats ?? { total: 0, scheduled: 0, running: 0, completed: 0 };
+
+ const upcomingRaces = filteredRaces.filter(r => r.isUpcoming).slice(0, 5);
+ const liveRaces = filteredRaces.filter(r => r.isLive);
+ const recentResults = filteredRaces.filter(r => r.isPast).slice(0, 5);
+ const stats = {
+ total: pageData?.races.length ?? 0,
+ scheduled: pageData?.races.filter(r => r.status === 'scheduled').length ?? 0,
+ running: pageData?.races.filter(r => r.status === 'running').length ?? 0,
+ completed: pageData?.races.filter(r => r.status === 'completed').length ?? 0,
+ };
const formatDate = (date: Date | string) => {
const d = typeof date === 'string' ? new Date(date) : date;
diff --git a/apps/website/lib/apiClient.ts b/apps/website/lib/apiClient.ts
index 9dafc9194..e85afafdb 100644
--- a/apps/website/lib/apiClient.ts
+++ b/apps/website/lib/apiClient.ts
@@ -291,6 +291,161 @@ export interface RaceStatsDto {
totalRaces: number;
}
+// Race Management Types
+export interface RaceDetailEntryViewModel {
+ id: string;
+ name: string;
+ country: string;
+ avatarUrl: string;
+ rating: number | null;
+ isCurrentUser: boolean;
+}
+
+export interface RaceDetailUserResultViewModel {
+ position: number;
+ startPosition: number;
+ incidents: number;
+ fastestLap: number;
+ positionChange: number;
+ isPodium: boolean;
+ isClean: boolean;
+ ratingChange: number | null;
+}
+
+export interface RaceDetailRaceViewModel {
+ id: string;
+ leagueId: string;
+ track: string;
+ car: string;
+ scheduledAt: string;
+ sessionType: string;
+ status: string;
+ strengthOfField: number | null;
+ registeredCount?: number;
+ maxParticipants?: number;
+}
+
+export interface RaceDetailLeagueViewModel {
+ id: string;
+ name: string;
+ description: string;
+ settings: {
+ maxDrivers?: number;
+ qualifyingFormat?: string;
+ };
+}
+
+export interface RaceDetailRegistrationViewModel {
+ isUserRegistered: boolean;
+ canRegister: boolean;
+}
+
+export interface RaceDetailViewModel {
+ race: RaceDetailRaceViewModel | null;
+ league: RaceDetailLeagueViewModel | null;
+ entryList: RaceDetailEntryViewModel[];
+ registration: RaceDetailRegistrationViewModel;
+ userResult: RaceDetailUserResultViewModel | null;
+ error?: string;
+}
+
+export interface RacesPageDataRaceViewModel {
+ id: string;
+ track: string;
+ car: string;
+ scheduledAt: string;
+ status: string;
+ leagueId: string;
+ leagueName: string;
+ strengthOfField: number | null;
+ isUpcoming: boolean;
+ isLive: boolean;
+ isPast: boolean;
+}
+
+export interface RacesPageDataViewModel {
+ races: RacesPageDataRaceViewModel[];
+}
+
+export interface RaceResultViewModel {
+ driverId: string;
+ driverName: string;
+ avatarUrl: string;
+ position: number;
+ startPosition: number;
+ incidents: number;
+ fastestLap: number;
+ positionChange: number;
+ isPodium: boolean;
+ isClean: boolean;
+}
+
+export interface RaceResultsDetailViewModel {
+ raceId: string;
+ track: string;
+ results: RaceResultViewModel[];
+}
+
+export interface RaceWithSOFViewModel {
+ id: string;
+ track: string;
+ strengthOfField: number | null;
+}
+
+export interface RaceProtestViewModel {
+ id: string;
+ protestingDriverId: string;
+ accusedDriverId: string;
+ incident: {
+ lap: number;
+ description: string;
+ };
+ status: string;
+ filedAt: string;
+}
+
+export interface RaceProtestsViewModel {
+ protests: RaceProtestViewModel[];
+ driverMap: Record;
+}
+
+export interface RacePenaltyViewModel {
+ id: string;
+ driverId: string;
+ type: string;
+ value: number;
+ reason: string;
+ issuedBy: string;
+ issuedAt: string;
+ notes?: string;
+}
+
+export interface RacePenaltiesViewModel {
+ penalties: RacePenaltyViewModel[];
+ driverMap: Record;
+}
+
+export interface RegisterForRaceParams {
+ leagueId: string;
+ driverId: string;
+}
+
+export interface WithdrawFromRaceParams {
+ driverId: string;
+}
+
+export interface ImportRaceResultsInput {
+ resultsFileContent: string;
+}
+
+export interface ImportRaceResultsSummaryViewModel {
+ success: boolean;
+ raceId: string;
+ driversProcessed: number;
+ resultsRecorded: number;
+ errors?: string[];
+}
+
// Sponsor Types
export interface GetEntitySponsorshipPricingResultDto {
mainSlotPrice: number;
@@ -738,6 +893,66 @@ class RacesApiClient extends BaseApiClient {
getTotal(): Promise {
return this.get('/races/total-races');
}
+
+ /** Get races page data */
+ getPageData(): Promise {
+ return this.get('/races/page-data');
+ }
+
+ /** Get all races page data */
+ getAllPageData(): Promise {
+ return this.get('/races/all/page-data');
+ }
+
+ /** Get race detail */
+ getDetail(raceId: string, driverId: string): Promise {
+ return this.get(`/races/${raceId}?driverId=${driverId}`);
+ }
+
+ /** Get race results detail */
+ getResultsDetail(raceId: string): Promise {
+ return this.get(`/races/${raceId}/results`);
+ }
+
+ /** Get race with strength of field */
+ getWithSOF(raceId: string): Promise {
+ return this.get(`/races/${raceId}/sof`);
+ }
+
+ /** Get race protests */
+ getProtests(raceId: string): Promise {
+ return this.get(`/races/${raceId}/protests`);
+ }
+
+ /** Get race penalties */
+ getPenalties(raceId: string): Promise {
+ return this.get(`/races/${raceId}/penalties`);
+ }
+
+ /** Register for race */
+ register(raceId: string, params: RegisterForRaceParams): Promise {
+ return this.post(`/races/${raceId}/register`, params);
+ }
+
+ /** Withdraw from race */
+ withdraw(raceId: string, params: WithdrawFromRaceParams): Promise {
+ return this.post(`/races/${raceId}/withdraw`, params);
+ }
+
+ /** Cancel race */
+ cancel(raceId: string): Promise {
+ return this.post(`/races/${raceId}/cancel`, {});
+ }
+
+ /** Complete race */
+ complete(raceId: string): Promise {
+ return this.post(`/races/${raceId}/complete`, {});
+ }
+
+ /** Import race results */
+ importResults(raceId: string, input: ImportRaceResultsInput): Promise {
+ return this.post(`/races/${raceId}/import-results`, input);
+ }
}
class SponsorsApiClient extends BaseApiClient {
diff --git a/core/racing/application/dto/TeamCommandAndQueryDTO.ts b/core/racing/application/dto/TeamCommandAndQueryDTO.ts
deleted file mode 100644
index e8a861073..000000000
--- a/core/racing/application/dto/TeamCommandAndQueryDTO.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Team } from '../../domain/entities/Team';
-import type { TeamMembership } from '../../domain/types/TeamMembership';
-
-export interface JoinTeamCommandDTO {
- teamId: string;
- driverId: string;
-}
-
-export interface LeaveTeamCommandDTO {
- teamId: string;
- driverId: string;
-}
-
-export interface ApproveTeamJoinRequestCommandDTO {
- teamId: string;
- requestId: string;
-}
-
-export interface RejectTeamJoinRequestCommandDTO {
- requestId: string;
-}
-
-export interface UpdateTeamCommandDTO {
- teamId: string;
- updates: Partial>;
- updatedBy: string;
-}
-
-export type GetAllTeamsQueryResultDTO = Team[];
-
-export interface GetTeamDetailsQueryParamsDTO {
- teamId: string;
- driverId: string;
-}
-
-export interface GetTeamDetailsQueryResultDTO {
- team: Team;
- membership: TeamMembership | null;
-}
-
-export interface GetTeamMembersQueryParamsDTO {
- teamId: string;
-}
-
-export interface GetTeamJoinRequestsQueryParamsDTO {
- teamId: string;
-}
-
-export interface GetDriverTeamQueryParamsDTO {
- driverId: string;
-}
-
-export interface GetDriverTeamQueryResultDTO {
- team: Team;
- membership: TeamMembership;
-}
\ No newline at end of file
diff --git a/core/racing/application/dto/AcceptSponsorshipRequestDTO.ts b/core/racing/application/dtos/AcceptSponsorshipRequestDTO.ts
similarity index 100%
rename from core/racing/application/dto/AcceptSponsorshipRequestDTO.ts
rename to core/racing/application/dtos/AcceptSponsorshipRequestDTO.ts
diff --git a/core/racing/application/dto/AcceptSponsorshipRequestResultDTO.ts b/core/racing/application/dtos/AcceptSponsorshipRequestResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/AcceptSponsorshipRequestResultDTO.ts
rename to core/racing/application/dtos/AcceptSponsorshipRequestResultDTO.ts
diff --git a/core/racing/application/dto/ApplyForSponsorshipDTO.ts b/core/racing/application/dtos/ApplyForSponsorshipDTO.ts
similarity index 100%
rename from core/racing/application/dto/ApplyForSponsorshipDTO.ts
rename to core/racing/application/dtos/ApplyForSponsorshipDTO.ts
diff --git a/core/racing/application/dto/ApplyForSponsorshipResultDTO.ts b/core/racing/application/dtos/ApplyForSponsorshipResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/ApplyForSponsorshipResultDTO.ts
rename to core/racing/application/dtos/ApplyForSponsorshipResultDTO.ts
diff --git a/core/racing/application/dto/ApplyPenaltyCommand.ts b/core/racing/application/dtos/ApplyPenaltyCommand.ts
similarity index 100%
rename from core/racing/application/dto/ApplyPenaltyCommand.ts
rename to core/racing/application/dtos/ApplyPenaltyCommand.ts
diff --git a/core/racing/application/dto/ApproveLeagueJoinRequestResultDTO.ts b/core/racing/application/dtos/ApproveLeagueJoinRequestResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/ApproveLeagueJoinRequestResultDTO.ts
rename to core/racing/application/dtos/ApproveLeagueJoinRequestResultDTO.ts
diff --git a/core/racing/application/dto/ApproveLeagueJoinRequestUseCaseParams.ts b/core/racing/application/dtos/ApproveLeagueJoinRequestUseCaseParams.ts
similarity index 100%
rename from core/racing/application/dto/ApproveLeagueJoinRequestUseCaseParams.ts
rename to core/racing/application/dtos/ApproveLeagueJoinRequestUseCaseParams.ts
diff --git a/core/racing/application/dtos/ApproveTeamJoinRequestCommandDTO.ts b/core/racing/application/dtos/ApproveTeamJoinRequestCommandDTO.ts
new file mode 100644
index 000000000..af6b2c2c2
--- /dev/null
+++ b/core/racing/application/dtos/ApproveTeamJoinRequestCommandDTO.ts
@@ -0,0 +1,4 @@
+export interface ApproveTeamJoinRequestCommandDTO {
+ teamId: string;
+ requestId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/CancelRaceCommandDTO.ts b/core/racing/application/dtos/CancelRaceCommandDTO.ts
similarity index 100%
rename from core/racing/application/dto/CancelRaceCommandDTO.ts
rename to core/racing/application/dtos/CancelRaceCommandDTO.ts
diff --git a/core/racing/application/dto/ChampionshipStandingsDTO.ts b/core/racing/application/dtos/ChampionshipStandingsDTO.ts
similarity index 100%
rename from core/racing/application/dto/ChampionshipStandingsDTO.ts
rename to core/racing/application/dtos/ChampionshipStandingsDTO.ts
diff --git a/core/racing/application/dto/CloseRaceEventStewardingCommand.ts b/core/racing/application/dtos/CloseRaceEventStewardingCommand.ts
similarity index 100%
rename from core/racing/application/dto/CloseRaceEventStewardingCommand.ts
rename to core/racing/application/dtos/CloseRaceEventStewardingCommand.ts
diff --git a/core/racing/application/dto/CompleteDriverOnboardingCommand.ts b/core/racing/application/dtos/CompleteDriverOnboardingCommand.ts
similarity index 100%
rename from core/racing/application/dto/CompleteDriverOnboardingCommand.ts
rename to core/racing/application/dtos/CompleteDriverOnboardingCommand.ts
diff --git a/core/racing/application/dto/CompleteRaceCommandDTO.ts b/core/racing/application/dtos/CompleteRaceCommandDTO.ts
similarity index 100%
rename from core/racing/application/dto/CompleteRaceCommandDTO.ts
rename to core/racing/application/dtos/CompleteRaceCommandDTO.ts
diff --git a/core/racing/application/dto/CreateLeagueWithSeasonAndScoringCommand.ts b/core/racing/application/dtos/CreateLeagueWithSeasonAndScoringCommand.ts
similarity index 100%
rename from core/racing/application/dto/CreateLeagueWithSeasonAndScoringCommand.ts
rename to core/racing/application/dtos/CreateLeagueWithSeasonAndScoringCommand.ts
diff --git a/core/racing/application/dto/CreateLeagueWithSeasonAndScoringResultDTO.ts b/core/racing/application/dtos/CreateLeagueWithSeasonAndScoringResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/CreateLeagueWithSeasonAndScoringResultDTO.ts
rename to core/racing/application/dtos/CreateLeagueWithSeasonAndScoringResultDTO.ts
diff --git a/core/racing/application/dto/CreateSponsorCommand.ts b/core/racing/application/dtos/CreateSponsorCommand.ts
similarity index 100%
rename from core/racing/application/dto/CreateSponsorCommand.ts
rename to core/racing/application/dtos/CreateSponsorCommand.ts
diff --git a/core/racing/application/dto/CreateSponsorResultDTO.ts b/core/racing/application/dtos/CreateSponsorResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/CreateSponsorResultDTO.ts
rename to core/racing/application/dtos/CreateSponsorResultDTO.ts
diff --git a/core/racing/application/dto/CreateTeamCommandDTO.ts b/core/racing/application/dtos/CreateTeamCommandDTO.ts
similarity index 54%
rename from core/racing/application/dto/CreateTeamCommandDTO.ts
rename to core/racing/application/dtos/CreateTeamCommandDTO.ts
index 6bca6d299..90ef84315 100644
--- a/core/racing/application/dto/CreateTeamCommandDTO.ts
+++ b/core/racing/application/dtos/CreateTeamCommandDTO.ts
@@ -1,13 +1,7 @@
-import type { Team } from '../../domain/entities/Team';
-
export interface CreateTeamCommandDTO {
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
-}
-
-export interface CreateTeamResultDTO {
- team: Team;
}
\ No newline at end of file
diff --git a/core/racing/application/dtos/CreateTeamResultDTO.ts b/core/racing/application/dtos/CreateTeamResultDTO.ts
new file mode 100644
index 000000000..6d3584fee
--- /dev/null
+++ b/core/racing/application/dtos/CreateTeamResultDTO.ts
@@ -0,0 +1,5 @@
+import type { Team } from '../../domain/entities/Team';
+
+export interface CreateTeamResultDTO {
+ team: Team;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/DashboardOverviewParams.ts b/core/racing/application/dtos/DashboardOverviewParams.ts
similarity index 100%
rename from core/racing/application/dto/DashboardOverviewParams.ts
rename to core/racing/application/dtos/DashboardOverviewParams.ts
diff --git a/core/racing/application/dto/DriverDTO.ts b/core/racing/application/dtos/DriverDTO.ts
similarity index 100%
rename from core/racing/application/dto/DriverDTO.ts
rename to core/racing/application/dtos/DriverDTO.ts
diff --git a/core/racing/application/dto/FileProtestCommand.ts b/core/racing/application/dtos/FileProtestCommand.ts
similarity index 100%
rename from core/racing/application/dto/FileProtestCommand.ts
rename to core/racing/application/dtos/FileProtestCommand.ts
diff --git a/core/racing/application/dtos/GetAllTeamsQueryResultDTO.ts b/core/racing/application/dtos/GetAllTeamsQueryResultDTO.ts
new file mode 100644
index 000000000..15f92139b
--- /dev/null
+++ b/core/racing/application/dtos/GetAllTeamsQueryResultDTO.ts
@@ -0,0 +1,3 @@
+import type { Team } from '../../domain/entities/Team';
+
+export type GetAllTeamsQueryResultDTO = Team[];
\ No newline at end of file
diff --git a/core/racing/application/dtos/GetDriverTeamQueryParamsDTO.ts b/core/racing/application/dtos/GetDriverTeamQueryParamsDTO.ts
new file mode 100644
index 000000000..e794a2ca4
--- /dev/null
+++ b/core/racing/application/dtos/GetDriverTeamQueryParamsDTO.ts
@@ -0,0 +1,3 @@
+export interface GetDriverTeamQueryParamsDTO {
+ driverId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dtos/GetDriverTeamQueryResultDTO.ts b/core/racing/application/dtos/GetDriverTeamQueryResultDTO.ts
new file mode 100644
index 000000000..f4a7d6db2
--- /dev/null
+++ b/core/racing/application/dtos/GetDriverTeamQueryResultDTO.ts
@@ -0,0 +1,7 @@
+import type { Team } from '../../domain/entities/Team';
+import type { TeamMembership } from '../../domain/types/TeamMembership';
+
+export interface GetDriverTeamQueryResultDTO {
+ team: Team;
+ membership: TeamMembership;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/GetEntitySponsorshipPricingDTO.ts b/core/racing/application/dtos/GetEntitySponsorshipPricingDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetEntitySponsorshipPricingDTO.ts
rename to core/racing/application/dtos/GetEntitySponsorshipPricingDTO.ts
diff --git a/core/racing/application/dto/GetEntitySponsorshipPricingResultDTO.ts b/core/racing/application/dtos/GetEntitySponsorshipPricingResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetEntitySponsorshipPricingResultDTO.ts
rename to core/racing/application/dtos/GetEntitySponsorshipPricingResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueAdminPermissionsResultDTO.ts b/core/racing/application/dtos/GetLeagueAdminPermissionsResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueAdminPermissionsResultDTO.ts
rename to core/racing/application/dtos/GetLeagueAdminPermissionsResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueAdminResultDTO.ts b/core/racing/application/dtos/GetLeagueAdminResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueAdminResultDTO.ts
rename to core/racing/application/dtos/GetLeagueAdminResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueJoinRequestsResultDTO.ts b/core/racing/application/dtos/GetLeagueJoinRequestsResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueJoinRequestsResultDTO.ts
rename to core/racing/application/dtos/GetLeagueJoinRequestsResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueJoinRequestsUseCaseParams.ts b/core/racing/application/dtos/GetLeagueJoinRequestsUseCaseParams.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueJoinRequestsUseCaseParams.ts
rename to core/racing/application/dtos/GetLeagueJoinRequestsUseCaseParams.ts
diff --git a/core/racing/application/dto/GetLeagueMembershipsResultDTO.ts b/core/racing/application/dtos/GetLeagueMembershipsResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueMembershipsResultDTO.ts
rename to core/racing/application/dtos/GetLeagueMembershipsResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueOwnerSummaryResultDTO.ts b/core/racing/application/dtos/GetLeagueOwnerSummaryResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueOwnerSummaryResultDTO.ts
rename to core/racing/application/dtos/GetLeagueOwnerSummaryResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueProtestsResultDTO.ts b/core/racing/application/dtos/GetLeagueProtestsResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueProtestsResultDTO.ts
rename to core/racing/application/dtos/GetLeagueProtestsResultDTO.ts
diff --git a/core/racing/application/dto/GetLeagueScheduleResultDTO.ts b/core/racing/application/dtos/GetLeagueScheduleResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/GetLeagueScheduleResultDTO.ts
rename to core/racing/application/dtos/GetLeagueScheduleResultDTO.ts
diff --git a/core/racing/application/dtos/GetTeamDetailsQueryParamsDTO.ts b/core/racing/application/dtos/GetTeamDetailsQueryParamsDTO.ts
new file mode 100644
index 000000000..f59c26568
--- /dev/null
+++ b/core/racing/application/dtos/GetTeamDetailsQueryParamsDTO.ts
@@ -0,0 +1,4 @@
+export interface GetTeamDetailsQueryParamsDTO {
+ teamId: string;
+ driverId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dtos/GetTeamDetailsQueryResultDTO.ts b/core/racing/application/dtos/GetTeamDetailsQueryResultDTO.ts
new file mode 100644
index 000000000..0375aa221
--- /dev/null
+++ b/core/racing/application/dtos/GetTeamDetailsQueryResultDTO.ts
@@ -0,0 +1,7 @@
+import type { Team } from '../../domain/entities/Team';
+import type { TeamMembership } from '../../domain/types/TeamMembership';
+
+export interface GetTeamDetailsQueryResultDTO {
+ team: Team;
+ membership: TeamMembership | null;
+}
\ No newline at end of file
diff --git a/core/racing/application/dtos/GetTeamJoinRequestsQueryParamsDTO.ts b/core/racing/application/dtos/GetTeamJoinRequestsQueryParamsDTO.ts
new file mode 100644
index 000000000..0e77992e5
--- /dev/null
+++ b/core/racing/application/dtos/GetTeamJoinRequestsQueryParamsDTO.ts
@@ -0,0 +1,3 @@
+export interface GetTeamJoinRequestsQueryParamsDTO {
+ teamId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dtos/GetTeamMembersQueryParamsDTO.ts b/core/racing/application/dtos/GetTeamMembersQueryParamsDTO.ts
new file mode 100644
index 000000000..3b57da6e4
--- /dev/null
+++ b/core/racing/application/dtos/GetTeamMembersQueryParamsDTO.ts
@@ -0,0 +1,3 @@
+export interface GetTeamMembersQueryParamsDTO {
+ teamId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/JoinLeagueCommandDTO.ts b/core/racing/application/dtos/JoinLeagueCommandDTO.ts
similarity index 100%
rename from core/racing/application/dto/JoinLeagueCommandDTO.ts
rename to core/racing/application/dtos/JoinLeagueCommandDTO.ts
diff --git a/core/racing/application/dtos/JoinTeamCommandDTO.ts b/core/racing/application/dtos/JoinTeamCommandDTO.ts
new file mode 100644
index 000000000..a6d8caf17
--- /dev/null
+++ b/core/racing/application/dtos/JoinTeamCommandDTO.ts
@@ -0,0 +1,4 @@
+export interface JoinTeamCommandDTO {
+ teamId: string;
+ driverId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/LeagueConfigFormDTO.ts b/core/racing/application/dtos/LeagueConfigFormDTO.ts
similarity index 100%
rename from core/racing/application/dto/LeagueConfigFormDTO.ts
rename to core/racing/application/dtos/LeagueConfigFormDTO.ts
diff --git a/core/racing/application/dto/LeagueDTO.ts b/core/racing/application/dtos/LeagueDTO.ts
similarity index 100%
rename from core/racing/application/dto/LeagueDTO.ts
rename to core/racing/application/dtos/LeagueDTO.ts
diff --git a/core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts b/core/racing/application/dtos/LeagueDriverSeasonStatsDTO.ts
similarity index 100%
rename from core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts
rename to core/racing/application/dtos/LeagueDriverSeasonStatsDTO.ts
diff --git a/core/racing/application/dto/LeagueScheduleDTO.ts b/core/racing/application/dtos/LeagueScheduleDTO.ts
similarity index 100%
rename from core/racing/application/dto/LeagueScheduleDTO.ts
rename to core/racing/application/dtos/LeagueScheduleDTO.ts
diff --git a/core/racing/application/dto/LeagueScoringConfigDTO.ts b/core/racing/application/dtos/LeagueScoringConfigDTO.ts
similarity index 100%
rename from core/racing/application/dto/LeagueScoringConfigDTO.ts
rename to core/racing/application/dtos/LeagueScoringConfigDTO.ts
diff --git a/core/racing/application/dto/LeagueSummaryDTO.ts b/core/racing/application/dtos/LeagueSummaryDTO.ts
similarity index 100%
rename from core/racing/application/dto/LeagueSummaryDTO.ts
rename to core/racing/application/dtos/LeagueSummaryDTO.ts
diff --git a/core/racing/application/dto/LeagueVisibilityInput.ts b/core/racing/application/dtos/LeagueVisibilityInput.ts
similarity index 100%
rename from core/racing/application/dto/LeagueVisibilityInput.ts
rename to core/racing/application/dtos/LeagueVisibilityInput.ts
diff --git a/core/racing/application/dtos/LeaveTeamCommandDTO.ts b/core/racing/application/dtos/LeaveTeamCommandDTO.ts
new file mode 100644
index 000000000..0d023140f
--- /dev/null
+++ b/core/racing/application/dtos/LeaveTeamCommandDTO.ts
@@ -0,0 +1,4 @@
+export interface LeaveTeamCommandDTO {
+ teamId: string;
+ driverId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/RaceDTO.ts b/core/racing/application/dtos/RaceDTO.ts
similarity index 100%
rename from core/racing/application/dto/RaceDTO.ts
rename to core/racing/application/dtos/RaceDTO.ts
diff --git a/core/racing/application/dto/RaceRegistrationQueryDTO.ts b/core/racing/application/dtos/RaceRegistrationQueryDTO.ts
similarity index 100%
rename from core/racing/application/dto/RaceRegistrationQueryDTO.ts
rename to core/racing/application/dtos/RaceRegistrationQueryDTO.ts
diff --git a/core/racing/application/dto/RegisterForRaceCommandDTO.ts b/core/racing/application/dtos/RegisterForRaceCommandDTO.ts
similarity index 100%
rename from core/racing/application/dto/RegisterForRaceCommandDTO.ts
rename to core/racing/application/dtos/RegisterForRaceCommandDTO.ts
diff --git a/core/racing/application/dtos/RejectTeamJoinRequestCommandDTO.ts b/core/racing/application/dtos/RejectTeamJoinRequestCommandDTO.ts
new file mode 100644
index 000000000..7bc80ced1
--- /dev/null
+++ b/core/racing/application/dtos/RejectTeamJoinRequestCommandDTO.ts
@@ -0,0 +1,3 @@
+export interface RejectTeamJoinRequestCommandDTO {
+ requestId: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/ResultDTO.ts b/core/racing/application/dtos/ResultDTO.ts
similarity index 100%
rename from core/racing/application/dto/ResultDTO.ts
rename to core/racing/application/dtos/ResultDTO.ts
diff --git a/core/racing/application/dto/SponsorshipSlotDTO.ts b/core/racing/application/dtos/SponsorshipSlotDTO.ts
similarity index 100%
rename from core/racing/application/dto/SponsorshipSlotDTO.ts
rename to core/racing/application/dtos/SponsorshipSlotDTO.ts
diff --git a/core/racing/application/dto/StandingDTO.ts b/core/racing/application/dtos/StandingDTO.ts
similarity index 100%
rename from core/racing/application/dto/StandingDTO.ts
rename to core/racing/application/dtos/StandingDTO.ts
diff --git a/core/racing/application/dtos/UpdateTeamCommandDTO.ts b/core/racing/application/dtos/UpdateTeamCommandDTO.ts
new file mode 100644
index 000000000..a7de39cfa
--- /dev/null
+++ b/core/racing/application/dtos/UpdateTeamCommandDTO.ts
@@ -0,0 +1,7 @@
+import type { Team } from '../../domain/entities/Team';
+
+export interface UpdateTeamCommandDTO {
+ teamId: string;
+ updates: Partial>;
+ updatedBy: string;
+}
\ No newline at end of file
diff --git a/core/racing/application/dto/WithdrawFromRaceCommandDTO.ts b/core/racing/application/dtos/WithdrawFromRaceCommandDTO.ts
similarity index 100%
rename from core/racing/application/dto/WithdrawFromRaceCommandDTO.ts
rename to core/racing/application/dtos/WithdrawFromRaceCommandDTO.ts
diff --git a/core/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts b/core/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts
index 20defd0d7..38da58bf4 100644
--- a/core/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts
+++ b/core/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts
@@ -3,7 +3,7 @@ import type {
TeamMembership,
TeamJoinRequest,
} from '../../domain/types/TeamMembership';
-import type { ApproveTeamJoinRequestCommandDTO } from '../dto/TeamCommandAndQueryDTO';
+import type { ApproveTeamJoinRequestCommandDTO } from '../dtos/ApproveTeamJoinRequestCommandDTO';
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
diff --git a/core/racing/application/use-cases/JoinTeamUseCase.ts b/core/racing/application/use-cases/JoinTeamUseCase.ts
index 3d6e0598b..a269752ba 100644
--- a/core/racing/application/use-cases/JoinTeamUseCase.ts
+++ b/core/racing/application/use-cases/JoinTeamUseCase.ts
@@ -5,7 +5,7 @@ import type {
TeamMembershipStatus,
TeamRole,
} from '../../domain/types/TeamMembership';
-import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
+import type { JoinTeamCommandDTO } from '../dtos/JoinTeamCommandDTO';
import type { AsyncUseCase, Logger } from '@core/shared/application';
import { Result as SharedResult } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
diff --git a/core/racing/application/use-cases/LeaveTeamUseCase.ts b/core/racing/application/use-cases/LeaveTeamUseCase.ts
index c7388da9d..99518b0b2 100644
--- a/core/racing/application/use-cases/LeaveTeamUseCase.ts
+++ b/core/racing/application/use-cases/LeaveTeamUseCase.ts
@@ -1,5 +1,5 @@
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
-import type { LeaveTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
+import type { LeaveTeamCommandDTO } from '../dtos/LeaveTeamCommandDTO';
import type { AsyncUseCase, Logger } from '@core/shared/application';
import { Result as SharedResult } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
diff --git a/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts b/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts
index 57685a187..ba135fed2 100644
--- a/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts
+++ b/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts
@@ -1,5 +1,5 @@
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
-import type { RejectTeamJoinRequestCommandDTO } from '../dto/TeamCommandAndQueryDTO';
+import type { RejectTeamJoinRequestCommandDTO } from '../dtos/RejectTeamJoinRequestCommandDTO';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
diff --git a/core/racing/application/use-cases/UpdateTeamUseCase.ts b/core/racing/application/use-cases/UpdateTeamUseCase.ts
index 538351d5a..e5064ab1a 100644
--- a/core/racing/application/use-cases/UpdateTeamUseCase.ts
+++ b/core/racing/application/use-cases/UpdateTeamUseCase.ts
@@ -3,7 +3,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
-import type { UpdateTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
+import type { UpdateTeamCommandDTO } from '../dtos/UpdateTeamCommandDTO';
type UpdateTeamErrorCode = 'INSUFFICIENT_PERMISSIONS' | 'TEAM_NOT_FOUND';
diff --git a/core/racing/index.ts b/core/racing/index.ts
index ae14fe76e..f91c68806 100644
--- a/core/racing/index.ts
+++ b/core/racing/index.ts
@@ -43,9 +43,9 @@ export * from './infrastructure/repositories/InMemorySponsorshipRequestRepositor
export * from './infrastructure/repositories/InMemorySponsorshipPricingRepository';
export * from './application/mappers/EntityMappers';
-export * from './application/dto/DriverDTO';
-export * from './application/dto/LeagueDriverSeasonStatsDTO';
-export * from './application/dto/LeagueScoringConfigDTO';
+export * from './application/dtos/DriverDTO';
+export * from './application/dtos/LeagueDriverSeasonStatsDTO';
+export * from './application/dtos/LeagueScoringConfigDTO';
export * from './application/use-cases/GetSponsorDashboardUseCase';
export * from './application/use-cases/GetSponsorSponsorshipsUseCase';
diff --git a/split.js b/split.js
new file mode 100644
index 000000000..21420adbf
--- /dev/null
+++ b/split.js
@@ -0,0 +1,119 @@
+const fs = require('fs');
+const path = require('path');
+
+const filePath = 'apps/api/src/domain/league/dtos/LeagueDto.ts';
+const content = fs.readFileSync(filePath, 'utf8');
+const lines = content.split('\n');
+
+let allImports = lines.filter(line => line.startsWith('import '));
+
+let classBlocks = [];
+let currentClassLines = [];
+let inClass = false;
+
+for (let i = 0; i < lines.length; i++) {
+ let line = lines[i];
+ if (line.startsWith('export class ')) {
+ if (inClass) {
+ classBlocks.push(currentClassLines);
+ currentClassLines = [];
+ }
+ inClass = true;
+ }
+ if (inClass) {
+ currentClassLines.push(line);
+ if (line.trim() === '}') {
+ classBlocks.push(currentClassLines);
+ currentClassLines = [];
+ inClass = false;
+ }
+ }
+}
+
+if (currentClassLines.length > 0) {
+ classBlocks.push(currentClassLines);
+}
+
+// Function to get imports for class
+function getImportsForClass(classLines, className) {
+ let classContent = classLines.join('\n');
+ let neededImports = [];
+
+ // ApiProperty
+ if (classContent.includes('@ApiProperty')) {
+ neededImports.push("import { ApiProperty } from '@nestjs/swagger';");
+ }
+
+ // class-validator
+ let validators = [];
+ if (classContent.includes('@IsString')) validators.push('IsString');
+ if (classContent.includes('@IsNumber')) validators.push('IsNumber');
+ if (classContent.includes('@IsBoolean')) validators.push('IsBoolean');
+ if (classContent.includes('@IsDate')) validators.push('IsDate');
+ if (classContent.includes('@IsOptional')) validators.push('IsOptional');
+ if (classContent.includes('@IsEnum')) validators.push('IsEnum');
+ if (classContent.includes('@IsArray')) validators.push('IsArray');
+ if (classContent.includes('@ValidateNested')) validators.push('ValidateNested');
+ if (validators.length > 0) {
+ neededImports.push(`import { ${validators.join(', ')} } from 'class-validator';`);
+ }
+
+ // class-transformer
+ if (classContent.includes('@Type')) {
+ neededImports.push("import { Type } from 'class-transformer';");
+ }
+
+ // Other DTOs
+ if (classContent.includes('DriverDto')) {
+ neededImports.push("import { DriverDto } from '../../driver/dto/DriverDto';");
+ }
+ if (classContent.includes('RaceDto')) {
+ neededImports.push("import { RaceDto } from '../../race/dto/RaceDto';");
+ }
+
+ // Local DTOs
+ let localDTOs = ['LeagueSettingsDTO', 'LeagueWithCapacityDTO', 'LeagueSummaryDTO', 'AllLeaguesWithCapacityDTO', 'AllLeaguesWithCapacityAndScoringDTO', 'LeagueStatsDTO', 'ProtestDTO', 'SeasonDTO', 'LeagueJoinRequestDTO', 'GetLeagueJoinRequestsQueryDTO', 'ApproveJoinRequestInputDTO', 'ApproveJoinRequestOutputDTO', 'RejectJoinRequestInputDTO', 'RejectJoinRequestOutputDTO', 'GetLeagueAdminPermissionsInputDTO', 'LeagueAdminPermissionsDTO', 'RemoveLeagueMemberInputDTO', 'RemoveLeagueMemberOutputDTO', 'UpdateLeagueMemberRoleInputDTO', 'UpdateLeagueMemberRoleOutputDTO', 'GetLeagueOwnerSummaryQueryDTO', 'LeagueOwnerSummaryDTO', 'LeagueConfigFormModelBasicsDTO', 'LeagueConfigFormModelStructureDTO', 'LeagueConfigFormModelScoringDTO', 'LeagueConfigFormModelDropPolicyDTO', 'LeagueConfigFormModelStewardingDTO', 'LeagueConfigFormModelTimingsDTO', 'LeagueConfigFormModelDTO', 'GetLeagueAdminConfigQueryDTO', 'GetLeagueAdminConfigOutputDTO', 'LeagueAdminConfigDTO', 'GetLeagueProtestsQueryDTO', 'LeagueAdminProtestsDTO', 'GetLeagueSeasonsQueryDTO', 'LeagueSeasonSummaryDTO', 'LeagueAdminDTO', 'LeagueMemberDTO', 'LeagueMembershipsDTO', 'LeagueStandingDTO', 'LeagueStandingsDTO', 'LeagueScheduleDTO', 'LeagueStatsDTO', 'CreateLeagueInputDTO', 'CreateLeagueOutputDTO'];
+
+ for (let dto of localDTOs) {
+ if (dto !== className && classContent.includes(dto)) {
+ neededImports.push(`import { ${dto} } from './${dto}';`);
+ }
+ }
+
+ return neededImports;
+}
+
+for (let classLines of classBlocks) {
+ let classNameLine = classLines.find(line => line.startsWith('export class '));
+ if (classNameLine) {
+ let match = classNameLine.match(/export class (\w+)/);
+ if (match) {
+ let className = match[1];
+ // Rename
+ if (className.endsWith('ViewModel')) {
+ className = className.replace('ViewModel', 'DTO');
+ } else if (className.endsWith('Dto')) {
+ className = className.replace('Dto', 'DTO');
+ } else if (className.endsWith('Input')) {
+ className = className + 'DTO';
+ } else if (className.endsWith('Output')) {
+ className = className + 'DTO';
+ } else if (className.endsWith('Query')) {
+ className = className + 'DTO';
+ } else {
+ className = className + 'DTO';
+ }
+ let fileName = className + '.ts';
+ // Update class name in lines
+ classLines[0] = classLines[0].replace(/export class \w+/, 'export class ' + className);
+ // Update references in the class
+ for (let i = 1; i < classLines.length; i++) {
+ classLines[i] = classLines[i].replace(/ViewModel/g, 'DTO').replace(/Dto/g, 'DTO').replace(/Input\b/g, 'InputDTO').replace(/Output\b/g, 'OutputDTO').replace(/Query\b/g, 'QueryDTO');
+ classLines[i] = classLines[i].replace(/DriverDTO/g, 'DriverDto').replace(/RaceDTO/g, 'RaceDto');
+ }
+ let imports = getImportsForClass(classLines, className);
+ let fileContent = imports.join('\n') + '\n\n' + classLines.join('\n');
+ fs.writeFileSync(path.join('apps/api/src/domain/league/dtos', fileName), fileContent);
+ }
+ }
+}
\ No newline at end of file