diff --git a/.eslintrc.json b/.eslintrc.json index 510787568..bd08576be 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -166,9 +166,9 @@ "error", { "args": "all", - "argsIgnorePattern": "^$", + "argsIgnorePattern": "^_", "vars": "all", - "varsIgnorePattern": "^$", + "varsIgnorePattern": "^_", "caughtErrors": "all" } ], diff --git a/apps/api/src/domain/driver/DriverController.test.ts b/apps/api/src/domain/driver/DriverController.test.ts index 57bf3d522..ee6cd367a 100644 --- a/apps/api/src/domain/driver/DriverController.test.ts +++ b/apps/api/src/domain/driver/DriverController.test.ts @@ -45,7 +45,7 @@ describe('DriverController', () => { describe('getDriversLeaderboard', () => { it('should return drivers leaderboard', async () => { - const leaderboard: DriversLeaderboardDTO = { items: [] }; + const leaderboard: DriversLeaderboardDTO = { drivers: [], totalRaces: 0, totalWins: 0, activeCount: 0 }; service.getDriversLeaderboard.mockResolvedValue({ viewModel: leaderboard } as never); const result = await controller.getDriversLeaderboard(); @@ -94,7 +94,7 @@ describe('DriverController', () => { describe('completeOnboarding', () => { it('should complete onboarding', async () => { const userId = 'user-123'; - const input: CompleteOnboardingInputDTO = { someField: 'value' } as CompleteOnboardingInputDTO; + const input: CompleteOnboardingInputDTO = { someField: 'value' } as unknown as CompleteOnboardingInputDTO; const output: CompleteOnboardingOutputDTO = { success: true }; service.completeOnboarding.mockResolvedValue({ viewModel: output } as never); @@ -111,7 +111,7 @@ describe('DriverController', () => { it('should return registration status', async () => { const driverId = 'driver-123'; const raceId = 'race-456'; - const status: DriverRegistrationStatusDTO = { registered: true } as DriverRegistrationStatusDTO; + const status: DriverRegistrationStatusDTO = { registered: true } as unknown as DriverRegistrationStatusDTO; service.getDriverRegistrationStatus.mockResolvedValue({ viewModel: status } as never); const result = await controller.getDriverRegistrationStatus(driverId, raceId); @@ -137,7 +137,7 @@ describe('DriverController', () => { describe('getDriverProfile', () => { it('should return driver profile', async () => { const driverId = 'driver-123'; - const profile: GetDriverProfileOutputDTO = { id: driverId, bio: 'Bio' }; + const profile: GetDriverProfileOutputDTO = { id: driverId, bio: 'Bio' } as unknown as GetDriverProfileOutputDTO; service.getDriverProfile.mockResolvedValue(profile); const result = await controller.getDriverProfile(driverId); @@ -151,7 +151,7 @@ describe('DriverController', () => { it('should update driver profile', async () => { const driverId = 'driver-123'; const body = { bio: 'New bio', country: 'US' }; - const updated: GetDriverOutputDTO = { id: driverId, name: 'Driver' }; + const updated: GetDriverOutputDTO = { id: driverId, name: 'Driver' } as unknown as GetDriverOutputDTO; service.updateDriverProfile.mockResolvedValue(updated); const result = await controller.updateDriverProfile(driverId, body); diff --git a/apps/api/src/domain/driver/DriverController.ts b/apps/api/src/domain/driver/DriverController.ts index dd3848e48..8ad72cd3f 100644 --- a/apps/api/src/domain/driver/DriverController.ts +++ b/apps/api/src/domain/driver/DriverController.ts @@ -1,20 +1,18 @@ -import { Controller, Get, Post, Put, Body, Req, Param } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, Req } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; -import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger'; +import { DriverService } from './DriverService'; +import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO'; +import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO'; +import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO'; +import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; +import { DriverStatsDTO } from './dtos/DriverStatsDTO'; +import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO'; +import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO'; interface AuthenticatedRequest extends Request { user?: { userId: string }; } -import { DriverService } from './DriverService'; -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'; -import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO'; -import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO'; @ApiTags('drivers') @Controller('drivers') diff --git a/apps/api/src/domain/driver/DriverProviders.ts b/apps/api/src/domain/driver/DriverProviders.ts index 5c978df3a..197e40907 100644 --- a/apps/api/src/domain/driver/DriverProviders.ts +++ b/apps/api/src/domain/driver/DriverProviders.ts @@ -2,48 +2,38 @@ import { Provider } from '@nestjs/common'; import { DriverService } from './DriverService'; // Import core interfaces -import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; -import { IRankingService } from '@core/racing/domain/services/IRankingService'; -import { IDriverStatsService } from '@core/racing/domain/services/IDriverStatsService'; -import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; import { DriverExtendedProfileProvider } from '@core/racing/application/ports/DriverExtendedProfileProvider'; -import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; +import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; -import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository'; -import type { Logger } from '@core/shared/application'; -import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; +import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; +import { IDriverStatsService } from '@core/racing/domain/services/IDriverStatsService'; +import { IRankingService } from '@core/racing/domain/services/IRankingService'; +import type { Logger } from '@core/shared/application'; import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository'; // Import use cases -import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; -import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; -import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; -import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; +import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; - -// Import presenters -import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; -import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; -import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter'; -import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; -import { DriverPresenter } from './presenters/DriverPresenter'; -import { DriverProfilePresenter } from './presenters/DriverProfilePresenter'; +import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; +import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; +import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; // Import concrete in-memory implementations -import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; -import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankingService'; -import { InMemoryDriverStatsService } from '@adapters/racing/services/InMemoryDriverStatsService'; -import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider'; -import { InMemoryDriverExtendedProfileProvider } from '@adapters/racing/ports/InMemoryDriverExtendedProfileProvider'; -import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; -import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository'; -import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository'; -import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository'; -import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository'; -import { InMemorySocialGraphRepository } from '@core/social/infrastructure/inmemory/InMemorySocialAndFeed'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; +import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; +import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository'; +import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; +import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository'; +import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository'; +import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository'; +import { InMemoryDriverExtendedProfileProvider } from '@adapters/racing/ports/InMemoryDriverExtendedProfileProvider'; +import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider'; +import { InMemoryDriverStatsService } from '@adapters/racing/services/InMemoryDriverStatsService'; +import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankingService'; +import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; +import { InMemorySocialGraphRepository } from '@core/social/infrastructure/inmemory/InMemorySocialAndFeed'; // Define injection tokens export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository'; @@ -69,13 +59,6 @@ export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase'; export const DriverProviders: Provider[] = [ DriverService, // Provide the service itself - // Presenters - DriversLeaderboardPresenter, - DriverStatsPresenter, - CompleteOnboardingPresenter, - DriverRegistrationStatusPresenter, - DriverPresenter, - DriverProfilePresenter, { provide: DRIVER_REPOSITORY_TOKEN, useFactory: (logger: Logger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository @@ -145,30 +128,29 @@ export const DriverProviders: Provider[] = [ driverStatsService: IDriverStatsService, imageService: IImageServicePort, logger: Logger, - presenter: DriversLeaderboardPresenter, - ) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger, presenter), - inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN, DriversLeaderboardPresenter.name], + ) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger), + inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN], }, { provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN, - useFactory: (driverRepo: IDriverRepository, logger: Logger) => new GetTotalDriversUseCase(driverRepo, logger), - inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: (driverRepo: IDriverRepository) => new GetTotalDriversUseCase(driverRepo), + inject: [DRIVER_REPOSITORY_TOKEN], }, { provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, - useFactory: (driverRepo: IDriverRepository, logger: Logger, presenter: CompleteOnboardingPresenter) => new CompleteDriverOnboardingUseCase(driverRepo, logger, presenter), - inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, CompleteOnboardingPresenter.name], + useFactory: (driverRepo: IDriverRepository, logger: Logger) => new CompleteDriverOnboardingUseCase(driverRepo, logger), + inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, - useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger, presenter: DriverRegistrationStatusPresenter) => - new IsDriverRegisteredForRaceUseCase(registrationRepo, logger, presenter), - inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN, DriverRegistrationStatusPresenter.name], + useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) => + new IsDriverRegisteredForRaceUseCase(registrationRepo, logger), + inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, - useFactory: (driverRepo: IDriverRepository, presenter: DriverPresenter, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, presenter, logger), - inject: [DRIVER_REPOSITORY_TOKEN, DriverPresenter.name, LOGGER_TOKEN], + useFactory: (driverRepo: IDriverRepository, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, logger), + inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, @@ -181,14 +163,13 @@ export const DriverProviders: Provider[] = [ driverExtendedProfileProvider: DriverExtendedProfileProvider, driverStatsService: IDriverStatsService, rankingService: IRankingService, - presenter: DriverProfilePresenter, ) => new GetProfileOverviewUseCase( driverRepo, teamRepository, teamMembershipRepository, socialRepository, - imageService, + (driverId: string) => Promise.resolve(imageService.getDriverAvatar(driverId)), driverExtendedProfileProvider, (driverId: string) => { const stats = driverStatsService.getDriverStats(driverId); @@ -216,7 +197,6 @@ export const DriverProviders: Provider[] = [ rating: ranking.rating, overallRank: ranking.overallRank, })), - presenter, ), inject: [ DRIVER_REPOSITORY_TOKEN, @@ -227,7 +207,6 @@ export const DriverProviders: Provider[] = [ DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN, DRIVER_STATS_SERVICE_TOKEN, RANKING_SERVICE_TOKEN, - DriverProfilePresenter.name, ], }, ]; diff --git a/apps/api/src/domain/driver/DriverService.test.ts b/apps/api/src/domain/driver/DriverService.test.ts index f1e101b95..a5477f039 100644 --- a/apps/api/src/domain/driver/DriverService.test.ts +++ b/apps/api/src/domain/driver/DriverService.test.ts @@ -1,16 +1,13 @@ +import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; +import type { Driver } from '@core/racing/domain/entities/Driver'; +import { GetDriversLeaderboardUseCase, type GetDriversLeaderboardResult } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; +import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; +import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; +import type { Logger } from '@core/shared/application'; +import { Result } from '@core/shared/application/Result'; import { Test, TestingModule } from '@nestjs/testing'; import { vi } from 'vitest'; import { DriverService } from './DriverService'; -import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; -import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; -import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; -import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; -import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; -import type { Logger } from '@core/shared/application'; -import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; -import { Result } from '@core/shared/application/Result'; -import type { CompleteDriverOnboardingOutputPort } from '@core/racing/application/ports/output/CompleteDriverOnboardingOutputPort'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; describe('DriverService', () => { let service: DriverService; @@ -18,10 +15,6 @@ describe('DriverService', () => { let getTotalDriversUseCase: ReturnType>; let completeDriverOnboardingUseCase: ReturnType>; let isDriverRegisteredForRaceUseCase: ReturnType>; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let updateDriverProfileUseCase: ReturnType>; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let driverRepository: ReturnType>; let logger: ReturnType>; beforeEach(async () => { @@ -58,6 +51,12 @@ describe('DriverService', () => { execute: vi.fn(), }, }, + { + provide: 'GetProfileOverviewUseCase', + useValue: { + execute: vi.fn(), + }, + }, { provide: 'IDriverRepository', useValue: { @@ -79,8 +78,6 @@ describe('DriverService', () => { getTotalDriversUseCase = vi.mocked(module.get('GetTotalDriversUseCase')); completeDriverOnboardingUseCase = vi.mocked(module.get('CompleteDriverOnboardingUseCase')); isDriverRegisteredForRaceUseCase = vi.mocked(module.get('IsDriverRegisteredForRaceUseCase')); - updateDriverProfileUseCase = vi.mocked(module.get('UpdateDriverProfileUseCase')); - driverRepository = vi.mocked(module.get('IDriverRepository')); logger = vi.mocked(module.get('Logger')); }); @@ -107,11 +104,27 @@ describe('DriverService', () => { activeCount: 1, }; - getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(mockViewModel)); + const businessResult = { + items: mockViewModel.drivers.map(dto => ({ + driver: { id: dto.id, name: dto.name, country: dto.nationality }, + rating: dto.rating, + skillLevel: dto.skillLevel, + racesCompleted: dto.racesCompleted, + wins: dto.wins, + podiums: dto.podiums, + isActive: dto.isActive, + rank: dto.rank, + avatarUrl: dto.avatarUrl, + })), + totalRaces: mockViewModel.totalRaces, + totalWins: mockViewModel.totalWins, + activeCount: mockViewModel.activeCount, + }; + getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(businessResult as unknown as GetDriversLeaderboardResult)); const result = await service.getDriversLeaderboard(); - expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith(); + expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith({}); expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.'); expect(result).toEqual(mockViewModel); }); @@ -125,7 +138,7 @@ describe('DriverService', () => { const result = await service.getTotalDrivers(); - expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith(); + expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith({}); expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.'); expect(result).toEqual(mockOutput); }); @@ -138,12 +151,11 @@ describe('DriverService', () => { lastName: 'Doe', displayName: 'John Doe', country: 'US', - timezone: 'America/New_York', bio: 'Racing enthusiast', }; completeDriverOnboardingUseCase.execute.mockResolvedValue( - Result.ok>({ driverId: 'user-123' }) + Result.ok({ driver: { id: 'user-123' } as Driver }) ); const result = await service.completeOnboarding('user-123', input); @@ -165,12 +177,11 @@ describe('DriverService', () => { lastName: 'Doe', displayName: 'John Doe', country: 'US', - timezone: 'America/New_York', bio: 'Racing enthusiast', }; completeDriverOnboardingUseCase.execute.mockResolvedValue( - Result.err>({ code: 'DRIVER_ALREADY_EXISTS' }) + Result.err({ code: 'DRIVER_ALREADY_EXISTS', details: { message: 'Driver already exists' } }) ); const result = await service.completeOnboarding('user-123', input); diff --git a/apps/api/src/domain/driver/DriverService.ts b/apps/api/src/domain/driver/DriverService.ts index fda401fb2..23edabaf4 100644 --- a/apps/api/src/domain/driver/DriverService.ts +++ b/apps/api/src/domain/driver/DriverService.ts @@ -1,45 +1,54 @@ -import { Injectable, Inject } from '@nestjs/common'; +import { Result } from '@core/shared/application/Result'; +import { Inject, Injectable } from '@nestjs/common'; +import type { Driver } from '@core/racing/domain/entities/Driver'; import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO'; -import { GetDriverRegistrationStatusQueryDTO } from './dtos/GetDriverRegistrationStatusQueryDTO'; -import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; -import { DriverStatsDTO } from './dtos/DriverStatsDTO'; import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO'; import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO'; +import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; +import { DriverStatsDTO } from './dtos/DriverStatsDTO'; import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO'; import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO'; +import { GetDriverRegistrationStatusQueryDTO } from './dtos/GetDriverRegistrationStatusQueryDTO'; // Use cases -import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; -import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; -import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; +import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; +import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; +import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { UpdateDriverProfileUseCase, type UpdateDriverProfileInput } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; // Presenters -import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; -import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter'; -import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; import { DriverPresenter } from './presenters/DriverPresenter'; import { DriverProfilePresenter } from './presenters/DriverProfilePresenter'; +import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; +import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; +import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; // 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, - UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, - GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, - LOGGER_TOKEN, - DRIVER_REPOSITORY_TOKEN, -} from './DriverProviders'; +import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import type { Logger } from '@core/shared/application'; -import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; +import { + COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, + DRIVER_REPOSITORY_TOKEN, + GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, + GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, + GET_TOTAL_DRIVERS_USE_CASE_TOKEN, + IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, + LOGGER_TOKEN, + UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, +} from './DriverProviders'; @Injectable() export class DriverService { + private readonly driversLeaderboardPresenter = new DriversLeaderboardPresenter(); + private readonly driverStatsPresenter = new DriverStatsPresenter(); + private readonly completeOnboardingPresenter = new CompleteOnboardingPresenter(); + private readonly driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter(); + private readonly driverPresenter = new DriverPresenter(); + private readonly driverProfilePresenter = new DriverProfilePresenter(); + constructor( @Inject(GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN) private readonly getDriversLeaderboardUseCase: GetDriversLeaderboardUseCase, @@ -54,15 +63,9 @@ export class DriverService { @Inject(GET_PROFILE_OVERVIEW_USE_CASE_TOKEN) private readonly getProfileOverviewUseCase: GetProfileOverviewUseCase, @Inject(DRIVER_REPOSITORY_TOKEN) - private readonly driverRepository: IDriverRepository, + private readonly driverRepository: IDriverRepository, // TODO must be removed from service @Inject(LOGGER_TOKEN) private readonly logger: Logger, - private readonly driversLeaderboardPresenter: DriversLeaderboardPresenter, - private readonly driverStatsPresenter: DriverStatsPresenter, - private readonly completeOnboardingPresenter: CompleteOnboardingPresenter, - private readonly driverRegistrationStatusPresenter: DriverRegistrationStatusPresenter, - private readonly driverPresenter: DriverPresenter, - private readonly driverProfilePresenter: DriverProfilePresenter, ) {} async getDriversLeaderboard(): Promise { @@ -70,11 +73,8 @@ export class DriverService { const result = await this.getDriversLeaderboardUseCase.execute({}); - if (result.isErr()) { - const error = result.unwrapErr(); - throw new Error(error.details?.message ?? 'Failed to load drivers leaderboard'); - } - + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.driversLeaderboardPresenter.present(result as Result); return this.driversLeaderboardPresenter.getResponseModel(); } @@ -88,6 +88,7 @@ export class DriverService { throw new Error(error.details?.message ?? 'Failed to load driver stats'); } + this.driverStatsPresenter.present(result.unwrap()); return this.driverStatsPresenter.getResponseModel(); } @@ -106,11 +107,7 @@ export class DriverService { ...(input.bio !== undefined ? { bio: input.bio } : {}), }); - if (result.isErr()) { - const error = result.unwrapErr(); - throw new Error(error.details?.message ?? 'Failed to complete onboarding'); - } - + this.completeOnboardingPresenter.present(result); return this.completeOnboardingPresenter.getResponseModel(); } @@ -129,15 +126,17 @@ export class DriverService { throw new Error(error.details?.message ?? 'Failed to check registration status'); } + this.driverRegistrationStatusPresenter.present(result.unwrap()); return this.driverRegistrationStatusPresenter.getResponseModel(); } async getCurrentDriver(userId: string): Promise { this.logger.debug(`[DriverService] Fetching current driver for userId: ${userId}`); - const driver = await this.driverRepository.findById(userId); + const result = Result.ok(await this.driverRepository.findById(userId)); - this.driverPresenter.present(driver ?? null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.driverPresenter.present(result as Result); return this.driverPresenter.getResponseModel(); } @@ -157,12 +156,11 @@ export class DriverService { if (result.isErr()) { this.logger.error(`Failed to update driver profile: ${result.unwrapErr().code}`); - this.driverPresenter.present(null); + this.driverPresenter.present(Result.ok(null)); return this.driverPresenter.getResponseModel(); } - const updatedDriver = await this.driverRepository.findById(driverId); - this.driverPresenter.present(updatedDriver ?? null); + this.driverPresenter.present(Result.ok(result.unwrap())); return this.driverPresenter.getResponseModel(); } @@ -171,7 +169,7 @@ export class DriverService { const driver = await this.driverRepository.findById(driverId); - this.driverPresenter.present(driver ?? null); + this.driverPresenter.present(Result.ok(driver)); return this.driverPresenter.getResponseModel(); } @@ -186,6 +184,7 @@ export class DriverService { throw new Error(error.details?.message ?? 'Failed to load driver profile'); } + this.driverProfilePresenter.present(result.unwrap()); return this.driverProfilePresenter.getResponseModel(); } } diff --git a/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts b/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts index 5462b0d76..10cd5c0d6 100644 --- a/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts +++ b/apps/api/src/domain/driver/dtos/CompleteOnboardingInputDTO.ts @@ -5,22 +5,22 @@ export class CompleteOnboardingInputDTO { @ApiProperty() @IsString() @IsNotEmpty() - firstName: string; + firstName!: string; @ApiProperty() @IsString() @IsNotEmpty() - lastName: string; + lastName!: string; @ApiProperty() @IsString() @IsNotEmpty() - displayName: string; + displayName!: string; @ApiProperty() @IsString() @IsNotEmpty() - country: string; + country!: string; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts b/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts index b425fa9df..0bae6d904 100644 --- a/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts +++ b/apps/api/src/domain/driver/dtos/CompleteOnboardingOutputDTO.ts @@ -4,7 +4,7 @@ import { IsBoolean, IsString } from 'class-validator'; export class CompleteOnboardingOutputDTO { @ApiProperty() @IsBoolean() - success: boolean; + success!: boolean; @ApiProperty({ required: false }) @IsString() diff --git a/apps/api/src/domain/driver/dtos/DriverDTO.ts b/apps/api/src/domain/driver/dtos/DriverDTO.ts index 947befe75..8a4ae3a68 100644 --- a/apps/api/src/domain/driver/dtos/DriverDTO.ts +++ b/apps/api/src/domain/driver/dtos/DriverDTO.ts @@ -2,20 +2,20 @@ import { ApiProperty } from '@nestjs/swagger'; export class DriverDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - iracingId: string; + iracingId!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - country: string; + country!: string; @ApiProperty({ required: false }) bio?: string; @ApiProperty() - joinedAt: string; + joinedAt!: 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 index ad715c419..190ee85cf 100644 --- a/apps/api/src/domain/driver/dtos/DriverLeaderboardItemDTO.ts +++ b/apps/api/src/domain/driver/dtos/DriverLeaderboardItemDTO.ts @@ -2,34 +2,34 @@ import { ApiProperty } from '@nestjs/swagger'; export class DriverLeaderboardItemDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - rating: number; + rating!: number; @ApiProperty() - skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc. + skillLevel!: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc. @ApiProperty() - nationality: string; + nationality!: string; @ApiProperty() - racesCompleted: number; + racesCompleted!: number; @ApiProperty() - wins: number; + wins!: number; @ApiProperty() - podiums: number; + podiums!: number; @ApiProperty() - isActive: boolean; + isActive!: boolean; @ApiProperty() - rank: number; + rank!: number; @ApiProperty({ nullable: true }) avatarUrl?: string; diff --git a/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts b/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts index 14b001e56..ab502f9fe 100644 --- a/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts +++ b/apps/api/src/domain/driver/dtos/DriverStatsDTO.ts @@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger'; export class DriverStatsDTO { @ApiProperty() - totalDrivers: number; + 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 index 31aa83112..b656e86f2 100644 --- a/apps/api/src/domain/driver/dtos/DriversLeaderboardDTO.ts +++ b/apps/api/src/domain/driver/dtos/DriversLeaderboardDTO.ts @@ -3,14 +3,14 @@ import { DriverLeaderboardItemDTO } from './DriverLeaderboardItemDTO'; export class DriversLeaderboardDTO { @ApiProperty({ type: [DriverLeaderboardItemDTO] }) - drivers: DriverLeaderboardItemDTO[]; + drivers!: DriverLeaderboardItemDTO[]; @ApiProperty() - totalRaces: number; + totalRaces!: number; @ApiProperty() - totalWins: number; + totalWins!: number; @ApiProperty() - activeCount: number; + activeCount!: number; } \ No newline at end of file diff --git a/apps/api/src/domain/driver/dtos/GetDriverOutputDTO.ts b/apps/api/src/domain/driver/dtos/GetDriverOutputDTO.ts index 9632d9c27..9cb42b610 100644 --- a/apps/api/src/domain/driver/dtos/GetDriverOutputDTO.ts +++ b/apps/api/src/domain/driver/dtos/GetDriverOutputDTO.ts @@ -2,20 +2,20 @@ import { ApiProperty } from '@nestjs/swagger'; export class GetDriverOutputDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - iracingId: string; + iracingId!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - country: string; + country!: string; @ApiProperty() bio?: string; @ApiProperty() - joinedAt: string; + joinedAt!: string; } \ No newline at end of file diff --git a/apps/api/src/domain/driver/dtos/GetDriverProfileOutputDTO.ts b/apps/api/src/domain/driver/dtos/GetDriverProfileOutputDTO.ts index 2f4d15d62..8ffa26507 100644 --- a/apps/api/src/domain/driver/dtos/GetDriverProfileOutputDTO.ts +++ b/apps/api/src/domain/driver/dtos/GetDriverProfileOutputDTO.ts @@ -2,81 +2,81 @@ import { ApiProperty } from '@nestjs/swagger'; export class DriverProfileDriverSummaryDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - country: string; + country!: string; @ApiProperty() - avatarUrl: string; + avatarUrl!: string; @ApiProperty({ nullable: true }) - iracingId: string | null; + iracingId!: string | null; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty({ nullable: true }) - rating: number | null; + rating!: number | null; @ApiProperty({ nullable: true }) - globalRank: number | null; + globalRank!: number | null; @ApiProperty({ nullable: true }) - consistency: number | null; + consistency!: number | null; @ApiProperty({ nullable: true }) - bio: string | null; + bio!: string | null; @ApiProperty({ nullable: true }) - totalDrivers: number | null; + totalDrivers!: number | null; } export class DriverProfileStatsDTO { @ApiProperty() - totalRaces: number; + totalRaces!: number; @ApiProperty() - wins: number; + wins!: number; @ApiProperty() - podiums: number; + podiums!: number; @ApiProperty() - dnfs: number; + dnfs!: number; @ApiProperty({ nullable: true }) - avgFinish: number | null; + avgFinish!: number | null; @ApiProperty({ nullable: true }) - bestFinish: number | null; + bestFinish!: number | null; @ApiProperty({ nullable: true }) - worstFinish: number | null; + worstFinish!: number | null; @ApiProperty({ nullable: true }) - finishRate: number | null; + finishRate!: number | null; @ApiProperty({ nullable: true }) - winRate: number | null; + winRate!: number | null; @ApiProperty({ nullable: true }) - podiumRate: number | null; + podiumRate!: number | null; @ApiProperty({ nullable: true }) - percentile: number | null; + percentile!: number | null; @ApiProperty({ nullable: true }) - rating: number | null; + rating!: number | null; @ApiProperty({ nullable: true }) - consistency: number | null; + consistency!: number | null; @ApiProperty({ nullable: true }) - overallRank: number | null; + overallRank!: number | null; } export class DriverProfileFinishDistributionDTO { diff --git a/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts b/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts index 32cff73a5..206e2cbfa 100644 --- a/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts +++ b/apps/api/src/domain/driver/dtos/GetDriverRegistrationStatusQueryDTO.ts @@ -4,9 +4,9 @@ import { IsString } from 'class-validator'; export class GetDriverRegistrationStatusQueryDTO { @ApiProperty() @IsString() - raceId: string; + raceId!: string; @ApiProperty() @IsString() - driverId: string; + 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 bb9a1416f..7d9fd11d2 100644 --- a/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts +++ b/apps/api/src/domain/driver/presenters/CompleteOnboardingPresenter.ts @@ -1,18 +1,20 @@ -import type { - CompleteDriverOnboardingResult, -} from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; +import { Result } from '@core/shared/application/Result'; import type { CompleteOnboardingOutputDTO } from '../dtos/CompleteOnboardingOutputDTO'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export class CompleteOnboardingPresenter - implements UseCaseOutputPort -{ +export class CompleteOnboardingPresenter { private responseModel: CompleteOnboardingOutputDTO | null = null; - present(result: CompleteDriverOnboardingResult): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + present(result: Result): void { + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to complete onboarding'); + } + + const data = result.unwrap(); this.responseModel = { success: true, - driverId: result.driver.id, + driverId: data.driver.id, }; } diff --git a/apps/api/src/domain/driver/presenters/DriverPresenter.ts b/apps/api/src/domain/driver/presenters/DriverPresenter.ts index 48bfe3a36..1a0bbe403 100644 --- a/apps/api/src/domain/driver/presenters/DriverPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriverPresenter.ts @@ -1,10 +1,18 @@ +import { Result } from '@core/shared/application/Result'; import type { Driver } from '@core/racing/domain/entities/Driver'; import type { GetDriverOutputDTO } from '../dtos/GetDriverOutputDTO'; export class DriverPresenter { private responseModel: GetDriverOutputDTO | null = null; - present(driver: Driver | null): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + present(result: Result): void { + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to get driver'); + } + + const driver = result.unwrap(); if (!driver) { this.responseModel = null; return; @@ -15,8 +23,8 @@ export class DriverPresenter { iracingId: driver.iracingId.toString(), name: driver.name.toString(), country: driver.country.toString(), - bio: driver.bio?.toString(), joinedAt: driver.joinedAt.toDate().toISOString(), + ...(driver.bio ? { bio: driver.bio.toString() } : {}), }; } diff --git a/apps/api/src/domain/driver/presenters/DriverProfilePresenter.ts b/apps/api/src/domain/driver/presenters/DriverProfilePresenter.ts index 0d391a130..bc3eca1f1 100644 --- a/apps/api/src/domain/driver/presenters/DriverProfilePresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriverProfilePresenter.ts @@ -1,11 +1,9 @@ import type { GetProfileOverviewResult, } from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; -import type { GetDriverProfileOutputDTO } from '../dtos/GetDriverProfileOutputDTO'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import type { GetDriverProfileOutputDTO, DriverProfileExtendedProfileDTO } from '../dtos/GetDriverProfileOutputDTO'; export class DriverProfilePresenter - implements UseCaseOutputPort { private responseModel: GetDriverProfileOutputDTO | null = null; @@ -45,7 +43,7 @@ export class DriverProfilePresenter avatarUrl: '', // TODO: get avatar })), }, - extendedProfile: result.extendedProfile as any, + extendedProfile: result.extendedProfile as DriverProfileExtendedProfileDTO | null, }; } @@ -54,7 +52,7 @@ export class DriverProfilePresenter return this.responseModel; } - private getAvatarUrl(driverId: string): string | undefined { + private getAvatarUrl(_driverId: string): string | undefined { // Avatar resolution is delegated to infrastructure; keep as-is for now. return undefined; } diff --git a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts index 26069fa3e..d43c91104 100644 --- a/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriverRegistrationStatusPresenter.ts @@ -2,11 +2,8 @@ import type { IsDriverRegisteredForRaceResult, } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { DriverRegistrationStatusDTO } from '../dtos/DriverRegistrationStatusDTO'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export class DriverRegistrationStatusPresenter - implements UseCaseOutputPort -{ +export class DriverRegistrationStatusPresenter { private responseModel: DriverRegistrationStatusDTO | null = null; reset(): void { diff --git a/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts b/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts index a106be153..8f8706dac 100644 --- a/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriverStatsPresenter.ts @@ -2,11 +2,8 @@ import { DriverStatsDTO } from '../dtos/DriverStatsDTO'; import type { GetTotalDriversResult, } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export class DriverStatsPresenter - implements UseCaseOutputPort -{ +export class DriverStatsPresenter { private responseModel: DriverStatsDTO | null = null; present(result: GetTotalDriversResult): void { diff --git a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.test.ts b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.test.ts index 833833bf8..c484855f1 100644 --- a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.test.ts +++ b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.test.ts @@ -19,11 +19,11 @@ describe('DriversLeaderboardPresenter', () => { { driver: { id: 'driver-1', - name: 'Driver One' as any, - country: 'US' as any, - } as any, + name: 'Driver One' as unknown, + country: 'US' as unknown, + } as unknown, rating: 2500, - skillLevel: 'advanced' as any, + skillLevel: 'advanced' as unknown, racesCompleted: 50, wins: 10, podiums: 20, @@ -34,11 +34,11 @@ describe('DriversLeaderboardPresenter', () => { { driver: { id: 'driver-2', - name: 'Driver Two' as any, - country: 'DE' as any, - } as any, + name: 'Driver Two' as unknown, + country: 'DE' as unknown, + } as unknown, rating: 2400, - skillLevel: 'intermediate' as any, + skillLevel: 'intermediate' as unknown, racesCompleted: 40, wins: 5, podiums: 15, diff --git a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts index 91aea5b7f..efb3fc21c 100644 --- a/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts +++ b/apps/api/src/domain/driver/presenters/DriversLeaderboardPresenter.ts @@ -1,15 +1,23 @@ import { DriversLeaderboardDTO } from '../dtos/DriversLeaderboardDTO'; +import { Result } from '@core/shared/application/Result'; +import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { GetDriversLeaderboardResult, + GetDriversLeaderboardErrorCode, } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export class DriversLeaderboardPresenter implements UseCaseOutputPort { +export class DriversLeaderboardPresenter { private responseModel: DriversLeaderboardDTO | null = null; - present(result: GetDriversLeaderboardResult): void { + present(result: Result>): void { + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to get drivers leaderboard'); + } + + const data = result.unwrap(); this.responseModel = { - drivers: result.items.map(item => ({ + drivers: data.items.map(item => ({ id: item.driver.id, name: item.driver.name.toString(), rating: item.rating, @@ -20,11 +28,11 @@ export class DriversLeaderboardPresenter implements UseCaseOutputPort { diff --git a/core/racing/application/ports/IImageServicePort.ts b/core/racing/application/ports/IImageServicePort.ts new file mode 100644 index 000000000..45730d46c --- /dev/null +++ b/core/racing/application/ports/IImageServicePort.ts @@ -0,0 +1,6 @@ +export interface IImageServicePort { + getDriverAvatar(driverId: string): string; + getTeamLogo(teamId: string): string; + getLeagueCover(leagueId: string): string; + getLeagueLogo(leagueId: string): string; +} \ No newline at end of file diff --git a/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts b/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts index 0558ca277..c8f7c1549 100644 --- a/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts +++ b/core/racing/application/use-cases/CompleteDriverOnboardingUseCase.ts @@ -2,7 +2,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit import { Driver } from '../../domain/entities/Driver'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { UseCaseOutputPort, UseCase } from '@core/shared/application'; +import type { UseCase, UseCaseOutputPort } from '@core/shared/application'; import type { Logger } from '@core/shared/application/Logger'; export interface CompleteDriverOnboardingInput { @@ -30,16 +30,15 @@ export type CompleteDriverOnboardingApplicationError = ApplicationErrorCode< /** * Use Case for completing driver onboarding. */ -export class CompleteDriverOnboardingUseCase implements UseCase { +export class CompleteDriverOnboardingUseCase implements UseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly logger: Logger, - private readonly output: UseCaseOutputPort, ) {} async execute( input: CompleteDriverOnboardingInput, - ): Promise> { + ): Promise> { try { const existing = await this.driverRepository.findById(input.userId); if (existing) { @@ -59,9 +58,8 @@ export class CompleteDriverOnboardingUseCase implements UseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly rankingService: IRankingService, private readonly driverStatsService: IDriverStatsService, private readonly getDriverAvatar: (driverId: string) => Promise, private readonly logger: Logger, - private readonly output: UseCaseOutputPort, ) {} async execute( input: GetDriversLeaderboardInput, ): Promise< Result< - void, + GetDriversLeaderboardResult, ApplicationErrorCode > > { @@ -108,9 +107,7 @@ export class GetDriversLeaderboardUseCase { this.logger.debug('Successfully computed drivers leaderboard'); - this.output.present(result); - - return Result.ok(undefined); + return Result.ok(result); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); diff --git a/core/racing/application/use-cases/GetProfileOverviewUseCase.ts b/core/racing/application/use-cases/GetProfileOverviewUseCase.ts index 4f6f4ad74..2d6efebe1 100644 --- a/core/racing/application/use-cases/GetProfileOverviewUseCase.ts +++ b/core/racing/application/use-cases/GetProfileOverviewUseCase.ts @@ -92,7 +92,7 @@ export type GetProfileOverviewErrorCode = | 'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR'; -export class GetProfileOverviewUseCase implements UseCase { +export class GetProfileOverviewUseCase implements UseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly teamRepository: ITeamRepository, @@ -102,13 +102,12 @@ export class GetProfileOverviewUseCase implements UseCase ProfileDriverStatsAdapter | null, private readonly getAllDriverRankings: () => DriverRankingEntry[], - private readonly output: UseCaseOutputPort, ) {} async execute( input: GetProfileOverviewInput, ): Promise< - Result> + Result> > { try { const { driverId } = input; @@ -144,9 +143,7 @@ export class GetProfileOverviewUseCase implements UseCase { +export class GetTotalDriversUseCase implements UseCase { constructor( private readonly driverRepository: IDriverRepository, - private readonly output: UseCaseOutputPort, ) {} async execute( _input: GetTotalDriversInput, - ): Promise>> { + ): Promise>> { try { const drivers = await this.driverRepository.findAll(); const result: GetTotalDriversResult = { totalDrivers: drivers.length }; - this.output.present(result); - - return Result.ok(undefined); + return Result.ok(result); } catch (error) { const message = (error as Error | undefined)?.message ?? 'Failed to compute total drivers'; diff --git a/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts b/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts index 317dcfe0d..c2b3afe4d 100644 --- a/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts +++ b/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.ts @@ -1,5 +1,5 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; -import type { Logger, UseCaseOutputPort, UseCase } from '@core/shared/application'; +import type { Logger, UseCase } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -26,23 +26,20 @@ export type IsDriverRegisteredForRaceResult = { * * Checks if a driver is registered for a specific race. */ -export class IsDriverRegisteredForRaceUseCase implements UseCase { +export class IsDriverRegisteredForRaceUseCase implements UseCase { constructor( private readonly registrationRepository: IRaceRegistrationRepository, private readonly logger: Logger, - private readonly output: UseCaseOutputPort, ) {} - async execute(params: IsDriverRegisteredForRaceInput): Promise> { + async execute(params: IsDriverRegisteredForRaceInput): Promise> { this.logger.debug('IsDriverRegisteredForRaceUseCase:execute', { params }); const { raceId, driverId } = params; try { const isRegistered = await this.registrationRepository.isRegistered(raceId, driverId); - this.output.present({ isRegistered, raceId, driverId }); - - return Result.ok(undefined); + return Result.ok({ isRegistered, raceId, driverId }); } catch (error) { this.logger.error( 'IsDriverRegisteredForRaceUseCase:execution error', diff --git a/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts b/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts index 78123a98b..04943051d 100644 --- a/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts +++ b/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts @@ -1,5 +1,5 @@ import { Result } from '@core/shared/application/Result'; -import type { UseCaseOutputPort, UseCase } from '@core/shared/application'; +import type { UseCase } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Logger } from '@core/shared/application/Logger'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; @@ -23,16 +23,15 @@ export type UpdateDriverProfileErrorCode = * Encapsulates domain entity mutation. Mapping to DTOs is handled by presenters * in the presentation layer through the output port. */ -export class UpdateDriverProfileUseCase implements UseCase { +export class UpdateDriverProfileUseCase implements UseCase { constructor( private readonly driverRepository: IDriverRepository, - private readonly output: UseCaseOutputPort, private readonly logger: Logger, ) {} async execute( input: UpdateDriverProfileInput, - ): Promise>> { + ): Promise>> { const { driverId, bio, country } = input; if ((bio !== undefined && bio.trim().length === 0) || (country !== undefined && country.trim().length === 0)) { @@ -62,9 +61,7 @@ export class UpdateDriverProfileUseCase implements UseCase=20.0.0" - }, - "workspaces": [ - "core/*", - "apps/*", - "testing/*" - ], - "scripts": { - "dev": "echo 'Development server placeholder - to be configured'", - "build": "echo 'Build all packages placeholder - to be configured'", - "docker:dev": "docker-compose -f docker-compose.dev.yml up", - "docker:dev:build": "docker-compose -f docker-compose.dev.yml up --build", - "docker:dev:down": "docker-compose -f docker-compose.dev.yml down", - "docker:dev:logs": "docker-compose -f docker-compose.dev.yml logs -f", - "docker:dev:clean": "docker-compose -f docker-compose.dev.yml down -v", - "docker:prod": "docker-compose -f docker-compose.prod.yml up -d", - "docker:prod:build": "docker-compose -f docker-compose.prod.yml up -d --build", - "docker:prod:down": "docker-compose -f docker-compose.prod.yml down", - "docker:prod:logs": "docker-compose -f docker-compose.prod.yml logs -f", - "docker:prod:clean": "docker-compose -f docker-compose.prod.yml down -v", - "api:build": "npm run build --workspace=@gridpilot/api", - "api:generate-spec": "tsx scripts/generate-openapi-spec.ts", - "api:generate-types": "tsx scripts/generate-api-types.ts", - "api:sync-types": "npm run api:generate-spec && npm run api:generate-types", - "test": "vitest run \"$@\"", - "test:unit": "vitest run tests/unit", - "test:integration": "vitest run tests/integration", - "test:e2e": "vitest run --config vitest.e2e.config.ts", - "test:e2e:docker": "vitest run --config vitest.e2e.config.ts tests/e2e/docker/", - "test:watch": "vitest watch", - "test:smoke": "vitest run --config vitest.smoke.config.ts", - "test:smoke:watch": "vitest watch --config vitest.smoke.config.ts", - "test:smoke:electron": "playwright test --config=playwright.smoke.config.ts", - "smoke:website": "npm run website:build && npx playwright test -c playwright.website.config.ts", - "test:hosted-real": "vitest run --config vitest.e2e.config.ts tests/e2e/hosted-real/", - "test:companion-hosted": "vitest run --config vitest.e2e.config.ts tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts", - "typecheck": "npx tsc --noEmit", - "test:types": "tsc --noEmit -p tsconfig.tests.json", - "companion:dev": "npm run dev --workspace=@gridpilot/companion", - "companion:build": "npm run build --workspace=@gridpilot/companion", - "companion:start": "npm run start --workspace=@gridpilot/companion", - "env:website:merge": "node scripts/merge-website-env.js", - "website:dev": "npm run env:website:merge && npm run dev --workspace=@gridpilot/website", - "website:build": "npm run env:website:merge && npm run build --workspace=@gridpilot/website", - "website:start": "npm run start --workspace=@gridpilot/website", - "website:lint": "npm run lint --workspace=@gridpilot/website", - "website:type-check": "npm run type-check --workspace=@gridpilot/website", - "website:clean": "npm run clean --workspace=@gridpilot/website", - "deploy:website:preview": "npx vercel deploy --cwd apps/website", - "deploy:website:prod": "npx vercel deploy --prod", - "deploy:website": "npm run deploy:website:prod", - "chrome:debug": "open -a 'Google Chrome' --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug", - "docker:e2e:up": "docker-compose -f docker/docker-compose.e2e.yml up -d", - "docker:e2e:down": "docker-compose -f docker/docker-compose.e2e.yml down", - "generate-templates": "npx tsx scripts/generate-templates/index.ts", - "minify-fixtures": "npx tsx scripts/minify-fixtures.ts", - "minify-fixtures:force": "npx tsx scripts/minify-fixtures.ts --force", - "dom:process": "npx tsx scripts/dom-export/processWorkflows.ts", - "prepare": "husky install || true" - }, "devDependencies": { "@cucumber/cucumber": "^11.0.1", "@playwright/test": "^1.57.0", @@ -93,17 +42,69 @@ "typescript": "^5.9.3", "vitest": "^4.0.15" }, - "dependencies": { - "@core/social": "file:core/social", - "@nestjs/swagger": "11.2.3", - "bcrypt": "^6.0.0", - "electron-vite": "3.1.0", - "next": "15.5.9", - "playwright-extra": "^4.3.6", - "puppeteer-extra-plugin-stealth": "^2.11.2", - "reflect-metadata": "^0.2.2", - "tsyringe": "^4.10.0", - "uuid": "^13.0.0", - "vite": "6.4.1" - } -} + "engines": { + "node": ">=20.0.0" + }, + "name": "gridpilot", + "private": true, + "scripts": { + "api:build": "npm run build --workspace=@gridpilot/api", + "api:generate-spec": "tsx scripts/generate-openapi-spec.ts", + "api:generate-types": "tsx scripts/generate-api-types.ts", + "api:sync-types": "npm run api:generate-spec && npm run api:generate-types", + "build": "echo 'Build all packages placeholder - to be configured'", + "chrome:debug": "open -a 'Google Chrome' --args --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug", + "companion:build": "npm run build --workspace=@gridpilot/companion", + "companion:dev": "npm run dev --workspace=@gridpilot/companion", + "companion:start": "npm run start --workspace=@gridpilot/companion", + "deploy:website": "npm run deploy:website:prod", + "deploy:website:preview": "npx vercel deploy --cwd apps/website", + "deploy:website:prod": "npx vercel deploy --prod", + "dev": "echo 'Development server placeholder - to be configured'", + "docker:dev": "docker-compose -f docker-compose.dev.yml up", + "docker:dev:build": "docker-compose -f docker-compose.dev.yml up --build", + "docker:dev:clean": "docker-compose -f docker-compose.dev.yml down -v", + "docker:dev:down": "docker-compose -f docker-compose.dev.yml down", + "docker:dev:logs": "docker-compose -f docker-compose.dev.yml logs -f", + "docker:e2e:down": "docker-compose -f docker/docker-compose.e2e.yml down", + "docker:e2e:up": "docker-compose -f docker/docker-compose.e2e.yml up -d", + "docker:prod": "docker-compose -f docker-compose.prod.yml up -d", + "docker:prod:build": "docker-compose -f docker-compose.prod.yml up -d --build", + "docker:prod:clean": "docker-compose -f docker-compose.prod.yml down -v", + "docker:prod:down": "docker-compose -f docker-compose.prod.yml down", + "docker:prod:logs": "docker-compose -f docker-compose.prod.yml logs -f", + "dom:process": "npx tsx scripts/dom-export/processWorkflows.ts", + "env:website:merge": "node scripts/merge-website-env.js", + "generate-templates": "npx tsx scripts/generate-templates/index.ts", + "minify-fixtures": "npx tsx scripts/minify-fixtures.ts", + "minify-fixtures:force": "npx tsx scripts/minify-fixtures.ts --force", + "prepare": "husky install || true", + "smoke:website": "npm run website:build && npx playwright test -c playwright.website.config.ts", + "test": "vitest run \"$@\"", + "test:companion-hosted": "vitest run --config vitest.e2e.config.ts tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts", + "test:e2e": "vitest run --config vitest.e2e.config.ts", + "test:e2e:docker": "vitest run --config vitest.e2e.config.ts tests/e2e/docker/", + "test:hosted-real": "vitest run --config vitest.e2e.config.ts tests/e2e/hosted-real/", + "test:integration": "vitest run tests/integration", + "test:smoke": "vitest run --config vitest.smoke.config.ts", + "test:smoke:electron": "playwright test --config=playwright.smoke.config.ts", + "test:smoke:watch": "vitest watch --config vitest.smoke.config.ts", + "test:types": "tsc --noEmit -p tsconfig.tests.json", + "test:unit": "vitest run tests/unit", + "test:watch": "vitest watch", + "typecheck": "npx tsc --noEmit --project tsconfig.json", + "typecheck:grep": "npm run typescript | grep", + "website:build": "npm run env:website:merge && npm run build --workspace=@gridpilot/website", + "website:clean": "npm run clean --workspace=@gridpilot/website", + "website:dev": "npm run env:website:merge && npm run dev --workspace=@gridpilot/website", + "website:lint": "npm run lint --workspace=@gridpilot/website", + "website:start": "npm run start --workspace=@gridpilot/website", + "website:type-check": "npm run type-check --workspace=@gridpilot/website" + }, + "version": "0.1.0", + "workspaces": [ + "core/*", + "apps/*", + "testing/*" + ] +} \ No newline at end of file