From e3c6146c1d7d371520b0a2b7be545be1c8001f23 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 14 Jan 2026 17:25:30 +0100 Subject: [PATCH] website refactor --- apps/api/openapi.json | 16 +++++ .../domain/driver/DriverController.test.ts | 15 ++++ .../api/src/domain/driver/DriverController.ts | 9 +++ apps/api/src/domain/driver/DriverProviders.ts | 18 +++++ .../src/domain/driver/DriverService.test.ts | 37 ++++++++++ apps/api/src/domain/driver/DriverService.ts | 18 +++++ apps/api/src/domain/driver/DriverTokens.ts | 5 +- .../driver/dtos/GetDriverLiveriesOutputDTO.ts | 23 ++++++ .../GetDriverLiveriesPresenter.test.ts | 70 +++++++++++++++++++ .../presenters/GetDriverLiveriesPresenter.ts | 33 +++++++++ 10 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/domain/driver/dtos/GetDriverLiveriesOutputDTO.ts create mode 100644 apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.test.ts create mode 100644 apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.ts diff --git a/apps/api/openapi.json b/apps/api/openapi.json index 0167968fd..581e32ded 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -175,6 +175,22 @@ } } }, + "/drivers/{driverId}/liveries": { + "get": { + "responses": { + "200": { + "description": "List of driver liveries", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetDriverLiveriesOutputDTO" + } + } + } + } + } + } + }, "/drivers/{driverId}/races/{raceId}/registration-status": { "get": { "responses": { diff --git a/apps/api/src/domain/driver/DriverController.test.ts b/apps/api/src/domain/driver/DriverController.test.ts index e82559537..9d6f3fbed 100644 --- a/apps/api/src/domain/driver/DriverController.test.ts +++ b/apps/api/src/domain/driver/DriverController.test.ts @@ -20,6 +20,7 @@ import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO'; import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO'; import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; import { DriverStatsDTO } from './dtos/DriverStatsDTO'; +import { GetDriverLiveriesOutputDTO } from './dtos/GetDriverLiveriesOutputDTO'; import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO'; import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO'; import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO'; @@ -43,6 +44,7 @@ describe('DriverController', () => { getDriver: vi.fn(), getDriverProfile: vi.fn(), updateDriverProfile: vi.fn(), + getDriverLiveries: vi.fn(), }, }, ], @@ -170,6 +172,19 @@ describe('DriverController', () => { }); }); + describe('getDriverLiveries', () => { + it('should return driver liveries', async () => { + const driverId = 'driver-123'; + const liveries: GetDriverLiveriesOutputDTO = { liveries: [] }; + service.getDriverLiveries.mockResolvedValue(liveries); + + const result = await controller.getDriverLiveries(driverId); + + expect(service.getDriverLiveries).toHaveBeenCalledWith(driverId); + expect(result).toEqual(liveries); + }); + }); + describe('auth guards (HTTP)', () => { let app: any; diff --git a/apps/api/src/domain/driver/DriverController.ts b/apps/api/src/domain/driver/DriverController.ts index d0085eb7b..3ec168de8 100644 --- a/apps/api/src/domain/driver/DriverController.ts +++ b/apps/api/src/domain/driver/DriverController.ts @@ -12,6 +12,7 @@ import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO' import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO'; import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; import { DriverStatsDTO } from './dtos/DriverStatsDTO'; +import { GetDriverLiveriesOutputDTO } from './dtos/GetDriverLiveriesOutputDTO'; import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO'; import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO'; @@ -111,5 +112,13 @@ export class DriverController { return await this.driverService.updateDriverProfile(driverId, body.bio, body.country); } + @Public() + @Get(':driverId/liveries') + @ApiOperation({ summary: 'Get driver liveries' }) + @ApiResponse({ status: 200, description: 'List of driver liveries', type: GetDriverLiveriesOutputDTO }) + async getDriverLiveries(@Param('driverId') driverId: string): Promise { + return await this.driverService.getDriverLiveries(driverId); + } + // 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 7eede172f..7e219951f 100644 --- a/apps/api/src/domain/driver/DriverProviders.ts +++ b/apps/api/src/domain/driver/DriverProviders.ts @@ -3,6 +3,7 @@ import { Provider } from '@nestjs/common'; // Import core interfaces import { DriverExtendedProfileProvider } from '@core/racing/application/ports/DriverExtendedProfileProvider'; import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; +import { ILiveryRepository } from '@core/racing/domain/repositories/ILiveryRepository'; import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; @@ -15,6 +16,7 @@ import type { MediaResolverPort } from '@core/ports/media/MediaResolverPort'; // Import use cases import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase'; +import { GetDriverLiveriesUseCase } from '@core/racing/application/use-cases/GetDriverLiveriesUseCase'; 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'; @@ -26,6 +28,7 @@ import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImage import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository'; import { InMemoryDriverExtendedProfileProvider } from '@adapters/racing/ports/InMemoryDriverExtendedProfileProvider'; import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider'; +import { InMemoryLiveryRepository } from '@adapters/racing/persistence/inmemory/InMemoryLiveryRepository'; // Import new use cases import { RankingUseCase } from '@core/racing/application/use-cases/RankingUseCase'; import { DriverStatsUseCase } from '@core/racing/application/use-cases/DriverStatsUseCase'; @@ -47,6 +50,7 @@ import { DriverProfilePresenter } from './presenters/DriverProfilePresenter'; import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; +import { GetDriverLiveriesPresenter } from './presenters/GetDriverLiveriesPresenter'; import { DRIVER_REPOSITORY_TOKEN, @@ -58,9 +62,11 @@ import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN, + LIVERY_REPOSITORY_TOKEN, LOGGER_TOKEN, GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, + GET_DRIVER_LIVERIES_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, @@ -112,6 +118,7 @@ export const DriverProviders: Provider[] = createLoggedProviders([ }, inject: [MEDIA_RESOLVER_TOKEN], }, + GetDriverLiveriesPresenter, // Logger { @@ -189,6 +196,11 @@ export const DriverProviders: Provider[] = createLoggedProviders([ useFactory: (logger: Logger) => new InMemoryNotificationPreferenceRepository(logger), inject: [LOGGER_TOKEN], }, + { + provide: LIVERY_REPOSITORY_TOKEN, + useFactory: (logger: Logger) => new InMemoryLiveryRepository(logger), + inject: [LOGGER_TOKEN], + }, // Use cases { @@ -258,4 +270,10 @@ export const DriverProviders: Provider[] = createLoggedProviders([ RANKING_SERVICE_TOKEN, ], }, + { + provide: GET_DRIVER_LIVERIES_USE_CASE_TOKEN, + useFactory: (liveryRepository: ILiveryRepository, logger: Logger) => + new GetDriverLiveriesUseCase(liveryRepository, logger), + inject: [LIVERY_REPOSITORY_TOKEN, LOGGER_TOKEN], + }, ], initLogger); diff --git a/apps/api/src/domain/driver/DriverService.test.ts b/apps/api/src/domain/driver/DriverService.test.ts index e5893d039..9ab0c5395 100644 --- a/apps/api/src/domain/driver/DriverService.test.ts +++ b/apps/api/src/domain/driver/DriverService.test.ts @@ -317,4 +317,41 @@ describe('DriverService', () => { expect(getProfileOverviewUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1' }); expect(driverProfilePresenter.getResponseModel).toHaveBeenCalled(); }); + + it('getDriverLiveries executes use case and returns presenter model', async () => { + const getDriverLiveriesUseCase = { execute: vi.fn(async () => Result.ok(undefined)) }; + const getDriverLiveriesPresenter = { + present: vi.fn(), + getResponseModel: vi.fn(() => ({ liveries: [] })) + }; + const driverPresenter = { + setMediaResolver: vi.fn(), + setBaseUrl: vi.fn(), + present: vi.fn(), + getResponseModel: vi.fn(() => null) + }; + + const service = new DriverService( + { execute: vi.fn() } as any, + { execute: vi.fn() } as any, + getDriverLiveriesUseCase as any, + { execute: vi.fn() } as any, + { execute: vi.fn() } as any, + { execute: vi.fn() } as any, + { findById: vi.fn() } as any, + logger as any, + { getResponseModel: vi.fn(() => ({ items: [] })) } as any, + { getResponseModel: vi.fn(() => ({ totalDrivers: 0 })) } as any, + { getResponseModel: vi.fn(() => ({ success: true })) } as any, + { getResponseModel: vi.fn(() => ({ isRegistered: false })) } as any, + driverPresenter as any, + { getResponseModel: vi.fn(() => ({ profile: {} })) } as any, + getDriverLiveriesPresenter as any, + ); + + await expect(service.getDriverLiveries('d1')).resolves.toEqual({ liveries: [] }); + expect(getDriverLiveriesUseCase.execute).toHaveBeenCalledWith({ driverId: 'd1' }); + expect(getDriverLiveriesPresenter.present).toHaveBeenCalled(); + expect(getDriverLiveriesPresenter.getResponseModel).toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/apps/api/src/domain/driver/DriverService.ts b/apps/api/src/domain/driver/DriverService.ts index c6ac5bb35..e477f9207 100644 --- a/apps/api/src/domain/driver/DriverService.ts +++ b/apps/api/src/domain/driver/DriverService.ts @@ -6,11 +6,13 @@ import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO' import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO'; import { DriverStatsDTO } from './dtos/DriverStatsDTO'; import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO'; +import { GetDriverLiveriesOutputDTO } from './dtos/GetDriverLiveriesOutputDTO'; import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO'; import { GetDriverRegistrationStatusQueryDTO } from './dtos/GetDriverRegistrationStatusQueryDTO'; // Use cases import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; +import { GetDriverLiveriesUseCase } from '@core/racing/application/use-cases/GetDriverLiveriesUseCase'; 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'; @@ -24,6 +26,7 @@ import { DriverProfilePresenter } from './presenters/DriverProfilePresenter'; import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter'; import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter'; import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; +import { GetDriverLiveriesPresenter } from './presenters/GetDriverLiveriesPresenter'; // Tokens import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; @@ -31,6 +34,7 @@ import type { Logger } from '@core/shared/application'; import { COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, DRIVER_REPOSITORY_TOKEN, + GET_DRIVER_LIVERIES_USE_CASE_TOKEN, GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, @@ -46,6 +50,8 @@ export class DriverService { private readonly getDriversLeaderboardUseCase: GetDriversLeaderboardUseCase, @Inject(GET_TOTAL_DRIVERS_USE_CASE_TOKEN) private readonly getTotalDriversUseCase: GetTotalDriversUseCase, + @Inject(GET_DRIVER_LIVERIES_USE_CASE_TOKEN) + private readonly getDriverLiveriesUseCase: GetDriverLiveriesUseCase, @Inject(COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN) private readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase, @Inject(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) @@ -65,6 +71,7 @@ export class DriverService { private readonly driverRegistrationStatusPresenter?: DriverRegistrationStatusPresenter, private readonly driverPresenter?: DriverPresenter, private readonly driverProfilePresenter?: DriverProfilePresenter, + private readonly getDriverLiveriesPresenter?: GetDriverLiveriesPresenter, ) { // Presenters are configured by providers, no need to configure here } @@ -175,4 +182,15 @@ export class DriverService { } return this.driverProfilePresenter!.getResponseModel(); } + + async getDriverLiveries(driverId: string): Promise { + this.logger.debug(`[DriverService] Fetching driver liveries for driverId: ${driverId}`); + + const result = await this.getDriverLiveriesUseCase.execute({ driverId }); + if (result.isErr()) { + throw new Error(result.unwrapErr().details.message); + } + await this.getDriverLiveriesPresenter!.present(result); + return this.getDriverLiveriesPresenter!.getResponseModel()!; + } } \ No newline at end of file diff --git a/apps/api/src/domain/driver/DriverTokens.ts b/apps/api/src/domain/driver/DriverTokens.ts index c0603f86f..1fc73e683 100644 --- a/apps/api/src/domain/driver/DriverTokens.ts +++ b/apps/api/src/domain/driver/DriverTokens.ts @@ -11,6 +11,7 @@ import { SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../../persistence/social/SocialPe export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository'; export { SOCIAL_GRAPH_REPOSITORY_TOKEN }; +export const LIVERY_REPOSITORY_TOKEN = 'ILiveryRepository'; export const LOGGER_TOKEN = 'Logger'; // New tokens for clean architecture @@ -24,10 +25,12 @@ export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardi export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase'; export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase'; export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase'; +export const GET_DRIVER_LIVERIES_USE_CASE_TOKEN = 'GetDriverLiveriesUseCase'; export const GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN = 'GetDriversLeaderboardOutputPort_TOKEN'; export const GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN = 'GetTotalDriversOutputPort_TOKEN'; export const COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN = 'CompleteDriverOnboardingOutputPort_TOKEN'; export const IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN = 'IsDriverRegisteredForRaceOutputPort_TOKEN'; export const UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN = 'UpdateDriverProfileOutputPort_TOKEN'; -export const GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN = 'GetProfileOverviewOutputPort_TOKEN'; \ No newline at end of file +export const GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN = 'GetProfileOverviewOutputPort_TOKEN'; +export const GET_DRIVER_LIVERIES_OUTPUT_PORT_TOKEN = 'GetDriverLiveriesOutputPort_TOKEN'; \ No newline at end of file diff --git a/apps/api/src/domain/driver/dtos/GetDriverLiveriesOutputDTO.ts b/apps/api/src/domain/driver/dtos/GetDriverLiveriesOutputDTO.ts new file mode 100644 index 000000000..a382764de --- /dev/null +++ b/apps/api/src/domain/driver/dtos/GetDriverLiveriesOutputDTO.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; + +class LiveryDTO { + @ApiProperty() + id!: string; + + @ApiProperty() + name!: string; + + @ApiProperty() + imageUrl!: string; + + @ApiProperty() + createdAt!: string; + + @ApiProperty() + isActive!: boolean; +} + +export class GetDriverLiveriesOutputDTO { + @ApiProperty({ type: [LiveryDTO] }) + liveries!: LiveryDTO[]; +} \ No newline at end of file diff --git a/apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.test.ts b/apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.test.ts new file mode 100644 index 000000000..7434c29aa --- /dev/null +++ b/apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Result } from '@core/shared/application/Result'; +import { GetDriverLiveriesPresenter } from './GetDriverLiveriesPresenter'; +import { DriverLivery } from '@core/racing/domain/entities/DriverLivery'; + +describe('GetDriverLiveriesPresenter', () => { + let presenter: GetDriverLiveriesPresenter; + + beforeEach(() => { + presenter = new GetDriverLiveriesPresenter(); + }); + + describe('present', () => { + it('should map core result to API response model correctly', () => { + const mockLiveries: DriverLivery[] = [ + DriverLivery.create({ + id: 'livery1', + driverId: 'driver1', + gameId: 'game1', + carId: 'car1', + uploadedImageUrl: 'http://example.com/livery1.png', + createdAt: new Date('2023-01-01'), + }), + DriverLivery.create({ + id: 'livery2', + driverId: 'driver1', + gameId: 'game1', + carId: 'car2', + uploadedImageUrl: 'http://example.com/livery2.png', + createdAt: new Date('2023-01-02'), + }), + ]; + + const result = Result.ok(mockLiveries); + presenter.present(result); + + const response = presenter.getResponseModel(); + + expect(response).toEqual({ + liveries: [ + { + id: 'livery1', + name: 'Livery for car1', + imageUrl: 'http://example.com/livery1.png', + createdAt: '2023-01-01T00:00:00.000Z', + isActive: true, + }, + { + id: 'livery2', + name: 'Livery for car2', + imageUrl: 'http://example.com/livery2.png', + createdAt: '2023-01-02T00:00:00.000Z', + isActive: false, + }, + ], + }); + }); + + it('should handle empty liveries array', () => { + const result = Result.ok([]); + presenter.present(result); + + const response = presenter.getResponseModel(); + + expect(response).toEqual({ + liveries: [], + }); + }); + }); +}); \ No newline at end of file diff --git a/apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.ts b/apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.ts new file mode 100644 index 000000000..68357872d --- /dev/null +++ b/apps/api/src/domain/driver/presenters/GetDriverLiveriesPresenter.ts @@ -0,0 +1,33 @@ +import { Result } from '@core/shared/application/Result'; +import type { DriverLivery } from '@core/racing/domain/entities/DriverLivery'; +import type { GetDriverLiveriesOutputDTO } from '../dtos/GetDriverLiveriesOutputDTO'; + +export class GetDriverLiveriesPresenter { + private responseModel: GetDriverLiveriesOutputDTO | null = null; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async present(result: Result): Promise { + if (result.isErr()) { + const error = result.unwrapErr(); + throw new Error(error.details?.message ?? 'Failed to get driver liveries'); + } + + const liveries = result.unwrap(); + + const dto: GetDriverLiveriesOutputDTO = { + liveries: liveries.map((livery, index) => ({ + id: livery.id, + name: `Livery for ${livery.carId.toString()}`, // Simple name generation + imageUrl: livery.uploadedImageUrl.toString(), + createdAt: livery.createdAt.toISOString(), + isActive: index === 0, // First livery is active by default + })), + }; + + this.responseModel = dto; + } + + getResponseModel(): GetDriverLiveriesOutputDTO | null { + return this.responseModel; + } +} \ No newline at end of file