refactor driver module (wip)

This commit is contained in:
2025-12-22 10:24:40 +01:00
parent e7dbec4a85
commit 9da528d5bd
108 changed files with 842 additions and 947 deletions

View File

@@ -128,7 +128,7 @@ export const DriverProviders: Provider[] = [
driverStatsService: IDriverStatsService, driverStatsService: IDriverStatsService,
imageService: IImageServicePort, imageService: IImageServicePort,
logger: Logger, logger: Logger,
) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger), ) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, (driverId: string) => Promise.resolve(imageService.getDriverAvatar(driverId)), logger),
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN], inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
}, },
{ {
@@ -169,7 +169,7 @@ export const DriverProviders: Provider[] = [
teamRepository, teamRepository,
teamMembershipRepository, teamMembershipRepository,
socialRepository, socialRepository,
(driverId: string) => Promise.resolve(imageService.getDriverAvatar(driverId)), imageService,
driverExtendedProfileProvider, driverExtendedProfileProvider,
(driverId: string) => { (driverId: string) => {
const stats = driverStatsService.getDriverStats(driverId); const stats = driverStatsService.getDriverStats(driverId);

View File

@@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
export enum DriverProfileAchievementRarity {
COMMON = 'common',
RARE = 'rare',
EPIC = 'epic',
LEGENDARY = 'legendary',
}
export class DriverProfileAchievementDTO {
@ApiProperty()
id!: string;
@ApiProperty()
title!: string;
@ApiProperty()
description!: string;
@ApiProperty({ enum: ['trophy', 'medal', 'star', 'crown', 'target', 'zap'] })
icon!: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
@ApiProperty({ enum: DriverProfileAchievementRarity })
rarity!: DriverProfileAchievementRarity;
@ApiProperty()
earnedAt!: string;
}

View File

@@ -0,0 +1,36 @@
import { ApiProperty } from '@nestjs/swagger';
export class DriverProfileDriverSummaryDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
country!: string;
@ApiProperty()
avatarUrl!: string;
@ApiProperty({ nullable: true })
iracingId!: string | null;
@ApiProperty()
joinedAt!: string;
@ApiProperty({ nullable: true })
rating!: number | null;
@ApiProperty({ nullable: true })
globalRank!: number | null;
@ApiProperty({ nullable: true })
consistency!: number | null;
@ApiProperty({ nullable: true })
bio!: string | null;
@ApiProperty({ nullable: true })
totalDrivers!: number | null;
}

View File

@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { DriverProfileSocialHandleDTO } from './DriverProfileSocialHandleDTO';
import { DriverProfileAchievementDTO } from './DriverProfileAchievementDTO';
export class DriverProfileExtendedProfileDTO {
@ApiProperty({ type: [DriverProfileSocialHandleDTO] })
socialHandles!: DriverProfileSocialHandleDTO[];
@ApiProperty({ type: [DriverProfileAchievementDTO] })
achievements!: DriverProfileAchievementDTO[];
@ApiProperty()
racingStyle!: string;
@ApiProperty()
favoriteTrack!: string;
@ApiProperty()
favoriteCar!: string;
@ApiProperty()
timezone!: string;
@ApiProperty()
availableHours!: string;
@ApiProperty()
lookingForTeam!: boolean;
@ApiProperty()
openToRequests!: boolean;
}

View File

@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
export class DriverProfileFinishDistributionDTO {
@ApiProperty()
totalRaces!: number;
@ApiProperty()
wins!: number;
@ApiProperty()
podiums!: number;
@ApiProperty()
topTen!: number;
@ApiProperty()
dnfs!: number;
@ApiProperty()
other!: number;
}

View File

@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
export class DriverProfileSocialFriendSummaryDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
country!: string;
@ApiProperty()
avatarUrl!: string;
}

View File

@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
export class DriverProfileSocialHandleDTO {
@ApiProperty({ enum: ['twitter', 'youtube', 'twitch', 'discord'] })
platform!: DriverProfileSocialPlatform;
@ApiProperty()
handle!: string;
@ApiProperty()
url!: string;
}

View File

@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { DriverProfileSocialFriendSummaryDTO } from './DriverProfileSocialFriendSummaryDTO';
export class DriverProfileSocialSummaryDTO {
@ApiProperty()
friendsCount!: number;
@ApiProperty({ type: [DriverProfileSocialFriendSummaryDTO] })
friends!: DriverProfileSocialFriendSummaryDTO[];
}

View File

@@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';
export class DriverProfileStatsDTO {
@ApiProperty()
totalRaces!: number;
@ApiProperty()
wins!: number;
@ApiProperty()
podiums!: number;
@ApiProperty()
dnfs!: number;
@ApiProperty({ nullable: true })
avgFinish!: number | null;
@ApiProperty({ nullable: true })
bestFinish!: number | null;
@ApiProperty({ nullable: true })
worstFinish!: number | null;
@ApiProperty({ nullable: true })
finishRate!: number | null;
@ApiProperty({ nullable: true })
winRate!: number | null;
@ApiProperty({ nullable: true })
podiumRate!: number | null;
@ApiProperty({ nullable: true })
percentile!: number | null;
@ApiProperty({ nullable: true })
rating!: number | null;
@ApiProperty({ nullable: true })
consistency!: number | null;
@ApiProperty({ nullable: true })
overallRank!: number | null;
}

View File

@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
export class DriverProfileTeamMembershipDTO {
@ApiProperty()
teamId!: string;
@ApiProperty()
teamName!: string;
@ApiProperty({ nullable: true })
teamTag!: string | null;
@ApiProperty()
role!: string;
@ApiProperty()
joinedAt!: string;
@ApiProperty()
isCurrent!: boolean;
}

View File

@@ -1,231 +1,28 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class DriverProfileDriverSummaryDTO { import { DriverProfileDriverSummaryDTO } from './DriverProfileDriverSummaryDTO';
@ApiProperty() import { DriverProfileStatsDTO } from './DriverProfileStatsDTO';
id!: string; import { DriverProfileFinishDistributionDTO } from './DriverProfileFinishDistributionDTO';
import { DriverProfileTeamMembershipDTO } from './DriverProfileTeamMembershipDTO';
@ApiProperty() import { DriverProfileSocialSummaryDTO } from './DriverProfileSocialSummaryDTO';
name!: string; import { DriverProfileExtendedProfileDTO } from './DriverProfileExtendedProfileDTO';
@ApiProperty()
country!: string;
@ApiProperty()
avatarUrl!: string;
@ApiProperty({ nullable: true })
iracingId!: string | null;
@ApiProperty()
joinedAt!: string;
@ApiProperty({ nullable: true })
rating!: number | null;
@ApiProperty({ nullable: true })
globalRank!: number | null;
@ApiProperty({ nullable: true })
consistency!: number | null;
@ApiProperty({ nullable: true })
bio!: string | null;
@ApiProperty({ nullable: true })
totalDrivers!: number | null;
}
export class DriverProfileStatsDTO {
@ApiProperty()
totalRaces!: number;
@ApiProperty()
wins!: number;
@ApiProperty()
podiums!: number;
@ApiProperty()
dnfs!: number;
@ApiProperty({ nullable: true })
avgFinish!: number | null;
@ApiProperty({ nullable: true })
bestFinish!: number | null;
@ApiProperty({ nullable: true })
worstFinish!: number | null;
@ApiProperty({ nullable: true })
finishRate!: number | null;
@ApiProperty({ nullable: true })
winRate!: number | null;
@ApiProperty({ nullable: true })
podiumRate!: number | null;
@ApiProperty({ nullable: true })
percentile!: number | null;
@ApiProperty({ nullable: true })
rating!: number | null;
@ApiProperty({ nullable: true })
consistency!: number | null;
@ApiProperty({ nullable: true })
overallRank!: number | null;
}
export class DriverProfileFinishDistributionDTO {
@ApiProperty()
totalRaces: number;
@ApiProperty()
wins: number;
@ApiProperty()
podiums: number;
@ApiProperty()
topTen: number;
@ApiProperty()
dnfs: number;
@ApiProperty()
other: number;
}
export class DriverProfileTeamMembershipDTO {
@ApiProperty()
teamId: string;
@ApiProperty()
teamName: string;
@ApiProperty({ nullable: true })
teamTag: string | null;
@ApiProperty()
role: string;
@ApiProperty()
joinedAt: string;
@ApiProperty()
isCurrent: boolean;
}
export class DriverProfileSocialFriendSummaryDTO {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty()
country: string;
@ApiProperty()
avatarUrl: string;
}
export class DriverProfileSocialSummaryDTO {
@ApiProperty()
friendsCount: number;
@ApiProperty({ type: [DriverProfileSocialFriendSummaryDTO] })
friends: DriverProfileSocialFriendSummaryDTO[];
}
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
export enum DriverProfileAchievementRarity {
COMMON = 'common',
RARE = 'rare',
EPIC = 'epic',
LEGENDARY = 'legendary',
}
export class DriverProfileAchievementDTO {
@ApiProperty()
id: string;
@ApiProperty()
title: string;
@ApiProperty()
description: string;
@ApiProperty({ enum: ['trophy', 'medal', 'star', 'crown', 'target', 'zap'] })
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
@ApiProperty({ enum: DriverProfileAchievementRarity })
rarity: DriverProfileAchievementRarity;
@ApiProperty()
earnedAt: string;
}
export class DriverProfileSocialHandleDTO {
@ApiProperty({ enum: DriverProfileSocialPlatform })
platform: DriverProfileSocialPlatform;
@ApiProperty()
handle: string;
@ApiProperty()
url: string;
}
export class DriverProfileExtendedProfileDTO {
@ApiProperty({ type: [DriverProfileSocialHandleDTO] })
socialHandles: DriverProfileSocialHandleDTO[];
@ApiProperty({ type: [DriverProfileAchievementDTO] })
achievements: DriverProfileAchievementDTO[];
@ApiProperty()
racingStyle: string;
@ApiProperty()
favoriteTrack: string;
@ApiProperty()
favoriteCar: string;
@ApiProperty()
timezone: string;
@ApiProperty()
availableHours: string;
@ApiProperty()
lookingForTeam: boolean;
@ApiProperty()
openToRequests: boolean;
}
export class GetDriverProfileOutputDTO { export class GetDriverProfileOutputDTO {
@ApiProperty({ type: DriverProfileDriverSummaryDTO, nullable: true }) @ApiProperty({ type: DriverProfileDriverSummaryDTO, nullable: true })
currentDriver: DriverProfileDriverSummaryDTO | null; currentDriver!: DriverProfileDriverSummaryDTO | null;
@ApiProperty({ type: DriverProfileStatsDTO, nullable: true }) @ApiProperty({ type: DriverProfileStatsDTO, nullable: true })
stats: DriverProfileStatsDTO | null; stats!: DriverProfileStatsDTO | null;
@ApiProperty({ type: DriverProfileFinishDistributionDTO, nullable: true }) @ApiProperty({ type: DriverProfileFinishDistributionDTO, nullable: true })
finishDistribution: DriverProfileFinishDistributionDTO | null; finishDistribution!: DriverProfileFinishDistributionDTO | null;
@ApiProperty({ type: [DriverProfileTeamMembershipDTO] }) @ApiProperty({ type: [DriverProfileTeamMembershipDTO] })
teamMemberships: DriverProfileTeamMembershipDTO[]; teamMemberships!: DriverProfileTeamMembershipDTO[];
@ApiProperty({ type: DriverProfileSocialSummaryDTO }) @ApiProperty({ type: DriverProfileSocialSummaryDTO })
socialSummary: DriverProfileSocialSummaryDTO; socialSummary!: DriverProfileSocialSummaryDTO;
@ApiProperty({ type: DriverProfileExtendedProfileDTO, nullable: true }) @ApiProperty({ type: DriverProfileExtendedProfileDTO, nullable: true })
extendedProfile: DriverProfileExtendedProfileDTO | null; extendedProfile!: DriverProfileExtendedProfileDTO | null;
} }

View File

@@ -1,7 +1,8 @@
import type { import type {
GetProfileOverviewResult, GetProfileOverviewResult,
} from '@core/racing/application/use-cases/GetProfileOverviewUseCase'; } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
import type { GetDriverProfileOutputDTO, DriverProfileExtendedProfileDTO } from '../dtos/GetDriverProfileOutputDTO'; import type { GetDriverProfileOutputDTO } from '../dtos/GetDriverProfileOutputDTO';
import type { DriverProfileExtendedProfileDTO } from '../dtos/DriverProfileExtendedProfileDTO';
export class DriverProfilePresenter export class DriverProfilePresenter
{ {

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { Result } from '@core/shared/application/Result';
import { DriverStatsPresenter } from './DriverStatsPresenter'; import { DriverStatsPresenter } from './DriverStatsPresenter';
import type { GetTotalDriversResult } from '../../../../../core/racing/application/use-cases/GetTotalDriversUseCase'; import type { GetTotalDriversResult } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
describe('DriverStatsPresenter', () => { describe('DriverStatsPresenter', () => {
let presenter: DriverStatsPresenter; let presenter: DriverStatsPresenter;
@@ -16,31 +15,13 @@ describe('DriverStatsPresenter', () => {
totalDrivers: 42, totalDrivers: 42,
}; };
const result = Result.ok<GetTotalDriversResult, never>(output); presenter.present(output);
presenter.present(result); const response = presenter.getResponseModel();
const response = presenter.responseModel;
expect(response).toEqual({ expect(response).toEqual({
totalDrivers: 42, totalDrivers: 42,
}); });
}); });
}); });
describe('reset', () => {
it('should reset the result', () => {
const output: GetTotalDriversResult = {
totalDrivers: 10,
};
const result = Result.ok<GetTotalDriversResult, never>(output);
presenter.present(result);
expect(presenter.responseModel).toBeDefined();
presenter.reset();
expect(() => presenter.responseModel).toThrow('Presenter not presented');
});
});
}); });

View File

@@ -2,6 +2,8 @@ import { GetDriversLeaderboardResult } from '@core/racing/application/use-cases/
import { Result } from '@core/shared/application/Result'; import { Result } from '@core/shared/application/Result';
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { DriversLeaderboardPresenter } from './DriversLeaderboardPresenter'; import { DriversLeaderboardPresenter } from './DriversLeaderboardPresenter';
import type { Driver } from '@core/racing/domain/entities/Driver';
import type { SkillLevel } from '@core/racing/domain/services/SkillLevelService';
// TODO fix eslint issues // TODO fix eslint issues
@@ -19,11 +21,11 @@ describe('DriversLeaderboardPresenter', () => {
{ {
driver: { driver: {
id: 'driver-1', id: 'driver-1',
name: 'Driver One' as unknown, name: 'Driver One',
country: 'US' as unknown, country: 'US',
} as unknown, } as unknown as Driver,
rating: 2500, rating: 2500,
skillLevel: 'advanced' as unknown, skillLevel: 'advanced' as unknown as SkillLevel,
racesCompleted: 50, racesCompleted: 50,
wins: 10, wins: 10,
podiums: 20, podiums: 20,
@@ -34,11 +36,11 @@ describe('DriversLeaderboardPresenter', () => {
{ {
driver: { driver: {
id: 'driver-2', id: 'driver-2',
name: 'Driver Two' as unknown, name: 'Driver Two',
country: 'DE' as unknown, country: 'DE',
} as unknown, } as unknown as Driver,
rating: 2400, rating: 2400,
skillLevel: 'intermediate' as unknown, skillLevel: 'intermediate' as unknown as SkillLevel,
racesCompleted: 40, racesCompleted: 40,
wins: 5, wins: 5,
podiums: 15, podiums: 15,
@@ -56,9 +58,10 @@ describe('DriversLeaderboardPresenter', () => {
presenter.present(result); presenter.present(result);
const output = presenter.getResponseModel();
expect(result.drivers).toHaveLength(2); expect(output.drivers).toHaveLength(2);
expect(result.drivers[0]).toEqual({ expect(output.drivers[0]).toEqual({
id: 'driver-1', id: 'driver-1',
name: 'Driver One', name: 'Driver One',
rating: 2500, rating: 2500,
@@ -71,7 +74,7 @@ describe('DriversLeaderboardPresenter', () => {
rank: 1, rank: 1,
avatarUrl: 'https://example.com/avatar1.png', avatarUrl: 'https://example.com/avatar1.png',
}); });
expect(result.drivers[1]).toEqual({ expect(output.drivers[1]).toEqual({
id: 'driver-2', id: 'driver-2',
name: 'Driver Two', name: 'Driver Two',
rating: 2400, rating: 2400,
@@ -84,126 +87,10 @@ describe('DriversLeaderboardPresenter', () => {
rank: 2, rank: 2,
avatarUrl: 'https://example.com/avatar2.png', avatarUrl: 'https://example.com/avatar2.png',
}); });
expect(result.totalRaces).toBe(90); expect(output.totalRaces).toBe(90);
expect(result.totalWins).toBe(15); expect(output.totalWins).toBe(15);
expect(result.activeCount).toBe(2); expect(output.activeCount).toBe(2);
}); });
it('should sort drivers by rating descending', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver One',
country: 'US',
iracingId: '12345',
joinedAt: new Date(),
},
{
id: 'driver-2',
name: 'Driver Two',
country: 'DE',
iracingId: '67890',
joinedAt: new Date(),
},
],
rankings: [
{ driverId: 'driver-1', rating: 2400, overallRank: 2 },
{ driverId: 'driver-2', rating: 2500, overallRank: 1 },
],
stats: {},
avatarUrls: {},
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result.drivers[0].id).toBe('driver-2'); // Higher rating first
expect(result.drivers[1].id).toBe('driver-1');
});
it('should handle missing stats gracefully', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver One',
country: 'US',
iracingId: '12345',
joinedAt: new Date(),
},
],
rankings: [
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
],
stats: {}, // No stats
avatarUrls: {},
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result.drivers[0].racesCompleted).toBe(0);
expect(result.drivers[0].wins).toBe(0);
expect(result.drivers[0].podiums).toBe(0);
expect(result.drivers[0].isActive).toBe(false);
});
it('should derive skill level from rating bands', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [
{ id: 'd1', name: 'Beginner', country: 'US', iracingId: '1', joinedAt: new Date() },
{ id: 'd2', name: 'Intermediate', country: 'US', iracingId: '2', joinedAt: new Date() },
{ id: 'd3', name: 'Advanced', country: 'US', iracingId: '3', joinedAt: new Date() },
{ id: 'd4', name: 'Pro', country: 'US', iracingId: '4', joinedAt: new Date() },
],
rankings: [
{ driverId: 'd1', rating: 1700, overallRank: 4 },
{ driverId: 'd2', rating: 2000, overallRank: 3 },
{ driverId: 'd3', rating: 2600, overallRank: 2 },
{ driverId: 'd4', rating: 3100, overallRank: 1 },
],
stats: {
d1: { racesCompleted: 5, wins: 0, podiums: 0 },
d2: { racesCompleted: 5, wins: 0, podiums: 0 },
d3: { racesCompleted: 5, wins: 0, podiums: 0 },
d4: { racesCompleted: 5, wins: 0, podiums: 0 },
},
avatarUrls: {
d1: 'avatar-1',
d2: 'avatar-2',
d3: 'avatar-3',
d4: 'avatar-4',
},
};
presenter.present(dto);
const result = presenter.viewModel;
const levels = result.drivers
.sort((a, b) => a.rating - b.rating)
.map(d => d.skillLevel);
expect(levels).toEqual(['beginner', 'intermediate', 'advanced', 'pro']);
});
});
describe('reset', () => {
it('should reset the result', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [],
rankings: [],
stats: {},
avatarUrls: {},
};
presenter.present(dto);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
}); });
}); });

View File

@@ -8,6 +8,8 @@ import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeason
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
// Import concrete in-memory implementations // Import concrete in-memory implementations
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository'; import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
@@ -52,6 +54,7 @@ import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-ca
// Import presenters // Import presenters
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter'; import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
// Define injection tokens // Define injection tokens
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository'; export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
@@ -145,6 +148,10 @@ export const LeagueProviders: Provider[] = [
provide: 'AllLeaguesWithCapacityPresenter', provide: 'AllLeaguesWithCapacityPresenter',
useClass: AllLeaguesWithCapacityPresenter, useClass: AllLeaguesWithCapacityPresenter,
}, },
{
provide: 'GetLeagueProtestsPresenter',
useClass: GetLeagueProtestsPresenter,
},
// Use cases // Use cases
{ {
provide: GetAllLeaguesWithCapacityUseCase, provide: GetAllLeaguesWithCapacityUseCase,
@@ -167,7 +174,17 @@ export const LeagueProviders: Provider[] = [
RemoveLeagueMemberUseCase, RemoveLeagueMemberUseCase,
UpdateLeagueMemberRoleUseCase, UpdateLeagueMemberRoleUseCase,
GetLeagueOwnerSummaryUseCase, GetLeagueOwnerSummaryUseCase,
GetLeagueProtestsUseCase, {
provide: GetLeagueProtestsUseCase,
useFactory: (
raceRepo: IRaceRepository,
protestRepo: IProtestRepository,
driverRepo: IDriverRepository,
leagueRepo: ILeagueRepository,
presenter: GetLeagueProtestsPresenter,
) => new GetLeagueProtestsUseCase(raceRepo, protestRepo, driverRepo, leagueRepo, presenter),
inject: [RACE_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, 'GetLeagueProtestsPresenter'],
},
GetLeagueSeasonsUseCase, GetLeagueSeasonsUseCase,
GetLeagueMembershipsUseCase, GetLeagueMembershipsUseCase,
GetLeagueScheduleUseCase, GetLeagueScheduleUseCase,

View File

@@ -257,9 +257,7 @@ export class LeagueService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new GetLeagueProtestsPresenter(); return (this.getLeagueProtestsUseCase.outputPort as GetLeagueProtestsPresenter).getResponseModel()!;
presenter.present(result.unwrap());
return presenter.getViewModel()!;
} }
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> { async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
@@ -268,9 +266,7 @@ export class LeagueService {
if (result.isErr()) { if (result.isErr()) {
throw new Error(result.unwrapErr().code); throw new Error(result.unwrapErr().code);
} }
const presenter = new GetLeagueSeasonsPresenter(); return (this.getLeagueSeasonsUseCase.output as GetLeagueSeasonsPresenter).getResponseModel()!;
presenter.present(result.unwrap());
return presenter.getViewModel()!;
} }
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> { async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {

View File

@@ -8,9 +8,9 @@ export class AllLeaguesWithCapacityAndScoringDTO {
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => LeagueSummaryDTO) @Type(() => LeagueSummaryDTO)
leagues: LeagueSummaryDTO[]; leagues!: LeagueSummaryDTO[];
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
totalCount: number; totalCount!: number;
} }

View File

@@ -1,5 +1,17 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export interface LeagueSettings {
maxDrivers: number;
sessionDuration?: number;
visibility?: string;
}
export interface SocialLinks {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
}
export class LeagueWithCapacityDTO { export class LeagueWithCapacityDTO {
@ApiProperty() @ApiProperty()
id!: string; id!: string;
@@ -14,21 +26,13 @@ export class LeagueWithCapacityDTO {
ownerId!: string; ownerId!: string;
@ApiProperty() @ApiProperty()
settings!: { settings!: LeagueSettings;
maxDrivers: number;
sessionDuration?: number;
visibility?: string;
};
@ApiProperty() @ApiProperty()
createdAt!: string; createdAt!: string;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
socialLinks?: { socialLinks?: SocialLinks;
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
@ApiProperty() @ApiProperty()
usedSlots!: number; usedSlots!: number;

View File

@@ -4,9 +4,9 @@ import { IsString } from 'class-validator';
export class ApproveJoinRequestInputDTO { export class ApproveJoinRequestInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
requestId: string; requestId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -4,7 +4,7 @@ import { IsString, IsBoolean } from 'class-validator';
export class ApproveJoinRequestOutputDTO { export class ApproveJoinRequestOutputDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsString() @IsString()

View File

@@ -4,17 +4,17 @@ import { IsString, IsEnum } from 'class-validator';
export class CreateLeagueInputDTO { export class CreateLeagueInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
description: string; description!: string;
@ApiProperty({ enum: ['public', 'private'] }) @ApiProperty({ enum: ['public', 'private'] })
@IsEnum(['public', 'private']) @IsEnum(['public', 'private'])
visibility: 'public' | 'private'; visibility!: 'public' | 'private';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
ownerId: string; ownerId!: string;
} }

View File

@@ -4,9 +4,9 @@ import { IsString, IsBoolean } from 'class-validator';
export class CreateLeagueOutputDTO { export class CreateLeagueOutputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
} }

View File

@@ -8,5 +8,5 @@ export class GetLeagueAdminConfigOutputDTO {
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelDTO) @Type(() => LeagueConfigFormModelDTO)
form: LeagueConfigFormModelDTO | null; form!: LeagueConfigFormModelDTO | null;
} }

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetLeagueAdminConfigQueryDTO { export class GetLeagueAdminConfigQueryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -4,9 +4,9 @@ import { IsString } from 'class-validator';
export class GetLeagueAdminPermissionsInputDTO { export class GetLeagueAdminPermissionsInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
performerDriverId: string; performerDriverId!: string;
} }

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetLeagueJoinRequestsQueryDTO { export class GetLeagueJoinRequestsQueryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -4,9 +4,9 @@ import { IsString } from 'class-validator';
export class GetLeagueOwnerSummaryQueryDTO { export class GetLeagueOwnerSummaryQueryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
ownerId: string; ownerId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetLeagueProtestsQueryDTO { export class GetLeagueProtestsQueryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -3,5 +3,5 @@ import { RaceDTO } from '../../race/dtos/RaceDTO';
export class GetLeagueRacesOutputDTO { export class GetLeagueRacesOutputDTO {
@ApiProperty({ type: [RaceDTO] }) @ApiProperty({ type: [RaceDTO] })
races: RaceDTO[]; races!: RaceDTO[];
} }

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetLeagueSeasonsQueryDTO { export class GetLeagueSeasonsQueryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -2,28 +2,28 @@ import { ApiProperty } from '@nestjs/swagger';
export class WalletTransactionDTO { export class WalletTransactionDTO {
@ApiProperty() @ApiProperty()
id: string; id!: string;
@ApiProperty({ enum: ['sponsorship', 'membership', 'withdrawal', 'prize'] }) @ApiProperty({ enum: ['sponsorship', 'membership', 'withdrawal', 'prize'] })
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize'; type!: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
@ApiProperty() @ApiProperty()
description: string; description!: string;
@ApiProperty() @ApiProperty()
amount: number; amount!: number;
@ApiProperty() @ApiProperty()
fee: number; fee!: number;
@ApiProperty() @ApiProperty()
netAmount: number; netAmount!: number;
@ApiProperty() @ApiProperty()
date: string; date!: string;
@ApiProperty({ enum: ['completed', 'pending', 'failed'] }) @ApiProperty({ enum: ['completed', 'pending', 'failed'] })
status: 'completed' | 'pending' | 'failed'; status!: 'completed' | 'pending' | 'failed';
@ApiProperty({ required: false }) @ApiProperty({ required: false })
reference?: string; reference?: string;
@@ -31,29 +31,29 @@ export class WalletTransactionDTO {
export class GetLeagueWalletOutputDTO { export class GetLeagueWalletOutputDTO {
@ApiProperty() @ApiProperty()
balance: number; balance!: number;
@ApiProperty() @ApiProperty()
currency: string; currency!: string;
@ApiProperty() @ApiProperty()
totalRevenue: number; totalRevenue!: number;
@ApiProperty() @ApiProperty()
totalFees: number; totalFees!: number;
@ApiProperty() @ApiProperty()
totalWithdrawals: number; totalWithdrawals!: number;
@ApiProperty() @ApiProperty()
pendingPayouts: number; pendingPayouts!: number;
@ApiProperty() @ApiProperty()
canWithdraw: boolean; canWithdraw!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
withdrawalBlockReason?: string; withdrawalBlockReason?: string;
@ApiProperty({ type: [WalletTransactionDTO] }) @ApiProperty({ type: [WalletTransactionDTO] })
transactions: WalletTransactionDTO[]; transactions!: WalletTransactionDTO[];
} }

View File

@@ -3,5 +3,5 @@ import { SponsorshipDetailDTO } from '../../sponsor/dtos/SponsorshipDetailDTO';
export class GetSeasonSponsorshipsOutputDTO { export class GetSeasonSponsorshipsOutputDTO {
@ApiProperty({ type: [SponsorshipDetailDTO] }) @ApiProperty({ type: [SponsorshipDetailDTO] })
sponsorships: SponsorshipDetailDTO[]; sponsorships!: SponsorshipDetailDTO[];
} }

View File

@@ -8,5 +8,5 @@ export class LeagueAdminConfigDTO {
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelDTO) @Type(() => LeagueConfigFormModelDTO)
form: LeagueConfigFormModelDTO | null; form!: LeagueConfigFormModelDTO | null;
} }

View File

@@ -12,27 +12,27 @@ export class LeagueAdminDTO {
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => LeagueJoinRequestDTO) @Type(() => LeagueJoinRequestDTO)
joinRequests: LeagueJoinRequestDTO[]; joinRequests!: LeagueJoinRequestDTO[];
@ApiProperty({ type: () => LeagueOwnerSummaryDTO, nullable: true }) @ApiProperty({ type: () => LeagueOwnerSummaryDTO, nullable: true })
@IsOptional() @IsOptional()
@ValidateNested() @ValidateNested()
@Type(() => LeagueOwnerSummaryDTO) @Type(() => LeagueOwnerSummaryDTO)
ownerSummary: LeagueOwnerSummaryDTO | null; ownerSummary!: LeagueOwnerSummaryDTO | null;
@ApiProperty({ type: () => LeagueAdminConfigDTO }) @ApiProperty({ type: () => LeagueAdminConfigDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueAdminConfigDTO) @Type(() => LeagueAdminConfigDTO)
config: LeagueAdminConfigDTO; config!: LeagueAdminConfigDTO;
@ApiProperty({ type: () => LeagueAdminProtestsDTO }) @ApiProperty({ type: () => LeagueAdminProtestsDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueAdminProtestsDTO) @Type(() => LeagueAdminProtestsDTO)
protests: LeagueAdminProtestsDTO; protests!: LeagueAdminProtestsDTO;
@ApiProperty({ type: [LeagueSeasonSummaryDTO] }) @ApiProperty({ type: [LeagueSeasonSummaryDTO] })
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => LeagueSeasonSummaryDTO) @Type(() => LeagueSeasonSummaryDTO)
seasons: LeagueSeasonSummaryDTO[]; seasons!: LeagueSeasonSummaryDTO[];
} }

View File

@@ -4,9 +4,9 @@ import { IsBoolean } from 'class-validator';
export class LeagueAdminPermissionsDTO { export class LeagueAdminPermissionsDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
canRemoveMember: boolean; canRemoveMember!: boolean;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
canUpdateRoles: boolean; canUpdateRoles!: boolean;
} }

View File

@@ -1,24 +1,24 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsArray, ValidateNested } from 'class-validator'; import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { DriverDto } from '../../driver/dto/DriverDto'; import { DriverDTO } from '../../driver/dtos/DriverDTO';
import { RaceDto } from '../../race/dto/RaceDto'; import { RaceDTO } from '../../race/dtos/RaceDTO';
import { ProtestDTO } from './ProtestDTO'; import { ProtestDTO } from './ProtestDTO';
export class LeagueAdminProtestsDTO { export class LeagueAdminProtestsDTO {
@ApiProperty({ type: [ProtestDTO] }) @ApiProperty({ type: [ProtestDTO] })
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => ProtestDTO) @Type(() => ProtestDTO)
protests: ProtestDTO[]; protests!: ProtestDTO[];
@ApiProperty({ type: () => RaceDto }) @ApiProperty({ type: () => RaceDTO })
@ValidateNested() @ValidateNested()
@Type(() => RaceDto) @Type(() => RaceDTO)
racesById: { [raceId: string]: RaceDto }; racesById!: { [raceId: string]: RaceDTO };
@ApiProperty({ type: () => DriverDto }) @ApiProperty({ type: () => DriverDTO })
@ValidateNested() @ValidateNested()
@Type(() => DriverDto) @Type(() => DriverDTO)
driversById: { [driverId: string]: DriverDto }; driversById!: { [driverId: string]: DriverDTO };
} }

View File

@@ -4,13 +4,13 @@ import { IsString, IsEnum } from 'class-validator';
export class LeagueConfigFormModelBasicsDTO { export class LeagueConfigFormModelBasicsDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
description: string; description!: string;
@ApiProperty({ enum: ['public', 'private'] }) @ApiProperty({ enum: ['public', 'private'] })
@IsEnum(['public', 'private']) @IsEnum(['public', 'private'])
visibility: 'public' | 'private'; visibility!: 'public' | 'private';
} }

View File

@@ -11,39 +11,39 @@ import { LeagueConfigFormModelTimingsDTO } from './LeagueConfigFormModelTimingsD
export class LeagueConfigFormModelDTO { export class LeagueConfigFormModelDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty({ type: LeagueConfigFormModelBasicsDTO }) @ApiProperty({ type: LeagueConfigFormModelBasicsDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelBasicsDTO) @Type(() => LeagueConfigFormModelBasicsDTO)
basics: LeagueConfigFormModelBasicsDTO; basics!: LeagueConfigFormModelBasicsDTO;
@ApiProperty({ type: LeagueConfigFormModelStructureDTO }) @ApiProperty({ type: LeagueConfigFormModelStructureDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelStructureDTO) @Type(() => LeagueConfigFormModelStructureDTO)
structure: LeagueConfigFormModelStructureDTO; structure!: LeagueConfigFormModelStructureDTO;
@ApiProperty({ type: [Object] }) @ApiProperty({ type: [Object] })
@IsArray() @IsArray()
championships: Object[]; championships!: Object[];
@ApiProperty({ type: LeagueConfigFormModelScoringDTO }) @ApiProperty({ type: LeagueConfigFormModelScoringDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelScoringDTO) @Type(() => LeagueConfigFormModelScoringDTO)
scoring: LeagueConfigFormModelScoringDTO; scoring!: LeagueConfigFormModelScoringDTO;
@ApiProperty({ type: LeagueConfigFormModelDropPolicyDTO }) @ApiProperty({ type: LeagueConfigFormModelDropPolicyDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelDropPolicyDTO) @Type(() => LeagueConfigFormModelDropPolicyDTO)
dropPolicy: LeagueConfigFormModelDropPolicyDTO; dropPolicy!: LeagueConfigFormModelDropPolicyDTO;
@ApiProperty({ type: LeagueConfigFormModelTimingsDTO }) @ApiProperty({ type: LeagueConfigFormModelTimingsDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelTimingsDTO) @Type(() => LeagueConfigFormModelTimingsDTO)
timings: LeagueConfigFormModelTimingsDTO; timings!: LeagueConfigFormModelTimingsDTO;
@ApiProperty({ type: LeagueConfigFormModelStewardingDTO }) @ApiProperty({ type: LeagueConfigFormModelStewardingDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueConfigFormModelStewardingDTO) @Type(() => LeagueConfigFormModelStewardingDTO)
stewarding: LeagueConfigFormModelStewardingDTO; stewarding!: LeagueConfigFormModelStewardingDTO;
} }

View File

@@ -4,7 +4,7 @@ import { IsNumber, IsOptional, IsEnum } from 'class-validator';
export class LeagueConfigFormModelDropPolicyDTO { export class LeagueConfigFormModelDropPolicyDTO {
@ApiProperty({ enum: ['none', 'worst_n'] }) @ApiProperty({ enum: ['none', 'worst_n'] })
@IsEnum(['none', 'worst_n']) @IsEnum(['none', 'worst_n'])
strategy: 'none' | 'worst_n'; strategy!: 'none' | 'worst_n';
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()

View File

@@ -4,9 +4,9 @@ import { IsString, IsNumber } from 'class-validator';
export class LeagueConfigFormModelScoringDTO { export class LeagueConfigFormModelScoringDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
type: string; type!: string;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
points: number; points!: number;
} }

View File

@@ -4,7 +4,7 @@ import { IsNumber, IsBoolean, IsOptional, IsEnum } from 'class-validator';
export class LeagueConfigFormModelStewardingDTO { export class LeagueConfigFormModelStewardingDTO {
@ApiProperty({ enum: ['single_steward', 'committee_vote'] }) @ApiProperty({ enum: ['single_steward', 'committee_vote'] })
@IsEnum(['single_steward', 'committee_vote']) @IsEnum(['single_steward', 'committee_vote'])
decisionMode: 'single_steward' | 'committee_vote'; decisionMode!: 'single_steward' | 'committee_vote';
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -13,29 +13,29 @@ export class LeagueConfigFormModelStewardingDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
requireDefense: boolean; requireDefense!: boolean;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
defenseTimeLimit: number; defenseTimeLimit!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
voteTimeLimit: number; voteTimeLimit!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
protestDeadlineHours: number; protestDeadlineHours!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
stewardingClosesHours: number; stewardingClosesHours!: number;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
notifyAccusedOnProtest: boolean; notifyAccusedOnProtest!: boolean;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
notifyOnVoteRequired: boolean; notifyOnVoteRequired!: boolean;
} }

View File

@@ -5,5 +5,5 @@ export class LeagueConfigFormModelStructureDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsEnum(['solo', 'team']) @IsEnum(['solo', 'team'])
mode: 'solo' | 'team'; mode!: 'solo' | 'team';
} }

View File

@@ -4,13 +4,13 @@ import { IsString, IsNumber } from 'class-validator';
export class LeagueConfigFormModelTimingsDTO { export class LeagueConfigFormModelTimingsDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
raceDayOfWeek: string; raceDayOfWeek!: string;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
raceTimeHour: number; raceTimeHour!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
raceTimeMinute: number; raceTimeMinute!: number;
} }

View File

@@ -2,23 +2,28 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsDate, IsOptional } from 'class-validator'; import { IsString, IsDate, IsOptional } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
export interface DriverInfo {
id: string;
name: string;
}
export class LeagueJoinRequestDTO { export class LeagueJoinRequestDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
driverId: string; driverId!: string;
@ApiProperty() @ApiProperty()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
requestedAt: Date; requestedAt!: Date;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -30,8 +35,5 @@ export class LeagueJoinRequestDTO {
type: () => Object, type: () => Object,
}) })
@IsOptional() @IsOptional()
driver?: { driver?: DriverInfo;
id: string;
name: string;
};
} }

View File

@@ -1,24 +1,24 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsDate, IsEnum, ValidateNested } from 'class-validator'; import { IsString, IsDate, IsEnum, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { DriverDto } from '../../driver/dto/DriverDto'; import { DriverDTO } from '../../driver/dtos/DriverDTO';
export class LeagueMemberDTO { export class LeagueMemberDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
driverId: string; driverId!: string;
@ApiProperty({ type: () => DriverDto }) @ApiProperty({ type: () => DriverDTO })
@ValidateNested() @ValidateNested()
@Type(() => DriverDto) @Type(() => DriverDTO)
driver: DriverDto; driver!: DriverDTO;
@ApiProperty({ enum: ['owner', 'manager', 'member'] }) @ApiProperty({ enum: ['owner', 'manager', 'member'] })
@IsEnum(['owner', 'manager', 'member']) @IsEnum(['owner', 'manager', 'member'])
role: 'owner' | 'manager' | 'member'; role!: 'owner' | 'manager' | 'member';
@ApiProperty() @ApiProperty()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
joinedAt: Date; joinedAt!: Date;
} }

View File

@@ -4,25 +4,25 @@ import { IsString, IsEnum } from 'class-validator';
export class LeagueMembershipDTO { export class LeagueMembershipDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
driverId: string; driverId!: string;
@ApiProperty({ enum: ['owner', 'admin', 'steward', 'member'] }) @ApiProperty({ enum: ['owner', 'admin', 'steward', 'member'] })
@IsEnum(['owner', 'admin', 'steward', 'member']) @IsEnum(['owner', 'admin', 'steward', 'member'])
role: 'owner' | 'admin' | 'steward' | 'member'; role!: 'owner' | 'admin' | 'steward' | 'member';
@ApiProperty({ enum: ['active', 'inactive', 'pending'] }) @ApiProperty({ enum: ['active', 'inactive', 'pending'] })
@IsEnum(['active', 'inactive', 'pending']) @IsEnum(['active', 'inactive', 'pending'])
status: 'active' | 'inactive' | 'pending'; status!: 'active' | 'inactive' | 'pending';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
joinedAt: string; joinedAt!: string;
} }

View File

@@ -8,5 +8,5 @@ export class LeagueMembershipsDTO {
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => LeagueMemberDTO) @Type(() => LeagueMemberDTO)
members: LeagueMemberDTO[]; members!: LeagueMemberDTO[];
} }

View File

@@ -1,21 +1,21 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNumber, IsOptional, ValidateNested } from 'class-validator'; import { IsNumber, IsOptional, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { DriverDto } from '../../driver/dto/DriverDto'; import { DriverDTO } from '../../driver/dtos/DriverDTO';
export class LeagueOwnerSummaryDTO { export class LeagueOwnerSummaryDTO {
@ApiProperty({ type: () => DriverDto }) @ApiProperty({ type: () => DriverDTO })
@ValidateNested() @ValidateNested()
@Type(() => DriverDto) @Type(() => DriverDTO)
driver: DriverDto; driver!: DriverDTO;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
rating: number | null; rating!: number | null;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
rank: number | null; rank!: number | null;
} }

View File

@@ -4,5 +4,5 @@ import { IsEnum } from 'class-validator';
export class LeagueRoleDTO { export class LeagueRoleDTO {
@ApiProperty({ enum: ['owner', 'admin', 'steward', 'member'] }) @ApiProperty({ enum: ['owner', 'admin', 'steward', 'member'] })
@IsEnum(['owner', 'admin', 'steward', 'member']) @IsEnum(['owner', 'admin', 'steward', 'member'])
value: 'owner' | 'admin' | 'steward' | 'member'; value!: 'owner' | 'admin' | 'steward' | 'member';
} }

View File

@@ -1,12 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsArray, ValidateNested } from 'class-validator'; import { IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { RaceDto } from '../../race/dto/RaceDto'; import { RaceDTO } from '../../race/dtos/RaceDTO';
export class LeagueScheduleDTO { export class LeagueScheduleDTO {
@ApiProperty({ type: [RaceDto] }) @ApiProperty({ type: [RaceDTO] })
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => RaceDto) @Type(() => RaceDTO)
races: RaceDto[]; races!: RaceDTO[];
} }

View File

@@ -4,29 +4,29 @@ import { IsString, IsEnum } from 'class-validator';
export class LeagueScoringPresetDTO { export class LeagueScoringPresetDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
description: string; description!: string;
@ApiProperty({ enum: ['driver', 'team', 'nations', 'trophy'] }) @ApiProperty({ enum: ['driver', 'team', 'nations', 'trophy'] })
@IsEnum(['driver', 'team', 'nations', 'trophy']) @IsEnum(['driver', 'team', 'nations', 'trophy'])
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy'; primaryChampionshipType!: 'driver' | 'team' | 'nations' | 'trophy';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
sessionSummary: string; sessionSummary!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
bonusSummary: string; bonusSummary!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
dropPolicySummary: string; dropPolicySummary!: string;
} }

View File

@@ -5,15 +5,15 @@ import { Type } from 'class-transformer';
export class LeagueSeasonSummaryDTO { export class LeagueSeasonSummaryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
seasonId: string; seasonId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
status: string; status!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -29,9 +29,9 @@ export class LeagueSeasonSummaryDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
isPrimary: boolean; isPrimary!: boolean;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
isParallelActive: boolean; isParallelActive!: boolean;
} }

View File

@@ -1,23 +1,23 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, ValidateNested } from 'class-validator'; import { IsString, IsNumber, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { DriverDto } from '../../driver/dto/DriverDto'; import { DriverDTO } from '../../driver/dtos/DriverDTO';
export class LeagueStandingDTO { export class LeagueStandingDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
driverId: string; driverId!: string;
@ApiProperty({ type: () => DriverDto }) @ApiProperty({ type: () => DriverDTO })
@ValidateNested() @ValidateNested()
@Type(() => DriverDto) @Type(() => DriverDTO)
driver: DriverDto; driver!: DriverDTO;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
points: number; points!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
rank: number; rank!: number;
} }

View File

@@ -8,5 +8,5 @@ export class LeagueStandingsDTO {
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => LeagueStandingDTO) @Type(() => LeagueStandingDTO)
standings: LeagueStandingDTO[]; standings!: LeagueStandingDTO[];
} }

View File

@@ -4,13 +4,13 @@ import { IsNumber } from 'class-validator';
export class LeagueStatsDTO { export class LeagueStatsDTO {
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
totalMembers: number; totalMembers!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
totalRaces: number; totalRaces!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
averageRating: number; averageRating!: number;
} }

View File

@@ -4,11 +4,11 @@ import { IsString, IsNumber, IsBoolean, IsOptional } from 'class-validator';
export class LeagueSummaryDTO { export class LeagueSummaryDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@IsOptional() @IsOptional()
@@ -27,19 +27,19 @@ export class LeagueSummaryDTO {
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
memberCount: number; memberCount!: number;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
maxMembers: number; maxMembers!: number;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
isPublic: boolean; isPublic!: boolean;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
ownerId: string; ownerId!: string;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@IsOptional() @IsOptional()

View File

@@ -6,11 +6,11 @@ import { LeagueSettingsDTO } from './LeagueSettingsDTO';
export class LeagueWithCapacityDTO { export class LeagueWithCapacityDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
// ... other properties of LeagueWithCapacityDTO // ... other properties of LeagueWithCapacityDTO
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@@ -20,20 +20,20 @@ export class LeagueWithCapacityDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
ownerId: string; ownerId!: string;
@ApiProperty({ type: () => LeagueSettingsDTO }) @ApiProperty({ type: () => LeagueSettingsDTO })
@ValidateNested() @ValidateNested()
@Type(() => LeagueSettingsDTO) @Type(() => LeagueSettingsDTO)
settings: LeagueSettingsDTO; settings!: LeagueSettingsDTO;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
createdAt: string; createdAt!: string;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
usedSlots: number; usedSlots!: number;
@ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links @ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
@IsOptional() @IsOptional()

View File

@@ -4,5 +4,5 @@ import { IsEnum } from 'class-validator';
export class MembershipRoleDTO { export class MembershipRoleDTO {
@ApiProperty({ enum: ['owner', 'admin', 'steward', 'member'] }) @ApiProperty({ enum: ['owner', 'admin', 'steward', 'member'] })
@IsEnum(['owner', 'admin', 'steward', 'member']) @IsEnum(['owner', 'admin', 'steward', 'member'])
value: 'owner' | 'admin' | 'steward' | 'member'; value!: 'owner' | 'admin' | 'steward' | 'member';
} }

View File

@@ -4,5 +4,5 @@ import { IsEnum } from 'class-validator';
export class MembershipStatusDTO { export class MembershipStatusDTO {
@ApiProperty({ enum: ['active', 'inactive', 'pending'] }) @ApiProperty({ enum: ['active', 'inactive', 'pending'] })
@IsEnum(['active', 'inactive', 'pending']) @IsEnum(['active', 'inactive', 'pending'])
value: 'active' | 'inactive' | 'pending'; value!: 'active' | 'inactive' | 'pending';
} }

View File

@@ -13,34 +13,34 @@ import { Type } from 'class-transformer';
export class ProtestDTO { export class ProtestDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
raceId: string; raceId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
protestingDriverId: string; protestingDriverId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
accusedDriverId: string; accusedDriverId!: string;
@ApiProperty() @ApiProperty()
@IsDate() @IsDate()
@Type(() => Date) @Type(() => Date)
submittedAt: Date; submittedAt!: Date;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
description: string; description!: string;
@ApiProperty({ enum: ['pending', 'accepted', 'rejected'] }) @ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
@IsEnum(['pending', 'accepted', 'rejected']) @IsEnum(['pending', 'accepted', 'rejected'])
status: 'pending' | 'accepted' | 'rejected'; status!: 'pending' | 'accepted' | 'rejected';
} }

View File

@@ -4,9 +4,9 @@ import { IsString } from 'class-validator';
export class RejectJoinRequestInputDTO { export class RejectJoinRequestInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
requestId: string; requestId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
} }

View File

@@ -4,7 +4,7 @@ import { IsString, IsBoolean } from 'class-validator';
export class RejectJoinRequestOutputDTO { export class RejectJoinRequestOutputDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsString() @IsString()

View File

@@ -4,13 +4,13 @@ import { IsString } from 'class-validator';
export class RemoveLeagueMemberInputDTO { export class RemoveLeagueMemberInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
performerDriverId: string; performerDriverId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
targetDriverId: string; targetDriverId!: string;
} }

View File

@@ -4,7 +4,7 @@ import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class RemoveLeagueMemberOutputDTO { export class RemoveLeagueMemberOutputDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()

View File

@@ -5,15 +5,15 @@ import { Type } from 'class-transformer';
export class SeasonDTO { export class SeasonDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
seasonId: string; seasonId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -29,11 +29,11 @@ export class SeasonDTO {
@ApiProperty({ enum: ['planned', 'active', 'completed'] }) @ApiProperty({ enum: ['planned', 'active', 'completed'] })
@IsEnum(['planned', 'active', 'completed']) @IsEnum(['planned', 'active', 'completed'])
status: 'planned' | 'active' | 'completed'; status!: 'planned' | 'active' | 'completed';
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
isPrimary: boolean; isPrimary!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()

View File

@@ -4,5 +4,5 @@ import { IsNumber } from 'class-validator';
export class TotalLeaguesDTO { export class TotalLeaguesDTO {
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
totalLeagues: number; totalLeagues!: number;
} }

View File

@@ -4,17 +4,17 @@ import { IsString, IsEnum } from 'class-validator';
export class UpdateLeagueMemberRoleInputDTO { export class UpdateLeagueMemberRoleInputDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
leagueId: string; leagueId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
performerDriverId: string; performerDriverId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
targetDriverId: string; targetDriverId!: string;
@ApiProperty({ enum: ['owner', 'manager', 'member'] }) @ApiProperty({ enum: ['owner', 'manager', 'member'] })
@IsEnum(['owner', 'manager', 'member']) @IsEnum(['owner', 'manager', 'member'])
newRole: 'owner' | 'manager' | 'member'; newRole!: 'owner' | 'manager' | 'member';
} }

View File

@@ -4,7 +4,7 @@ import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class UpdateLeagueMemberRoleOutputDTO { export class UpdateLeagueMemberRoleOutputDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()

View File

@@ -2,14 +2,14 @@ import { ApiProperty } from '@nestjs/swagger';
export class WithdrawFromLeagueWalletInputDTO { export class WithdrawFromLeagueWalletInputDTO {
@ApiProperty() @ApiProperty()
amount: number; amount!: number;
@ApiProperty() @ApiProperty()
currency: string; currency!: string;
@ApiProperty() @ApiProperty()
seasonId: string; seasonId!: string;
@ApiProperty() @ApiProperty()
destinationAccount: string; destinationAccount!: string;
} }

View File

@@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
export class WithdrawFromLeagueWalletOutputDTO { export class WithdrawFromLeagueWalletOutputDTO {
@ApiProperty() @ApiProperty()
success: boolean; success!: boolean;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
message?: string; message?: string;

View File

@@ -4,5 +4,5 @@ import { IsEnum } from 'class-validator';
export class WizardStepDTO { export class WizardStepDTO {
@ApiProperty({ enum: [1, 2, 3, 4, 5, 6, 7] }) @ApiProperty({ enum: [1, 2, 3, 4, 5, 6, 7] })
@IsEnum([1, 2, 3, 4, 5, 6, 7]) @IsEnum([1, 2, 3, 4, 5, 6, 7])
value: 1 | 2 | 3 | 4 | 5 | 6 | 7; value!: 1 | 2 | 3 | 4 | 5 | 6 | 7;
} }

View File

@@ -1,30 +1,26 @@
import { GetLeagueOwnerSummaryOutputPort } from '@core/racing/application/ports/output/GetLeagueOwnerSummaryOutputPort'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetLeagueOwnerSummaryResult } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { LeagueOwnerSummaryDTO } from '../dtos/LeagueOwnerSummaryDTO'; import { LeagueOwnerSummaryDTO } from '../dtos/LeagueOwnerSummaryDTO';
export class GetLeagueOwnerSummaryPresenter { export class GetLeagueOwnerSummaryPresenter implements UseCaseOutputPort<GetLeagueOwnerSummaryResult> {
private result: LeagueOwnerSummaryDTO | null = null; private result: LeagueOwnerSummaryDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(output: GetLeagueOwnerSummaryOutputPort) { present(output: GetLeagueOwnerSummaryResult) {
if (!output.summary) {
this.result = null;
return;
}
this.result = { this.result = {
driver: { driver: {
id: output.summary.driver.id, id: output.owner.id,
iracingId: output.summary.driver.iracingId, iracingId: output.owner.iracingId.toString(),
name: output.summary.driver.name, name: output.owner.name.toString(),
country: output.summary.driver.country, country: output.owner.country.toString(),
bio: output.summary.driver.bio, joinedAt: output.owner.joinedAt.toDate().toISOString(),
joinedAt: output.summary.driver.joinedAt, ...(output.owner.bio ? { bio: output.owner.bio.toString() } : {}),
}, },
rating: output.summary.rating, rating: output.rating,
rank: output.summary.rank, rank: output.rank,
}; };
} }

View File

@@ -1,10 +1,11 @@
import { GetLeagueProtestsOutputPort, type ProtestOutputPort } from '@core/racing/application/ports/output/GetLeagueProtestsOutputPort'; import { Presenter } from '@core/shared/presentation';
import type { GetLeagueProtestsResult } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
import { LeagueAdminProtestsDTO } from '../dtos/LeagueAdminProtestsDTO'; import { LeagueAdminProtestsDTO } from '../dtos/LeagueAdminProtestsDTO';
import { ProtestDTO } from '../dtos/ProtestDTO'; import { ProtestDTO } from '../dtos/ProtestDTO';
import { RaceDTO } from '../../race/dtos/RaceDTO'; import { RaceDTO } from '../../race/dtos/RaceDTO';
import { DriverDTO } from '../../driver/dtos/DriverDTO'; import { DriverDTO } from '../../driver/dtos/DriverDTO';
function mapProtestStatus(status: ProtestOutputPort['status']): ProtestDTO['status'] { function mapProtestStatus(status: string): ProtestDTO['status'] {
switch (status) { switch (status) {
case 'pending': case 'pending':
case 'awaiting_defense': case 'awaiting_defense':
@@ -20,53 +21,63 @@ function mapProtestStatus(status: ProtestOutputPort['status']): ProtestDTO['stat
} }
} }
export class GetLeagueProtestsPresenter { export class GetLeagueProtestsPresenter implements Presenter<GetLeagueProtestsResult, LeagueAdminProtestsDTO> {
private result: LeagueAdminProtestsDTO | null = null; private result: LeagueAdminProtestsDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(output: GetLeagueProtestsOutputPort, leagueName?: string) { present(input: GetLeagueProtestsResult) {
const protests: ProtestDTO[] = output.protests.map((protest) => { const protests: ProtestDTO[] = input.protests.map((protestWithEntities) => {
const race = output.racesById[protest.raceId]; const { protest, race } = protestWithEntities;
return { return {
id: protest.id, id: protest.id.toString(),
leagueId: race?.leagueId || '', leagueId: race?.leagueId.toString() || '',
raceId: protest.raceId, raceId: protest.raceId.toString(),
protestingDriverId: protest.protestingDriverId, protestingDriverId: protest.protestingDriverId.toString(),
accusedDriverId: protest.accusedDriverId, accusedDriverId: protest.accusedDriverId.toString(),
submittedAt: new Date(protest.filedAt), submittedAt: new Date(protest.filedAt),
description: protest.incident.description, description: protest.incident.description.toString(),
status: mapProtestStatus(protest.status), status: mapProtestStatus(protest.status.toString()),
}; };
}); });
const racesById: { [raceId: string]: RaceDTO } = {}; const racesById: { [raceId: string]: RaceDTO } = {};
for (const raceId in output.racesById) { for (const protestWithEntities of input.protests) {
const race = output.racesById[raceId]; const { race } = protestWithEntities;
if (race) { if (race) {
racesById[raceId] = { racesById[race.id.toString()] = {
id: race.id, id: race.id.toString(),
name: race.track, name: race.track.toString(),
date: race.scheduledAt.toISOString(), date: race.scheduledAt.toISOString(),
leagueName, leagueName: input.league.name.toString(),
}; };
} }
} }
const driversById: { [driverId: string]: DriverDTO } = {}; const driversById: { [driverId: string]: DriverDTO } = {};
for (const driverId in output.driversById) { for (const protestWithEntities of input.protests) {
const driver = output.driversById[driverId]; const { protestingDriver, accusedDriver } = protestWithEntities;
if (driver) { if (protestingDriver) {
driversById[driverId] = { driversById[protestingDriver.id.toString()] = {
id: driver.id, id: protestingDriver.id.toString(),
iracingId: driver.iracingId, iracingId: protestingDriver.iracingId.toString(),
name: driver.name, name: protestingDriver.name.toString(),
country: driver.country, country: protestingDriver.country.toString(),
bio: driver.bio, bio: protestingDriver.bio?.toString(),
joinedAt: driver.joinedAt, joinedAt: protestingDriver.joinedAt.toDate().toISOString(),
};
}
if (accusedDriver) {
driversById[accusedDriver.id.toString()] = {
id: accusedDriver.id.toString(),
iracingId: accusedDriver.iracingId.toString(),
name: accusedDriver.name.toString(),
country: accusedDriver.country.toString(),
bio: accusedDriver.bio?.toString(),
joinedAt: accusedDriver.joinedAt.toISOString(),
}; };
} }
} }
@@ -78,7 +89,7 @@ export class GetLeagueProtestsPresenter {
}; };
} }
getViewModel(): LeagueAdminProtestsDTO | null { getResponseModel(): LeagueAdminProtestsDTO | null {
return this.result; return this.result;
} }
} }

View File

@@ -1,26 +1,27 @@
import { GetLeagueSeasonsOutputPort } from '@core/racing/application/ports/output/GetLeagueSeasonsOutputPort'; import { Presenter } from '@core/shared/presentation';
import type { GetLeagueSeasonsResult } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
import { LeagueSeasonSummaryDTO } from '../dtos/LeagueSeasonSummaryDTO'; import { LeagueSeasonSummaryDTO } from '../dtos/LeagueSeasonSummaryDTO';
export class GetLeagueSeasonsPresenter { export class GetLeagueSeasonsPresenter implements Presenter<GetLeagueSeasonsResult, LeagueSeasonSummaryDTO[]> {
private result: LeagueSeasonSummaryDTO[] | null = null; private result: LeagueSeasonSummaryDTO[] | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(output: GetLeagueSeasonsOutputPort) { present(input: GetLeagueSeasonsResult) {
this.result = output.seasons.map(season => ({ this.result = input.seasons.map(seasonSummary => ({
seasonId: season.seasonId, seasonId: seasonSummary.season.id.toString(),
name: season.name, name: seasonSummary.season.name.toString(),
status: season.status, status: seasonSummary.season.status.toString(),
startDate: season.startDate, startDate: seasonSummary.season.startDate.toISOString(),
endDate: season.endDate, endDate: seasonSummary.season.endDate?.toISOString(),
isPrimary: season.isPrimary, isPrimary: seasonSummary.isPrimary,
isParallelActive: season.isParallelActive, isParallelActive: seasonSummary.isParallelActive,
})); }));
} }
getViewModel(): LeagueSeasonSummaryDTO[] | null { getResponseModel(): LeagueSeasonSummaryDTO[] | null {
return this.result; return this.result;
} }
} }

View File

@@ -217,7 +217,7 @@ describe('MediaController', () => {
describe('updateAvatar', () => { describe('updateAvatar', () => {
it('should update avatar and return result', async () => { it('should update avatar and return result', async () => {
const driverId = 'driver-123'; const driverId = 'driver-123';
const input = { mediaUrl: 'https://example.com/new-avatar.png' } as UpdateAvatarOutputDTO; const input: UpdateAvatarInputDTO = { avatarUrl: 'https://example.com/new-avatar.png' };
const dto: UpdateAvatarOutputDTO = { const dto: UpdateAvatarOutputDTO = {
success: true, success: true,
}; };
@@ -225,9 +225,9 @@ describe('MediaController', () => {
const res = createMockResponse(); const res = createMockResponse();
await controller.updateAvatar(driverId, input as any, res); await controller.updateAvatar(driverId, input, res);
expect(service.updateAvatar).toHaveBeenCalledWith(driverId, input as any); expect(service.updateAvatar).toHaveBeenCalledWith(driverId, input);
expect(res.status).toHaveBeenCalledWith(200); expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(dto); expect(res.json).toHaveBeenCalledWith(dto);
}); });

View File

@@ -92,7 +92,7 @@ export class MediaService {
} }
async uploadMedia( async uploadMedia(
input: UploadMediaInput & { file: Express.Multer.File } & { userId?: string; metadata?: Record<string, any> }, input: UploadMediaInput & { file: Express.Multer.File } & { userId?: string; metadata?: Record<string, unknown> },
): Promise<UploadMediaOutputDTO> { ): Promise<UploadMediaOutputDTO> {
this.logger.debug('[MediaService] Uploading media.'); this.logger.debug('[MediaService] Uploading media.');

View File

@@ -11,7 +11,7 @@ export class UpdateAvatarPresenter implements UseCaseOutputPort<UpdateAvatarResu
this.model = null; this.model = null;
} }
present(result: UpdateAvatarResult): void { present(_result: UpdateAvatarResult): void {
this.model = { this.model = {
success: true, success: true,
}; };

View File

@@ -44,17 +44,11 @@ import type {
UpdateMemberPaymentInput, UpdateMemberPaymentInput,
UpdateMemberPaymentOutput, UpdateMemberPaymentOutput,
GetPrizesQuery, GetPrizesQuery,
GetPrizesOutput,
CreatePrizeInput, CreatePrizeInput,
CreatePrizeOutput,
AwardPrizeInput, AwardPrizeInput,
AwardPrizeOutput,
DeletePrizeInput, DeletePrizeInput,
DeletePrizeOutput,
GetWalletQuery, GetWalletQuery,
GetWalletOutput,
ProcessWalletTransactionInput, ProcessWalletTransactionInput,
ProcessWalletTransactionOutput,
} from './dtos/PaymentsDto'; } from './dtos/PaymentsDto';
// Injection tokens // Injection tokens

View File

@@ -28,7 +28,7 @@ export class GetPaymentsPresenter implements UseCaseOutputPort<GetPaymentsResult
}; };
} }
getResponseModel(): GetPaymentsOutput { get responseModel(): GetPaymentsOutput {
if (!this.responseModel) throw new Error('Presenter not presented'); if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel; return this.responseModel;
} }

View File

@@ -10,6 +10,11 @@ import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './present
// Tokens // Tokens
import { LOGGER_TOKEN } from './ProtestsProviders'; import { LOGGER_TOKEN } from './ProtestsProviders';
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN } from './ProtestsProviders';
@Injectable() @Injectable()
export class ProtestsService { export class ProtestsService {
constructor( constructor(

View File

@@ -1,5 +1,5 @@
import type { UseCaseOutputPort } from '@core/shared/application'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { ReviewProtestResult } from '@core/racing/application/use-cases/ReviewProtestUseCase'; import type { ReviewProtestResult, ReviewProtestApplicationError } from '@core/racing/application/use-cases/ReviewProtestUseCase';
export interface ReviewProtestResponseDTO { export interface ReviewProtestResponseDTO {
success: boolean; success: boolean;
@@ -25,6 +25,14 @@ export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestRe
}; };
} }
presentError(error: ReviewProtestApplicationError): void {
this.model = {
success: false,
errorCode: error.code,
message: error.details.message,
};
}
getResponseModel(): ReviewProtestResponseDTO | null { getResponseModel(): ReviewProtestResponseDTO | null {
return this.model; return this.model;
} }

View File

@@ -65,7 +65,7 @@ export class RaceController {
@Query('driverId') driverId: string, @Query('driverId') driverId: string,
): Promise<RaceDetailDTO> { ): Promise<RaceDetailDTO> {
const presenter = await this.raceService.getRaceDetail({ raceId, driverId }); const presenter = await this.raceService.getRaceDetail({ raceId, driverId });
return presenter.viewModel; return await presenter.viewModel;
} }
@Get(':raceId/results') @Get(':raceId/results')
@@ -74,7 +74,7 @@ export class RaceController {
@ApiResponse({ status: 200, description: 'Race results detail', type: RaceResultsDetailDTO }) @ApiResponse({ status: 200, description: 'Race results detail', type: RaceResultsDetailDTO })
async getRaceResultsDetail(@Param('raceId') raceId: string): Promise<RaceResultsDetailDTO> { async getRaceResultsDetail(@Param('raceId') raceId: string): Promise<RaceResultsDetailDTO> {
const presenter = await this.raceService.getRaceResultsDetail(raceId); const presenter = await this.raceService.getRaceResultsDetail(raceId);
return presenter.viewModel; return await presenter.viewModel;
} }
@Get(':raceId/sof') @Get(':raceId/sof')

View File

@@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort'; import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort'; import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort'; import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
@@ -130,14 +129,15 @@ export class RaceService {
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> { async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
this.logger.debug('[RaceService] Fetching race detail:', params); this.logger.debug('[RaceService] Fetching race detail:', params);
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService, params);
this.getRaceDetailUseCase.setOutput(presenter);
const result = await this.getRaceDetailUseCase.execute(params); const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) { if (result.isErr()) {
throw new Error('Failed to get race detail'); throw new Error('Failed to get race detail');
} }
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService);
await presenter.present(result.value as RaceDetailOutputPort, params);
return presenter; return presenter;
} }

View File

@@ -47,4 +47,8 @@ export class AllRacesPageDataPresenter {
return this.model; return this.model;
} }
get viewModel(): AllRacesPageDataResponseModel {
return this.responseModel;
}
} }

View File

@@ -1,4 +1,3 @@
import { Result } from '@core/shared/application/Result';
import { GetAllRacesPresenter } from './GetAllRacesPresenter'; import { GetAllRacesPresenter } from './GetAllRacesPresenter';
import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase'; import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase';
@@ -6,7 +5,7 @@ describe('GetAllRacesPresenter', () => {
it('should map races and distinct leagues into the DTO', async () => { it('should map races and distinct leagues into the DTO', async () => {
const presenter = new GetAllRacesPresenter(); const presenter = new GetAllRacesPresenter();
const output: GetAllRacesOutputPort = { const output: GetAllRacesResult = {
races: [ races: [
{ {
id: 'race-1', id: 'race-1',
@@ -14,9 +13,8 @@ describe('GetAllRacesPresenter', () => {
track: 'Track A', track: 'Track A',
car: 'Car A', car: 'Car A',
status: 'scheduled', status: 'scheduled',
scheduledAt: '2025-01-01T10:00:00.000Z', scheduledAt: new Date('2025-01-01T10:00:00.000Z'),
strengthOfField: 1500, strengthOfField: 1500,
leagueName: 'League One',
}, },
{ {
id: 'race-2', id: 'race-2',
@@ -24,9 +22,8 @@ describe('GetAllRacesPresenter', () => {
track: 'Track B', track: 'Track B',
car: 'Car B', car: 'Car B',
status: 'completed', status: 'completed',
scheduledAt: '2025-01-02T10:00:00.000Z', scheduledAt: new Date('2025-01-02T10:00:00.000Z'),
strengthOfField: null, strengthOfField: undefined,
leagueName: 'League One',
}, },
{ {
id: 'race-3', id: 'race-3',
@@ -34,16 +31,22 @@ describe('GetAllRacesPresenter', () => {
track: 'Track C', track: 'Track C',
car: 'Car C', car: 'Car C',
status: 'running', status: 'running',
scheduledAt: '2025-01-03T10:00:00.000Z', scheduledAt: new Date('2025-01-03T10:00:00.000Z'),
strengthOfField: 1800, strengthOfField: 1800,
leagueName: 'League Two',
}, },
], // eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leagues: [
{ id: 'league-1', name: 'League One' },
{ id: 'league-2', name: 'League Two' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any,
totalCount: 3, totalCount: 3,
}; };
await presenter.present(output); await presenter.present(output);
const viewModel = presenter.getViewModel(); const viewModel = presenter.getResponseModel();
expect(viewModel).not.toBeNull(); expect(viewModel).not.toBeNull();
expect(viewModel!.races).toHaveLength(3); expect(viewModel!.races).toHaveLength(3);
@@ -61,13 +64,16 @@ describe('GetAllRacesPresenter', () => {
it('should handle empty races by returning empty leagues', async () => { it('should handle empty races by returning empty leagues', async () => {
const presenter = new GetAllRacesPresenter(); const presenter = new GetAllRacesPresenter();
const output: GetAllRacesOutputPort = { const output: GetAllRacesResult = {
races: [], // eslint-disable-next-line @typescript-eslint/no-explicit-any
races: [] as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leagues: [] as any,
totalCount: 0, totalCount: 0,
}; };
await presenter.present(output); await presenter.present(output);
const viewModel = presenter.getViewModel(); const viewModel = presenter.getResponseModel();
expect(viewModel).not.toBeNull(); expect(viewModel).not.toBeNull();
expect(viewModel!.races).toHaveLength(0); expect(viewModel!.races).toHaveLength(0);

View File

@@ -53,4 +53,8 @@ export class GetAllRacesPresenter implements UseCaseOutputPort<GetAllRacesResult
return this.model; return this.model;
} }
get viewModel(): GetAllRacesResponseModel {
return this.responseModel;
}
} }

View File

@@ -44,4 +44,8 @@ export class GetTotalRacesPresenter {
return this.model; return this.model;
} }
get viewModel(): GetTotalRacesResponseModel {
return this.responseModel;
}
} }

View File

@@ -50,4 +50,8 @@ export class ImportRaceResultsApiPresenter {
return this.model; return this.model;
} }
get viewModel(): ImportRaceResultsApiResponseModel {
return this.responseModel;
}
} }

View File

@@ -1,9 +1,5 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { GetRaceDetailResult } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type {
GetRaceDetailResult,
GetRaceDetailErrorCode,
} from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO'; import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO';
@@ -16,47 +12,26 @@ import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO';
export type GetRaceDetailResponseModel = RaceDetailDTO; export type GetRaceDetailResponseModel = RaceDetailDTO;
export type GetRaceDetailApplicationError = ApplicationErrorCode< export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResult> {
GetRaceDetailErrorCode, private result: GetRaceDetailResult | null = null;
{ message: string }
>;
export class RaceDetailPresenter {
private model: GetRaceDetailResponseModel | null = null;
constructor( constructor(
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort, private readonly imageService: IImageServicePort,
private readonly params: GetRaceDetailParamsDTO,
) {} ) {}
reset(): void { present(result: GetRaceDetailResult): void {
this.model = null; this.result = result;
} }
async present( async getResponseModel(): Promise<GetRaceDetailResponseModel | null> {
result: Result<GetRaceDetailResult, GetRaceDetailApplicationError>, if (!this.result) {
params: GetRaceDetailParamsDTO, return null;
): Promise<void> {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
race: null,
league: null,
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
userResult: null,
} as RaceDetailDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race detail');
} }
const output = result.unwrap(); const output = this.result;
const params = this.params;
const raceDTO: RaceDetailRaceDTO | null = output.race const raceDTO: RaceDetailRaceDTO | null = output.race
? { ? {
@@ -118,7 +93,7 @@ export class RaceDetailPresenter {
} }
: null; : null;
this.model = { return {
race: raceDTO, race: raceDTO,
league: leagueDTO, league: leagueDTO,
entryList: entryListDTO, entryList: entryListDTO,
@@ -127,16 +102,11 @@ export class RaceDetailPresenter {
} as RaceDetailDTO; } as RaceDetailDTO;
} }
getResponseModel(): GetRaceDetailResponseModel | null { get viewModel(): Promise<GetRaceDetailResponseModel> {
return this.model; return this.getResponseModel().then(model => {
} if (!model) throw new Error('Presenter not presented');
return model;
get responseModel(): GetRaceDetailResponseModel { });
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
} }
private calculateRatingChange(position: number): number { private calculateRatingChange(position: number): number {

View File

@@ -62,4 +62,8 @@ export class RacePenaltiesPresenter {
return this.model; return this.model;
} }
get viewModel(): GetRacePenaltiesResponseModel {
return this.responseModel;
}
} }

View File

@@ -63,4 +63,8 @@ export class RaceProtestsPresenter {
return this.model; return this.model;
} }
get viewModel(): GetRaceProtestsResponseModel {
return this.responseModel;
}
} }

View File

@@ -1,4 +1,3 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { import type {
GetRaceResultsDetailResult, GetRaceResultsDetailResult,
@@ -16,33 +15,20 @@ export type GetRaceResultsDetailApplicationError = ApplicationErrorCode<
>; >;
export class RaceResultsDetailPresenter { export class RaceResultsDetailPresenter {
private model: GetRaceResultsDetailResponseModel | null = null; private result: GetRaceResultsDetailResult | null = null;
constructor(private readonly imageService: IImageServicePort) {} constructor(private readonly imageService: IImageServicePort) {}
reset(): void { present(result: GetRaceResultsDetailResult): void {
this.model = null; this.result = result;
} }
async present( async getResponseModel(): Promise<GetRaceResultsDetailResponseModel | null> {
result: Result<GetRaceResultsDetailResult, GetRaceResultsDetailApplicationError>, if (!this.result) {
): Promise<void> { return null;
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
raceId: '',
track: '',
results: [],
} as RaceResultsDetailDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race results detail');
} }
const output = result.unwrap(); const output = this.result;
const driverMap = new Map(output.drivers.map(driver => [driver.id, driver])); const driverMap = new Map(output.drivers.map(driver => [driver.id, driver]));
@@ -70,22 +56,17 @@ export class RaceResultsDetailPresenter {
}), }),
); );
this.model = { return {
raceId: output.race.id, raceId: output.race.id,
track: output.race.track, track: output.race.track,
results, results,
} as RaceResultsDetailDTO; } as RaceResultsDetailDTO;
} }
getResponseModel(): GetRaceResultsDetailResponseModel | null { get viewModel(): Promise<GetRaceResultsDetailResponseModel> {
return this.model; return this.getResponseModel().then(model => {
} if (!model) throw new Error('Presenter not presented');
return model;
get responseModel(): GetRaceResultsDetailResponseModel { });
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
} }
} }

View File

@@ -55,4 +55,8 @@ export class RaceWithSOFPresenter {
return this.model; return this.model;
} }
get viewModel(): GetRaceWithSOFResponseModel {
return this.responseModel;
}
} }

View File

@@ -59,4 +59,8 @@ export class RacesPageDataPresenter {
return this.model; return this.model;
} }
get viewModel(): GetRacesPageDataResponseModel {
return this.responseModel;
}
} }

View File

@@ -5,7 +5,7 @@ import { SponsorService } from './SponsorService';
import { NotificationService } from '@core/notifications/application/ports/NotificationService'; import { NotificationService } from '@core/notifications/application/ports/NotificationService';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository'; import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository'; import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import { IPaymentGateway } from '@core/racing/application/ports/IPaymentGateway'; import { IPaymentGateway } from '@core/payments/domain/ports/IPaymentGateway';
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository'; import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';

View File

@@ -2,15 +2,17 @@ import { Test, TestingModule } from '@nestjs/testing';
import { TeamService } from './TeamService'; import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import type { Logger } from '@core/shared/application/Logger';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter'; import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter'; import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
import { DriverTeamViewModel } from './dtos/TeamDto';
describe('TeamService', () => { describe('TeamService', () => {
let service: TeamService; let service: TeamService;
let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>; let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>;
let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>; let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>;
let logger: jest.Mocked<Logger>;
beforeEach(async () => { beforeEach(async () => {
const mockGetAllTeamsUseCase = { const mockGetAllTeamsUseCase = {
@@ -46,7 +48,6 @@ describe('TeamService', () => {
service = module.get<TeamService>(TeamService); service = module.get<TeamService>(TeamService);
getAllTeamsUseCase = module.get(GetAllTeamsUseCase); getAllTeamsUseCase = module.get(GetAllTeamsUseCase);
getDriverTeamUseCase = module.get(GetDriverTeamUseCase); getDriverTeamUseCase = module.get(GetDriverTeamUseCase);
logger = module.get('Logger');
}); });
it('should be defined', () => { it('should be defined', () => {
@@ -56,12 +57,14 @@ describe('TeamService', () => {
describe('getAll', () => { describe('getAll', () => {
it('should call use case and return result', async () => { it('should call use case and return result', async () => {
const mockResult = { isOk: () => true, value: { teams: [], totalCount: 0 } }; const mockResult = { isOk: () => true, value: { teams: [], totalCount: 0 } };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any); getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = { const mockPresenter = {
present: jest.fn(), present: jest.fn(),
getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }), getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }),
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter); (AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
const result = await service.getAll(); const result = await service.getAll();
@@ -74,12 +77,14 @@ describe('TeamService', () => {
describe('getDriverTeam', () => { describe('getDriverTeam', () => {
it('should call use case and return result', async () => { it('should call use case and return result', async () => {
const mockResult = { isOk: () => true, value: {} }; const mockResult = { isOk: () => true, value: {} };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any); getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = { const mockPresenter = {
present: jest.fn(), present: jest.fn(),
getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel), getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel),
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter); (DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
const result = await service.getDriverTeam('driver1'); const result = await service.getDriverTeam('driver1');
@@ -90,6 +95,7 @@ describe('TeamService', () => {
it('should return null on error', async () => { it('should return null on error', async () => {
const mockResult = { isErr: () => true, error: {} }; const mockResult = { isErr: () => true, error: {} };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any); getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
const result = await service.getDriverTeam('driver1'); const result = await service.getDriverTeam('driver1');

View File

@@ -12,14 +12,18 @@ import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO';
// Core imports // Core imports
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application/Logger';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Use cases // Use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase'; import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase'; import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase'; import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase'; import { CreateTeamUseCase, CreateTeamInput } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase'; import { UpdateTeamUseCase, UpdateTeamInput } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase'; import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
@@ -34,97 +38,90 @@ import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter'; import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
// Tokens // Tokens
import { LOGGER_TOKEN } from './TeamProviders'; import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN } from './TeamProviders';
@Injectable() @Injectable()
export class TeamService { export class TeamService {
constructor( constructor(
private readonly getAllTeamsUseCase: GetAllTeamsUseCase, @Inject(TEAM_REPOSITORY_TOKEN) private readonly teamRepository: ITeamRepository,
private readonly getTeamDetailsUseCase: GetTeamDetailsUseCase, @Inject(TEAM_MEMBERSHIP_REPOSITORY_TOKEN) private readonly membershipRepository: ITeamMembershipRepository,
private readonly getTeamMembersUseCase: GetTeamMembersUseCase, @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
private readonly getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase, @Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
private readonly createTeamUseCase: CreateTeamUseCase,
private readonly updateTeamUseCase: UpdateTeamUseCase,
private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
private readonly getTeamMembershipUseCase: GetTeamMembershipUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {} ) {}
async getAll(): Promise<any> { // TODO: type async getAll(): Promise<GetAllTeamsOutputDTO> {
this.logger.debug('[TeamService] Fetching all teams.'); this.logger.debug('[TeamService] Fetching all teams.');
const result = await this.getAllTeamsUseCase.execute();
const presenter = new AllTeamsPresenter(); const presenter = new AllTeamsPresenter();
const useCase = new GetAllTeamsUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute();
if (result.isErr()) { if (result.isErr()) {
this.logger.error('Error fetching all teams', result.error); this.logger.error('Error fetching all teams', result.error?.details?.message || 'Unknown error');
await presenter.present({ teams: [], totalCount: 0 }); return { teams: [], totalCount: 0 };
return presenter.responseModel;
} }
await presenter.present(result.value);
return presenter.responseModel; return presenter.responseModel;
} }
async getDetails(teamId: string, userId?: string): Promise<TeamDetailsPresenter> { async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`); this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`);
const presenter = new TeamDetailsPresenter(); const presenter = new TeamDetailsPresenter();
const result = await this.getTeamDetailsUseCase.execute({ teamId, driverId: userId || '' }); const useCase = new GetTeamDetailsUseCase(this.teamRepository, this.membershipRepository, presenter);
const result = await useCase.execute({ teamId, driverId: userId || '' });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team details for teamId: ${teamId}`, result.error); this.logger.error(`Error fetching team details for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
return presenter; return null;
} }
await presenter.present(result.value); return presenter.getResponseModel();
return presenter;
} }
async getMembers(teamId: string): Promise<TeamMembersPresenter> { async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`); this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter(); const presenter = new TeamMembersPresenter();
const result = await this.getTeamMembersUseCase.execute({ teamId }); const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.imageService, this.logger, presenter);
const result = await useCase.execute({ teamId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team members for teamId: ${teamId}`, result.error); this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
await presenter.present({ return {
members: [], members: [],
totalCount: 0, totalCount: 0,
ownerCount: 0, ownerCount: 0,
managerCount: 0, managerCount: 0,
memberCount: 0, memberCount: 0,
} as unknown as any); };
return presenter;
} }
await presenter.present(result.value); return presenter.getResponseModel()!;
return presenter;
} }
async getJoinRequests(teamId: string): Promise<TeamJoinRequestsPresenter> { async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`); this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`);
const presenter = new TeamJoinRequestsPresenter(); const presenter = new TeamJoinRequestsPresenter();
const result = await this.getTeamJoinRequestsUseCase.execute({ teamId }); const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter);
const result = await useCase.execute({ teamId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, result.error); this.logger.error(new Error(`Error fetching team join requests for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`));
await presenter.present({ return {
requests: [], requests: [],
pendingCount: 0, pendingCount: 0,
totalCount: 0, totalCount: 0,
} as unknown as any); };
return presenter;
} }
await presenter.present(result.value); return presenter.getResponseModel()!;
return presenter;
} }
async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamPresenter> { async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> {
this.logger.debug('[TeamService] Creating team', { input, userId }); this.logger.debug('[TeamService] Creating team', { input, userId });
const presenter = new CreateTeamPresenter(); const presenter = new CreateTeamPresenter();
const command = { const command: CreateTeamInput = {
name: input.name, name: input.name,
tag: input.tag, tag: input.tag,
description: input.description ?? '', description: input.description ?? '',
@@ -132,68 +129,66 @@ export class TeamService {
leagues: [], leagues: [],
}; };
const result = await this.createTeamUseCase.execute(command as any); const useCase = new CreateTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
this.logger.error('Error creating team', result.error); this.logger.error(`Error creating team: ${result.error?.details?.message || 'Unknown error'}`);
presenter.presentError(); return { id: '', success: false };
return presenter;
} }
presenter.presentSuccess(result.value); return presenter.responseModel;
return presenter;
} }
async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamPresenter> { async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> {
this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId }); this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId });
const presenter = new UpdateTeamPresenter(); const presenter = new UpdateTeamPresenter();
const command = { const command: UpdateTeamInput = {
teamId, teamId,
updates: { updates: {
name: input.name, ...(input.name !== undefined && { name: input.name }),
tag: input.tag, ...(input.tag !== undefined && { tag: input.tag }),
description: input.description, ...(input.description !== undefined && { description: input.description }),
}, },
updatedBy: userId || '', updatedBy: userId || '',
}; };
const result = await this.updateTeamUseCase.execute(command as any); const useCase = new UpdateTeamUseCase(this.teamRepository, this.membershipRepository, presenter);
const result = await useCase.execute(command);
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error updating team ${teamId}`, result.error); this.logger.error(`Error updating team ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
presenter.presentError(); return { success: false };
return presenter;
} }
presenter.presentSuccess(); return presenter.responseModel;
return presenter;
} }
async getDriverTeam(driverId: string): Promise<DriverTeamPresenter> { async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${driverId}`); this.logger.debug(`[TeamService] Fetching driver team for driverId: ${driverId}`);
const presenter = new DriverTeamPresenter(); const presenter = new DriverTeamPresenter();
const result = await this.getDriverTeamUseCase.execute({ driverId }); const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute({ driverId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching driver team for driverId: ${driverId}`, result.error); this.logger.error(`Error fetching driver team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return presenter; return null;
} }
await presenter.present(result.value); return presenter.getResponseModel();
return presenter;
} }
async getMembership(teamId: string, driverId: string): Promise<TeamMembershipPresenter> { async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`); this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`);
const presenter = new TeamMembershipPresenter(); const presenter = new TeamMembershipPresenter();
const result = await this.getTeamMembershipUseCase.execute({ teamId, driverId }); const useCase = new GetTeamMembershipUseCase(this.membershipRepository, this.logger, presenter);
const result = await useCase.execute({ teamId, driverId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}`, result.error); this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
return presenter; return null;
} }
presenter.present(result.value as any); return presenter.responseModel;
return presenter;
} }
} }

View File

@@ -1,36 +1,26 @@
import type { GetAllTeamsErrorCode, GetAllTeamsResult } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { Result } from '@core/shared/application/Result'; import type { GetAllTeamsResult } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO'; import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO';
export type GetAllTeamsError = ApplicationErrorCode<GetAllTeamsErrorCode, { message: string }>; export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
export class AllTeamsPresenter {
private model: GetAllTeamsOutputDTO | null = null; private model: GetAllTeamsOutputDTO | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<GetAllTeamsResult, GetAllTeamsError>): void { present(result: GetAllTeamsResult): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get teams');
}
const output = result.unwrap();
this.model = { this.model = {
teams: output.teams.map(team => ({ teams: result.teams.map(team => ({
id: team.id, id: team.id,
name: team.name, name: team.name.toString(),
tag: team.tag, tag: team.tag.toString(),
description: team.description, description: team.description?.toString() || '',
memberCount: team.memberCount, memberCount: team.memberCount,
leagues: team.leagues || [], leagues: team.leagues?.map(l => l.toString()) || [],
// Note: specialization, region, languages not available in output // Note: specialization, region, languages not available in output
})), })),
totalCount: output.totalCount ?? output.teams.length, totalCount: result.totalCount ?? result.teams.length,
}; };
} }

View File

@@ -1,36 +1,17 @@
import type { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { CreateTeamResult } from '@core/racing/application/use-cases/CreateTeamUseCase';
import type { CreateTeamErrorCode, CreateTeamResult } from '@core/racing/application/use-cases/CreateTeamUseCase';
import type { CreateTeamOutputDTO } from '../dtos/CreateTeamOutputDTO'; import type { CreateTeamOutputDTO } from '../dtos/CreateTeamOutputDTO';
export type CreateTeamError = ApplicationErrorCode<CreateTeamErrorCode, { message: string }>; export class CreateTeamPresenter implements UseCaseOutputPort<CreateTeamResult> {
export class CreateTeamPresenter {
private model: CreateTeamOutputDTO | null = null; private model: CreateTeamOutputDTO | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<CreateTeamResult, CreateTeamError>): void { present(result: CreateTeamResult): void {
if (result.isErr()) {
const error = result.unwrapErr();
// Validation and expected domain errors map to an unsuccessful DTO
if (error.code === 'VALIDATION_ERROR' || error.code === 'LEAGUE_NOT_FOUND') {
this.model = {
id: '',
success: false,
};
return;
}
throw new Error(error.details?.message ?? 'Failed to create team');
}
const output = result.unwrap();
this.model = { this.model = {
id: output.team.id, id: result.team.id,
success: true, success: true,
}; };
} }

View File

@@ -1,38 +1,39 @@
import { DriverTeamOutputPort } from '@core/racing/application/ports/output/DriverTeamOutputPort'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetDriverTeamResult } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetDriverTeamOutputDTO } from '../dtos/GetDriverTeamOutputDTO'; import { GetDriverTeamOutputDTO } from '../dtos/GetDriverTeamOutputDTO';
export class DriverTeamPresenter { export class DriverTeamPresenter implements UseCaseOutputPort<GetDriverTeamResult> {
private result: GetDriverTeamOutputDTO | null = null; private result: GetDriverTeamOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
async present(output: DriverTeamOutputPort) { present(result: GetDriverTeamResult): void {
const isOwner = output.team.ownerId === output.driverId; const isOwner = result.team.ownerId.toString() === result.driverId;
const canManage = isOwner || output.membership.role === 'owner' || output.membership.role === 'manager'; const canManage = isOwner || result.membership.role === 'owner' || result.membership.role === 'manager';
this.result = { this.result = {
team: { team: {
id: output.team.id, id: result.team.id,
name: output.team.name, name: result.team.name.toString(),
tag: output.team.tag, tag: result.team.tag.toString(),
description: output.team.description || '', description: result.team.description?.toString() || '',
ownerId: output.team.ownerId, ownerId: result.team.ownerId.toString(),
leagues: output.team.leagues || [], leagues: result.team.leagues?.map(l => l.toString()) || [],
createdAt: output.team.createdAt.toISOString(), createdAt: result.team.createdAt.toDate().toISOString(),
}, },
membership: { membership: {
role: output.membership.role === 'driver' ? 'member' : output.membership.role, role: result.membership.role === 'driver' ? 'member' : result.membership.role,
joinedAt: output.membership.joinedAt.toISOString(), joinedAt: result.membership.joinedAt.toISOString(),
isActive: output.membership.status === 'active', isActive: result.membership.status === 'active',
}, },
isOwner, isOwner,
canManage, canManage,
}; };
} }
getViewModel(): GetDriverTeamOutputDTO | null { getResponseModel(): GetDriverTeamOutputDTO | null {
return this.result; return this.result;
} }
} }

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