refactor core presenters

This commit is contained in:
2025-12-19 19:42:19 +01:00
parent 8116fe888f
commit 94fc538f44
228 changed files with 2817 additions and 3097 deletions

View File

@@ -43,6 +43,118 @@
] ]
} }
}, },
{
"files": ["core/*/application/ports/*/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSInterfaceDeclaration[id.name=/^Get.*Port$/]",
"message": "Port interface names should not start with 'Get'. Use descriptive names without the 'Get' prefix."
}
]
}
},
{
"files": ["core/**/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSClassDeclaration[id.name=/Blocker$/], TSInterfaceDeclaration[id.name=/Blocker$/]",
"message": "Blocker classes/interfaces are not allowed in core. Use Guards in backend."
},
{
"selector": "TSClassDeclaration[id.name=/Presenter$/], TSInterfaceDeclaration[id.name=/Presenter$/]",
"message": "Presenter classes/interfaces are not allowed in core. Presenters belong in API or frontend layers."
},
{
"selector": "TSClassDeclaration[id.name=/Dto$/], TSInterfaceDeclaration[id.name=/Dto$/]",
"message": "DTO classes/interfaces are not allowed in core. DTOs belong in API or frontend layers."
},
{
"selector": "TSClassDeclaration[id.name=/ViewModel$/], TSInterfaceDeclaration[id.name=/ViewModel$/]",
"message": "ViewModel classes/interfaces are not allowed in core. View Models belong in frontend."
},
{
"selector": "TSClassDeclaration[id.name=/CommandModel$/], TSInterfaceDeclaration[id.name=/CommandModel$/]",
"message": "CommandModel classes/interfaces are not allowed in core. Command Models belong in frontend."
}
]
}
},
{
"files": ["apps/website/**/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSClassDeclaration[id.name=/Guard$/], TSInterfaceDeclaration[id.name=/Guard$/]",
"message": "Guard classes/interfaces are not allowed in frontend. Use Blockers in frontend."
}
]
}
},
{
"files": ["apps/api/**/*.ts", "apps/website/lib/dtos/**/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSEnumDeclaration[id.name=/^(?!.*Enum$).+/]",
"message": "Transport enums must end with 'Enum'."
}
]
}
},
{
"files": ["core/*/application/use-cases/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSClassDeclaration[id.name=/^(?!.*UseCase$).+/]",
"message": "Use Case classes must end with 'UseCase'."
}
]
}
},
{
"files": ["core/*/application/services/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSClassDeclaration[id.name=/^(?!.*Service$).+/]",
"message": "Application Service classes must end with 'Service'."
}
]
}
},
{
"files": ["apps/website/lib/view-models/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSClassDeclaration[id.name=/^(?!.*ViewModel$).+/]",
"message": "View Model classes must end with 'ViewModel'."
}
]
}
},
{
"files": ["apps/website/lib/commands/*.ts"],
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSClassDeclaration[id.name=/^(?!.*CommandModel$).+/]",
"message": "Command Model classes must end with 'CommandModel'."
}
]
}
},
{ {
"files": ["**/*.ts", "**/*.tsx"], "files": ["**/*.ts", "**/*.tsx"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",

View File

@@ -1,6 +1,8 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase'; import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import type { DashboardOverviewViewModel } from '@core/racing/application/presenters/IDashboardOverviewPresenter'; import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
@@ -62,7 +64,7 @@ export class DashboardService {
); );
} }
async getDashboardOverview(driverId: string): Promise<DashboardOverviewViewModel> { async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId }); this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
const result = await this.dashboardOverviewUseCase.execute({ driverId }); const result = await this.dashboardOverviewUseCase.execute({ driverId });
@@ -71,6 +73,6 @@ export class DashboardService {
throw new Error(result.error?.message || 'Failed to get dashboard overview'); throw new Error(result.error?.message || 'Failed to get dashboard overview');
} }
return result.value!; return plainToClass(DashboardOverviewDTO, result.value);
} }
} }

View File

@@ -1,23 +1,224 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator'; import { IsString, IsNumber, IsOptional, IsBoolean, IsArray, ValidateNested } from 'class-validator';
import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO'; import { Type } from 'class-transformer';
import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
import { DashboardRecentResultDTO } from './DashboardRecentResultDTO'; export class DashboardDriverSummaryDTO {
import { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO'; @ApiProperty()
import { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO'; @IsString()
import { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO'; id!: string;
@ApiProperty()
@IsString()
name!: string;
@ApiProperty()
@IsString()
country!: string;
@ApiProperty()
@IsString()
avatarUrl!: string;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
rating?: number | null;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
globalRank?: number | null;
@ApiProperty()
@IsNumber()
totalRaces!: number;
@ApiProperty()
@IsNumber()
wins!: number;
@ApiProperty()
@IsNumber()
podiums!: number;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
consistency?: number | null;
}
export class DashboardRaceSummaryDTO {
@ApiProperty()
@IsString()
id!: string;
@ApiProperty()
@IsString()
leagueId!: string;
@ApiProperty()
@IsString()
leagueName!: string;
@ApiProperty()
@IsString()
track!: string;
@ApiProperty()
@IsString()
car!: string;
@ApiProperty()
@IsString()
scheduledAt!: string;
@ApiProperty()
@IsString()
status!: 'scheduled' | 'running' | 'completed' | 'cancelled';
@ApiProperty()
@IsBoolean()
isMyLeague!: boolean;
}
export class DashboardRecentResultDTO {
@ApiProperty()
@IsString()
raceId!: string;
@ApiProperty()
@IsString()
raceName!: string;
@ApiProperty()
@IsString()
leagueId!: string;
@ApiProperty()
@IsString()
leagueName!: string;
@ApiProperty()
@IsString()
finishedAt!: string;
@ApiProperty()
@IsNumber()
position!: number;
@ApiProperty()
@IsNumber()
incidents!: number;
}
export class DashboardLeagueStandingSummaryDTO {
@ApiProperty()
@IsString()
leagueId!: string;
@ApiProperty()
@IsString()
leagueName!: string;
@ApiProperty()
@IsNumber()
position!: number;
@ApiProperty()
@IsNumber()
totalDrivers!: number;
@ApiProperty()
@IsNumber()
points!: number;
}
export class DashboardFeedItemSummaryDTO {
@ApiProperty()
@IsString()
id!: string;
@ApiProperty()
@IsString()
type!: string;
@ApiProperty()
@IsString()
headline!: string;
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
body?: string;
@ApiProperty()
@IsString()
timestamp!: string;
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
ctaLabel?: string;
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
ctaHref?: string;
}
export class DashboardFeedSummaryDTO {
@ApiProperty()
@IsNumber()
notificationCount!: number;
@ApiProperty({ type: [DashboardFeedItemSummaryDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardFeedItemSummaryDTO)
items!: DashboardFeedItemSummaryDTO[];
}
export class DashboardFriendSummaryDTO {
@ApiProperty()
@IsString()
id!: string;
@ApiProperty()
@IsString()
name!: string;
@ApiProperty()
@IsString()
country!: string;
@ApiProperty()
@IsString()
avatarUrl!: string;
}
export class DashboardOverviewDTO { export class DashboardOverviewDTO {
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
currentDriver!: DashboardDriverSummaryDTO | null; @IsOptional()
@ValidateNested()
@Type(() => DashboardDriverSummaryDTO)
currentDriver?: DashboardDriverSummaryDTO | null;
@ApiProperty({ type: [DashboardRaceSummaryDTO] }) @ApiProperty({ type: [DashboardRaceSummaryDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardRaceSummaryDTO)
myUpcomingRaces!: DashboardRaceSummaryDTO[]; myUpcomingRaces!: DashboardRaceSummaryDTO[];
@ApiProperty({ type: [DashboardRaceSummaryDTO] }) @ApiProperty({ type: [DashboardRaceSummaryDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardRaceSummaryDTO)
otherUpcomingRaces!: DashboardRaceSummaryDTO[]; otherUpcomingRaces!: DashboardRaceSummaryDTO[];
@ApiProperty({ type: [DashboardRaceSummaryDTO] }) @ApiProperty({ type: [DashboardRaceSummaryDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardRaceSummaryDTO)
upcomingRaces!: DashboardRaceSummaryDTO[]; upcomingRaces!: DashboardRaceSummaryDTO[];
@ApiProperty() @ApiProperty()
@@ -25,17 +226,31 @@ export class DashboardOverviewDTO {
activeLeaguesCount!: number; activeLeaguesCount!: number;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
nextRace!: DashboardRaceSummaryDTO | null; @IsOptional()
@ValidateNested()
@Type(() => DashboardRaceSummaryDTO)
nextRace?: DashboardRaceSummaryDTO | null;
@ApiProperty({ type: [DashboardRecentResultDTO] }) @ApiProperty({ type: [DashboardRecentResultDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardRecentResultDTO)
recentResults!: DashboardRecentResultDTO[]; recentResults!: DashboardRecentResultDTO[];
@ApiProperty({ type: [DashboardLeagueStandingSummaryDTO] }) @ApiProperty({ type: [DashboardLeagueStandingSummaryDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardLeagueStandingSummaryDTO)
leagueStandingsSummaries!: DashboardLeagueStandingSummaryDTO[]; leagueStandingsSummaries!: DashboardLeagueStandingSummaryDTO[];
@ApiProperty() @ApiProperty()
@ValidateNested()
@Type(() => DashboardFeedSummaryDTO)
feedSummary!: DashboardFeedSummaryDTO; feedSummary!: DashboardFeedSummaryDTO;
@ApiProperty({ type: [DashboardFriendSummaryDTO] }) @ApiProperty({ type: [DashboardFriendSummaryDTO] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => DashboardFriendSummaryDTO)
friends!: DashboardFriendSummaryDTO[]; friends!: DashboardFriendSummaryDTO[];
} }

View File

@@ -17,6 +17,7 @@ import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTo
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
@@ -44,6 +45,7 @@ export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase';
export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase'; export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase';
export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase'; export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase';
export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase'; export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
export const DriverProviders: Provider[] = [ export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself DriverService, // Provide the service itself
@@ -113,4 +115,19 @@ export const DriverProviders: Provider[] = [
useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo), useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo),
inject: [DRIVER_REPOSITORY_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN],
}, },
{
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetProfileOverviewUseCase(
driverRepo,
// TODO: Add teamRepository, teamMembershipRepository, socialRepository, etc.
null as any, // teamRepository
null as any, // teamMembershipRepository
null as any, // socialRepository
imageService,
() => null, // getDriverStats
() => [], // getAllDriverRankings
),
inject: [DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
},
]; ];

View File

@@ -8,6 +8,9 @@ import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-c
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; 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', () => { describe('DriverService', () => {
let service: DriverService; let service: DriverService;
@@ -15,7 +18,9 @@ describe('DriverService', () => {
let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>; let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>;
let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>; let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>;
let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>; let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let updateDriverProfileUseCase: ReturnType<typeof vi.mocked<UpdateDriverProfileUseCase>>; let updateDriverProfileUseCase: ReturnType<typeof vi.mocked<UpdateDriverProfileUseCase>>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let driverRepository: ReturnType<typeof vi.mocked<IDriverRepository>>; let driverRepository: ReturnType<typeof vi.mocked<IDriverRepository>>;
let logger: ReturnType<typeof vi.mocked<Logger>>; let logger: ReturnType<typeof vi.mocked<Logger>>;
@@ -102,17 +107,11 @@ describe('DriverService', () => {
activeCount: 1, activeCount: 1,
}; };
const mockPresenter = { getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(mockViewModel));
viewModel: mockViewModel,
};
getDriversLeaderboardUseCase.execute.mockImplementation(async (input, presenter) => {
Object.assign(presenter, mockPresenter);
});
const result = await service.getDriversLeaderboard(); const result = await service.getDriversLeaderboard();
expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object)); expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith();
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.'); expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.');
expect(result).toEqual(mockViewModel); expect(result).toEqual(mockViewModel);
}); });
@@ -120,26 +119,20 @@ describe('DriverService', () => {
describe('getTotalDrivers', () => { describe('getTotalDrivers', () => {
it('should call GetTotalDriversUseCase and return the view model', async () => { it('should call GetTotalDriversUseCase and return the view model', async () => {
const mockViewModel = { totalDrivers: 5 }; const mockOutput = { totalDrivers: 5 };
const mockPresenter = { getTotalDriversUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
viewModel: mockViewModel,
};
getTotalDriversUseCase.execute.mockImplementation(async (input, presenter) => {
Object.assign(presenter, mockPresenter);
});
const result = await service.getTotalDrivers(); const result = await service.getTotalDrivers();
expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object)); expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith();
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.'); expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.');
expect(result).toEqual(mockViewModel); expect(result).toEqual(mockOutput);
}); });
}); });
describe('completeOnboarding', () => { describe('completeOnboarding', () => {
it('should call CompleteDriverOnboardingUseCase and return the view model', async () => { it('should call CompleteDriverOnboardingUseCase and return success', async () => {
const input = { const input = {
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
@@ -149,30 +142,43 @@ describe('DriverService', () => {
bio: 'Racing enthusiast', bio: 'Racing enthusiast',
}; };
const mockViewModel = { completeDriverOnboardingUseCase.execute.mockResolvedValue(
success: true, Result.ok<CompleteDriverOnboardingOutputPort, ApplicationErrorCode<string>>({ driverId: 'user-123' })
driverId: 'user-123', );
};
const mockPresenter = {
viewModel: mockViewModel,
};
completeDriverOnboardingUseCase.execute.mockImplementation(async (input, presenter) => {
Object.assign(presenter, mockPresenter);
});
const result = await service.completeOnboarding('user-123', input); const result = await service.completeOnboarding('user-123', input);
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith( expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith({
{ userId: 'user-123',
userId: 'user-123', ...input,
...input, });
},
expect.any(Object)
);
expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123'); expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123');
expect(result).toEqual(mockViewModel); expect(result).toEqual({
success: true,
driverId: 'user-123',
});
});
it('should handle error from use case', async () => {
const input = {
firstName: 'John',
lastName: 'Doe',
displayName: 'John Doe',
country: 'US',
timezone: 'America/New_York',
bio: 'Racing enthusiast',
};
completeDriverOnboardingUseCase.execute.mockResolvedValue(
Result.err<CompleteDriverOnboardingOutputPort, ApplicationErrorCode<string>>({ code: 'DRIVER_ALREADY_EXISTS' })
);
const result = await service.completeOnboarding('user-123', input);
expect(result).toEqual({
success: false,
errorMessage: 'DRIVER_ALREADY_EXISTS',
});
}); });
}); });
@@ -183,25 +189,19 @@ describe('DriverService', () => {
raceId: 'race-1', raceId: 'race-1',
}; };
const mockViewModel = { const mockOutput = {
isRegistered: true, isRegistered: true,
raceId: 'race-1', raceId: 'race-1',
driverId: 'driver-1', driverId: 'driver-1',
}; };
const mockPresenter = { isDriverRegisteredForRaceUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
viewModel: mockViewModel,
};
isDriverRegisteredForRaceUseCase.execute.mockImplementation(async (params, presenter) => {
Object.assign(presenter, mockPresenter);
});
const result = await service.getDriverRegistrationStatus(query); const result = await service.getDriverRegistrationStatus(query);
expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query, expect.any(Object)); expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query);
expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query); expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query);
expect(result).toEqual(mockViewModel); expect(result).toEqual(mockOutput);
}); });
}); });
}); });

View File

@@ -13,15 +13,14 @@ import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase'; import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase'; import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase'; import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
// Presenters // Presenters
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter'; import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
// Tokens // 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, LOGGER_TOKEN, DRIVER_REPOSITORY_TOKEN } from './DriverProviders'; import { GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN, GET_PROFILE_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DRIVER_REPOSITORY_TOKEN } from './DriverProviders';
import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase'; import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
@@ -34,6 +33,7 @@ export class DriverService {
@Inject(COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN) private readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase, @Inject(COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN) private readonly completeDriverOnboardingUseCase: CompleteDriverOnboardingUseCase,
@Inject(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) private readonly isDriverRegisteredForRaceUseCase: IsDriverRegisteredForRaceUseCase, @Inject(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) private readonly isDriverRegisteredForRaceUseCase: IsDriverRegisteredForRaceUseCase,
@Inject(UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN) private readonly updateDriverProfileUseCase: UpdateDriverProfileUseCase, @Inject(UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN) private readonly updateDriverProfileUseCase: UpdateDriverProfileUseCase,
@Inject(GET_PROFILE_OVERVIEW_USE_CASE_TOKEN) private readonly getProfileOverviewUseCase: GetProfileOverviewUseCase,
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository, @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {} ) {}
@@ -41,24 +41,31 @@ export class DriverService {
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> { async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
this.logger.debug('[DriverService] Fetching drivers leaderboard.'); this.logger.debug('[DriverService] Fetching drivers leaderboard.');
const presenter = new DriversLeaderboardPresenter(); const result = await this.getDriversLeaderboardUseCase.execute();
await this.getDriversLeaderboardUseCase.execute(undefined, presenter); if (result.isOk()) {
return presenter.viewModel; return result.value as DriversLeaderboardDTO;
} else {
throw new Error(`Failed to fetch drivers leaderboard: ${result.error.details.message}`);
}
} }
async getTotalDrivers(): Promise<DriverStatsDTO> { async getTotalDrivers(): Promise<DriverStatsDTO> {
this.logger.debug('[DriverService] Fetching total drivers count.'); this.logger.debug('[DriverService] Fetching total drivers count.');
const result = await this.getTotalDriversUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new DriverStatsPresenter(); const presenter = new DriverStatsPresenter();
await this.getTotalDriversUseCase.execute(undefined, presenter); presenter.present(result.unwrap());
return presenter.viewModel; return presenter.viewModel;
} }
async completeOnboarding(userId: string, input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> { async completeOnboarding(userId: string, input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
this.logger.debug('Completing onboarding for user:', userId); this.logger.debug('Completing onboarding for user:', userId);
const presenter = new CompleteOnboardingPresenter(); const result = await this.completeDriverOnboardingUseCase.execute({
await this.completeDriverOnboardingUseCase.execute({
userId, userId,
firstName: input.firstName, firstName: input.firstName,
lastName: input.lastName, lastName: input.lastName,
@@ -66,16 +73,31 @@ export class DriverService {
country: input.country, country: input.country,
timezone: input.timezone, timezone: input.timezone,
bio: input.bio, bio: input.bio,
}, presenter); });
return presenter.viewModel;
if (result.isOk()) {
return {
success: true,
driverId: result.value.driverId,
};
} else {
return {
success: false,
errorMessage: result.error.code,
};
}
} }
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQueryDTO): Promise<DriverRegistrationStatusDTO> { async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQueryDTO): Promise<DriverRegistrationStatusDTO> {
this.logger.debug('Checking driver registration status:', query); this.logger.debug('Checking driver registration status:', query);
const presenter = new DriverRegistrationStatusPresenter(); const result = await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId });
await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }, presenter); if (result.isOk()) {
return presenter.viewModel; return result.value;
} else {
// For now, throw error or handle appropriately. Since it's a query, perhaps return default or throw.
throw new Error(`Failed to check registration status: ${result.error.code}`);
}
} }
async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> { async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> {
@@ -129,18 +151,42 @@ export class DriverService {
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> { async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`); this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`);
// TODO: Implement proper driver profile fetching with all the detailed data const result = await this.getProfileOverviewUseCase.execute({ driverId });
// For now, return a placeholder structure if (result.isErr()) {
throw new Error(`Failed to fetch driver profile: ${result.error.code}`);
}
const outputPort = result.value;
return this.mapProfileOverviewToDTO(outputPort);
}
private mapProfileOverviewToDTO(outputPort: ProfileOverviewOutputPort): GetDriverProfileOutputDTO {
return { return {
currentDriver: null, currentDriver: outputPort.driver ? {
stats: null, id: outputPort.driver.id,
finishDistribution: null, name: outputPort.driver.name,
teamMemberships: [], country: outputPort.driver.country,
socialSummary: { avatarUrl: outputPort.driver.avatarUrl,
friendsCount: 0, iracingId: outputPort.driver.iracingId,
friends: [], joinedAt: outputPort.driver.joinedAt.toISOString(),
}, rating: outputPort.driver.rating,
extendedProfile: null, globalRank: outputPort.driver.globalRank,
consistency: outputPort.driver.consistency,
bio: outputPort.driver.bio,
totalDrivers: outputPort.driver.totalDrivers,
} : null,
stats: outputPort.stats,
finishDistribution: outputPort.finishDistribution,
teamMemberships: outputPort.teamMemberships.map(membership => ({
teamId: membership.teamId,
teamName: membership.teamName,
teamTag: membership.teamTag,
role: membership.role,
joinedAt: membership.joinedAt.toISOString(),
isCurrent: membership.isCurrent,
})),
socialSummary: outputPort.socialSummary,
extendedProfile: outputPort.extendedProfile,
}; };
} }
} }

View File

@@ -1,62 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CompleteOnboardingPresenter } from './CompleteOnboardingPresenter';
import type { CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
describe('CompleteOnboardingPresenter', () => {
let presenter: CompleteOnboardingPresenter;
beforeEach(() => {
presenter = new CompleteOnboardingPresenter();
});
describe('present', () => {
it('should map successful core DTO to API view model', () => {
const dto: CompleteDriverOnboardingResultDTO = {
success: true,
driverId: 'driver-123',
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result).toEqual({
success: true,
driverId: 'driver-123',
errorMessage: undefined,
});
});
it('should map failed core DTO to API view model', () => {
const dto: CompleteDriverOnboardingResultDTO = {
success: false,
errorMessage: 'Driver already exists',
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result).toEqual({
success: false,
driverId: undefined,
errorMessage: 'Driver already exists',
});
});
});
describe('reset', () => {
it('should reset the result', () => {
const dto: CompleteDriverOnboardingResultDTO = {
success: true,
driverId: 'driver-123',
};
presenter.present(dto);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -1,23 +0,0 @@
import { CompleteOnboardingOutputDTO } from '../dtos/CompleteOnboardingOutputDTO';
import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPresenter {
private result: CompleteOnboardingOutputDTO | null = null;
reset() {
this.result = null;
}
present(dto: CompleteDriverOnboardingResultDTO) {
this.result = {
success: dto.success,
driverId: dto.driverId,
errorMessage: dto.errorMessage,
};
}
get viewModel(): CompleteOnboardingOutputDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -1,28 +0,0 @@
import { DriverRegistrationStatusDTO } from '../dtos/DriverRegistrationStatusDTO';
import type { IDriverRegistrationStatusPresenter } from '../../../../../core/racing/application/presenters/IDriverRegistrationStatusPresenter';
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
private result: DriverRegistrationStatusDTO | null = null;
present(isRegistered: boolean, raceId: string, driverId: string) {
this.result = {
isRegistered,
raceId,
driverId,
};
}
getViewModel(): DriverRegistrationStatusDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
// For consistency with other presenters
reset() {
this.result = null;
}
get viewModel(): DriverRegistrationStatusDTO {
return this.getViewModel();
}
}

View File

@@ -1,16 +1,16 @@
import { DriverStatsDTO } from '../dtos/DriverStatsDTO'; import { DriverStatsDTO } from '../dtos/DriverStatsDTO';
import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter'; import type { TotalDriversOutputPort } from '../../../../../core/racing/application/ports/output/TotalDriversOutputPort';
export class DriverStatsPresenter implements ITotalDriversPresenter { export class DriverStatsPresenter {
private result: DriverStatsDTO | null = null; private result: DriverStatsDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: TotalDriversResultDTO) { present(output: TotalDriversOutputPort) {
this.result = { this.result = {
totalDrivers: dto.totalDrivers, totalDrivers: output.totalDrivers,
}; };
} }

View File

@@ -16,6 +16,7 @@ import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/In
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository'; import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
// Import use cases // Import use cases
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase'; import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
@@ -131,4 +132,8 @@ export const LeagueProviders: Provider[] = [
GetLeagueScheduleUseCase, GetLeagueScheduleUseCase,
GetLeagueStatsUseCase, GetLeagueStatsUseCase,
GetLeagueAdminPermissionsUseCase, GetLeagueAdminPermissionsUseCase,
{
provide: ListLeagueScoringPresetsUseCase,
useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),
},
]; ];

View File

@@ -14,6 +14,7 @@ import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-case
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase'; import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase'; import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import { Result } from '@core/shared/application/Result';
describe('LeagueService', () => { describe('LeagueService', () => {
let service: LeagueService; let service: LeagueService;
@@ -60,9 +61,7 @@ describe('LeagueService', () => {
}); });
it('should get total leagues', async () => { it('should get total leagues', async () => {
mockGetTotalLeaguesUseCase.execute.mockImplementation(async (params, presenter) => { mockGetTotalLeaguesUseCase.execute.mockResolvedValue(Result.ok({ totalLeagues: 5 }));
presenter.present({ totalLeagues: 5 });
});
const result = await service.getTotalLeagues(); const result = await service.getTotalLeagues();

View File

@@ -10,30 +10,27 @@ import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO'; import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO'; import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO'; import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO'; import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO'; import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO'; import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO'; import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO'; import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
import { TransferLeagueOwnershipOutputDTO } from './dtos/TransferLeagueOwnershipOutputDTO';
import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
// Core imports for view models // Core imports for view models
import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter'; import type { LeagueScoringConfigViewModel } from './presenters/LeagueScoringConfigPresenter';
import type { LeagueScoringPresetsViewModel } from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter'; import type { LeagueScoringPresetsViewModel } from './presenters/LeagueScoringPresetsPresenter';
import type { AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; import type { AllLeaguesWithCapacityDTO as AllLeaguesWithCapacityViewModel } from '../dtos/AllLeaguesWithCapacityDTO';
import type { GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter';
import type { GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter'; import type { GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter';
import type { ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter'; import { TotalLeaguesDTO } from './dtos/TotalLeaguesDTO';
import type { RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter'; import type { ApproveLeagueJoinRequestDTO } from './dtos/ApproveLeagueJoinRequestDTO';
import type { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter'; import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter';
import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter'; import type { CreateLeagueViewModel } from './dtos/CreateLeagueDTO';
import type { UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter';
import type { GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter';
import type { LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter';
import type { LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter';
import type { LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter';
import type { CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter';
import type { JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter';
import type { TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
@@ -67,22 +64,22 @@ import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapa
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter'; import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter'; import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter';
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter'; import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter'; import { mapApproveLeagueJoinRequestPortToDTO } from './presenters/ApproveLeagueJoinRequestPresenter';
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter'; import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter'; import { mapGetLeagueOwnerSummaryOutputPortToDTO } from './presenters/GetLeagueOwnerSummaryPresenter';
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter'; import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter'; import { mapGetLeagueScheduleOutputPortToDTO } from './presenters/LeagueSchedulePresenter';
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter'; import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter'; import { mapRejectLeagueJoinRequestOutputPortToDTO } from './presenters/RejectLeagueJoinRequestPresenter';
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter'; import { mapRemoveLeagueMemberOutputPortToDTO } from './presenters/RemoveLeagueMemberPresenter';
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter'; import { mapUpdateLeagueMemberRoleOutputPortToDTO } from './presenters/UpdateLeagueMemberRolePresenter';
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter'; import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter'; import { mapJoinLeagueOutputPortToDTO } from './presenters/JoinLeaguePresenter';
import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter'; import { mapTransferLeagueOwnershipOutputPortToDTO } from './presenters/TransferLeagueOwnershipPresenter';
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter'; import { mapGetLeagueProtestsOutputPortToDTO } from './presenters/GetLeagueProtestsPresenter';
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter'; import { mapGetLeagueSeasonsOutputPortToDTO } from './presenters/GetLeagueSeasonsPresenter';
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter'; import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter';
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
// Tokens // Tokens
import { LOGGER_TOKEN } from './LeagueProviders'; import { LOGGER_TOKEN } from './LeagueProviders';
@@ -126,7 +123,7 @@ export class LeagueService {
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getTotalLeagues(): Promise<GetTotalLeaguesViewModel> { async getTotalLeagues(): Promise<TotalLeaguesDTO> {
this.logger.debug('[LeagueService] Fetching total leagues count.'); this.logger.debug('[LeagueService] Fetching total leagues count.');
const result = await this.getTotalLeaguesUseCase.execute(); const result = await this.getTotalLeaguesUseCase.execute();
if (result.isErr()) { if (result.isErr()) {
@@ -148,26 +145,22 @@ export class LeagueService {
return presenter.getViewModel(); return presenter.getViewModel();
} }
async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestViewModel> { async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestDTO> {
this.logger.debug('Approving join request:', input); this.logger.debug('Approving join request:', input);
const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId }); const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new ApproveLeagueJoinRequestPresenter(); return mapApproveLeagueJoinRequestPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectLeagueJoinRequestViewModel> { async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectJoinRequestOutputDTO> {
this.logger.debug('Rejecting join request:', input); this.logger.debug('Rejecting join request:', input);
const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId }); const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new RejectLeagueJoinRequestPresenter(); return mapRejectLeagueJoinRequestOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise<GetLeagueAdminPermissionsViewModel> { async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise<GetLeagueAdminPermissionsViewModel> {
@@ -179,40 +172,34 @@ export class LeagueService {
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise<RemoveLeagueMemberViewModel> { async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise<RemoveLeagueMemberOutputDTO> {
this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId }); this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId });
const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId }); const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new RemoveLeagueMemberPresenter(); return mapRemoveLeagueMemberOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise<UpdateLeagueMemberRoleViewModel> { async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise<UpdateLeagueMemberRoleOutputDTO> {
this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }); this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }); const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new UpdateLeagueMemberRolePresenter(); return mapUpdateLeagueMemberRoleOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<GetLeagueOwnerSummaryViewModel> { async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<LeagueOwnerSummaryDTO | null> {
this.logger.debug('Getting league owner summary:', query); this.logger.debug('Getting league owner summary:', query);
const result = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId }); const result = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new GetLeagueOwnerSummaryPresenter(); return mapGetLeagueOwnerSummaryOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormViewModel | null> { async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormModelDTO | null> {
this.logger.debug('Getting league full config', { query }); this.logger.debug('Getting league full config', { query });
try { try {
@@ -221,7 +208,9 @@ export class LeagueService {
this.logger.error('Error getting league full config', new Error(result.unwrapErr().code)); this.logger.error('Error getting league full config', new Error(result.unwrapErr().code));
return null; return null;
} }
return result.unwrap(); const presenter = new LeagueConfigPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel();
} catch (error) { } catch (error) {
this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error))); this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error)));
return null; return null;
@@ -234,9 +223,7 @@ export class LeagueService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new GetLeagueProtestsPresenter(); return mapGetLeagueProtestsOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel() as LeagueAdminProtestsDTO;
} }
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> { async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
@@ -245,9 +232,7 @@ export class LeagueService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new GetLeagueSeasonsPresenter(); return mapGetLeagueSeasonsOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel().seasons;
} }
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> { async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
@@ -261,25 +246,27 @@ export class LeagueService {
return presenter.getViewModel().memberships as LeagueMembershipsDTO; return presenter.getViewModel().memberships as LeagueMembershipsDTO;
} }
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> { async getLeagueStandings(leagueId: string): Promise<LeagueStandingsDTO> {
this.logger.debug('Getting league standings', { leagueId }); this.logger.debug('Getting league standings', { leagueId });
const result = await this.getLeagueStandingsUseCase.execute(leagueId); const result = await this.getLeagueStandingsUseCase.execute({ leagueId });
// The use case returns a view model directly, so we return it as-is if (result.isErr()) {
return result as unknown as LeagueStandingsViewModel; throw new Error(result.unwrapErr().code);
}
const presenter = new LeagueStandingsPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async getLeagueSchedule(leagueId: string): Promise<ReturnType<LeagueSchedulePresenter['getViewModel']>> { async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
this.logger.debug('Getting league schedule', { leagueId }); this.logger.debug('Getting league schedule', { leagueId });
const result = await this.getLeagueScheduleUseCase.execute({ leagueId }); const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new LeagueSchedulePresenter(); return mapGetLeagueScheduleOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel()!;
} }
async getLeagueStats(leagueId: string): Promise<LeagueStatsViewModel> { async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> {
this.logger.debug('Getting league stats', { leagueId }); this.logger.debug('Getting league stats', { leagueId });
const result = await this.getLeagueStatsUseCase.execute({ leagueId }); const result = await this.getLeagueStatsUseCase.execute({ leagueId });
if (result.isErr()) { if (result.isErr()) {
@@ -404,7 +391,7 @@ export class LeagueService {
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueViewModel> { async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueOutputDTO> {
this.logger.debug('Joining league', { leagueId, driverId }); this.logger.debug('Joining league', { leagueId, driverId });
const result = await this.joinLeagueUseCase.execute({ leagueId, driverId }); const result = await this.joinLeagueUseCase.execute({ leagueId, driverId });
@@ -415,12 +402,10 @@ export class LeagueService {
error: error.code, error: error.code,
}; };
} }
const presenter = new JoinLeaguePresenter(); return mapJoinLeagueOutputPortToDTO(result.unwrap());
presenter.present(result.unwrap());
return presenter.getViewModel();
} }
async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipViewModel> { async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipOutputDTO> {
this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId }); this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId });
const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId }); const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
@@ -431,9 +416,7 @@ export class LeagueService {
error: error.code, error: error.code,
}; };
} }
const presenter = new TransferLeagueOwnershipPresenter(); return mapTransferLeagueOwnershipOutputPortToDTO(result.unwrap());
presenter.present({ success: true });
return presenter.getViewModel();
} }
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> { async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {

View File

@@ -1,16 +1,43 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; export class LeagueWithCapacityDTO {
import { LeagueWithCapacityDTO } from './LeagueWithCapacityDTO'; @ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
description!: string;
@ApiProperty()
ownerId!: string;
@ApiProperty()
settings!: {
maxDrivers: number;
sessionDuration?: number;
visibility?: string;
};
@ApiProperty()
createdAt!: string;
@ApiProperty({ nullable: true })
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
@ApiProperty()
usedSlots!: number;
}
export class AllLeaguesWithCapacityDTO { export class AllLeaguesWithCapacityDTO {
@ApiProperty({ type: [LeagueWithCapacityDTO] }) @ApiProperty({ type: [LeagueWithCapacityDTO] })
@IsArray() leagues!: LeagueWithCapacityDTO[];
@ValidateNested({ each: true })
@Type(() => LeagueWithCapacityDTO)
leagues: LeagueWithCapacityDTO[];
@ApiProperty() @ApiProperty()
@IsNumber() totalCount!: number;
totalCount: number;
} }

View File

@@ -0,0 +1,4 @@
export interface ApproveLeagueJoinRequestDTO {
success: boolean;
message: string;
}

View File

@@ -0,0 +1,4 @@
export interface CreateLeagueViewModel {
leagueId: string;
success: boolean;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class TotalLeaguesDTO {
@ApiProperty()
@IsNumber()
totalLeagues: number;
}

View File

@@ -1,21 +1,22 @@
import { IAllLeaguesWithCapacityPresenter, AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; import type { AllLeaguesWithCapacityOutputPort } from '@core/racing/application/ports/output/AllLeaguesWithCapacityOutputPort';
import { AllLeaguesWithCapacityDTO, LeagueWithCapacityDTO } from '../dtos/AllLeaguesWithCapacityDTO';
export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter { export class AllLeaguesWithCapacityPresenter {
private result: AllLeaguesWithCapacityViewModel | null = null; private result: AllLeaguesWithCapacityDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: AllLeaguesWithCapacityResultDTO) { present(output: AllLeaguesWithCapacityOutputPort) {
const leagues = dto.leagues.map(league => ({ const leagues: LeagueWithCapacityDTO[] = output.leagues.map(league => ({
id: league.id, id: league.id,
name: league.name, name: league.name,
description: league.description, description: league.description,
ownerId: league.ownerId, ownerId: league.ownerId,
settings: { maxDrivers: league.settings.maxDrivers || 0 }, settings: { maxDrivers: league.settings.maxDrivers || 0 },
createdAt: league.createdAt.toISOString(), createdAt: league.createdAt.toISOString(),
usedSlots: dto.memberCounts.get(league.id) || 0, usedSlots: output.memberCounts[league.id] || 0,
socialLinks: league.socialLinks, socialLinks: league.socialLinks,
})); }));
this.result = { this.result = {
@@ -24,7 +25,7 @@ export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityP
}; };
} }
getViewModel(): AllLeaguesWithCapacityViewModel | null { getViewModel(): AllLeaguesWithCapacityDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -1,18 +1,6 @@
import { IApproveLeagueJoinRequestPresenter, ApproveLeagueJoinRequestResultPort, ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter'; import type { ApproveLeagueJoinRequestResultPort } from '@core/racing/application/ports/output/ApproveLeagueJoinRequestResultPort';
import type { ApproveLeagueJoinRequestDTO } from '../dtos/ApproveLeagueJoinRequestDTO';
export class ApproveLeagueJoinRequestPresenter implements IApproveLeagueJoinRequestPresenter { export function mapApproveLeagueJoinRequestPortToDTO(port: ApproveLeagueJoinRequestResultPort): ApproveLeagueJoinRequestDTO {
private result: ApproveLeagueJoinRequestViewModel | null = null; return port;
reset() {
this.result = null;
}
present(dto: ApproveLeagueJoinRequestResultPort) {
this.result = dto;
}
getViewModel(): ApproveLeagueJoinRequestViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -1,13 +1,14 @@
import { ICreateLeaguePresenter, CreateLeagueResultDTO, CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter'; import type { CreateLeagueWithSeasonAndScoringOutputPort } from '@core/racing/application/ports/output/CreateLeagueWithSeasonAndScoringOutputPort';
import type { CreateLeagueViewModel } from '../dtos/CreateLeagueDTO';
export class CreateLeaguePresenter implements ICreateLeaguePresenter { export class CreateLeaguePresenter {
private result: CreateLeagueViewModel | null = null; private result: CreateLeagueViewModel | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: CreateLeagueResultDTO): void { present(dto: CreateLeagueWithSeasonAndScoringOutputPort): void {
this.result = { this.result = {
leagueId: dto.leagueId, leagueId: dto.leagueId,
success: true, success: true,

View File

@@ -1,17 +0,0 @@
import { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter';
export class GetLeagueAdminPermissionsPresenter implements IGetLeagueAdminPermissionsPresenter {
private result: GetLeagueAdminPermissionsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueAdminPermissionsResultDTO) {
this.result = dto;
}
getViewModel(): GetLeagueAdminPermissionsViewModel | null {
return this.result;
}
}

View File

@@ -1,47 +0,0 @@
import { IGetLeagueMembershipsPresenter, GetLeagueMembershipsResultDTO, GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter';
import { LeagueMembershipsDTO } from '../dtos/LeagueMembershipsDTO';
export class GetLeagueMembershipsPresenter implements IGetLeagueMembershipsPresenter {
private result: GetLeagueMembershipsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueMembershipsResultDTO) {
const driverMap = new Map(dto.drivers.map(d => [d.id, d]));
const members = dto.memberships.map(membership => ({
driverId: membership.driverId,
driver: driverMap.get(membership.driverId)!,
role: this.mapRole(membership.role) as 'owner' | 'manager' | 'member',
joinedAt: membership.joinedAt,
}));
this.result = { memberships: { members } };
}
private mapRole(role: string): 'owner' | 'manager' | 'member' {
switch (role) {
case 'owner':
return 'owner';
case 'admin':
return 'manager'; // Map admin to manager for API
case 'steward':
return 'member'; // Map steward to member for API
case 'member':
return 'member';
default:
return 'member';
}
}
getViewModel(): GetLeagueMembershipsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
// API-specific method
get apiViewModel(): LeagueMembershipsDTO | null {
if (!this.result?.memberships) return null;
return this.result.memberships as LeagueMembershipsViewModel;
}
}

View File

@@ -1,18 +1,19 @@
import { IGetLeagueOwnerSummaryPresenter, GetLeagueOwnerSummaryResultDTO, GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter'; import { GetLeagueOwnerSummaryOutputPort } from '@core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort';
import { LeagueOwnerSummaryDTO } from '../dtos/LeagueOwnerSummaryDTO';
export class GetLeagueOwnerSummaryPresenter implements IGetLeagueOwnerSummaryPresenter { export function mapGetLeagueOwnerSummaryOutputPortToDTO(output: GetLeagueOwnerSummaryOutputPort): LeagueOwnerSummaryDTO | null {
private result: GetLeagueOwnerSummaryViewModel | null = null; if (!output.summary) return null;
reset() { return {
this.result = null; driver: {
} id: output.summary.driver.id,
iracingId: output.summary.driver.iracingId,
present(dto: GetLeagueOwnerSummaryResultDTO) { name: output.summary.driver.name,
this.result = { summary: dto.summary }; country: output.summary.driver.country,
} bio: output.summary.driver.bio,
joinedAt: output.summary.driver.joinedAt,
getViewModel(): GetLeagueOwnerSummaryViewModel { },
if (!this.result) throw new Error('Presenter not presented'); rating: output.summary.rating,
return this.result; rank: output.summary.rank,
} };
} }

View File

@@ -1,30 +1,47 @@
import { IGetLeagueProtestsPresenter, GetLeagueProtestsResultDTO, GetLeagueProtestsViewModel } from '@core/racing/application/presenters/IGetLeagueProtestsPresenter'; import { GetLeagueProtestsOutputPort } from '@core/racing/application/ports/output/GetLeagueProtestsOutputPort';
import { LeagueAdminProtestsDTO } from '../dtos/LeagueAdminProtestsDTO';
import { ProtestDTO } from '../dtos/ProtestDTO';
import { RaceDTO } from '../../race/dtos/RaceDTO';
import { DriverDTO } from '../../driver/dtos/DriverDTO';
export class GetLeagueProtestsPresenter implements IGetLeagueProtestsPresenter { export function mapGetLeagueProtestsOutputPortToDTO(output: GetLeagueProtestsOutputPort): LeagueAdminProtestsDTO {
private result: GetLeagueProtestsViewModel | null = null; const protests: ProtestDTO[] = output.protests.map(protest => ({
id: protest.id,
raceId: protest.raceId,
protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId,
submittedAt: new Date(protest.filedAt),
description: protest.incident.description,
status: protest.status as 'pending' | 'accepted' | 'rejected', // TODO: map properly
}));
reset() { const racesById: { [raceId: string]: RaceDTO } = {};
this.result = null; for (const raceId in output.racesById) {
} const race = output.racesById[raceId];
racesById[raceId] = {
present(dto: GetLeagueProtestsResultDTO) { id: race.id,
const racesById = {}; name: race.track, // assuming name is track
dto.races.forEach(race => { date: race.scheduledAt,
racesById[race.id] = race; leagueName: undefined, // TODO: get league name if needed
});
const driversById = {};
dto.drivers.forEach(driver => {
driversById[driver.id] = driver;
});
this.result = {
protests: dto.protests,
racesById,
driversById,
}; };
} }
getViewModel(): GetLeagueProtestsViewModel { const driversById: { [driverId: string]: DriverDTO } = {};
if (!this.result) throw new Error('Presenter not presented'); for (const driverId in output.driversById) {
return this.result; const driver = output.driversById[driverId];
driversById[driverId] = {
id: driver.id,
iracingId: driver.iracingId,
name: driver.name,
country: driver.country,
bio: driver.bio,
joinedAt: driver.joinedAt,
};
} }
return {
protests,
racesById,
driversById,
};
} }

View File

@@ -1,27 +1,14 @@
import { IGetLeagueSeasonsPresenter, GetLeagueSeasonsResultDTO, GetLeagueSeasonsViewModel } from '@core/racing/application/presenters/IGetLeagueSeasonsPresenter'; import { GetLeagueSeasonsOutputPort } from '@core/racing/application/ports/output/GetLeagueSeasonsOutputPort';
import { LeagueSeasonSummaryDTO } from '../dtos/LeagueSeasonSummaryDTO';
export class GetLeagueSeasonsPresenter implements IGetLeagueSeasonsPresenter { export function mapGetLeagueSeasonsOutputPortToDTO(output: GetLeagueSeasonsOutputPort): LeagueSeasonSummaryDTO[] {
private result: GetLeagueSeasonsViewModel | null = null; return output.seasons.map(season => ({
seasonId: season.seasonId,
reset() { name: season.name,
this.result = null; status: season.status,
} startDate: season.startDate,
endDate: season.endDate,
present(dto: GetLeagueSeasonsResultDTO) { isPrimary: season.isPrimary,
const seasons = dto.seasons.map(season => ({ isParallelActive: season.isParallelActive,
seasonId: season.id, }));
name: season.name,
status: season.status,
startDate: season.startDate,
endDate: season.endDate,
isPrimary: season.isPrimary,
isParallelActive: season.isParallelActive,
}));
this.result = { seasons };
}
getViewModel(): GetLeagueSeasonsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -1,21 +1,9 @@
import { IJoinLeaguePresenter, JoinLeagueResultDTO, JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter'; import type { JoinLeagueOutputPort } from '@core/racing/application/ports/output/JoinLeagueOutputPort';
import type { JoinLeagueOutputDTO } from '../dtos/JoinLeagueOutputDTO';
export class JoinLeaguePresenter implements IJoinLeaguePresenter { export function mapJoinLeagueOutputPortToDTO(port: JoinLeagueOutputPort): JoinLeagueOutputDTO {
private result: JoinLeagueViewModel | null = null; return {
success: true,
reset() { membershipId: port.membershipId,
this.result = null; };
}
present(dto: JoinLeagueResultDTO): void {
this.result = {
success: true,
membershipId: dto.id,
};
}
getViewModel(): JoinLeagueViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -1,15 +1,16 @@
import { ILeagueFullConfigPresenter, LeagueFullConfigData, LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter'; import { LeagueFullConfigOutputPort } from '@core/racing/application/ports/output/LeagueFullConfigOutputPort';
import { LeagueConfigFormModelDTO } from '../dtos/LeagueConfigFormModelDTO'; import { LeagueConfigFormModelDTO } from '../dtos/LeagueConfigFormModelDTO';
import type { Presenter } from '@core/shared/presentation';
export class LeagueConfigPresenter implements ILeagueFullConfigPresenter { export class LeagueConfigPresenter implements Presenter<LeagueFullConfigOutputPort, LeagueConfigFormModelDTO> {
private result: LeagueConfigFormViewModel | null = null; private result: LeagueConfigFormModelDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: LeagueFullConfigData) { present(dto: LeagueFullConfigOutputPort) {
// Map from LeagueFullConfigData to LeagueConfigFormViewModel // Map from LeagueFullConfigOutputPort to LeagueConfigFormModelDTO
const league = dto.league; const league = dto.league;
const settings = league.settings; const settings = league.settings;
const stewarding = settings.stewarding; const stewarding = settings.stewarding;
@@ -20,64 +21,9 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
name: league.name, name: league.name,
description: league.description, description: league.description,
visibility: 'public', // TODO: Map visibility from league visibility: 'public', // TODO: Map visibility from league
gameId: 'iracing', // TODO: Map from game
}, },
structure: { structure: {
mode: 'solo', // TODO: Map from league settings mode: 'solo', // TODO: Map from league settings
maxDrivers: settings.maxDrivers || 32,
multiClassEnabled: false, // TODO: Map
},
championships: {
enableDriverChampionship: true, // TODO: Map
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
},
scoring: {
customScoringEnabled: false, // TODO: Map
},
dropPolicy: {
strategy: 'none', // TODO: Map
},
timings: {
practiceMinutes: 30, // TODO: Map
qualifyingMinutes: 15,
mainRaceMinutes: 60,
sessionCount: 1,
roundsPlanned: 10, // TODO: Map
},
stewarding: {
decisionMode: stewarding?.decisionMode || 'admin_only',
requireDefense: stewarding?.requireDefense || false,
defenseTimeLimit: stewarding?.defenseTimeLimit || 48,
voteTimeLimit: stewarding?.voteTimeLimit || 72,
protestDeadlineHours: stewarding?.protestDeadlineHours || 48,
stewardingClosesHours: stewarding?.stewardingClosesHours || 168,
notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true,
notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true,
requiredVotes: stewarding?.requiredVotes,
},
};
}
getViewModel(): LeagueConfigFormViewModel | null {
return this.result;
}
// API-specific method to get the DTO
get viewModel(): LeagueConfigFormModelDTO | null {
if (!this.result) return null;
// Map from LeagueConfigFormViewModel to LeagueConfigFormModelDto
return {
leagueId: this.result.leagueId,
basics: {
name: this.result.basics.name,
description: this.result.basics.description,
visibility: this.result.basics.visibility as 'public' | 'private',
},
structure: {
mode: this.result.structure.mode as 'solo' | 'team',
}, },
championships: [], // TODO: Map championships championships: [], // TODO: Map championships
scoring: { scoring: {
@@ -85,8 +31,8 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
points: 25, // TODO: Map points points: 25, // TODO: Map points
}, },
dropPolicy: { dropPolicy: {
strategy: this.result.dropPolicy.strategy as 'none' | 'worst_n', strategy: 'none', // TODO: Map
n: this.result.dropPolicy.n, n: 0,
}, },
timings: { timings: {
raceDayOfWeek: 'sunday', // TODO: Map from timings raceDayOfWeek: 'sunday', // TODO: Map from timings
@@ -94,16 +40,20 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
raceTimeMinute: 0, raceTimeMinute: 0,
}, },
stewarding: { stewarding: {
decisionMode: this.result.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward', decisionMode: stewarding?.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
requireDefense: this.result.stewarding.requireDefense, requireDefense: stewarding?.requireDefense || false,
defenseTimeLimit: this.result.stewarding.defenseTimeLimit, defenseTimeLimit: stewarding?.defenseTimeLimit || 48,
voteTimeLimit: this.result.stewarding.voteTimeLimit, voteTimeLimit: stewarding?.voteTimeLimit || 72,
protestDeadlineHours: this.result.stewarding.protestDeadlineHours, protestDeadlineHours: stewarding?.protestDeadlineHours || 48,
stewardingClosesHours: this.result.stewarding.stewardingClosesHours, stewardingClosesHours: stewarding?.stewardingClosesHours || 168,
notifyAccusedOnProtest: this.result.stewarding.notifyAccusedOnProtest, notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true,
notifyOnVoteRequired: this.result.stewarding.notifyOnVoteRequired, notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true,
requiredVotes: this.result.stewarding.requiredVotes, requiredVotes: stewarding?.requiredVotes || 0,
}, },
}; };
} }
getViewModel(): LeagueConfigFormModelDTO | null {
return this.result;
}
} }

View File

@@ -1,27 +0,0 @@
import { IGetLeagueJoinRequestsPresenter, GetLeagueJoinRequestsResultDTO, GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter';
export class LeagueJoinRequestsPresenter implements IGetLeagueJoinRequestsPresenter {
private result: GetLeagueJoinRequestsViewModel | null = null;
reset() {
this.result = null;
}
present(dto: GetLeagueJoinRequestsResultDTO) {
const driverMap = new Map(dto.drivers.map(d => [d.id, d]));
const joinRequests = dto.joinRequests.map(request => ({
id: request.id,
leagueId: request.leagueId,
driverId: request.driverId,
requestedAt: request.requestedAt,
message: request.message,
driver: driverMap.get(request.driverId) || null,
}));
this.result = { joinRequests };
}
getViewModel(): GetLeagueJoinRequestsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -1,23 +1,14 @@
import { IGetLeagueSchedulePresenter, GetLeagueScheduleResultDTO, LeagueScheduleViewModel } from '@core/racing/application/presenters/IGetLeagueSchedulePresenter'; import { GetLeagueScheduleOutputPort } from '@core/racing/application/ports/output/GetLeagueScheduleOutputPort';
import { LeagueScheduleDTO } from '../dtos/LeagueScheduleDTO';
import { RaceDTO } from '../../race/dtos/RaceDTO';
export class LeagueSchedulePresenter implements IGetLeagueSchedulePresenter { export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort): LeagueScheduleDTO {
private result: LeagueScheduleViewModel | null = null; return {
races: output.races.map(race => ({
reset() { id: race.id,
this.result = null; name: race.name,
} date: race.scheduledAt.toISOString(),
leagueName: undefined, // TODO: get league name if needed
present(dto: GetLeagueScheduleResultDTO) { })),
this.result = { };
races: dto.races.map(race => ({
id: race.id,
name: race.name,
date: race.scheduledAt.toISOString(),
})),
};
}
getViewModel(): LeagueScheduleViewModel | null {
return this.result;
}
} }

View File

@@ -1,20 +1,37 @@
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
import type { BonusRule } from '@core/racing/domain/types/BonusRule'; import type { BonusRule } from '@core/racing/domain/types/BonusRule';
import type { import type { LeagueScoringConfigOutputPort } from '@core/racing/application/ports/output/LeagueScoringConfigOutputPort';
ILeagueScoringConfigPresenter, import type { LeagueScoringPresetOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetOutputPort';
LeagueScoringConfigData,
LeagueScoringConfigViewModel,
LeagueScoringChampionshipViewModel,
} from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter { export interface LeagueScoringChampionshipViewModel {
id: string;
name: string;
type: string;
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription: string;
}
export interface LeagueScoringConfigViewModel {
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
scoringPresetId?: string;
scoringPresetName?: string;
dropPolicySummary: string;
championships: LeagueScoringChampionshipViewModel[];
}
export class LeagueScoringConfigPresenter {
private viewModel: LeagueScoringConfigViewModel | null = null; private viewModel: LeagueScoringConfigViewModel | null = null;
reset(): void { reset(): void {
this.viewModel = null; this.viewModel = null;
} }
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel { present(data: LeagueScoringConfigOutputPort): LeagueScoringConfigViewModel {
const championships: LeagueScoringChampionshipViewModel[] = const championships: LeagueScoringChampionshipViewModel[] =
data.championships.map((champ) => this.mapChampionship(champ)); data.championships.map((champ) => this.mapChampionship(champ));

View File

@@ -1,19 +1,22 @@
import type { import type { LeagueScoringPresetsOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetsOutputPort';
ILeagueScoringPresetsPresenter, import type { LeagueScoringPresetOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetOutputPort';
LeagueScoringPresetsResultDTO,
LeagueScoringPresetsViewModel,
} from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter { export interface LeagueScoringPresetsViewModel {
presets: LeagueScoringPresetOutputPort[];
totalCount: number;
}
export class LeagueScoringPresetsPresenter {
private viewModel: LeagueScoringPresetsViewModel | null = null; private viewModel: LeagueScoringPresetsViewModel | null = null;
reset(): void { reset(): void {
this.viewModel = null; this.viewModel = null;
} }
present(dto: LeagueScoringPresetsResultDTO): void { present(output: LeagueScoringPresetsOutputPort): void {
this.viewModel = { this.viewModel = {
presets: dto.presets, presets: output.presets,
totalCount: output.presets.length,
}; };
} }

View File

@@ -1,26 +1,29 @@
import { ILeagueStandingsPresenter, LeagueStandingsResultDTO, LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter'; import { LeagueStandingsOutputPort } from '@core/racing/application/ports/output/LeagueStandingsOutputPort';
import { LeagueStandingsDTO } from '../dtos/LeagueStandingsDTO';
import type { Presenter } from '@core/shared/presentation';
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter { export class LeagueStandingsPresenter implements Presenter<LeagueStandingsOutputPort, LeagueStandingsDTO> {
private result: LeagueStandingsViewModel | null = null; private result: LeagueStandingsDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: LeagueStandingsResultDTO) { present(dto: LeagueStandingsOutputPort) {
const driverMap = new Map(dto.drivers.map(d => [d.id, { id: d.id, name: d.name }])); const standings = dto.standings.map(standing => ({
const standings = dto.standings driverId: standing.driverId,
.sort((a, b) => a.position - b.position) driver: {
.map(standing => ({ id: standing.driver.id,
driverId: standing.driverId, name: standing.driver.name,
driver: driverMap.get(standing.driverId)!, // Add other DriverDto fields if needed, but for now just id and name
points: standing.points, },
rank: standing.position, points: standing.points,
})); rank: standing.rank,
}));
this.result = { standings }; this.result = { standings };
} }
getViewModel(): LeagueStandingsViewModel { getViewModel(): LeagueStandingsDTO {
if (!this.result) throw new Error('Presenter not presented'); if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }

View File

@@ -1,17 +1,19 @@
import { ILeagueStatsPresenter, LeagueStatsResultDTO, LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter'; import { LeagueStatsOutputPort } from '@core/racing/application/ports/output/LeagueStatsOutputPort';
import { LeagueStatsDTO } from '../dtos/LeagueStatsDTO';
import type { Presenter } from '@core/shared/presentation';
export class LeagueStatsPresenter implements ILeagueStatsPresenter { export class LeagueStatsPresenter implements Presenter<LeagueStatsOutputPort, LeagueStatsDTO> {
private result: LeagueStatsViewModel | null = null; private result: LeagueStatsDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: LeagueStatsResultDTO) { present(dto: LeagueStatsOutputPort) {
this.result = dto; this.result = dto;
} }
getViewModel(): LeagueStatsViewModel | null { getViewModel(): LeagueStatsDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -1,18 +1,9 @@
import { IRejectLeagueJoinRequestPresenter, RejectLeagueJoinRequestResultDTO, RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter'; import type { RejectLeagueJoinRequestOutputPort } from '@core/racing/application/ports/output/RejectLeagueJoinRequestOutputPort';
import type { RejectJoinRequestOutputDTO } from '../dtos/RejectJoinRequestOutputDTO';
export class RejectLeagueJoinRequestPresenter implements IRejectLeagueJoinRequestPresenter { export function mapRejectLeagueJoinRequestOutputPortToDTO(port: RejectLeagueJoinRequestOutputPort): RejectJoinRequestOutputDTO {
private result: RejectLeagueJoinRequestViewModel | null = null; return {
success: port.success,
reset() { message: port.message,
this.result = null; };
}
present(dto: RejectLeagueJoinRequestResultDTO) {
this.result = dto;
}
getViewModel(): RejectLeagueJoinRequestViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -1,18 +1,8 @@
import { IRemoveLeagueMemberPresenter, RemoveLeagueMemberResultDTO, RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter'; import type { RemoveLeagueMemberOutputPort } from '@core/racing/application/ports/output/RemoveLeagueMemberOutputPort';
import type { RemoveLeagueMemberOutputDTO } from '../dtos/RemoveLeagueMemberOutputDTO';
export class RemoveLeagueMemberPresenter implements IRemoveLeagueMemberPresenter { export function mapRemoveLeagueMemberOutputPortToDTO(port: RemoveLeagueMemberOutputPort): RemoveLeagueMemberOutputDTO {
private result: RemoveLeagueMemberViewModel | null = null; return {
success: port.success,
reset() { };
this.result = null;
}
present(dto: RemoveLeagueMemberResultDTO) {
this.result = dto;
}
getViewModel(): RemoveLeagueMemberViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -1,20 +1,20 @@
import { IGetTotalLeaguesPresenter, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter'; import { GetTotalLeaguesOutputPort } from '@core/racing/application/ports/output/GetTotalLeaguesOutputPort';
import { LeagueStatsDTO } from '../dtos/LeagueStatsDTO'; import { TotalLeaguesDTO } from '../dtos/TotalLeaguesDTO';
export class TotalLeaguesPresenter implements IGetTotalLeaguesPresenter { export class TotalLeaguesPresenter {
private result: LeagueStatsDto | null = null; private result: TotalLeaguesDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: GetTotalLeaguesResultDTO) { present(output: GetTotalLeaguesOutputPort) {
this.result = { this.result = {
totalLeagues: dto.totalLeagues, totalLeagues: output.totalLeagues,
}; };
} }
getViewModel(): LeagueStatsDto | null { getViewModel(): TotalLeaguesDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -1,20 +1,8 @@
import { ITransferLeagueOwnershipPresenter, TransferLeagueOwnershipResultDTO, TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter'; import type { TransferLeagueOwnershipOutputPort } from '@core/racing/application/ports/output/TransferLeagueOwnershipOutputPort';
import type { TransferLeagueOwnershipOutputDTO } from '../dtos/TransferLeagueOwnershipOutputDTO';
export class TransferLeagueOwnershipPresenter implements ITransferLeagueOwnershipPresenter { export function mapTransferLeagueOwnershipOutputPortToDTO(port: TransferLeagueOwnershipOutputPort): TransferLeagueOwnershipOutputDTO {
private result: TransferLeagueOwnershipViewModel | null = null; return {
success: port.success,
reset() { };
this.result = null;
}
present(dto: TransferLeagueOwnershipResultDTO): void {
this.result = {
success: dto.success,
};
}
getViewModel(): TransferLeagueOwnershipViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -1,18 +1,8 @@
import { IUpdateLeagueMemberRolePresenter, UpdateLeagueMemberRoleResultDTO, UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter'; import type { UpdateLeagueMemberRoleOutputPort } from '@core/racing/application/ports/output/UpdateLeagueMemberRoleOutputPort';
import type { UpdateLeagueMemberRoleOutputDTO } from '../dtos/UpdateLeagueMemberRoleOutputDTO';
export class UpdateLeagueMemberRolePresenter implements IUpdateLeagueMemberRolePresenter { export function mapUpdateLeagueMemberRoleOutputPortToDTO(port: UpdateLeagueMemberRoleOutputPort): UpdateLeagueMemberRoleOutputDTO {
private result: UpdateLeagueMemberRoleViewModel | null = null; return {
success: port.success,
reset() { };
this.result = null;
}
present(dto: UpdateLeagueMemberRoleResultDTO) {
this.result = dto;
}
getViewModel(): UpdateLeagueMemberRoleViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
} }

View File

@@ -46,8 +46,8 @@ export class RaceController {
@Get('all/page-data') @Get('all/page-data')
@ApiOperation({ summary: 'Get all races page data' }) @ApiOperation({ summary: 'Get all races page data' })
@ApiResponse({ status: 200, description: 'All races page data', type: RacesPageDataDTO }) @ApiResponse({ status: 200, description: 'All races page data', type: AllRacesPageDTO })
async getAllRacesPageData(): Promise<RacesPageDataDTO> { async getAllRacesPageData(): Promise<AllRacesPageDTO> {
return this.raceService.getAllRacesPageData(); return this.raceService.getAllRacesPageData();
} }

View File

@@ -146,8 +146,6 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository, resultRepo: IResultRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
driverRatingProvider: DriverRatingProvider,
imageService: IImageServicePort,
) => new GetRaceDetailUseCase( ) => new GetRaceDetailUseCase(
raceRepo, raceRepo,
leagueRepo, leagueRepo,
@@ -155,8 +153,6 @@ export const RaceProviders: Provider[] = [
raceRegRepo, raceRegRepo,
resultRepo, resultRepo,
leagueMembershipRepo, leagueMembershipRepo,
driverRatingProvider,
imageService,
), ),
inject: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
@@ -165,8 +161,6 @@ export const RaceProviders: Provider[] = [
RACE_REGISTRATION_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
DRIVER_RATING_PROVIDER_TOKEN,
IMAGE_SERVICE_TOKEN,
], ],
}, },
{ {

View File

@@ -1,12 +1,11 @@
import { Injectable, Inject } from '@nestjs/common'; import { Injectable, Inject } from '@nestjs/common';
import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter'; import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
import type { GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter'; import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
import type { RaceDetailViewModel } from '@core/racing/application/presenters/IRaceDetailPresenter'; import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
import type { RacesPageViewModel } from '@core/racing/application/presenters/IRacesPagePresenter'; import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
import type { RaceResultsDetailViewModel } from '@core/racing/application/presenters/IRaceResultsDetailPresenter'; import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
import type { RaceWithSOFViewModel } from '@core/racing/application/presenters/IRaceWithSOFPresenter'; import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort';
import type { RaceProtestsViewModel } from '@core/racing/application/presenters/IRaceProtestsPresenter'; import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort';
import type { RacePenaltiesViewModel } from '@core/racing/application/presenters/IRacePenaltiesPresenter';
// DTOs // DTOs
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO'; import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
@@ -14,9 +13,18 @@ import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO'; import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO'; import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO'; import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
import { AllRacesPageDTO } from './dtos/AllRacesPageDTO';
import { RaceStatsDTO } from './dtos/RaceStatsDTO';
import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO';
import { RaceProtestsDTO } from './dtos/RaceProtestsDTO';
import { RaceResultsDetailDTO } from './dtos/RaceResultsDetailDTO';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import { Result } from '@core/shared/application/Result';
import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
// Use cases // Use cases
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase'; import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
@@ -53,7 +61,7 @@ import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCom
import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO'; import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO';
// Tokens // Tokens
import { LOGGER_TOKEN } from './RaceProviders'; import { LOGGER_TOKEN, DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN, LEAGUE_REPOSITORY_TOKEN } from './RaceProviders';
@Injectable() @Injectable()
export class RaceService { export class RaceService {
@@ -77,7 +85,10 @@ export class RaceService {
private readonly applyPenaltyUseCase: ApplyPenaltyUseCase, private readonly applyPenaltyUseCase: ApplyPenaltyUseCase,
private readonly requestProtestDefenseUseCase: RequestProtestDefenseUseCase, private readonly requestProtestDefenseUseCase: RequestProtestDefenseUseCase,
private readonly reviewProtestUseCase: ReviewProtestUseCase, private readonly reviewProtestUseCase: ReviewProtestUseCase,
@Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository: ILeagueRepository,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
) {} ) {}
async getAllRaces(): Promise<AllRacesPageViewModel> { async getAllRaces(): Promise<AllRacesPageViewModel> {
@@ -88,21 +99,29 @@ export class RaceService {
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getTotalRaces(): Promise<GetTotalRacesViewModel> { async getTotalRaces(): Promise<RaceStatsDTO> {
this.logger.debug('[RaceService] Fetching total races count.'); this.logger.debug('[RaceService] Fetching total races count.');
const result = await this.getTotalRacesUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new GetTotalRacesPresenter(); const presenter = new GetTotalRacesPresenter();
await this.getTotalRacesUseCase.execute({}, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsSummaryDTO> { async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsSummaryDTO> {
this.logger.debug('Importing race results:', input); this.logger.debug('Importing race results:', input);
const result = await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new ImportRaceResultsApiPresenter(); const presenter = new ImportRaceResultsApiPresenter();
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter); presenter.present(result.unwrap());
return presenter.getViewModel()!; return presenter.getViewModel()!;
} }
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailViewModel> { async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
this.logger.debug('[RaceService] Fetching race detail:', params); this.logger.debug('[RaceService] Fetching race detail:', params);
const result = await this.getRaceDetailUseCase.execute(params); const result = await this.getRaceDetailUseCase.execute(params);
@@ -111,10 +130,71 @@ export class RaceService {
throw new Error('Failed to get race detail'); throw new Error('Failed to get race detail');
} }
return result.value; const outputPort = result.value;
// Map to DTO
const raceDTO = outputPort.race ? {
id: outputPort.race.id,
leagueId: outputPort.race.leagueId,
track: outputPort.race.track,
car: outputPort.race.car,
scheduledAt: outputPort.race.scheduledAt.toISOString(),
sessionType: outputPort.race.sessionType,
status: outputPort.race.status,
strengthOfField: outputPort.race.strengthOfField ?? null,
registeredCount: outputPort.race.registeredCount ?? undefined,
maxParticipants: outputPort.race.maxParticipants ?? undefined,
} : null;
const leagueDTO = outputPort.league ? {
id: outputPort.league.id.toString(),
name: outputPort.league.name.toString(),
description: outputPort.league.description.toString(),
settings: {
maxDrivers: outputPort.league.settings.maxDrivers ?? undefined,
qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined,
},
} : null;
const entryListDTO = await Promise.all(outputPort.drivers.map(async driver => {
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id });
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
return {
id: driver.id,
name: driver.name.toString(),
country: driver.country.toString(),
avatarUrl: avatarResult.avatarUrl,
rating: ratingResult.rating,
isCurrentUser: driver.id === params.driverId,
};
}));
const registrationDTO = {
isUserRegistered: outputPort.isUserRegistered,
canRegister: outputPort.canRegister,
};
const userResultDTO = outputPort.userResult ? {
position: outputPort.userResult.position.toNumber(),
startPosition: outputPort.userResult.startPosition.toNumber(),
incidents: outputPort.userResult.incidents.toNumber(),
fastestLap: outputPort.userResult.fastestLap.toNumber(),
positionChange: outputPort.userResult.getPositionChange(),
isPodium: outputPort.userResult.isPodium(),
isClean: outputPort.userResult.isClean(),
ratingChange: this.calculateRatingChange(outputPort.userResult.position.toNumber()),
} : null;
return {
race: raceDTO,
league: leagueDTO,
entryList: entryListDTO,
registration: registrationDTO,
userResult: userResultDTO,
};
} }
async getRacesPageData(): Promise<RacesPageViewModel> { async getRacesPageData(): Promise<RacesPageDataDTO> {
this.logger.debug('[RaceService] Fetching races page data.'); this.logger.debug('[RaceService] Fetching races page data.');
const result = await this.getRacesPageDataUseCase.execute(); const result = await this.getRacesPageDataUseCase.execute();
@@ -123,10 +203,33 @@ export class RaceService {
throw new Error('Failed to get races page data'); throw new Error('Failed to get races page data');
} }
return result.value; const outputPort = result.value;
// Fetch leagues for league names
const allLeagues = await this.leagueRepository.findAll();
const leagueMap = new Map(allLeagues.map(l => [l.id, l.name]));
// Map to DTO
const racesDTO = outputPort.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt.toISOString(),
status: race.status,
leagueId: race.leagueId,
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
strengthOfField: race.strengthOfField,
isUpcoming: race.scheduledAt > new Date(),
isLive: race.status === 'running',
isPast: race.scheduledAt < new Date() && race.status === 'completed',
}));
return {
races: racesDTO,
};
} }
async getAllRacesPageData(): Promise<RacesPageViewModel> { async getAllRacesPageData(): Promise<AllRacesPageDTO> {
this.logger.debug('[RaceService] Fetching all races page data.'); this.logger.debug('[RaceService] Fetching all races page data.');
const result = await this.getAllRacesPageDataUseCase.execute(); const result = await this.getAllRacesPageDataUseCase.execute();
@@ -135,10 +238,10 @@ export class RaceService {
throw new Error('Failed to get all races page data'); throw new Error('Failed to get all races page data');
} }
return result.value; return result.value as AllRacesPageDTO;
} }
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailViewModel> { async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailDTO> {
this.logger.debug('[RaceService] Fetching race results detail:', { raceId }); this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
const result = await this.getRaceResultsDetailUseCase.execute({ raceId }); const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
@@ -147,10 +250,41 @@ export class RaceService {
throw new Error('Failed to get race results detail'); throw new Error('Failed to get race results detail');
} }
return result.value; const outputPort = result.value;
// Create a map of driverId to driver for easy lookup
const driverMap = new Map(outputPort.drivers.map(driver => [driver.id, driver]));
const resultsDTO = await Promise.all(outputPort.results.map(async (result) => {
const driver = driverMap.get(result.driverId.toString());
if (!driver) {
throw new Error(`Driver not found for result: ${result.driverId}`);
}
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
return {
driverId: result.driverId.toString(),
driverName: driver.name.toString(),
avatarUrl: avatarResult.avatarUrl,
position: result.position.toNumber(),
startPosition: result.startPosition.toNumber(),
incidents: result.incidents.toNumber(),
fastestLap: result.fastestLap.toNumber(),
positionChange: result.getPositionChange(),
isPodium: result.isPodium(),
isClean: result.isClean(),
};
}));
return {
raceId: outputPort.race.id,
track: outputPort.race.track,
results: resultsDTO,
};
} }
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFViewModel> { async getRaceWithSOF(raceId: string): Promise<RaceWithSOFDTO> {
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId }); this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
const result = await this.getRaceWithSOFUseCase.execute({ raceId }); const result = await this.getRaceWithSOFUseCase.execute({ raceId });
@@ -159,10 +293,17 @@ export class RaceService {
throw new Error('Failed to get race with SOF'); throw new Error('Failed to get race with SOF');
} }
return result.value; const outputPort = result.value;
// Map to DTO
return {
id: outputPort.id,
track: outputPort.track,
strengthOfField: outputPort.strengthOfField,
};
} }
async getRaceProtests(raceId: string): Promise<RaceProtestsViewModel> { async getRaceProtests(raceId: string): Promise<RaceProtestsDTO> {
this.logger.debug('[RaceService] Fetching race protests:', { raceId }); this.logger.debug('[RaceService] Fetching race protests:', { raceId });
const result = await this.getRaceProtestsUseCase.execute({ raceId }); const result = await this.getRaceProtestsUseCase.execute({ raceId });
@@ -171,10 +312,32 @@ export class RaceService {
throw new Error('Failed to get race protests'); throw new Error('Failed to get race protests');
} }
return result.value; const outputPort = result.value;
const protestsDTO = outputPort.protests.map(protest => ({
id: protest.id,
protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId,
incident: {
lap: protest.incident.lap,
description: protest.incident.description,
},
status: protest.status,
filedAt: protest.filedAt.toISOString(),
}));
const driverMap: Record<string, string> = {};
outputPort.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString();
});
return {
protests: protestsDTO,
driverMap,
};
} }
async getRacePenalties(raceId: string): Promise<RacePenaltiesViewModel> { async getRacePenalties(raceId: string): Promise<RacePenaltiesDTO> {
this.logger.debug('[RaceService] Fetching race penalties:', { raceId }); this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
const result = await this.getRacePenaltiesUseCase.execute({ raceId }); const result = await this.getRacePenaltiesUseCase.execute({ raceId });
@@ -183,7 +346,28 @@ export class RaceService {
throw new Error('Failed to get race penalties'); throw new Error('Failed to get race penalties');
} }
return result.value; const outputPort = result.value;
const penaltiesDTO = outputPort.penalties.map(penalty => ({
id: penalty.id,
driverId: penalty.driverId,
type: penalty.type,
value: penalty.value ?? 0,
reason: penalty.reason,
issuedBy: penalty.issuedBy,
issuedAt: penalty.issuedAt.toISOString(),
notes: penalty.notes,
}));
const driverMap: Record<string, string> = {};
outputPort.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString();
});
return {
penalties: penaltiesDTO,
driverMap,
};
} }
async registerForRace(params: RegisterForRaceParamsDTO): Promise<void> { async registerForRace(params: RegisterForRaceParamsDTO): Promise<void> {
@@ -277,4 +461,10 @@ export class RaceService {
throw new Error('Failed to review protest'); throw new Error('Failed to review protest');
} }
} }
private calculateRatingChange(position: number): number {
const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5;
const positionBonus = Math.max(0, (20 - position) * 2);
return baseChange + positionBonus;
}
} }

View File

@@ -1,10 +1,45 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { RaceViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
export class AllRacesPageDTO { export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
@ApiProperty({ type: [RaceViewModel] })
races!: RaceViewModel[]; export class AllRacesListItemDTO {
@ApiProperty()
id!: string;
@ApiProperty() @ApiProperty()
totalCount!: number; track!: string;
@ApiProperty()
car!: string;
@ApiProperty()
scheduledAt!: string;
@ApiProperty()
status!: 'scheduled' | 'running' | 'completed' | 'cancelled';
@ApiProperty()
leagueId!: string;
@ApiProperty()
leagueName!: string;
@ApiProperty({ nullable: true })
strengthOfField!: number | null;
}
export class AllRacesFilterOptionsDTO {
@ApiProperty({ type: [{ value: String, label: String }] })
statuses!: { value: AllRacesStatus; label: string }[];
@ApiProperty({ type: [{ id: String, name: String }] })
leagues!: { id: string; name: string }[];
}
export class AllRacesPageDTO {
@ApiProperty({ type: [AllRacesListItemDTO] })
races!: AllRacesListItemDTO[];
@ApiProperty({ type: AllRacesFilterOptionsDTO })
filters!: AllRacesFilterOptionsDTO;
} }

View File

@@ -1,17 +1,39 @@
import { IGetAllRacesPresenter, GetAllRacesResultDTO, AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter'; import { GetAllRacesOutputPort } from '@core/racing/application/ports/output/GetAllRacesOutputPort';
import { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export class GetAllRacesPresenter implements IGetAllRacesPresenter { export class GetAllRacesPresenter {
private result: AllRacesPageViewModel | null = null; private result: AllRacesPageDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: GetAllRacesResultDTO) { async present(output: GetAllRacesOutputPort) {
this.result = dto; this.result = {
races: output.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status,
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField,
})),
filters: {
statuses: [
{ value: 'all', label: 'All' },
{ value: 'scheduled', label: 'Scheduled' },
{ value: 'running', label: 'Running' },
{ value: 'completed', label: 'Completed' },
{ value: 'cancelled', label: 'Cancelled' },
],
leagues: [], // TODO: populate if needed
},
};
} }
getViewModel(): AllRacesPageViewModel | null { getViewModel(): AllRacesPageDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -1,19 +1,20 @@
import { IGetTotalRacesPresenter, GetTotalRacesResultDTO, GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter'; import { GetTotalRacesOutputPort } from '@core/racing/application/ports/output/GetTotalRacesOutputPort';
import { RaceStatsDTO } from '../dtos/RaceStatsDTO';
export class GetTotalRacesPresenter implements IGetTotalRacesPresenter { export class GetTotalRacesPresenter {
private result: GetTotalRacesViewModel | null = null; private result: RaceStatsDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: GetTotalRacesResultDTO) { present(output: GetTotalRacesOutputPort) {
this.result = { this.result = {
totalRaces: dto.totalRaces, totalRaces: output.totalRaces,
}; };
} }
getViewModel(): GetTotalRacesViewModel | null { getViewModel(): RaceStatsDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -1,17 +1,24 @@
import { IImportRaceResultsApiPresenter, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel } from '@core/racing/application/presenters/IImportRaceResultsApiPresenter'; import { ImportRaceResultsApiOutputPort } from '@core/racing/application/ports/output/ImportRaceResultsApiOutputPort';
import { ImportRaceResultsSummaryDTO } from '../dtos/ImportRaceResultsSummaryDTO';
export class ImportRaceResultsApiPresenter implements IImportRaceResultsApiPresenter { export class ImportRaceResultsApiPresenter {
private result: ImportRaceResultsSummaryViewModel | null = null; private result: ImportRaceResultsSummaryDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: ImportRaceResultsApiResultDTO) { present(output: ImportRaceResultsApiOutputPort) {
this.result = dto; this.result = {
success: output.success,
raceId: output.raceId,
driversProcessed: output.driversProcessed,
resultsRecorded: output.resultsRecorded,
errors: output.errors,
};
} }
getViewModel(): ImportRaceResultsSummaryViewModel | null { getViewModel(): ImportRaceResultsSummaryDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -2,6 +2,12 @@ import { ApiProperty } from '@nestjs/swagger';
import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO'; import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO';
export class GetEntitySponsorshipPricingResultDTO { export class GetEntitySponsorshipPricingResultDTO {
@ApiProperty()
entityType: string;
@ApiProperty()
entityId: string;
@ApiProperty({ type: [SponsorshipPricingItemDTO] }) @ApiProperty({ type: [SponsorshipPricingItemDTO] })
pricing: SponsorshipPricingItemDTO[]; pricing: SponsorshipPricingItemDTO[];
} }

View File

@@ -7,9 +7,15 @@ export class SponsorDTO {
@ApiProperty() @ApiProperty()
name: string; name: string;
@ApiProperty({ required: false })
contactEmail?: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
logoUrl?: string; logoUrl?: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
websiteUrl?: string; websiteUrl?: string;
@ApiProperty({ required: false })
createdAt?: Date;
} }

View File

@@ -10,9 +10,9 @@ describe('CreateSponsorPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result to null', () => { it('should reset the result to null', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' }; const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockResult); presenter.present(mockPort);
expect(presenter.viewModel).toEqual(mockResult); expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
presenter.reset(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +21,11 @@ describe('CreateSponsorPresenter', () => {
describe('present', () => { describe('present', () => {
it('should store the result', () => { it('should store the result', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com' }; const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockResult); presenter.present(mockPort);
expect(presenter.viewModel).toEqual(mockResult); expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
}); });
}); });
@@ -35,10 +35,10 @@ describe('CreateSponsorPresenter', () => {
}); });
it('should return the result when presented', () => { it('should return the result when presented', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' }; const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockResult); presenter.present(mockPort);
expect(presenter.getViewModel()).toEqual(mockResult); expect(presenter.getViewModel()).toEqual({ sponsor: mockPort.sponsor });
}); });
}); });
@@ -48,10 +48,10 @@ describe('CreateSponsorPresenter', () => {
}); });
it('should return the result when presented', () => { it('should return the result when presented', () => {
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' }; const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockResult); presenter.present(mockPort);
expect(presenter.viewModel).toEqual(mockResult); expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
}); });
}); });
}); });

View File

@@ -1,21 +1,31 @@
import { CreateSponsorViewModel, CreateSponsorOutputPort, ICreateSponsorPresenter } from '@core/racing/application/presenters/ICreateSponsorPresenter'; import type { CreateSponsorOutputPort } from '@core/racing/application/ports/output/CreateSponsorOutputPort';
import type { CreateSponsorOutputDTO } from '../dtos/CreateSponsorOutputDTO';
export class CreateSponsorPresenter implements ICreateSponsorPresenter { export class CreateSponsorPresenter {
private result: CreateSponsorViewModel | null = null; private result: CreateSponsorOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: CreateSponsorOutputPort) { present(port: CreateSponsorOutputPort) {
this.result = dto; this.result = {
sponsor: {
id: port.sponsor.id,
name: port.sponsor.name,
contactEmail: port.sponsor.contactEmail,
logoUrl: port.sponsor.logoUrl,
websiteUrl: port.sponsor.websiteUrl,
createdAt: port.sponsor.createdAt,
},
};
} }
getViewModel(): CreateSponsorViewModel | null { getViewModel(): CreateSponsorOutputDTO | null {
return this.result; return this.result;
} }
get viewModel(): CreateSponsorViewModel { get viewModel(): CreateSponsorOutputDTO {
if (!this.result) throw new Error('Presenter not presented'); if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }

View File

@@ -1,22 +1,41 @@
import type { GetEntitySponsorshipPricingResultDTO } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase'; import type { GetEntitySponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetEntitySponsorshipPricingOutputPort';
import type { IEntitySponsorshipPricingPresenter } from '@core/racing/application/presenters/IEntitySponsorshipPricingPresenter'; import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetEntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter { export class GetEntitySponsorshipPricingPresenter {
private result: GetEntitySponsorshipPricingResultDTO | null = null; private result: GetEntitySponsorshipPricingResultDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: GetEntitySponsorshipPricingResultDTO | null) { async present(output: GetEntitySponsorshipPricingOutputPort | null) {
this.result = dto; if (!output) {
this.result = { pricing: [] };
return;
}
const pricing = [];
if (output.mainSlot) {
pricing.push({
id: `${output.entityType}-${output.entityId}-main`,
level: 'main',
price: output.mainSlot.price,
currency: output.mainSlot.currency,
});
}
if (output.secondarySlot) {
pricing.push({
id: `${output.entityType}-${output.entityId}-secondary`,
level: 'secondary',
price: output.secondarySlot.price,
currency: output.secondarySlot.currency,
});
}
this.result = { pricing };
} }
getViewModel(): GetEntitySponsorshipPricingResultDTO | null { getViewModel(): GetEntitySponsorshipPricingResultDTO | null {
return this.result; return this.result;
} }
get viewModel(): GetEntitySponsorshipPricingResultDTO | null {
return this.result;
}
} }

View File

@@ -0,0 +1,13 @@
import type { PendingSponsorshipRequestsOutputPort } from '@core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort';
import { GetPendingSponsorshipRequestsOutputDTO } from '../dtos/GetPendingSponsorshipRequestsOutputDTO';
export class GetPendingSponsorshipRequestsPresenter {
present(outputPort: PendingSponsorshipRequestsOutputPort): GetPendingSponsorshipRequestsOutputDTO {
return {
entityType: outputPort.entityType,
entityId: outputPort.entityId,
requests: outputPort.requests,
totalCount: outputPort.totalCount,
};
}
}

View File

@@ -1,22 +1,8 @@
import type { SponsorDashboardDTO } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; import type { SponsorDashboardOutputPort } from '@core/racing/application/ports/output/SponsorDashboardOutputPort';
import type { ISponsorDashboardPresenter, SponsorDashboardViewModel } from '@core/racing/application/presenters/ISponsorDashboardPresenter'; import { SponsorDashboardDTO } from '../dtos/SponsorDashboardDTO';
export class GetSponsorDashboardPresenter implements ISponsorDashboardPresenter { export class GetSponsorDashboardPresenter {
private result: SponsorDashboardViewModel | null = null; present(outputPort: SponsorDashboardOutputPort | null): SponsorDashboardDTO | null {
return outputPort;
reset() {
this.result = null;
}
present(dto: SponsorDashboardDTO | null) {
this.result = dto;
}
getViewModel(): SponsorDashboardViewModel | null {
return this.result;
}
get viewModel(): SponsorDashboardViewModel | null {
return this.result;
} }
} }

View File

@@ -1,22 +1,8 @@
import type { SponsorSponsorshipsDTO } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; import type { SponsorSponsorshipsOutputPort } from '@core/racing/application/ports/output/SponsorSponsorshipsOutputPort';
import type { ISponsorSponsorshipsPresenter, SponsorSponsorshipsViewModel } from '@core/racing/application/presenters/ISponsorSponsorshipsPresenter'; import { SponsorSponsorshipsDTO } from '../dtos/SponsorSponsorshipsDTO';
export class GetSponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter { export class GetSponsorSponsorshipsPresenter {
private result: SponsorSponsorshipsViewModel | null = null; present(outputPort: SponsorSponsorshipsOutputPort | null): SponsorSponsorshipsDTO | null {
return outputPort;
reset() {
this.result = null;
}
present(dto: SponsorSponsorshipsDTO | null) {
this.result = dto;
}
getViewModel(): SponsorSponsorshipsViewModel | null {
return this.result;
}
get viewModel(): SponsorSponsorshipsViewModel | null {
return this.result;
} }
} }

View File

@@ -1,22 +1,10 @@
import { GetSponsorsViewModel, GetSponsorsResultDTO, IGetSponsorsPresenter } from '@core/racing/application/presenters/IGetSponsorsPresenter'; import type { GetSponsorsOutputPort } from '@core/racing/application/ports/output/GetSponsorsOutputPort';
import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO';
export class GetSponsorsPresenter implements IGetSponsorsPresenter { export class GetSponsorsPresenter {
private result: GetSponsorsViewModel | null = null; present(outputPort: GetSponsorsOutputPort): GetSponsorsOutputDTO {
return {
reset() { sponsors: outputPort.sponsors,
this.result = null; };
}
present(dto: GetSponsorsResultDTO) {
this.result = dto;
}
getViewModel(): GetSponsorsViewModel | null {
return this.result;
}
get viewModel(): GetSponsorsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
} }
} }

View File

@@ -1,22 +1,12 @@
import { GetSponsorshipPricingViewModel, GetSponsorshipPricingResultDTO, IGetSponsorshipPricingPresenter } from '@core/racing/application/presenters/IGetSponsorshipPricingPresenter'; import type { GetSponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetSponsorshipPricingOutputPort';
import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetSponsorshipPricingPresenter implements IGetSponsorshipPricingPresenter { export class GetSponsorshipPricingPresenter {
private result: GetSponsorshipPricingViewModel | null = null; present(outputPort: GetSponsorshipPricingOutputPort): GetEntitySponsorshipPricingResultDTO {
return {
reset() { entityType: outputPort.entityType,
this.result = null; entityId: outputPort.entityId,
} pricing: outputPort.pricing,
};
present(dto: GetSponsorshipPricingResultDTO) {
this.result = dto;
}
getViewModel(): GetSponsorshipPricingViewModel | null {
return this.result;
}
get viewModel(): GetSponsorshipPricingViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
} }
} }

View File

@@ -0,0 +1,58 @@
import { ApiProperty } from '@nestjs/swagger';
export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
class TeamLeaderboardItemDTO {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty()
memberCount: number;
@ApiProperty({ nullable: true })
rating: number | null;
@ApiProperty()
totalWins: number;
@ApiProperty()
totalRaces: number;
@ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel: SkillLevel;
@ApiProperty()
isRecruiting: boolean;
@ApiProperty()
createdAt: string;
@ApiProperty({ required: false })
description?: string;
@ApiProperty({ enum: ['endurance', 'sprint', 'mixed'], required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
export class GetTeamsLeaderboardOutputDTO {
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
teams: TeamLeaderboardItemDTO[];
@ApiProperty()
recruitingCount: number;
@ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } })
groupsBySkillLevel: Record<SkillLevel, TeamLeaderboardItemDTO[]>;
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
topTeams: TeamLeaderboardItemDTO[];
}

View File

@@ -1,33 +1,33 @@
import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel, TeamListItemViewModel } from '@core/racing/application/presenters/IAllTeamsPresenter'; import { GetAllTeamsOutputPort } from '@core/racing/application/ports/output/GetAllTeamsOutputPort';
import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO';
export class AllTeamsPresenter implements IAllTeamsPresenter { export class AllTeamsPresenter {
private result: AllTeamsViewModel | null = null; private result: GetAllTeamsOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: AllTeamsResultDTO) { async present(output: GetAllTeamsOutputPort) {
const teams: TeamListItemViewModel[] = dto.teams.map(team => ({
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount,
leagues: team.leagues || [],
}));
this.result = { this.result = {
teams, teams: output.teams.map(team => ({
totalCount: teams.length, id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
memberCount: team.memberCount,
leagues: team.leagues || [],
// Note: specialization, region, languages not available in output port
})),
totalCount: output.totalCount || output.teams.length,
}; };
} }
getViewModel(): AllTeamsViewModel | null { getViewModel(): GetAllTeamsOutputDTO | null {
return this.result; return this.result;
} }
get viewModel(): AllTeamsViewModel { get viewModel(): GetAllTeamsOutputDTO {
if (!this.result) throw new Error('Presenter not presented'); if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }

View File

@@ -1,41 +1,38 @@
import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@core/racing/application/presenters/IDriverTeamPresenter'; import { DriverTeamOutputPort } from '@core/racing/application/ports/output/DriverTeamOutputPort';
import { GetDriverTeamOutputDTO } from '../dtos/GetDriverTeamOutputDTO';
export class DriverTeamPresenter implements IDriverTeamPresenter { export class DriverTeamPresenter {
private result: DriverTeamViewModel | null = null; private result: GetDriverTeamOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: DriverTeamResultDTO) { async present(output: DriverTeamOutputPort) {
const isOwner = dto.team.ownerId === dto.driverId; const isOwner = output.team.ownerId === output.driverId;
const canManage = isOwner || dto.membership.role === 'owner' || dto.membership.role === 'manager'; const canManage = isOwner || output.membership.role === 'owner' || output.membership.role === 'manager';
this.result = { this.result = {
team: { team: {
id: dto.team.id, id: output.team.id,
name: dto.team.name, name: output.team.name,
tag: dto.team.tag, tag: output.team.tag,
description: dto.team.description || '', description: output.team.description || '',
ownerId: dto.team.ownerId, ownerId: output.team.ownerId,
leagues: dto.team.leagues || [], leagues: output.team.leagues || [],
createdAt: output.team.createdAt.toISOString(),
}, },
membership: { membership: {
role: dto.membership.role as 'owner' | 'manager' | 'member', role: output.membership.role === 'driver' ? 'member' : output.membership.role,
joinedAt: dto.membership.joinedAt.toISOString(), joinedAt: output.membership.joinedAt.toISOString(),
isActive: dto.membership.status === 'active', isActive: output.membership.status === 'active',
}, },
isOwner, isOwner,
canManage, canManage,
}; };
} }
getViewModel(): DriverTeamViewModel | null { getViewModel(): GetDriverTeamOutputDTO | null {
return this.result;
}
get viewModel(): DriverTeamViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }
} }

View File

@@ -1,50 +1,36 @@
import { import type { GetTeamDetailsOutputPort } from '@core/racing/application/ports/output/GetTeamDetailsOutputPort';
ITeamDetailsPresenter, import type { GetTeamDetailsOutputDTO } from '../dtos/GetTeamDetailsOutputDTO';
TeamDetailsResultDTO,
TeamDetailsViewModel,
} from '@core/racing/application/presenters/ITeamDetailsPresenter';
export class TeamDetailsPresenter implements ITeamDetailsPresenter { export class TeamDetailsPresenter {
private result: TeamDetailsViewModel | null = null; private result: GetTeamDetailsOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: TeamDetailsResultDTO) { async present(outputPort: GetTeamDetailsOutputPort): Promise<void> {
const { team, membership } = dto;
const canManage =
membership !== null &&
(membership.role === 'owner' || membership.role === 'manager');
this.result = { this.result = {
team: { team: {
id: team.id, id: outputPort.team.id,
name: team.name, name: outputPort.team.name,
tag: team.tag, tag: outputPort.team.tag,
description: team.description, description: outputPort.team.description,
ownerId: team.ownerId, ownerId: outputPort.team.ownerId,
leagues: team.leagues || [], leagues: outputPort.team.leagues,
createdAt: team.createdAt?.toISOString() || new Date().toISOString(), createdAt: outputPort.team.createdAt.toISOString(),
}, },
membership: membership membership: outputPort.membership
? { ? {
role: membership.role as 'owner' | 'manager' | 'member', role: outputPort.membership.role,
joinedAt: membership.joinedAt.toISOString(), joinedAt: outputPort.membership.joinedAt.toISOString(),
isActive: membership.status === 'active', isActive: outputPort.membership.isActive,
} }
: null, : null,
canManage, canManage: outputPort.canManage,
}; };
} }
getViewModel(): TeamDetailsViewModel | null { getViewModel(): GetTeamDetailsOutputDTO | null {
return this.result;
}
get viewModel(): TeamDetailsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }
} }

View File

@@ -1,43 +1,30 @@
import { import type { TeamJoinRequestsOutputPort } from '@core/racing/application/ports/output/TeamJoinRequestsOutputPort';
ITeamJoinRequestsPresenter, import type { GetTeamJoinRequestsOutputDTO } from '../dtos/GetTeamJoinRequestsOutputDTO';
TeamJoinRequestsResultDTO,
TeamJoinRequestsViewModel,
TeamJoinRequestViewModel,
} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter';
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter { export class TeamJoinRequestsPresenter {
private result: TeamJoinRequestsViewModel | null = null; private result: GetTeamJoinRequestsOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: TeamJoinRequestsResultDTO) { async present(outputPort: TeamJoinRequestsOutputPort): Promise<void> {
const { requests, driverNames, avatarUrls } = dto;
const requestViewModels: TeamJoinRequestViewModel[] = requests.map((request) => ({
requestId: request.id,
driverId: request.driverId,
driverName: driverNames[request.driverId] || 'Unknown',
teamId: request.teamId,
status: 'pending' as const,
requestedAt: request.requestedAt.toISOString(),
avatarUrl: avatarUrls[request.driverId] || '',
}));
this.result = { this.result = {
requests: requestViewModels, requests: outputPort.requests.map(request => ({
pendingCount: requestViewModels.length, requestId: request.requestId,
totalCount: requestViewModels.length, driverId: request.driverId,
driverName: request.driverName,
teamId: request.teamId,
status: request.status,
requestedAt: request.requestedAt.toISOString(),
avatarUrl: request.avatarUrl,
})),
pendingCount: outputPort.pendingCount,
totalCount: outputPort.totalCount,
}; };
} }
getViewModel(): TeamJoinRequestsViewModel | null { getViewModel(): GetTeamJoinRequestsOutputDTO | null {
return this.result;
}
get viewModel(): TeamJoinRequestsViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }
} }

View File

@@ -1,48 +1,31 @@
import { import type { TeamMembersOutputPort } from '@core/racing/application/ports/output/TeamMembersOutputPort';
ITeamMembersPresenter, import type { GetTeamMembersOutputDTO } from '../dtos/GetTeamMembersOutputDTO';
TeamMembersResultDTO,
TeamMembersViewModel,
TeamMemberViewModel,
} from '@core/racing/application/presenters/ITeamMembersPresenter';
export class TeamMembersPresenter implements ITeamMembersPresenter { export class TeamMembersPresenter {
private result: TeamMembersViewModel | null = null; private result: GetTeamMembersOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: TeamMembersResultDTO) { async present(outputPort: TeamMembersOutputPort): Promise<void> {
const { memberships, driverNames, avatarUrls } = dto;
const members: TeamMemberViewModel[] = memberships.map((membership) => ({
driverId: membership.driverId,
driverName: driverNames[membership.driverId] || 'Unknown',
role: membership.role as 'owner' | 'manager' | 'member',
joinedAt: membership.joinedAt.toISOString(),
isActive: membership.status === 'active',
avatarUrl: avatarUrls[membership.driverId] || '',
}));
const ownerCount = members.filter((m) => m.role === 'owner').length;
const managerCount = members.filter((m) => m.role === 'manager').length;
const memberCount = members.filter((m) => m.role === 'member').length;
this.result = { this.result = {
members, members: outputPort.members.map(member => ({
totalCount: members.length, driverId: member.driverId,
ownerCount, driverName: member.driverName,
managerCount, role: member.role,
memberCount, joinedAt: member.joinedAt.toISOString(),
isActive: member.isActive,
avatarUrl: member.avatarUrl,
})),
totalCount: outputPort.totalCount,
ownerCount: outputPort.ownerCount,
managerCount: outputPort.managerCount,
memberCount: outputPort.memberCount,
}; };
} }
getViewModel(): TeamMembersViewModel | null { getViewModel(): GetTeamMembersOutputDTO | null {
return this.result;
}
get viewModel(): TeamMembersViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }
} }

View File

@@ -1,32 +1,112 @@
import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel, TeamLeaderboardItemViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter'; import type { TeamsLeaderboardOutputPort } from '@core/racing/application/ports/output/TeamsLeaderboardOutputPort';
import type { GetTeamsLeaderboardOutputDTO } from '../dtos/GetTeamsLeaderboardOutputDTO';
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter { export class TeamsLeaderboardPresenter {
private result: TeamsLeaderboardViewModel | null = null; private result: GetTeamsLeaderboardOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(dto: TeamsLeaderboardResultDTO) { async present(outputPort: TeamsLeaderboardOutputPort): Promise<void> {
this.result = { this.result = {
teams: dto.teams as TeamLeaderboardItemViewModel[], teams: outputPort.teams.map(team => ({
recruitingCount: dto.recruitingCount, id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
recruitingCount: outputPort.recruitingCount,
groupsBySkillLevel: { groupsBySkillLevel: {
beginner: [], beginner: outputPort.groupsBySkillLevel.beginner.map(team => ({
intermediate: [], id: team.id,
advanced: [], name: team.name,
pro: [], memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
intermediate: outputPort.groupsBySkillLevel.intermediate.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
advanced: outputPort.groupsBySkillLevel.advanced.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
pro: outputPort.groupsBySkillLevel.pro.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
}, },
topTeams: (dto.teams as TeamLeaderboardItemViewModel[]).slice(0, 10), topTeams: outputPort.topTeams.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
})),
}; };
} }
getViewModel(): TeamsLeaderboardViewModel | null { getViewModel(): GetTeamsLeaderboardOutputDTO | null {
return this.result;
}
get viewModel(): TeamsLeaderboardViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result; return this.result;
} }
} }

View File

@@ -10,7 +10,7 @@ import PerformanceMetrics from './PerformanceMetrics';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { DriverTeamPresenter } from '@/lib/presenters/DriverTeamPresenter'; import { DriverTeamPresenter } from '@/lib/presenters/DriverTeamPresenter';
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership'; import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
import type { ProfileOverviewViewModel } from '@core/racing/application/presenters/IProfileOverviewPresenter'; import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort';
import type { DriverTeamViewModel } from '@core/racing/application/presenters/IDriverTeamPresenter'; import type { DriverTeamViewModel } from '@core/racing/application/presenters/IDriverTeamPresenter';
interface DriverProfileProps { interface DriverProfileProps {
@@ -33,7 +33,7 @@ interface DriverProfileStatsViewModel {
overallRank?: number; overallRank?: number;
} }
type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null; type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null;
export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) { export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) {
const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null); const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null);
@@ -61,8 +61,8 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
const leagueRank = primaryLeagueId const leagueRank = primaryLeagueId
? getLeagueRankings(driver.id, primaryLeagueId) ? getLeagueRankings(driver.id, primaryLeagueId)
: { rank: 0, totalDrivers: 0, percentile: 0 }; : { rank: 0, totalDrivers: 0, percentile: 0 };
const globalRank = profileData?.currentDriver?.globalRank ?? null; const globalRank = profileData?.driver?.globalRank ?? null;
const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0; const totalDrivers = profileData?.driver?.totalDrivers ?? 0;
const performanceStats = driverStats ? { const performanceStats = driverStats ? {
winRate: driverStats.totalRaces > 0 ? (driverStats.wins / driverStats.totalRaces) * 100 : 0, winRate: driverStats.totalRaces > 0 ? (driverStats.wins / driverStats.totalRaces) * 100 : 0,

View File

@@ -4,7 +4,7 @@ import Card from '../ui/Card';
import RankBadge from './RankBadge'; import RankBadge from './RankBadge';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership'; import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
import type { ProfileOverviewViewModel } from '@core/racing/application/presenters/IProfileOverviewPresenter'; import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort';
interface ProfileStatsProps { interface ProfileStatsProps {
driverId?: string; driverId?: string;
@@ -18,7 +18,7 @@ interface ProfileStatsProps {
}; };
} }
type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null; type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null;
export default function ProfileStats({ driverId, stats }: ProfileStatsProps) { export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null); const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null);
@@ -35,7 +35,7 @@ export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
}, [driverId]); }, [driverId]);
const driverStats = profileData?.stats || null; const driverStats = profileData?.stats || null;
const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0; const totalDrivers = profileData?.driver?.totalDrivers ?? 0;
const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null; const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null;
const leagueRank = const leagueRank =
driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null; driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null;

View File

@@ -19,6 +19,7 @@ export * from './use-cases/GetLeagueDriverSeasonStatsUseCase';
export * from './use-cases/GetAllLeaguesWithCapacityUseCase'; export * from './use-cases/GetAllLeaguesWithCapacityUseCase';
export * from './use-cases/GetAllLeaguesWithCapacityAndScoringUseCase'; export * from './use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
export * from './use-cases/GetAllRacesUseCase'; export * from './use-cases/GetAllRacesUseCase';
export * from './use-cases/GetAllRacesPageDataUseCase';
export * from './use-cases/GetTotalRacesUseCase'; export * from './use-cases/GetTotalRacesUseCase';
export * from './use-cases/ImportRaceResultsApiUseCase'; export * from './use-cases/ImportRaceResultsApiUseCase';
export * from './use-cases/ListLeagueScoringPresetsUseCase'; export * from './use-cases/ListLeagueScoringPresetsUseCase';
@@ -74,6 +75,8 @@ export type {
} from './dto/LeagueScheduleDTO'; } from './dto/LeagueScheduleDTO';
export type { ChampionshipStandingsOutputPort } from './ports/output/ChampionshipStandingsOutputPort'; export type { ChampionshipStandingsOutputPort } from './ports/output/ChampionshipStandingsOutputPort';
export type { ChampionshipStandingsRowOutputPort } from './ports/output/ChampionshipStandingsRowOutputPort'; export type { ChampionshipStandingsRowOutputPort } from './ports/output/ChampionshipStandingsRowOutputPort';
export type { AllRacesPageOutputPort } from './ports/output/AllRacesPageOutputPort';
export type { DriverRegistrationStatusOutputPort } from './ports/output/DriverRegistrationStatusOutputPort';
export type { export type {
LeagueConfigFormModel, LeagueConfigFormModel,
LeagueStructureFormDTO, LeagueStructureFormDTO,

View File

@@ -0,0 +1,18 @@
import type { League } from '../../../domain/entities/League';
import type { Season } from '../../../domain/entities/season/Season';
import type { LeagueScoringConfig } from '../../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../../domain/entities/Game';
import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort';
export interface LeagueEnrichedData {
league: League;
usedDriverSlots: number;
season?: Season;
scoringConfig?: LeagueScoringConfig;
game?: Game;
preset?: LeagueScoringPresetOutputPort;
}
export interface AllLeaguesWithCapacityAndScoringOutputPort {
leagues: LeagueEnrichedData[];
}

View File

@@ -0,0 +1,6 @@
import type { League } from '../../domain/entities/League';
export interface AllLeaguesWithCapacityOutputPort {
leagues: League[];
memberCounts: Record<string, number>;
}

View File

@@ -0,0 +1,22 @@
export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
export interface AllRacesListItem {
id: string;
track: string;
car: string;
scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
leagueId: string;
leagueName: string;
strengthOfField: number | null;
}
export interface AllRacesFilterOptions {
statuses: { value: AllRacesStatus; label: string }[];
leagues: { id: string; name: string }[];
}
export interface AllRacesPageOutputPort {
races: AllRacesListItem[];
filters: AllRacesFilterOptions;
}

View File

@@ -0,0 +1,4 @@
export interface ApproveLeagueJoinRequestOutputPort {
success: boolean;
message: string;
}

View File

@@ -0,0 +1,3 @@
export interface CompleteDriverOnboardingOutputPort {
driverId: string;
}

View File

@@ -0,0 +1,4 @@
export interface CreateLeagueOutputPort {
leagueId: string;
success: boolean;
}

View File

@@ -1,7 +1,6 @@
import type { Presenter } from '@core/shared/presentation';
import type { FeedItemType } from '@core/social/domain/types/FeedItemType'; import type { FeedItemType } from '@core/social/domain/types/FeedItemType';
export interface DashboardDriverSummaryViewModel { export interface DashboardDriverSummaryOutputPort {
id: string; id: string;
name: string; name: string;
country: string; country: string;
@@ -14,7 +13,7 @@ export interface DashboardDriverSummaryViewModel {
consistency: number | null; consistency: number | null;
} }
export interface DashboardRaceSummaryViewModel { export interface DashboardRaceSummaryOutputPort {
id: string; id: string;
leagueId: string; leagueId: string;
leagueName: string; leagueName: string;
@@ -25,7 +24,7 @@ export interface DashboardRaceSummaryViewModel {
isMyLeague: boolean; isMyLeague: boolean;
} }
export interface DashboardRecentResultViewModel { export interface DashboardRecentResultOutputPort {
raceId: string; raceId: string;
raceName: string; raceName: string;
leagueId: string; leagueId: string;
@@ -35,7 +34,7 @@ export interface DashboardRecentResultViewModel {
incidents: number; incidents: number;
} }
export interface DashboardLeagueStandingSummaryViewModel { export interface DashboardLeagueStandingSummaryOutputPort {
leagueId: string; leagueId: string;
leagueName: string; leagueName: string;
position: number; position: number;
@@ -43,7 +42,7 @@ export interface DashboardLeagueStandingSummaryViewModel {
points: number; points: number;
} }
export interface DashboardFeedItemSummaryViewModel { export interface DashboardFeedItemSummaryOutputPort {
id: string; id: string;
type: FeedItemType; type: FeedItemType;
headline: string; headline: string;
@@ -53,39 +52,34 @@ export interface DashboardFeedItemSummaryViewModel {
ctaHref?: string; ctaHref?: string;
} }
export interface DashboardFeedSummaryViewModel { export interface DashboardFeedSummaryOutputPort {
notificationCount: number; notificationCount: number;
items: DashboardFeedItemSummaryViewModel[]; items: DashboardFeedItemSummaryOutputPort[];
} }
export interface DashboardFriendSummaryViewModel { export interface DashboardFriendSummaryOutputPort {
id: string; id: string;
name: string; name: string;
country: string; country: string;
avatarUrl: string; avatarUrl: string;
} }
export interface DashboardOverviewViewModel { export interface DashboardOverviewOutputPort {
currentDriver: DashboardDriverSummaryViewModel | null; currentDriver: DashboardDriverSummaryOutputPort | null;
myUpcomingRaces: DashboardRaceSummaryViewModel[]; myUpcomingRaces: DashboardRaceSummaryOutputPort[];
otherUpcomingRaces: DashboardRaceSummaryViewModel[]; otherUpcomingRaces: DashboardRaceSummaryOutputPort[];
/** /**
* All upcoming races for the driver, already sorted by scheduledAt ascending. * All upcoming races for the driver, already sorted by scheduledAt ascending.
*/ */
upcomingRaces: DashboardRaceSummaryViewModel[]; upcomingRaces: DashboardRaceSummaryOutputPort[];
/** /**
* Count of distinct leagues that are currently "active" for the driver, * Count of distinct leagues that are currently "active" for the driver,
* based on upcoming races and league standings. * based on upcoming races and league standings.
*/ */
activeLeaguesCount: number; activeLeaguesCount: number;
nextRace: DashboardRaceSummaryViewModel | null; nextRace: DashboardRaceSummaryOutputPort | null;
recentResults: DashboardRecentResultViewModel[]; recentResults: DashboardRecentResultOutputPort[];
leagueStandingsSummaries: DashboardLeagueStandingSummaryViewModel[]; leagueStandingsSummaries: DashboardLeagueStandingSummaryOutputPort[];
feedSummary: DashboardFeedSummaryViewModel; feedSummary: DashboardFeedSummaryOutputPort;
friends: DashboardFriendSummaryViewModel[]; friends: DashboardFriendSummaryOutputPort[];
} }
export type DashboardOverviewResultDTO = DashboardOverviewViewModel;
export interface IDashboardOverviewPresenter
extends Presenter<DashboardOverviewResultDTO, DashboardOverviewViewModel> {}

View File

@@ -0,0 +1,5 @@
export interface DriverRegistrationStatusOutputPort {
isRegistered: boolean;
raceId: string;
driverId: string;
}

View File

@@ -1,4 +1,5 @@
export interface GetDriverTeamOutputPort { export interface DriverTeamOutputPort {
driverId: string;
team: { team: {
id: string; id: string;
name: string; name: string;
@@ -9,9 +10,10 @@ export interface GetDriverTeamOutputPort {
createdAt: Date; createdAt: Date;
}; };
membership: { membership: {
driverId: string;
teamId: string; teamId: string;
role: 'member' | 'captain' | 'admin'; driverId: string;
role: 'owner' | 'manager' | 'driver';
status: 'active' | 'pending' | 'none';
joinedAt: Date; joinedAt: Date;
}; };
} }

View File

@@ -0,0 +1,22 @@
import type { SkillLevel } from '../../domain/services/SkillLevelService';
export interface DriverLeaderboardItemOutputPort {
id: string;
name: string;
rating: number;
skillLevel: SkillLevel;
nationality: string;
racesCompleted: number;
wins: number;
podiums: number;
isActive: boolean;
rank: number;
avatarUrl: string;
}
export interface DriversLeaderboardOutputPort {
drivers: DriverLeaderboardItemOutputPort[];
totalRaces: number;
totalWins: number;
activeCount: number;
}

View File

@@ -0,0 +1,13 @@
export interface GetAllRacesOutputPort {
races: {
id: string;
leagueId: string;
track: string;
car: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
scheduledAt: string;
strengthOfField: number | null;
leagueName: string;
}[];
totalCount: number;
}

View File

@@ -7,5 +7,7 @@ export interface GetAllTeamsOutputPort {
ownerId: string; ownerId: string;
leagues: string[]; leagues: string[];
createdAt: Date; createdAt: Date;
memberCount: number;
}>; }>;
totalCount?: number;
} }

View File

@@ -1,7 +1,4 @@
export interface GetLeagueAdminOutputPort { export interface GetLeagueAdminOutputPort {
league: { leagueId: string;
id: string; ownerId: string;
ownerId: string;
};
// Additional data would be populated by combining multiple use cases
} }

View File

@@ -1,13 +1,12 @@
export interface LeagueJoinRequestOutputPort {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message: string;
driver: { id: string; name: string } | null;
}
export interface GetLeagueJoinRequestsOutputPort { export interface GetLeagueJoinRequestsOutputPort {
joinRequests: Array<{ joinRequests: LeagueJoinRequestOutputPort[];
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message?: string;
driver: {
id: string;
name: string;
};
}>;
} }

View File

@@ -1,10 +1,14 @@
export interface LeagueMembershipOutputPort {
driverId: string;
driver: { id: string; name: string };
role: string;
joinedAt: Date;
}
export interface LeagueMembershipsOutputPort {
members: LeagueMembershipOutputPort[];
}
export interface GetLeagueMembershipsOutputPort { export interface GetLeagueMembershipsOutputPort {
memberships: Array<{ memberships: LeagueMembershipsOutputPort;
id: string;
leagueId: string;
driverId: string;
role: 'member' | 'admin' | 'owner';
joinedAt: Date;
}>;
drivers: { id: string; name: string }[];
} }

View File

@@ -1,3 +1,9 @@
export interface LeagueOwnerSummaryOutputPort {
driver: { id: string; iracingId: string; name: string; country: string; bio: string | undefined; joinedAt: string };
rating: number;
rank: number;
}
export interface GetLeagueOwnerSummaryOutputPort { export interface GetLeagueOwnerSummaryOutputPort {
summary: { driver: { id: string; name: string }; rating: number; rank: number } | null; summary: LeagueOwnerSummaryOutputPort | null;
} }

View File

@@ -1,20 +1,47 @@
export interface ProtestOutputPort {
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
incident: { lap: number; description: string; timeInRace: number | undefined };
comment: string | undefined;
proofVideoUrl: string | undefined;
status: string;
reviewedBy: string | undefined;
decisionNotes: string | undefined;
filedAt: string;
reviewedAt: string | undefined;
defense: { statement: string; videoUrl: string | undefined; submittedAt: string } | undefined;
defenseRequestedAt: string | undefined;
defenseRequestedBy: string | undefined;
}
export interface RaceOutputPort {
id: string;
leagueId: string;
scheduledAt: string;
track: string;
trackId: string | undefined;
car: string;
carId: string | undefined;
sessionType: string;
status: string;
strengthOfField: number | undefined;
registeredCount: number | undefined;
maxParticipants: number | undefined;
}
export interface DriverOutputPort {
id: string;
iracingId: string;
name: string;
country: string;
bio: string | undefined;
joinedAt: string;
}
export interface GetLeagueProtestsOutputPort { export interface GetLeagueProtestsOutputPort {
protests: Array<{ protests: ProtestOutputPort[];
id: string; racesById: Record<string, RaceOutputPort>;
raceId: string; driversById: Record<string, DriverOutputPort>;
protestingDriverId: string;
accusedDriverId: string;
submittedAt: Date;
description: string;
status: string;
}>;
races: Array<{
id: string;
name: string;
date: string;
}>;
drivers: Array<{
id: string;
name: string;
}>;
} }

View File

@@ -0,0 +1,13 @@
export interface LeagueSeasonSummaryOutputPort {
seasonId: string;
name: string;
status: string;
startDate: Date;
endDate: Date;
isPrimary: boolean;
isParallelActive: boolean;
}
export interface GetLeagueSeasonsOutputPort {
seasons: LeagueSeasonSummaryOutputPort[];
}

View File

@@ -0,0 +1,10 @@
export interface GetSponsorsOutputPort {
sponsors: {
id: string;
name: string;
contactEmail: string;
websiteUrl: string | undefined;
logoUrl: string | undefined;
createdAt: Date;
}[];
}

View File

@@ -0,0 +1,10 @@
export interface GetSponsorshipPricingOutputPort {
entityType: string;
entityId: string;
pricing: {
id: string;
level: string;
price: number;
currency: string;
}[];
}

View File

@@ -9,9 +9,9 @@ export interface GetTeamDetailsOutputPort {
createdAt: Date; createdAt: Date;
}; };
membership: { membership: {
driverId: string; role: 'owner' | 'manager' | 'member';
teamId: string;
role: 'member' | 'captain' | 'admin';
joinedAt: Date; joinedAt: Date;
isActive: boolean;
} | null; } | null;
canManage: boolean;
} }

View File

@@ -0,0 +1,3 @@
export interface GetTotalLeaguesOutputPort {
totalLeagues: number;
}

View File

@@ -0,0 +1,3 @@
export interface GetTotalRacesOutputPort {
totalRaces: number;
}

View File

@@ -0,0 +1,8 @@
export interface ImportRaceResultsApiOutputPort {
success: boolean;
raceId: string;
leagueId: string;
driversProcessed: number;
resultsRecorded: number;
errors?: string[];
}

View File

@@ -0,0 +1,5 @@
export interface JoinLeagueOutputPort {
membershipId: string;
leagueId: string;
status: string;
}

View File

@@ -1,4 +1,4 @@
export interface LeagueDriverSeasonStatsOutputPort { export interface LeagueDriverSeasonStatsItemOutputPort {
leagueId: string; leagueId: string;
driverId: string; driverId: string;
position: number; position: number;
@@ -17,4 +17,9 @@ export interface LeagueDriverSeasonStatsOutputPort {
avgFinish: number | null; avgFinish: number | null;
rating: number | null; rating: number | null;
ratingChange: number | null; ratingChange: number | null;
}
export interface LeagueDriverSeasonStatsOutputPort {
leagueId: string;
stats: LeagueDriverSeasonStatsItemOutputPort[];
} }

View File

@@ -0,0 +1,11 @@
import type { League } from '../../domain/entities/League';
import type { Season } from '../../domain/entities/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../domain/entities/Game';
export interface LeagueFullConfigOutputPort {
league: League;
activeSeason?: Season;
scoringConfig?: LeagueScoringConfig;
game?: Game;
}

View File

@@ -1,18 +1,12 @@
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort';
export interface LeagueScoringConfigOutputPort { export interface LeagueScoringConfigOutputPort {
leagueId: string; leagueId: string;
seasonId: string; seasonId: string;
gameId: string; gameId: string;
gameName: string; gameName: string;
scoringPresetId?: string; scoringPresetId?: string;
scoringPresetName?: string; preset?: LeagueScoringPresetOutputPort;
dropPolicySummary: string; championships: ChampionshipConfig[];
championships: Array<{
id: string;
name: string;
type: 'driver' | 'team' | 'nations' | 'trophy';
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription: string;
}>;
} }

View File

@@ -0,0 +1,5 @@
import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort';
export interface LeagueScoringPresetsOutputPort {
presets: LeagueScoringPresetOutputPort[];
}

View File

@@ -0,0 +1,10 @@
export interface StandingItemOutputPort {
driverId: string;
driver: { id: string; name: string };
points: number;
rank: number;
}
export interface LeagueStandingsOutputPort {
standings: StandingItemOutputPort[];
}

View File

@@ -0,0 +1,5 @@
export interface LeagueStatsOutputPort {
totalMembers: number;
totalRaces: number;
averageRating: number;
}

View File

@@ -0,0 +1,24 @@
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
export interface PendingSponsorshipRequestOutput {
id: string;
sponsorId: string;
sponsorName: string;
sponsorLogo?: string;
tier: SponsorshipTier;
offeredAmount: number;
currency: string;
formattedAmount: string;
message?: string;
createdAt: Date;
platformFee: number;
netAmount: number;
}
export interface PendingSponsorshipRequestsOutputPort {
entityType: SponsorableEntityType;
entityId: string;
requests: PendingSponsorshipRequestOutput[];
totalCount: number;
}

View File

@@ -0,0 +1,57 @@
export interface ProfileOverviewOutputPort {
driver: {
id: string;
name: string;
country: string;
avatarUrl: string;
iracingId: string | null;
joinedAt: Date;
rating: number | null;
globalRank: number | null;
consistency: number | null;
bio: string | null;
totalDrivers: number | null;
};
stats: {
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number | null;
bestFinish: number | null;
worstFinish: number | null;
finishRate: number | null;
winRate: number | null;
podiumRate: number | null;
percentile: number | null;
rating: number | null;
consistency: number | null;
overallRank: number | null;
} | null;
finishDistribution: {
totalRaces: number;
wins: number;
podiums: number;
topTen: number;
dnfs: number;
other: number;
} | null;
teamMemberships: {
teamId: string;
teamName: string;
teamTag: string | null;
role: string;
joinedAt: Date;
isCurrent: boolean;
}[];
socialSummary: {
friendsCount: number;
friends: {
id: string;
name: string;
country: string;
avatarUrl: string;
}[];
};
extendedProfile: null;
}

View File

@@ -0,0 +1,15 @@
import type { Race } from '../../../domain/entities/Race';
import type { League } from '../../../domain/entities/League';
import type { RaceRegistration } from '../../../domain/entities/RaceRegistration';
import type { Driver } from '../../../domain/entities/Driver';
import type { Result } from '../../../domain/entities/result/Result';
export interface RaceDetailOutputPort {
race: Race;
league: League | null;
registrations: RaceRegistration[];
drivers: Driver[];
userResult: Result | null;
isUserRegistered: boolean;
canRegister: boolean;
}

View File

@@ -0,0 +1,7 @@
import type { Penalty } from '../../../domain/entities/Penalty';
import type { Driver } from '../../../domain/entities/Driver';
export interface RacePenaltiesOutputPort {
penalties: Penalty[];
drivers: Driver[];
}

Some files were not shown because too many files have changed in this diff Show More