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