This commit is contained in:
2025-12-21 22:35:38 +01:00
parent 3c64f328e2
commit 9bd2e630e6
38 changed files with 736 additions and 684 deletions

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
import { Controller, Get, Post, Put, Body, Req, Param } from '@nestjs/common';
import { Request } from 'express';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
@@ -25,16 +25,14 @@ export class DriverController {
@ApiOperation({ summary: 'Get drivers leaderboard' })
@ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardDTO })
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
const presenter = await this.driverService.getDriversLeaderboard();
return presenter.viewModel;
return await 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<DriverStatsDTO> {
const presenter = await this.driverService.getTotalDrivers();
return presenter.viewModel;
return await this.driverService.getTotalDrivers();
}
@Get('current')
@@ -47,8 +45,7 @@ export class DriverController {
return null;
}
const presenter = await this.driverService.getCurrentDriver(userId);
return presenter.viewModel;
return await this.driverService.getCurrentDriver(userId);
}
@Post('complete-onboarding')
@@ -59,8 +56,7 @@ export class DriverController {
@Req() req: AuthenticatedRequest,
): Promise<CompleteOnboardingOutputDTO> {
const userId = req.user!.userId;
const presenter = await this.driverService.completeOnboarding(userId, input);
return presenter.viewModel;
return await this.driverService.completeOnboarding(userId, input);
}
@Get(':driverId/races/:raceId/registration-status')
@@ -70,8 +66,7 @@ export class DriverController {
@Param('driverId') driverId: string,
@Param('raceId') raceId: string,
): Promise<DriverRegistrationStatusDTO> {
const presenter = await this.driverService.getDriverRegistrationStatus({ driverId, raceId });
return presenter.viewModel;
return await this.driverService.getDriverRegistrationStatus({ driverId, raceId });
}
@Get(':driverId')
@@ -79,8 +74,7 @@ export class DriverController {
@ApiResponse({ status: 200, description: 'Driver data', type: GetDriverOutputDTO })
@ApiResponse({ status: 404, description: 'Driver not found' })
async getDriver(@Param('driverId') driverId: string): Promise<GetDriverOutputDTO | null> {
const presenter = await this.driverService.getDriver(driverId);
return presenter.viewModel;
return await this.driverService.getDriver(driverId);
}
@Get(':driverId/profile')
@@ -88,8 +82,7 @@ export class DriverController {
@ApiResponse({ status: 200, description: 'Driver profile data', type: GetDriverProfileOutputDTO })
@ApiResponse({ status: 404, description: 'Driver not found' })
async getDriverProfile(@Param('driverId') driverId: string): Promise<GetDriverProfileOutputDTO> {
const presenter = await this.driverService.getDriverProfile(driverId);
return presenter.viewModel;
return await this.driverService.getDriverProfile(driverId);
}
@Put(':driverId/profile')
@@ -99,8 +92,7 @@ export class DriverController {
@Param('driverId') driverId: string,
@Body() body: { bio?: string; country?: string },
): Promise<GetDriverOutputDTO | null> {
const presenter = await this.driverService.updateDriverProfile(driverId, body.bio, body.country);
return presenter.viewModel;
return await this.driverService.updateDriverProfile(driverId, body.bio, body.country);
}
// Add other Driver endpoints here based on other presenters

View File

@@ -69,6 +69,13 @@ 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
@@ -138,8 +145,9 @@ export const DriverProviders: Provider[] = [
driverStatsService: IDriverStatsService,
imageService: IImageServicePort,
logger: Logger,
) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger),
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
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],
},
{
provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
@@ -148,19 +156,19 @@ export const DriverProviders: Provider[] = [
},
{
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository) => new CompleteDriverOnboardingUseCase(driverRepo),
inject: [DRIVER_REPOSITORY_TOKEN],
useFactory: (driverRepo: IDriverRepository, logger: Logger, presenter: CompleteOnboardingPresenter) => new CompleteDriverOnboardingUseCase(driverRepo, logger, presenter),
inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, CompleteOnboardingPresenter.name],
},
{
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) =>
new IsDriverRegisteredForRaceUseCase(registrationRepo, logger),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger, presenter: DriverRegistrationStatusPresenter) =>
new IsDriverRegisteredForRaceUseCase(registrationRepo, logger, presenter),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN, DriverRegistrationStatusPresenter.name],
},
{
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo),
inject: [DRIVER_REPOSITORY_TOKEN],
useFactory: (driverRepo: IDriverRepository, presenter: DriverPresenter, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, presenter, logger),
inject: [DRIVER_REPOSITORY_TOKEN, DriverPresenter.name, LOGGER_TOKEN],
},
{
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
@@ -173,6 +181,7 @@ export const DriverProviders: Provider[] = [
driverExtendedProfileProvider: DriverExtendedProfileProvider,
driverStatsService: IDriverStatsService,
rankingService: IRankingService,
presenter: DriverProfilePresenter,
) =>
new GetProfileOverviewUseCase(
driverRepo,
@@ -207,6 +216,7 @@ export const DriverProviders: Provider[] = [
rating: ranking.rating,
overallRank: ranking.overallRank,
})),
presenter,
),
inject: [
DRIVER_REPOSITORY_TOKEN,
@@ -217,6 +227,7 @@ export const DriverProviders: Provider[] = [
DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN,
DRIVER_STATS_SERVICE_TOKEN,
RANKING_SERVICE_TOKEN,
DriverProfilePresenter.name,
],
},
];

View File

@@ -14,7 +14,7 @@ import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTo
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
import { UpdateDriverProfileUseCase, type UpdateDriverProfileInput } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
// Presenters
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
@@ -70,10 +70,12 @@ export class DriverService {
const result = await this.getDriversLeaderboardUseCase.execute({});
const presenter = new DriversLeaderboardPresenter();
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load drivers leaderboard');
}
return presenter.getResponseModel();
return this.driversLeaderboardPresenter.getResponseModel();
}
async getTotalDrivers(): Promise<DriverStatsDTO> {
@@ -101,14 +103,15 @@ export class DriverService {
lastName: input.lastName,
displayName: input.displayName,
country: input.country,
timezone: input.timezone,
bio: input.bio,
...(input.bio !== undefined ? { bio: input.bio } : {}),
});
const presenter = new CompleteOnboardingPresenter();
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to complete onboarding');
}
return presenter.responseModel;
return this.completeOnboardingPresenter.getResponseModel();
}
async getDriverRegistrationStatus(
@@ -121,10 +124,12 @@ export class DriverService {
driverId: query.driverId,
});
const presenter = new DriverRegistrationStatusPresenter();
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to check registration status');
}
return presenter.responseModel;
return this.driverRegistrationStatusPresenter.getResponseModel();
}
async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> {
@@ -132,10 +137,9 @@ export class DriverService {
const driver = await this.driverRepository.findById(userId);
const presenter = new DriverPresenter();
presenter.present(driver ?? null);
this.driverPresenter.present(driver ?? null);
return presenter.responseModel;
return this.driverPresenter.getResponseModel();
}
async updateDriverProfile(
@@ -145,19 +149,21 @@ export class DriverService {
): Promise<GetDriverOutputDTO | null> {
this.logger.debug(`[DriverService] Updating driver profile for driverId: ${driverId}`);
const result = await this.updateDriverProfileUseCase.execute({ driverId, bio, country });
const input: UpdateDriverProfileInput = { driverId };
if (bio !== undefined) input.bio = bio;
if (country !== undefined) input.country = country;
const presenter = new DriverPresenter();
const result = await this.updateDriverProfileUseCase.execute(input);
if (result.isErr()) {
this.logger.error(`Failed to update driver profile: ${result.error.code}`);
presenter.present(null);
return presenter.responseModel;
this.logger.error(`Failed to update driver profile: ${result.unwrapErr().code}`);
this.driverPresenter.present(null);
return this.driverPresenter.getResponseModel();
}
const updatedDriver = await this.driverRepository.findById(driverId);
presenter.present(updatedDriver ?? null);
return presenter.responseModel;
this.driverPresenter.present(updatedDriver ?? null);
return this.driverPresenter.getResponseModel();
}
async getDriver(driverId: string): Promise<GetDriverOutputDTO | null> {
@@ -165,10 +171,9 @@ export class DriverService {
const driver = await this.driverRepository.findById(driverId);
const presenter = new DriverPresenter();
presenter.present(driver ?? null);
this.driverPresenter.present(driver ?? null);
return presenter.responseModel;
return this.driverPresenter.getResponseModel();
}
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
@@ -176,9 +181,11 @@ export class DriverService {
const result = await this.getProfileOverviewUseCase.execute({ driverId });
const presenter = new DriverProfilePresenter();
presenter.present(result);
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load driver profile');
}
return presenter.responseModel;
return this.driverProfilePresenter.getResponseModel();
}
}

View File

@@ -9,10 +9,10 @@ describe('DriverRegistrationStatusPresenter', () => {
});
describe('present', () => {
it('should map parameters to view model for registered driver', () => {
presenter.present(true, 'race-123', 'driver-456');
it('should map parameters to response model for registered driver', () => {
presenter.present({ isRegistered: true, raceId: 'race-123', driverId: 'driver-456' });
const result = presenter.viewModel;
const result = presenter.getResponseModel();
expect(result).toEqual({
isRegistered: true,
@@ -21,10 +21,10 @@ describe('DriverRegistrationStatusPresenter', () => {
});
});
it('should map parameters to view model for unregistered driver', () => {
presenter.present(false, 'race-789', 'driver-101');
it('should map parameters to response model for unregistered driver', () => {
presenter.present({ isRegistered: false, raceId: 'race-789', driverId: 'driver-101' });
const result = presenter.viewModel;
const result = presenter.getResponseModel();
expect(result).toEqual({
isRegistered: false,
@@ -36,11 +36,11 @@ describe('DriverRegistrationStatusPresenter', () => {
describe('reset', () => {
it('should reset the result', () => {
presenter.present(true, 'race-123', 'driver-456');
expect(presenter.viewModel).toBeDefined();
presenter.present({ isRegistered: true, raceId: 'race-123', driverId: 'driver-456' });
expect(presenter.getResponseModel()).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
expect(() => presenter.getResponseModel()).toThrow('Presenter not presented');
});
});
});

View File

@@ -9,6 +9,10 @@ export class DriverRegistrationStatusPresenter
{
private responseModel: DriverRegistrationStatusDTO | null = null;
reset(): void {
this.responseModel = null;
}
present(result: IsDriverRegisteredForRaceResult): void {
this.responseModel = {
isRegistered: result.isRegistered,

View File

@@ -1,29 +1,15 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { DriversLeaderboardDTO } from '../dtos/DriversLeaderboardDTO';
import type {
GetDriversLeaderboardResult,
GetDriversLeaderboardErrorCode,
} from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
export type DriversLeaderboardApplicationError = ApplicationErrorCode<
GetDriversLeaderboardErrorCode,
{ message: string }
>;
export class DriversLeaderboardPresenter implements UseCaseOutputPort<GetDriversLeaderboardResult> {
private responseModel: DriversLeaderboardDTO | null = null;
export class DriversLeaderboardPresenter {
present(
result: Result<GetDriversLeaderboardResult, DriversLeaderboardApplicationError>,
): DriversLeaderboardDTO {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load drivers leaderboard');
}
const output = result.unwrap();
return {
drivers: output.items.map(item => ({
present(result: GetDriversLeaderboardResult): void {
this.responseModel = {
drivers: result.items.map(item => ({
id: item.driver.id,
name: item.driver.name.toString(),
rating: item.rating,
@@ -36,9 +22,14 @@ export class DriversLeaderboardPresenter {
rank: item.rank,
avatarUrl: item.avatarUrl,
})),
totalRaces: output.items.reduce((sum, d) => sum + d.racesCompleted, 0),
totalWins: output.items.reduce((sum, d) => sum + d.wins, 0),
activeCount: output.items.filter(d => d.isActive).length,
totalRaces: result.totalRaces,
totalWins: result.totalWins,
activeCount: result.activeCount,
};
}
getResponseModel(): DriversLeaderboardDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}