refactor core presenters
This commit is contained in:
112
.eslintrc.json
112
.eslintrc.json
@@ -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"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
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
|
||||
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 });
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
return result.value!;
|
||||
return plainToClass(DashboardOverviewDTO, result.value);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,224 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO';
|
||||
import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
|
||||
import { DashboardRecentResultDTO } from './DashboardRecentResultDTO';
|
||||
import { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO';
|
||||
import { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO';
|
||||
import { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO';
|
||||
import { IsString, IsNumber, IsOptional, IsBoolean, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class DashboardDriverSummaryDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
country!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
avatarUrl!: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rating?: number | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
globalRank?: number | null;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalRaces!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
wins!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
podiums!: number;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
consistency?: number | null;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ApiProperty({ nullable: true })
|
||||
currentDriver!: DashboardDriverSummaryDTO | null;
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DashboardDriverSummaryDTO)
|
||||
currentDriver?: DashboardDriverSummaryDTO | null;
|
||||
|
||||
@ApiProperty({ type: [DashboardRaceSummaryDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DashboardRaceSummaryDTO)
|
||||
myUpcomingRaces!: DashboardRaceSummaryDTO[];
|
||||
|
||||
@ApiProperty({ type: [DashboardRaceSummaryDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DashboardRaceSummaryDTO)
|
||||
otherUpcomingRaces!: DashboardRaceSummaryDTO[];
|
||||
|
||||
@ApiProperty({ type: [DashboardRaceSummaryDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DashboardRaceSummaryDTO)
|
||||
upcomingRaces!: DashboardRaceSummaryDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
@@ -25,17 +226,31 @@ export class DashboardOverviewDTO {
|
||||
activeLeaguesCount!: number;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
nextRace!: DashboardRaceSummaryDTO | null;
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DashboardRaceSummaryDTO)
|
||||
nextRace?: DashboardRaceSummaryDTO | null;
|
||||
|
||||
@ApiProperty({ type: [DashboardRecentResultDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DashboardRecentResultDTO)
|
||||
recentResults!: DashboardRecentResultDTO[];
|
||||
|
||||
@ApiProperty({ type: [DashboardLeagueStandingSummaryDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DashboardLeagueStandingSummaryDTO)
|
||||
leagueStandingsSummaries!: DashboardLeagueStandingSummaryDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
@ValidateNested()
|
||||
@Type(() => DashboardFeedSummaryDTO)
|
||||
feedSummary!: DashboardFeedSummaryDTO;
|
||||
|
||||
@ApiProperty({ type: [DashboardFriendSummaryDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DashboardFriendSummaryDTO)
|
||||
friends!: DashboardFriendSummaryDTO[];
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTo
|
||||
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
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 IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase';
|
||||
export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
|
||||
export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
DriverService, // Provide the service itself
|
||||
@@ -113,4 +115,19 @@ export const DriverProviders: Provider[] = [
|
||||
useFactory: (driverRepo: IDriverRepository) => new UpdateDriverProfileUseCase(driverRepo),
|
||||
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],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,6 +8,9 @@ import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-c
|
||||
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { CompleteDriverOnboardingOutputPort } from '@core/racing/application/ports/output/CompleteDriverOnboardingOutputPort';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
describe('DriverService', () => {
|
||||
let service: DriverService;
|
||||
@@ -15,7 +18,9 @@ describe('DriverService', () => {
|
||||
let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>;
|
||||
let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>;
|
||||
let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let updateDriverProfileUseCase: ReturnType<typeof vi.mocked<UpdateDriverProfileUseCase>>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let driverRepository: ReturnType<typeof vi.mocked<IDriverRepository>>;
|
||||
let logger: ReturnType<typeof vi.mocked<Logger>>;
|
||||
|
||||
@@ -102,17 +107,11 @@ describe('DriverService', () => {
|
||||
activeCount: 1,
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
getDriversLeaderboardUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(mockViewModel));
|
||||
|
||||
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(result).toEqual(mockViewModel);
|
||||
});
|
||||
@@ -120,26 +119,20 @@ describe('DriverService', () => {
|
||||
|
||||
describe('getTotalDrivers', () => {
|
||||
it('should call GetTotalDriversUseCase and return the view model', async () => {
|
||||
const mockViewModel = { totalDrivers: 5 };
|
||||
const mockOutput = { totalDrivers: 5 };
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
getTotalDriversUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
getTotalDriversUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
|
||||
|
||||
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(result).toEqual(mockViewModel);
|
||||
expect(result).toEqual(mockOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeOnboarding', () => {
|
||||
it('should call CompleteDriverOnboardingUseCase and return the view model', async () => {
|
||||
it('should call CompleteDriverOnboardingUseCase and return success', async () => {
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
@@ -149,30 +142,43 @@ describe('DriverService', () => {
|
||||
bio: 'Racing enthusiast',
|
||||
};
|
||||
|
||||
const mockViewModel = {
|
||||
success: true,
|
||||
driverId: 'user-123',
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
completeDriverOnboardingUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
completeDriverOnboardingUseCase.execute.mockResolvedValue(
|
||||
Result.ok<CompleteDriverOnboardingOutputPort, ApplicationErrorCode<string>>({ driverId: 'user-123' })
|
||||
);
|
||||
|
||||
const result = await service.completeOnboarding('user-123', input);
|
||||
|
||||
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: 'user-123',
|
||||
...input,
|
||||
},
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith({
|
||||
userId: 'user-123',
|
||||
...input,
|
||||
});
|
||||
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',
|
||||
};
|
||||
|
||||
const mockViewModel = {
|
||||
const mockOutput = {
|
||||
isRegistered: true,
|
||||
raceId: 'race-1',
|
||||
driverId: 'driver-1',
|
||||
};
|
||||
|
||||
const mockPresenter = {
|
||||
viewModel: mockViewModel,
|
||||
};
|
||||
|
||||
isDriverRegisteredForRaceUseCase.execute.mockImplementation(async (params, presenter) => {
|
||||
Object.assign(presenter, mockPresenter);
|
||||
});
|
||||
isDriverRegisteredForRaceUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
|
||||
|
||||
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(result).toEqual(mockViewModel);
|
||||
expect(result).toEqual(mockOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,15 +13,14 @@ import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases
|
||||
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
|
||||
// Presenters
|
||||
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
|
||||
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
|
||||
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
|
||||
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
|
||||
|
||||
// Tokens
|
||||
import { GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN, GET_TOTAL_DRIVERS_USE_CASE_TOKEN, COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN, IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN, 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 { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
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(IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN) private readonly isDriverRegisteredForRaceUseCase: IsDriverRegisteredForRaceUseCase,
|
||||
@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(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
@@ -41,24 +41,31 @@ export class DriverService {
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
|
||||
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
|
||||
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
await this.getDriversLeaderboardUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
const result = await this.getDriversLeaderboardUseCase.execute();
|
||||
if (result.isOk()) {
|
||||
return result.value as DriversLeaderboardDTO;
|
||||
} else {
|
||||
throw new Error(`Failed to fetch drivers leaderboard: ${result.error.details.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsDTO> {
|
||||
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();
|
||||
await this.getTotalDriversUseCase.execute(undefined, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
|
||||
this.logger.debug('Completing onboarding for user:', userId);
|
||||
|
||||
const presenter = new CompleteOnboardingPresenter();
|
||||
await this.completeDriverOnboardingUseCase.execute({
|
||||
const result = await this.completeDriverOnboardingUseCase.execute({
|
||||
userId,
|
||||
firstName: input.firstName,
|
||||
lastName: input.lastName,
|
||||
@@ -66,16 +73,31 @@ export class DriverService {
|
||||
country: input.country,
|
||||
timezone: input.timezone,
|
||||
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> {
|
||||
this.logger.debug('Checking driver registration status:', query);
|
||||
|
||||
const presenter = new DriverRegistrationStatusPresenter();
|
||||
await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId }, presenter);
|
||||
return presenter.viewModel;
|
||||
const result = await this.isDriverRegisteredForRaceUseCase.execute({ raceId: query.raceId, driverId: query.driverId });
|
||||
if (result.isOk()) {
|
||||
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> {
|
||||
@@ -129,18 +151,42 @@ export class DriverService {
|
||||
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
|
||||
this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`);
|
||||
|
||||
// TODO: Implement proper driver profile fetching with all the detailed data
|
||||
// For now, return a placeholder structure
|
||||
const result = await this.getProfileOverviewUseCase.execute({ driverId });
|
||||
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 {
|
||||
currentDriver: null,
|
||||
stats: null,
|
||||
finishDistribution: null,
|
||||
teamMemberships: [],
|
||||
socialSummary: {
|
||||
friendsCount: 0,
|
||||
friends: [],
|
||||
},
|
||||
extendedProfile: null,
|
||||
currentDriver: outputPort.driver ? {
|
||||
id: outputPort.driver.id,
|
||||
name: outputPort.driver.name,
|
||||
country: outputPort.driver.country,
|
||||
avatarUrl: outputPort.driver.avatarUrl,
|
||||
iracingId: outputPort.driver.iracingId,
|
||||
joinedAt: outputPort.driver.joinedAt.toISOString(),
|
||||
rating: outputPort.driver.rating,
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
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;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TotalDriversResultDTO) {
|
||||
present(output: TotalDriversOutputPort) {
|
||||
this.result = {
|
||||
totalDrivers: dto.totalDrivers,
|
||||
totalDrivers: output.totalDrivers,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/In
|
||||
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
|
||||
|
||||
// Import use cases
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
@@ -131,4 +132,8 @@ export const LeagueProviders: Provider[] = [
|
||||
GetLeagueScheduleUseCase,
|
||||
GetLeagueStatsUseCase,
|
||||
GetLeagueAdminPermissionsUseCase,
|
||||
{
|
||||
provide: ListLeagueScoringPresetsUseCase,
|
||||
useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,6 +14,7 @@ import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-case
|
||||
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
|
||||
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
describe('LeagueService', () => {
|
||||
let service: LeagueService;
|
||||
@@ -60,9 +61,7 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
it('should get total leagues', async () => {
|
||||
mockGetTotalLeaguesUseCase.execute.mockImplementation(async (params, presenter) => {
|
||||
presenter.present({ totalLeagues: 5 });
|
||||
});
|
||||
mockGetTotalLeaguesUseCase.execute.mockResolvedValue(Result.ok({ totalLeagues: 5 }));
|
||||
|
||||
const result = await service.getTotalLeagues();
|
||||
|
||||
|
||||
@@ -10,30 +10,27 @@ import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
|
||||
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
|
||||
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
|
||||
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
|
||||
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
|
||||
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
|
||||
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
|
||||
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
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
|
||||
import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
|
||||
import type { LeagueScoringPresetsViewModel } from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
|
||||
import type { AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
|
||||
import type { GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter';
|
||||
import type { LeagueScoringConfigViewModel } from './presenters/LeagueScoringConfigPresenter';
|
||||
import type { LeagueScoringPresetsViewModel } from './presenters/LeagueScoringPresetsPresenter';
|
||||
import type { AllLeaguesWithCapacityDTO as AllLeaguesWithCapacityViewModel } from '../dtos/AllLeaguesWithCapacityDTO';
|
||||
import type { GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter';
|
||||
import type { ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter';
|
||||
import type { RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter';
|
||||
import { TotalLeaguesDTO } from './dtos/TotalLeaguesDTO';
|
||||
import type { ApproveLeagueJoinRequestDTO } from './dtos/ApproveLeagueJoinRequestDTO';
|
||||
import type { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
|
||||
import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter';
|
||||
import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter';
|
||||
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';
|
||||
import type { CreateLeagueViewModel } from './dtos/CreateLeagueDTO';
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
@@ -67,22 +64,22 @@ import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapa
|
||||
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
|
||||
import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter';
|
||||
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
|
||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||
import { mapApproveLeagueJoinRequestPortToDTO } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter';
|
||||
import { mapGetLeagueOwnerSummaryOutputPortToDTO } from './presenters/GetLeagueOwnerSummaryPresenter';
|
||||
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
||||
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter';
|
||||
import { mapGetLeagueScheduleOutputPortToDTO } from './presenters/LeagueSchedulePresenter';
|
||||
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
|
||||
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter';
|
||||
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
|
||||
import { mapRejectLeagueJoinRequestOutputPortToDTO } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { mapRemoveLeagueMemberOutputPortToDTO } from './presenters/RemoveLeagueMemberPresenter';
|
||||
import { mapUpdateLeagueMemberRoleOutputPortToDTO } from './presenters/UpdateLeagueMemberRolePresenter';
|
||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||
import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter';
|
||||
import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter';
|
||||
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
|
||||
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
|
||||
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
|
||||
|
||||
import { mapJoinLeagueOutputPortToDTO } from './presenters/JoinLeaguePresenter';
|
||||
import { mapTransferLeagueOwnershipOutputPortToDTO } from './presenters/TransferLeagueOwnershipPresenter';
|
||||
import { mapGetLeagueProtestsOutputPortToDTO } from './presenters/GetLeagueProtestsPresenter';
|
||||
import { mapGetLeagueSeasonsOutputPortToDTO } from './presenters/GetLeagueSeasonsPresenter';
|
||||
import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter';
|
||||
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './LeagueProviders';
|
||||
|
||||
@@ -126,7 +123,7 @@ export class LeagueService {
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<GetTotalLeaguesViewModel> {
|
||||
async getTotalLeagues(): Promise<TotalLeaguesDTO> {
|
||||
this.logger.debug('[LeagueService] Fetching total leagues count.');
|
||||
const result = await this.getTotalLeaguesUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
@@ -148,26 +145,22 @@ export class LeagueService {
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestViewModel> {
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestDTO> {
|
||||
this.logger.debug('Approving join request:', input);
|
||||
const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new ApproveLeagueJoinRequestPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
return mapApproveLeagueJoinRequestPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectLeagueJoinRequestViewModel> {
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectJoinRequestOutputDTO> {
|
||||
this.logger.debug('Rejecting join request:', input);
|
||||
const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new RejectLeagueJoinRequestPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
return mapRejectLeagueJoinRequestOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise<GetLeagueAdminPermissionsViewModel> {
|
||||
@@ -179,40 +172,34 @@ export class LeagueService {
|
||||
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 });
|
||||
const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new RemoveLeagueMemberPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
return mapRemoveLeagueMemberOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
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 });
|
||||
const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new UpdateLeagueMemberRolePresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
return mapUpdateLeagueMemberRoleOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<GetLeagueOwnerSummaryViewModel> {
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<LeagueOwnerSummaryDTO | null> {
|
||||
this.logger.debug('Getting league owner summary:', query);
|
||||
const result = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new GetLeagueOwnerSummaryPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
return mapGetLeagueOwnerSummaryOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormViewModel | null> {
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormModelDTO | null> {
|
||||
this.logger.debug('Getting league full config', { query });
|
||||
|
||||
try {
|
||||
@@ -221,7 +208,9 @@ export class LeagueService {
|
||||
this.logger.error('Error getting league full config', new Error(result.unwrapErr().code));
|
||||
return null;
|
||||
}
|
||||
return result.unwrap();
|
||||
const presenter = new LeagueConfigPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error)));
|
||||
return null;
|
||||
@@ -234,9 +223,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new GetLeagueProtestsPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel() as LeagueAdminProtestsDTO;
|
||||
return mapGetLeagueProtestsOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
@@ -245,9 +232,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new GetLeagueSeasonsPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel().seasons;
|
||||
return mapGetLeagueSeasonsOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
|
||||
@@ -261,25 +246,27 @@ export class LeagueService {
|
||||
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 });
|
||||
const result = await this.getLeagueStandingsUseCase.execute(leagueId);
|
||||
// The use case returns a view model directly, so we return it as-is
|
||||
return result as unknown as LeagueStandingsViewModel;
|
||||
const result = await this.getLeagueStandingsUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
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 });
|
||||
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new LeagueSchedulePresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
return mapGetLeagueScheduleOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getLeagueStats(leagueId: string): Promise<LeagueStatsViewModel> {
|
||||
async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> {
|
||||
this.logger.debug('Getting league stats', { leagueId });
|
||||
const result = await this.getLeagueStatsUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
@@ -404,7 +391,7 @@ export class LeagueService {
|
||||
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 });
|
||||
|
||||
const result = await this.joinLeagueUseCase.execute({ leagueId, driverId });
|
||||
@@ -415,12 +402,10 @@ export class LeagueService {
|
||||
error: error.code,
|
||||
};
|
||||
}
|
||||
const presenter = new JoinLeaguePresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
return mapJoinLeagueOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
|
||||
@@ -431,9 +416,7 @@ export class LeagueService {
|
||||
error: error.code,
|
||||
};
|
||||
}
|
||||
const presenter = new TransferLeagueOwnershipPresenter();
|
||||
presenter.present({ success: true });
|
||||
return presenter.getViewModel();
|
||||
return mapTransferLeagueOwnershipOutputPortToDTO(result.unwrap());
|
||||
}
|
||||
|
||||
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { LeagueWithCapacityDTO } from './LeagueWithCapacityDTO';
|
||||
|
||||
export class 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 {
|
||||
@ApiProperty({ type: [LeagueWithCapacityDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueWithCapacityDTO)
|
||||
leagues: LeagueWithCapacityDTO[];
|
||||
leagues!: LeagueWithCapacityDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalCount: number;
|
||||
totalCount!: number;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ApproveLeagueJoinRequestDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
4
apps/api/src/domain/league/dtos/CreateLeagueDTO.ts
Normal file
4
apps/api/src/domain/league/dtos/CreateLeagueDTO.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface CreateLeagueViewModel {
|
||||
leagueId: string;
|
||||
success: boolean;
|
||||
}
|
||||
8
apps/api/src/domain/league/dtos/TotalLeaguesDTO.ts
Normal file
8
apps/api/src/domain/league/dtos/TotalLeaguesDTO.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class TotalLeaguesDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalLeagues: number;
|
||||
}
|
||||
@@ -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 {
|
||||
private result: AllLeaguesWithCapacityViewModel | null = null;
|
||||
export class AllLeaguesWithCapacityPresenter {
|
||||
private result: AllLeaguesWithCapacityDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: AllLeaguesWithCapacityResultDTO) {
|
||||
const leagues = dto.leagues.map(league => ({
|
||||
present(output: AllLeaguesWithCapacityOutputPort) {
|
||||
const leagues: LeagueWithCapacityDTO[] = output.leagues.map(league => ({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: { maxDrivers: league.settings.maxDrivers || 0 },
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
usedSlots: dto.memberCounts.get(league.id) || 0,
|
||||
usedSlots: output.memberCounts[league.id] || 0,
|
||||
socialLinks: league.socialLinks,
|
||||
}));
|
||||
this.result = {
|
||||
@@ -24,7 +25,7 @@ export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityP
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityViewModel | null {
|
||||
getViewModel(): AllLeaguesWithCapacityDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: ApproveLeagueJoinRequestViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: ApproveLeagueJoinRequestResultPort) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): ApproveLeagueJoinRequestViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
export function mapApproveLeagueJoinRequestPortToDTO(port: ApproveLeagueJoinRequestResultPort): ApproveLeagueJoinRequestDTO {
|
||||
return port;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CreateLeagueResultDTO): void {
|
||||
present(dto: CreateLeagueWithSeasonAndScoringOutputPort): void {
|
||||
this.result = {
|
||||
leagueId: dto.leagueId,
|
||||
success: true,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: GetLeagueOwnerSummaryViewModel | null = null;
|
||||
export function mapGetLeagueOwnerSummaryOutputPortToDTO(output: GetLeagueOwnerSummaryOutputPort): LeagueOwnerSummaryDTO | null {
|
||||
if (!output.summary) return null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetLeagueOwnerSummaryResultDTO) {
|
||||
this.result = { summary: dto.summary };
|
||||
}
|
||||
|
||||
getViewModel(): GetLeagueOwnerSummaryViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
return {
|
||||
driver: {
|
||||
id: output.summary.driver.id,
|
||||
iracingId: output.summary.driver.iracingId,
|
||||
name: output.summary.driver.name,
|
||||
country: output.summary.driver.country,
|
||||
bio: output.summary.driver.bio,
|
||||
joinedAt: output.summary.driver.joinedAt,
|
||||
},
|
||||
rating: output.summary.rating,
|
||||
rank: output.summary.rank,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
private result: GetLeagueProtestsViewModel | null = null;
|
||||
export function mapGetLeagueProtestsOutputPortToDTO(output: GetLeagueProtestsOutputPort): LeagueAdminProtestsDTO {
|
||||
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() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetLeagueProtestsResultDTO) {
|
||||
const racesById = {};
|
||||
dto.races.forEach(race => {
|
||||
racesById[race.id] = race;
|
||||
});
|
||||
const driversById = {};
|
||||
dto.drivers.forEach(driver => {
|
||||
driversById[driver.id] = driver;
|
||||
});
|
||||
this.result = {
|
||||
protests: dto.protests,
|
||||
racesById,
|
||||
driversById,
|
||||
const racesById: { [raceId: string]: RaceDTO } = {};
|
||||
for (const raceId in output.racesById) {
|
||||
const race = output.racesById[raceId];
|
||||
racesById[raceId] = {
|
||||
id: race.id,
|
||||
name: race.track, // assuming name is track
|
||||
date: race.scheduledAt,
|
||||
leagueName: undefined, // TODO: get league name if needed
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): GetLeagueProtestsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
const driversById: { [driverId: string]: DriverDTO } = {};
|
||||
for (const driverId in output.driversById) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
private result: GetLeagueSeasonsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetLeagueSeasonsResultDTO) {
|
||||
const seasons = dto.seasons.map(season => ({
|
||||
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;
|
||||
}
|
||||
export function mapGetLeagueSeasonsOutputPortToDTO(output: GetLeagueSeasonsOutputPort): LeagueSeasonSummaryDTO[] {
|
||||
return output.seasons.map(season => ({
|
||||
seasonId: season.seasonId,
|
||||
name: season.name,
|
||||
status: season.status,
|
||||
startDate: season.startDate,
|
||||
endDate: season.endDate,
|
||||
isPrimary: season.isPrimary,
|
||||
isParallelActive: season.isParallelActive,
|
||||
}));
|
||||
}
|
||||
@@ -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 {
|
||||
private result: JoinLeagueViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
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;
|
||||
}
|
||||
export function mapJoinLeagueOutputPortToDTO(port: JoinLeagueOutputPort): JoinLeagueOutputDTO {
|
||||
return {
|
||||
success: true,
|
||||
membershipId: port.membershipId,
|
||||
};
|
||||
}
|
||||
@@ -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 type { Presenter } from '@core/shared/presentation';
|
||||
|
||||
export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
|
||||
private result: LeagueConfigFormViewModel | null = null;
|
||||
export class LeagueConfigPresenter implements Presenter<LeagueFullConfigOutputPort, LeagueConfigFormModelDTO> {
|
||||
private result: LeagueConfigFormModelDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueFullConfigData) {
|
||||
// Map from LeagueFullConfigData to LeagueConfigFormViewModel
|
||||
present(dto: LeagueFullConfigOutputPort) {
|
||||
// Map from LeagueFullConfigOutputPort to LeagueConfigFormModelDTO
|
||||
const league = dto.league;
|
||||
const settings = league.settings;
|
||||
const stewarding = settings.stewarding;
|
||||
@@ -20,64 +21,9 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
visibility: 'public', // TODO: Map visibility from league
|
||||
gameId: 'iracing', // TODO: Map from game
|
||||
},
|
||||
structure: {
|
||||
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
|
||||
scoring: {
|
||||
@@ -85,8 +31,8 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
|
||||
points: 25, // TODO: Map points
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: this.result.dropPolicy.strategy as 'none' | 'worst_n',
|
||||
n: this.result.dropPolicy.n,
|
||||
strategy: 'none', // TODO: Map
|
||||
n: 0,
|
||||
},
|
||||
timings: {
|
||||
raceDayOfWeek: 'sunday', // TODO: Map from timings
|
||||
@@ -94,16 +40,20 @@ export class LeagueConfigPresenter implements ILeagueFullConfigPresenter {
|
||||
raceTimeMinute: 0,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: this.result.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
|
||||
requireDefense: this.result.stewarding.requireDefense,
|
||||
defenseTimeLimit: this.result.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: this.result.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: this.result.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: this.result.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: this.result.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: this.result.stewarding.notifyOnVoteRequired,
|
||||
requiredVotes: this.result.stewarding.requiredVotes,
|
||||
decisionMode: stewarding?.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
|
||||
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 || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): LeagueConfigFormModelDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: LeagueScheduleViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort): LeagueScheduleDTO {
|
||||
return {
|
||||
races: output.races.map(race => ({
|
||||
id: race.id,
|
||||
name: race.name,
|
||||
date: race.scheduledAt.toISOString(),
|
||||
leagueName: undefined, // TODO: get league name if needed
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,37 @@
|
||||
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
|
||||
import type { BonusRule } from '@core/racing/domain/types/BonusRule';
|
||||
import type {
|
||||
ILeagueScoringConfigPresenter,
|
||||
LeagueScoringConfigData,
|
||||
LeagueScoringConfigViewModel,
|
||||
LeagueScoringChampionshipViewModel,
|
||||
} from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
|
||||
import type { LeagueScoringConfigOutputPort } from '@core/racing/application/ports/output/LeagueScoringConfigOutputPort';
|
||||
import type { LeagueScoringPresetOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetOutputPort';
|
||||
|
||||
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;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel {
|
||||
present(data: LeagueScoringConfigOutputPort): LeagueScoringConfigViewModel {
|
||||
const championships: LeagueScoringChampionshipViewModel[] =
|
||||
data.championships.map((champ) => this.mapChampionship(champ));
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import type {
|
||||
ILeagueScoringPresetsPresenter,
|
||||
LeagueScoringPresetsResultDTO,
|
||||
LeagueScoringPresetsViewModel,
|
||||
} from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
|
||||
import type { LeagueScoringPresetsOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetsOutputPort';
|
||||
import type { LeagueScoringPresetOutputPort } from '@core/racing/application/ports/output/LeagueScoringPresetOutputPort';
|
||||
|
||||
export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter {
|
||||
export interface LeagueScoringPresetsViewModel {
|
||||
presets: LeagueScoringPresetOutputPort[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class LeagueScoringPresetsPresenter {
|
||||
private viewModel: LeagueScoringPresetsViewModel | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueScoringPresetsResultDTO): void {
|
||||
present(output: LeagueScoringPresetsOutputPort): void {
|
||||
this.viewModel = {
|
||||
presets: dto.presets,
|
||||
presets: output.presets,
|
||||
totalCount: output.presets.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
private result: LeagueStandingsViewModel | null = null;
|
||||
export class LeagueStandingsPresenter implements Presenter<LeagueStandingsOutputPort, LeagueStandingsDTO> {
|
||||
private result: LeagueStandingsDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueStandingsResultDTO) {
|
||||
const driverMap = new Map(dto.drivers.map(d => [d.id, { id: d.id, name: d.name }]));
|
||||
const standings = dto.standings
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map(standing => ({
|
||||
driverId: standing.driverId,
|
||||
driver: driverMap.get(standing.driverId)!,
|
||||
points: standing.points,
|
||||
rank: standing.position,
|
||||
}));
|
||||
present(dto: LeagueStandingsOutputPort) {
|
||||
const standings = dto.standings.map(standing => ({
|
||||
driverId: standing.driverId,
|
||||
driver: {
|
||||
id: standing.driver.id,
|
||||
name: standing.driver.name,
|
||||
// Add other DriverDto fields if needed, but for now just id and name
|
||||
},
|
||||
points: standing.points,
|
||||
rank: standing.rank,
|
||||
}));
|
||||
this.result = { standings };
|
||||
}
|
||||
|
||||
getViewModel(): LeagueStandingsViewModel {
|
||||
getViewModel(): LeagueStandingsDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
private result: LeagueStatsViewModel | null = null;
|
||||
export class LeagueStatsPresenter implements Presenter<LeagueStatsOutputPort, LeagueStatsDTO> {
|
||||
private result: LeagueStatsDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: LeagueStatsResultDTO) {
|
||||
present(dto: LeagueStatsOutputPort) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): LeagueStatsViewModel | null {
|
||||
getViewModel(): LeagueStatsDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: RejectLeagueJoinRequestViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: RejectLeagueJoinRequestResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): RejectLeagueJoinRequestViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
export function mapRejectLeagueJoinRequestOutputPortToDTO(port: RejectLeagueJoinRequestOutputPort): RejectJoinRequestOutputDTO {
|
||||
return {
|
||||
success: port.success,
|
||||
message: port.message,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
private result: RemoveLeagueMemberViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: RemoveLeagueMemberResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): RemoveLeagueMemberViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
export function mapRemoveLeagueMemberOutputPortToDTO(port: RemoveLeagueMemberOutputPort): RemoveLeagueMemberOutputDTO {
|
||||
return {
|
||||
success: port.success,
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import { IGetTotalLeaguesPresenter, GetTotalLeaguesResultDTO, GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter';
|
||||
import { LeagueStatsDTO } from '../dtos/LeagueStatsDTO';
|
||||
import { GetTotalLeaguesOutputPort } from '@core/racing/application/ports/output/GetTotalLeaguesOutputPort';
|
||||
import { TotalLeaguesDTO } from '../dtos/TotalLeaguesDTO';
|
||||
|
||||
export class TotalLeaguesPresenter implements IGetTotalLeaguesPresenter {
|
||||
private result: LeagueStatsDto | null = null;
|
||||
export class TotalLeaguesPresenter {
|
||||
private result: TotalLeaguesDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetTotalLeaguesResultDTO) {
|
||||
present(output: GetTotalLeaguesOutputPort) {
|
||||
this.result = {
|
||||
totalLeagues: dto.totalLeagues,
|
||||
totalLeagues: output.totalLeagues,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): LeagueStatsDto | null {
|
||||
getViewModel(): TotalLeaguesDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: TransferLeagueOwnershipViewModel | null = null;
|
||||
|
||||
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;
|
||||
}
|
||||
export function mapTransferLeagueOwnershipOutputPortToDTO(port: TransferLeagueOwnershipOutputPort): TransferLeagueOwnershipOutputDTO {
|
||||
return {
|
||||
success: port.success,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
private result: UpdateLeagueMemberRoleViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: UpdateLeagueMemberRoleResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): UpdateLeagueMemberRoleViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
export function mapUpdateLeagueMemberRoleOutputPortToDTO(port: UpdateLeagueMemberRoleOutputPort): UpdateLeagueMemberRoleOutputDTO {
|
||||
return {
|
||||
success: port.success,
|
||||
};
|
||||
}
|
||||
@@ -46,8 +46,8 @@ export class RaceController {
|
||||
|
||||
@Get('all/page-data')
|
||||
@ApiOperation({ summary: 'Get all races page data' })
|
||||
@ApiResponse({ status: 200, description: 'All races page data', type: RacesPageDataDTO })
|
||||
async getAllRacesPageData(): Promise<RacesPageDataDTO> {
|
||||
@ApiResponse({ status: 200, description: 'All races page data', type: AllRacesPageDTO })
|
||||
async getAllRacesPageData(): Promise<AllRacesPageDTO> {
|
||||
return this.raceService.getAllRacesPageData();
|
||||
}
|
||||
|
||||
|
||||
@@ -146,8 +146,6 @@ export const RaceProviders: Provider[] = [
|
||||
raceRegRepo: IRaceRegistrationRepository,
|
||||
resultRepo: IResultRepository,
|
||||
leagueMembershipRepo: ILeagueMembershipRepository,
|
||||
driverRatingProvider: DriverRatingProvider,
|
||||
imageService: IImageServicePort,
|
||||
) => new GetRaceDetailUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
@@ -155,8 +153,6 @@ export const RaceProviders: Provider[] = [
|
||||
raceRegRepo,
|
||||
resultRepo,
|
||||
leagueMembershipRepo,
|
||||
driverRatingProvider,
|
||||
imageService,
|
||||
),
|
||||
inject: [
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
@@ -165,8 +161,6 @@ export const RaceProviders: Provider[] = [
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
DRIVER_RATING_PROVIDER_TOKEN,
|
||||
IMAGE_SERVICE_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
|
||||
import type { GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter';
|
||||
import type { RaceDetailViewModel } from '@core/racing/application/presenters/IRaceDetailPresenter';
|
||||
import type { RacesPageViewModel } from '@core/racing/application/presenters/IRacesPagePresenter';
|
||||
import type { RaceResultsDetailViewModel } from '@core/racing/application/presenters/IRaceResultsDetailPresenter';
|
||||
import type { RaceWithSOFViewModel } from '@core/racing/application/presenters/IRaceWithSOFPresenter';
|
||||
import type { RaceProtestsViewModel } from '@core/racing/application/presenters/IRaceProtestsPresenter';
|
||||
import type { RacePenaltiesViewModel } from '@core/racing/application/presenters/IRacePenaltiesPresenter';
|
||||
import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
|
||||
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
|
||||
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
|
||||
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
|
||||
import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort';
|
||||
import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort';
|
||||
|
||||
// DTOs
|
||||
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
|
||||
@@ -14,9 +13,18 @@ import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
|
||||
import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
|
||||
import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
|
||||
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
|
||||
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
|
||||
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
|
||||
@@ -53,7 +61,7 @@ import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCom
|
||||
import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './RaceProviders';
|
||||
import { LOGGER_TOKEN, DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN, LEAGUE_REPOSITORY_TOKEN } from './RaceProviders';
|
||||
|
||||
@Injectable()
|
||||
export class RaceService {
|
||||
@@ -77,7 +85,10 @@ export class RaceService {
|
||||
private readonly applyPenaltyUseCase: ApplyPenaltyUseCase,
|
||||
private readonly requestProtestDefenseUseCase: RequestProtestDefenseUseCase,
|
||||
private readonly reviewProtestUseCase: ReviewProtestUseCase,
|
||||
@Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository: ILeagueRepository,
|
||||
@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> {
|
||||
@@ -88,21 +99,29 @@ export class RaceService {
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getTotalRaces(): Promise<GetTotalRacesViewModel> {
|
||||
async getTotalRaces(): Promise<RaceStatsDTO> {
|
||||
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();
|
||||
await this.getTotalRacesUseCase.execute({}, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsSummaryDTO> {
|
||||
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();
|
||||
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailViewModel> {
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
|
||||
this.logger.debug('[RaceService] Fetching race detail:', params);
|
||||
|
||||
const result = await this.getRaceDetailUseCase.execute(params);
|
||||
@@ -111,10 +130,71 @@ export class RaceService {
|
||||
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.');
|
||||
|
||||
const result = await this.getRacesPageDataUseCase.execute();
|
||||
@@ -123,10 +203,33 @@ export class RaceService {
|
||||
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.');
|
||||
|
||||
const result = await this.getAllRacesPageDataUseCase.execute();
|
||||
@@ -135,10 +238,10 @@ export class RaceService {
|
||||
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 });
|
||||
|
||||
const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
|
||||
@@ -147,10 +250,41 @@ export class RaceService {
|
||||
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 });
|
||||
|
||||
const result = await this.getRaceWithSOFUseCase.execute({ raceId });
|
||||
@@ -159,10 +293,17 @@ export class RaceService {
|
||||
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 });
|
||||
|
||||
const result = await this.getRaceProtestsUseCase.execute({ raceId });
|
||||
@@ -171,10 +312,32 @@ export class RaceService {
|
||||
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 });
|
||||
|
||||
const result = await this.getRacePenaltiesUseCase.execute({ raceId });
|
||||
@@ -183,7 +346,28 @@ export class RaceService {
|
||||
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> {
|
||||
@@ -277,4 +461,10 @@ export class RaceService {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { RaceViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
|
||||
|
||||
export class AllRacesPageDTO {
|
||||
@ApiProperty({ type: [RaceViewModel] })
|
||||
races!: RaceViewModel[];
|
||||
export type AllRacesStatus = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
|
||||
|
||||
export class AllRacesListItemDTO {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -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 {
|
||||
private result: AllRacesPageViewModel | null = null;
|
||||
export class GetAllRacesPresenter {
|
||||
private result: AllRacesPageDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetAllRacesResultDTO) {
|
||||
this.result = dto;
|
||||
async present(output: GetAllRacesOutputPort) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: GetTotalRacesViewModel | null = null;
|
||||
export class GetTotalRacesPresenter {
|
||||
private result: RaceStatsDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetTotalRacesResultDTO) {
|
||||
present(output: GetTotalRacesOutputPort) {
|
||||
this.result = {
|
||||
totalRaces: dto.totalRaces,
|
||||
totalRaces: output.totalRaces,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): GetTotalRacesViewModel | null {
|
||||
getViewModel(): RaceStatsDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: ImportRaceResultsSummaryViewModel | null = null;
|
||||
export class ImportRaceResultsApiPresenter {
|
||||
private result: ImportRaceResultsSummaryDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: ImportRaceResultsApiResultDTO) {
|
||||
this.result = dto;
|
||||
present(output: ImportRaceResultsApiOutputPort) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO';
|
||||
|
||||
export class GetEntitySponsorshipPricingResultDTO {
|
||||
@ApiProperty()
|
||||
entityType: string;
|
||||
|
||||
@ApiProperty()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ type: [SponsorshipPricingItemDTO] })
|
||||
pricing: SponsorshipPricingItemDTO[];
|
||||
}
|
||||
@@ -7,9 +7,15 @@ export class SponsorDTO {
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
contactEmail?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
logoUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
createdAt?: Date;
|
||||
}
|
||||
@@ -10,9 +10,9 @@ describe('CreateSponsorPresenter', () => {
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
|
||||
presenter.present(mockPort);
|
||||
expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
@@ -21,11 +21,11 @@ describe('CreateSponsorPresenter', () => {
|
||||
|
||||
describe('present', () => {
|
||||
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', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
presenter.present(mockResult);
|
||||
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
|
||||
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', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
presenter.present(mockResult);
|
||||
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
|
||||
presenter.present(mockPort);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
private result: CreateSponsorViewModel | null = null;
|
||||
export class CreateSponsorPresenter {
|
||||
private result: CreateSponsorOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CreateSponsorOutputPort) {
|
||||
this.result = dto;
|
||||
present(port: CreateSponsorOutputPort) {
|
||||
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;
|
||||
}
|
||||
|
||||
get viewModel(): CreateSponsorViewModel {
|
||||
get viewModel(): CreateSponsorOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
import type { GetEntitySponsorshipPricingResultDTO } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
import type { IEntitySponsorshipPricingPresenter } from '@core/racing/application/presenters/IEntitySponsorshipPricingPresenter';
|
||||
import type { GetEntitySponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetEntitySponsorshipPricingOutputPort';
|
||||
import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
|
||||
|
||||
export class GetEntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter {
|
||||
export class GetEntitySponsorshipPricingPresenter {
|
||||
private result: GetEntitySponsorshipPricingResultDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetEntitySponsorshipPricingResultDTO | null) {
|
||||
this.result = dto;
|
||||
async present(output: GetEntitySponsorshipPricingOutputPort | null) {
|
||||
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 {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetEntitySponsorshipPricingResultDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,8 @@
|
||||
import type { SponsorDashboardDTO } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import type { ISponsorDashboardPresenter, SponsorDashboardViewModel } from '@core/racing/application/presenters/ISponsorDashboardPresenter';
|
||||
import type { SponsorDashboardOutputPort } from '@core/racing/application/ports/output/SponsorDashboardOutputPort';
|
||||
import { SponsorDashboardDTO } from '../dtos/SponsorDashboardDTO';
|
||||
|
||||
export class GetSponsorDashboardPresenter implements ISponsorDashboardPresenter {
|
||||
private result: SponsorDashboardViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: SponsorDashboardDTO | null) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): SponsorDashboardViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): SponsorDashboardViewModel | null {
|
||||
return this.result;
|
||||
export class GetSponsorDashboardPresenter {
|
||||
present(outputPort: SponsorDashboardOutputPort | null): SponsorDashboardDTO | null {
|
||||
return outputPort;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,8 @@
|
||||
import type { SponsorSponsorshipsDTO } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import type { ISponsorSponsorshipsPresenter, SponsorSponsorshipsViewModel } from '@core/racing/application/presenters/ISponsorSponsorshipsPresenter';
|
||||
import type { SponsorSponsorshipsOutputPort } from '@core/racing/application/ports/output/SponsorSponsorshipsOutputPort';
|
||||
import { SponsorSponsorshipsDTO } from '../dtos/SponsorSponsorshipsDTO';
|
||||
|
||||
export class GetSponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
|
||||
private result: SponsorSponsorshipsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: SponsorSponsorshipsDTO | null) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): SponsorSponsorshipsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): SponsorSponsorshipsViewModel | null {
|
||||
return this.result;
|
||||
export class GetSponsorSponsorshipsPresenter {
|
||||
present(outputPort: SponsorSponsorshipsOutputPort | null): SponsorSponsorshipsDTO | null {
|
||||
return outputPort;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: GetSponsorsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
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;
|
||||
export class GetSponsorsPresenter {
|
||||
present(outputPort: GetSponsorsOutputPort): GetSponsorsOutputDTO {
|
||||
return {
|
||||
sponsors: outputPort.sponsors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: GetSponsorshipPricingViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
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;
|
||||
export class GetSponsorshipPricingPresenter {
|
||||
present(outputPort: GetSponsorshipPricingOutputPort): GetEntitySponsorshipPricingResultDTO {
|
||||
return {
|
||||
entityType: outputPort.entityType,
|
||||
entityId: outputPort.entityId,
|
||||
pricing: outputPort.pricing,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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 {
|
||||
private result: AllTeamsViewModel | null = null;
|
||||
export class AllTeamsPresenter {
|
||||
private result: GetAllTeamsOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: AllTeamsResultDTO) {
|
||||
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 || [],
|
||||
}));
|
||||
|
||||
async present(output: GetAllTeamsOutputPort) {
|
||||
this.result = {
|
||||
teams,
|
||||
totalCount: teams.length,
|
||||
teams: output.teams.map(team => ({
|
||||
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;
|
||||
}
|
||||
|
||||
get viewModel(): AllTeamsViewModel {
|
||||
get viewModel(): GetAllTeamsOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
private result: DriverTeamViewModel | null = null;
|
||||
export class DriverTeamPresenter {
|
||||
private result: GetDriverTeamOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: DriverTeamResultDTO) {
|
||||
const isOwner = dto.team.ownerId === dto.driverId;
|
||||
const canManage = isOwner || dto.membership.role === 'owner' || dto.membership.role === 'manager';
|
||||
async present(output: DriverTeamOutputPort) {
|
||||
const isOwner = output.team.ownerId === output.driverId;
|
||||
const canManage = isOwner || output.membership.role === 'owner' || output.membership.role === 'manager';
|
||||
|
||||
this.result = {
|
||||
team: {
|
||||
id: dto.team.id,
|
||||
name: dto.team.name,
|
||||
tag: dto.team.tag,
|
||||
description: dto.team.description || '',
|
||||
ownerId: dto.team.ownerId,
|
||||
leagues: dto.team.leagues || [],
|
||||
id: output.team.id,
|
||||
name: output.team.name,
|
||||
tag: output.team.tag,
|
||||
description: output.team.description || '',
|
||||
ownerId: output.team.ownerId,
|
||||
leagues: output.team.leagues || [],
|
||||
createdAt: output.team.createdAt.toISOString(),
|
||||
},
|
||||
membership: {
|
||||
role: dto.membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: dto.membership.joinedAt.toISOString(),
|
||||
isActive: dto.membership.status === 'active',
|
||||
role: output.membership.role === 'driver' ? 'member' : output.membership.role,
|
||||
joinedAt: output.membership.joinedAt.toISOString(),
|
||||
isActive: output.membership.status === 'active',
|
||||
},
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): DriverTeamViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): DriverTeamViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
getViewModel(): GetDriverTeamOutputDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,36 @@
|
||||
import {
|
||||
ITeamDetailsPresenter,
|
||||
TeamDetailsResultDTO,
|
||||
TeamDetailsViewModel,
|
||||
} from '@core/racing/application/presenters/ITeamDetailsPresenter';
|
||||
import type { GetTeamDetailsOutputPort } from '@core/racing/application/ports/output/GetTeamDetailsOutputPort';
|
||||
import type { GetTeamDetailsOutputDTO } from '../dtos/GetTeamDetailsOutputDTO';
|
||||
|
||||
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
private result: TeamDetailsViewModel | null = null;
|
||||
export class TeamDetailsPresenter {
|
||||
private result: GetTeamDetailsOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamDetailsResultDTO) {
|
||||
const { team, membership } = dto;
|
||||
|
||||
const canManage =
|
||||
membership !== null &&
|
||||
(membership.role === 'owner' || membership.role === 'manager');
|
||||
|
||||
async present(outputPort: GetTeamDetailsOutputPort): Promise<void> {
|
||||
this.result = {
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: team.leagues || [],
|
||||
createdAt: team.createdAt?.toISOString() || new Date().toISOString(),
|
||||
id: outputPort.team.id,
|
||||
name: outputPort.team.name,
|
||||
tag: outputPort.team.tag,
|
||||
description: outputPort.team.description,
|
||||
ownerId: outputPort.team.ownerId,
|
||||
leagues: outputPort.team.leagues,
|
||||
createdAt: outputPort.team.createdAt.toISOString(),
|
||||
},
|
||||
membership: membership
|
||||
membership: outputPort.membership
|
||||
? {
|
||||
role: membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.status === 'active',
|
||||
role: outputPort.membership.role,
|
||||
joinedAt: outputPort.membership.joinedAt.toISOString(),
|
||||
isActive: outputPort.membership.isActive,
|
||||
}
|
||||
: null,
|
||||
canManage,
|
||||
canManage: outputPort.canManage,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamDetailsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamDetailsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
getViewModel(): GetTeamDetailsOutputDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,30 @@
|
||||
import {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestsResultDTO,
|
||||
TeamJoinRequestsViewModel,
|
||||
TeamJoinRequestViewModel,
|
||||
} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter';
|
||||
import type { TeamJoinRequestsOutputPort } from '@core/racing/application/ports/output/TeamJoinRequestsOutputPort';
|
||||
import type { GetTeamJoinRequestsOutputDTO } from '../dtos/GetTeamJoinRequestsOutputDTO';
|
||||
|
||||
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
|
||||
private result: TeamJoinRequestsViewModel | null = null;
|
||||
export class TeamJoinRequestsPresenter {
|
||||
private result: GetTeamJoinRequestsOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamJoinRequestsResultDTO) {
|
||||
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] || '',
|
||||
}));
|
||||
|
||||
async present(outputPort: TeamJoinRequestsOutputPort): Promise<void> {
|
||||
this.result = {
|
||||
requests: requestViewModels,
|
||||
pendingCount: requestViewModels.length,
|
||||
totalCount: requestViewModels.length,
|
||||
requests: outputPort.requests.map(request => ({
|
||||
requestId: request.requestId,
|
||||
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 {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamJoinRequestsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
getViewModel(): GetTeamJoinRequestsOutputDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,31 @@
|
||||
import {
|
||||
ITeamMembersPresenter,
|
||||
TeamMembersResultDTO,
|
||||
TeamMembersViewModel,
|
||||
TeamMemberViewModel,
|
||||
} from '@core/racing/application/presenters/ITeamMembersPresenter';
|
||||
import type { TeamMembersOutputPort } from '@core/racing/application/ports/output/TeamMembersOutputPort';
|
||||
import type { GetTeamMembersOutputDTO } from '../dtos/GetTeamMembersOutputDTO';
|
||||
|
||||
export class TeamMembersPresenter implements ITeamMembersPresenter {
|
||||
private result: TeamMembersViewModel | null = null;
|
||||
export class TeamMembersPresenter {
|
||||
private result: GetTeamMembersOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamMembersResultDTO) {
|
||||
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;
|
||||
|
||||
async present(outputPort: TeamMembersOutputPort): Promise<void> {
|
||||
this.result = {
|
||||
members,
|
||||
totalCount: members.length,
|
||||
ownerCount,
|
||||
managerCount,
|
||||
memberCount,
|
||||
members: outputPort.members.map(member => ({
|
||||
driverId: member.driverId,
|
||||
driverName: member.driverName,
|
||||
role: member.role,
|
||||
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 {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamMembersViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
getViewModel(): GetTeamMembersOutputDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
private result: TeamsLeaderboardViewModel | null = null;
|
||||
export class TeamsLeaderboardPresenter {
|
||||
private result: GetTeamsLeaderboardOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamsLeaderboardResultDTO) {
|
||||
async present(outputPort: TeamsLeaderboardOutputPort): Promise<void> {
|
||||
this.result = {
|
||||
teams: dto.teams as TeamLeaderboardItemViewModel[],
|
||||
recruitingCount: dto.recruitingCount,
|
||||
teams: outputPort.teams.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,
|
||||
})),
|
||||
recruitingCount: outputPort.recruitingCount,
|
||||
groupsBySkillLevel: {
|
||||
beginner: [],
|
||||
intermediate: [],
|
||||
advanced: [],
|
||||
pro: [],
|
||||
beginner: outputPort.groupsBySkillLevel.beginner.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,
|
||||
})),
|
||||
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 {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamsLeaderboardViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
getViewModel(): GetTeamsLeaderboardOutputDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import PerformanceMetrics from './PerformanceMetrics';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DriverTeamPresenter } from '@/lib/presenters/DriverTeamPresenter';
|
||||
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';
|
||||
|
||||
interface DriverProfileProps {
|
||||
@@ -33,7 +33,7 @@ interface DriverProfileStatsViewModel {
|
||||
overallRank?: number;
|
||||
}
|
||||
|
||||
type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null;
|
||||
type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null;
|
||||
|
||||
export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) {
|
||||
const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null);
|
||||
@@ -61,8 +61,8 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
|
||||
const leagueRank = primaryLeagueId
|
||||
? getLeagueRankings(driver.id, primaryLeagueId)
|
||||
: { rank: 0, totalDrivers: 0, percentile: 0 };
|
||||
const globalRank = profileData?.currentDriver?.globalRank ?? null;
|
||||
const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0;
|
||||
const globalRank = profileData?.driver?.globalRank ?? null;
|
||||
const totalDrivers = profileData?.driver?.totalDrivers ?? 0;
|
||||
|
||||
const performanceStats = driverStats ? {
|
||||
winRate: driverStats.totalRaces > 0 ? (driverStats.wins / driverStats.totalRaces) * 100 : 0,
|
||||
|
||||
@@ -4,7 +4,7 @@ import Card from '../ui/Card';
|
||||
import RankBadge from './RankBadge';
|
||||
import { useState, useEffect } from 'react';
|
||||
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 {
|
||||
driverId?: string;
|
||||
@@ -18,7 +18,7 @@ interface ProfileStatsProps {
|
||||
};
|
||||
}
|
||||
|
||||
type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null;
|
||||
type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null;
|
||||
|
||||
export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
|
||||
const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null);
|
||||
@@ -35,7 +35,7 @@ export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
|
||||
}, [driverId]);
|
||||
|
||||
const driverStats = profileData?.stats || null;
|
||||
const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0;
|
||||
const totalDrivers = profileData?.driver?.totalDrivers ?? 0;
|
||||
const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null;
|
||||
const leagueRank =
|
||||
driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null;
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from './use-cases/GetLeagueDriverSeasonStatsUseCase';
|
||||
export * from './use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
export * from './use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
export * from './use-cases/GetAllRacesUseCase';
|
||||
export * from './use-cases/GetAllRacesPageDataUseCase';
|
||||
export * from './use-cases/GetTotalRacesUseCase';
|
||||
export * from './use-cases/ImportRaceResultsApiUseCase';
|
||||
export * from './use-cases/ListLeagueScoringPresetsUseCase';
|
||||
@@ -74,6 +75,8 @@ export type {
|
||||
} from './dto/LeagueScheduleDTO';
|
||||
export type { ChampionshipStandingsOutputPort } from './ports/output/ChampionshipStandingsOutputPort';
|
||||
export type { ChampionshipStandingsRowOutputPort } from './ports/output/ChampionshipStandingsRowOutputPort';
|
||||
export type { AllRacesPageOutputPort } from './ports/output/AllRacesPageOutputPort';
|
||||
export type { DriverRegistrationStatusOutputPort } from './ports/output/DriverRegistrationStatusOutputPort';
|
||||
export type {
|
||||
LeagueConfigFormModel,
|
||||
LeagueStructureFormDTO,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { League } from '../../domain/entities/League';
|
||||
|
||||
export interface AllLeaguesWithCapacityOutputPort {
|
||||
leagues: League[];
|
||||
memberCounts: Record<string, number>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ApproveLeagueJoinRequestOutputPort {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface CompleteDriverOnboardingOutputPort {
|
||||
driverId: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CreateLeagueOutputPort {
|
||||
leagueId: string;
|
||||
success: boolean;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Presenter } from '@core/shared/presentation';
|
||||
import type { FeedItemType } from '@core/social/domain/types/FeedItemType';
|
||||
|
||||
export interface DashboardDriverSummaryViewModel {
|
||||
export interface DashboardDriverSummaryOutputPort {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
@@ -14,7 +13,7 @@ export interface DashboardDriverSummaryViewModel {
|
||||
consistency: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardRaceSummaryViewModel {
|
||||
export interface DashboardRaceSummaryOutputPort {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
@@ -25,7 +24,7 @@ export interface DashboardRaceSummaryViewModel {
|
||||
isMyLeague: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardRecentResultViewModel {
|
||||
export interface DashboardRecentResultOutputPort {
|
||||
raceId: string;
|
||||
raceName: string;
|
||||
leagueId: string;
|
||||
@@ -35,7 +34,7 @@ export interface DashboardRecentResultViewModel {
|
||||
incidents: number;
|
||||
}
|
||||
|
||||
export interface DashboardLeagueStandingSummaryViewModel {
|
||||
export interface DashboardLeagueStandingSummaryOutputPort {
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
position: number;
|
||||
@@ -43,7 +42,7 @@ export interface DashboardLeagueStandingSummaryViewModel {
|
||||
points: number;
|
||||
}
|
||||
|
||||
export interface DashboardFeedItemSummaryViewModel {
|
||||
export interface DashboardFeedItemSummaryOutputPort {
|
||||
id: string;
|
||||
type: FeedItemType;
|
||||
headline: string;
|
||||
@@ -53,39 +52,34 @@ export interface DashboardFeedItemSummaryViewModel {
|
||||
ctaHref?: string;
|
||||
}
|
||||
|
||||
export interface DashboardFeedSummaryViewModel {
|
||||
export interface DashboardFeedSummaryOutputPort {
|
||||
notificationCount: number;
|
||||
items: DashboardFeedItemSummaryViewModel[];
|
||||
items: DashboardFeedItemSummaryOutputPort[];
|
||||
}
|
||||
|
||||
export interface DashboardFriendSummaryViewModel {
|
||||
export interface DashboardFriendSummaryOutputPort {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface DashboardOverviewViewModel {
|
||||
currentDriver: DashboardDriverSummaryViewModel | null;
|
||||
myUpcomingRaces: DashboardRaceSummaryViewModel[];
|
||||
otherUpcomingRaces: DashboardRaceSummaryViewModel[];
|
||||
export interface DashboardOverviewOutputPort {
|
||||
currentDriver: DashboardDriverSummaryOutputPort | null;
|
||||
myUpcomingRaces: DashboardRaceSummaryOutputPort[];
|
||||
otherUpcomingRaces: DashboardRaceSummaryOutputPort[];
|
||||
/**
|
||||
* 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,
|
||||
* based on upcoming races and league standings.
|
||||
*/
|
||||
activeLeaguesCount: number;
|
||||
nextRace: DashboardRaceSummaryViewModel | null;
|
||||
recentResults: DashboardRecentResultViewModel[];
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummaryViewModel[];
|
||||
feedSummary: DashboardFeedSummaryViewModel;
|
||||
friends: DashboardFriendSummaryViewModel[];
|
||||
nextRace: DashboardRaceSummaryOutputPort | null;
|
||||
recentResults: DashboardRecentResultOutputPort[];
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummaryOutputPort[];
|
||||
feedSummary: DashboardFeedSummaryOutputPort;
|
||||
friends: DashboardFriendSummaryOutputPort[];
|
||||
}
|
||||
|
||||
export type DashboardOverviewResultDTO = DashboardOverviewViewModel;
|
||||
|
||||
export interface IDashboardOverviewPresenter
|
||||
extends Presenter<DashboardOverviewResultDTO, DashboardOverviewViewModel> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface DriverRegistrationStatusOutputPort {
|
||||
isRegistered: boolean;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface GetDriverTeamOutputPort {
|
||||
export interface DriverTeamOutputPort {
|
||||
driverId: string;
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -9,9 +10,10 @@ export interface GetDriverTeamOutputPort {
|
||||
createdAt: Date;
|
||||
};
|
||||
membership: {
|
||||
driverId: string;
|
||||
teamId: string;
|
||||
role: 'member' | 'captain' | 'admin';
|
||||
driverId: string;
|
||||
role: 'owner' | 'manager' | 'driver';
|
||||
status: 'active' | 'pending' | 'none';
|
||||
joinedAt: Date;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,5 +7,7 @@ export interface GetAllTeamsOutputPort {
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt: Date;
|
||||
memberCount: number;
|
||||
}>;
|
||||
totalCount?: number;
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
export interface GetLeagueAdminOutputPort {
|
||||
league: {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
};
|
||||
// Additional data would be populated by combining multiple use cases
|
||||
leagueId: string;
|
||||
ownerId: string;
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
export interface GetLeagueJoinRequestsOutputPort {
|
||||
joinRequests: Array<{
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string;
|
||||
driver: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
export interface LeagueJoinRequestOutputPort {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message: string;
|
||||
driver: { id: string; name: string } | null;
|
||||
}
|
||||
|
||||
export interface GetLeagueJoinRequestsOutputPort {
|
||||
joinRequests: LeagueJoinRequestOutputPort[];
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
export interface GetLeagueMembershipsOutputPort {
|
||||
memberships: Array<{
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
role: 'member' | 'admin' | 'owner';
|
||||
joinedAt: Date;
|
||||
}>;
|
||||
drivers: { id: string; name: string }[];
|
||||
export interface LeagueMembershipOutputPort {
|
||||
driverId: string;
|
||||
driver: { id: string; name: string };
|
||||
role: string;
|
||||
joinedAt: Date;
|
||||
}
|
||||
|
||||
export interface LeagueMembershipsOutputPort {
|
||||
members: LeagueMembershipOutputPort[];
|
||||
}
|
||||
|
||||
export interface GetLeagueMembershipsOutputPort {
|
||||
memberships: LeagueMembershipsOutputPort;
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
export interface GetLeagueOwnerSummaryOutputPort {
|
||||
summary: { driver: { id: string; name: string }; rating: number; rank: number } | null;
|
||||
export interface LeagueOwnerSummaryOutputPort {
|
||||
driver: { id: string; iracingId: string; name: string; country: string; bio: string | undefined; joinedAt: string };
|
||||
rating: number;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface GetLeagueOwnerSummaryOutputPort {
|
||||
summary: LeagueOwnerSummaryOutputPort | null;
|
||||
}
|
||||
@@ -1,20 +1,47 @@
|
||||
export interface GetLeagueProtestsOutputPort {
|
||||
protests: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
submittedAt: Date;
|
||||
description: string;
|
||||
status: string;
|
||||
}>;
|
||||
races: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
}>;
|
||||
drivers: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
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 {
|
||||
protests: ProtestOutputPort[];
|
||||
racesById: Record<string, RaceOutputPort>;
|
||||
driversById: Record<string, DriverOutputPort>;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface GetSponsorsOutputPort {
|
||||
sponsors: {
|
||||
id: string;
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
websiteUrl: string | undefined;
|
||||
logoUrl: string | undefined;
|
||||
createdAt: Date;
|
||||
}[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface GetSponsorshipPricingOutputPort {
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
pricing: {
|
||||
id: string;
|
||||
level: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
}[];
|
||||
}
|
||||
@@ -9,9 +9,9 @@ export interface GetTeamDetailsOutputPort {
|
||||
createdAt: Date;
|
||||
};
|
||||
membership: {
|
||||
driverId: string;
|
||||
teamId: string;
|
||||
role: 'member' | 'captain' | 'admin';
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
joinedAt: Date;
|
||||
isActive: boolean;
|
||||
} | null;
|
||||
canManage: boolean;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface GetTotalLeaguesOutputPort {
|
||||
totalLeagues: number;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface GetTotalRacesOutputPort {
|
||||
totalRaces: number;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ImportRaceResultsApiOutputPort {
|
||||
success: boolean;
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
driversProcessed: number;
|
||||
resultsRecorded: number;
|
||||
errors?: string[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface JoinLeagueOutputPort {
|
||||
membershipId: string;
|
||||
leagueId: string;
|
||||
status: string;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface LeagueDriverSeasonStatsOutputPort {
|
||||
export interface LeagueDriverSeasonStatsItemOutputPort {
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
@@ -18,3 +18,8 @@ export interface LeagueDriverSeasonStatsOutputPort {
|
||||
rating: number | null;
|
||||
ratingChange: number | null;
|
||||
}
|
||||
|
||||
export interface LeagueDriverSeasonStatsOutputPort {
|
||||
leagueId: string;
|
||||
stats: LeagueDriverSeasonStatsItemOutputPort[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
|
||||
import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort';
|
||||
|
||||
export interface LeagueScoringConfigOutputPort {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
scoringPresetId?: string;
|
||||
scoringPresetName?: string;
|
||||
dropPolicySummary: string;
|
||||
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;
|
||||
}>;
|
||||
preset?: LeagueScoringPresetOutputPort;
|
||||
championships: ChampionshipConfig[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { LeagueScoringPresetOutputPort } from './LeagueScoringPresetOutputPort';
|
||||
|
||||
export interface LeagueScoringPresetsOutputPort {
|
||||
presets: LeagueScoringPresetOutputPort[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface StandingItemOutputPort {
|
||||
driverId: string;
|
||||
driver: { id: string; name: string };
|
||||
points: number;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface LeagueStandingsOutputPort {
|
||||
standings: StandingItemOutputPort[];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface LeagueStatsOutputPort {
|
||||
totalMembers: number;
|
||||
totalRaces: number;
|
||||
averageRating: number;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
15
core/racing/application/ports/output/RaceDetailOutputPort.ts
Normal file
15
core/racing/application/ports/output/RaceDetailOutputPort.ts
Normal 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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user