This commit is contained in:
2025-12-12 01:11:36 +01:00
parent ec3ddc3a5c
commit 6a88fe93ab
125 changed files with 1513 additions and 803 deletions

View File

@@ -27,15 +27,14 @@ export class CreateTeamUseCase {
throw new Error('Driver already belongs to a team');
}
const team: Team = {
const team = Team.create({
id: `team-${Date.now()}`,
name,
tag,
description,
ownerId,
leagues,
createdAt: new Date(),
};
});
const createdTeam = await this.teamRepository.create(team);

View File

@@ -4,15 +4,25 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type {
AllLeaguesWithCapacityAndScoringViewModel,
IAllLeaguesWithCapacityAndScoringPresenter,
LeagueEnrichedData,
} from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case for retrieving all leagues with capacity and scoring information.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetAllLeaguesWithCapacityAndScoringUseCase
implements AsyncUseCase<void, void>
implements
UseCase<
void,
LeagueEnrichedData[],
AllLeaguesWithCapacityAndScoringViewModel,
IAllLeaguesWithCapacityAndScoringPresenter
>
{
constructor(
private readonly leagueRepository: ILeagueRepository,
@@ -21,10 +31,14 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: IAllLeaguesWithCapacityAndScoringPresenter,
) {}
async execute(): Promise<void> {
async execute(
_input: void,
presenter: IAllLeaguesWithCapacityAndScoringPresenter,
): Promise<void> {
presenter.reset();
const leagues = await this.leagueRepository.findAll();
const enrichedLeagues: LeagueEnrichedData[] = [];
@@ -42,18 +56,22 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
).length;
const seasons = await this.seasonRepository.findByLeagueId(league.id);
const activeSeason = seasons && seasons.length > 0
? seasons.find((s) => s.status === 'active') ?? seasons[0]
: undefined;
const activeSeason =
seasons && seasons.length > 0
? seasons.find((s) => s.status === 'active') ?? seasons[0]
: undefined;
let scoringConfig: LeagueEnrichedData['scoringConfig'];
let game: LeagueEnrichedData['game'];
let preset: LeagueEnrichedData['preset'];
if (activeSeason) {
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
const scoringConfigResult =
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
scoringConfig = scoringConfigResult ?? undefined;
if (scoringConfig) {
game = await this.gameRepository.findById(activeSeason.gameId);
const gameResult = await this.gameRepository.findById(activeSeason.gameId);
game = gameResult ?? undefined;
const presetId = scoringConfig.scoringPresetId;
if (presetId) {
preset = this.presetProvider.getPresetById(presetId);
@@ -64,13 +82,13 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
enrichedLeagues.push({
league,
usedDriverSlots,
season: activeSeason,
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
...(game ?? undefined ? { game } : {}),
...(preset ?? undefined ? { preset } : {}),
...(activeSeason ? { season: activeSeason } : {}),
...(scoringConfig ? { scoringConfig } : {}),
...(game ? { game } : {}),
...(preset ? { preset } : {}),
});
}
this.presenter.present(enrichedLeagues);
presenter.present(enrichedLeagues);
}
}

View File

@@ -1,22 +1,30 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type {
IAllLeaguesWithCapacityPresenter,
AllLeaguesWithCapacityResultDTO,
AllLeaguesWithCapacityViewModel,
} from '../presenters/IAllLeaguesWithCapacityPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case for retrieving all leagues with capacity information.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetAllLeaguesWithCapacityUseCase
implements AsyncUseCase<void, void>
implements UseCase<void, AllLeaguesWithCapacityResultDTO, AllLeaguesWithCapacityViewModel, IAllLeaguesWithCapacityPresenter>
{
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
public readonly presenter: IAllLeaguesWithCapacityPresenter,
) {}
async execute(): Promise<void> {
async execute(
_input: void,
presenter: IAllLeaguesWithCapacityPresenter,
): Promise<void> {
presenter.reset();
const leagues = await this.leagueRepository.findAll();
const memberCounts = new Map<string, number>();
@@ -36,6 +44,11 @@ export class GetAllLeaguesWithCapacityUseCase
memberCounts.set(league.id, usedSlots);
}
this.presenter.present(leagues, memberCounts);
const dto: AllLeaguesWithCapacityResultDTO = {
leagues,
memberCounts,
};
presenter.present(dto);
}
}

View File

@@ -2,21 +2,21 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type {
IAllRacesPagePresenter,
AllRacesPageResultDTO,
AllRacesPageViewModel,
AllRacesListItemViewModel,
AllRacesFilterOptionsViewModel,
} from '../presenters/IAllRacesPagePresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { UseCase } from '@gridpilot/shared/application';
export class GetAllRacesPageDataUseCase
implements AsyncUseCase<void, void> {
implements UseCase<void, AllRacesPageResultDTO, AllRacesPageViewModel, IAllRacesPagePresenter> {
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
public readonly presenter: IAllRacesPagePresenter,
) {}
async execute(): Promise<void> {
async execute(_input: void, presenter: IAllRacesPagePresenter): Promise<void> {
const [allRaces, allLeagues] = await Promise.all([
this.raceRepository.findAll(),
this.leagueRepository.findAll(),
@@ -59,6 +59,7 @@ export class GetAllRacesPageDataUseCase
filters,
};
this.presenter.present(viewModel);
presenter.reset();
presenter.present(viewModel);
}
}

View File

@@ -24,11 +24,17 @@ export class GetAllTeamsUseCase
const teams = await this.teamRepository.findAll();
const enrichedTeams: Array<Team & { memberCount: number }> = await Promise.all(
const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all(
teams.map(async (team) => {
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
return {
...team,
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: team.ownerId,
leagues: [...team.leagues],
createdAt: team.createdAt,
memberCount,
};
}),

View File

@@ -8,7 +8,6 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type {
IDashboardOverviewPresenter,
DashboardOverviewViewModel,
@@ -34,8 +33,7 @@ export interface GetDashboardOverviewParams {
driverId: string;
}
export class GetDashboardOverviewUseCase
implements AsyncUseCase<GetDashboardOverviewParams, void> {
export class GetDashboardOverviewUseCase {
constructor(
private readonly driverRepository: IDriverRepository,
private readonly raceRepository: IRaceRepository,
@@ -48,10 +46,9 @@ export class GetDashboardOverviewUseCase
private readonly socialRepository: ISocialGraphRepository,
private readonly imageService: IImageServicePort,
private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null,
public readonly presenter: IDashboardOverviewPresenter,
) {}
async execute(params: GetDashboardOverviewParams): Promise<void> {
async execute(params: GetDashboardOverviewParams, presenter: IDashboardOverviewPresenter): Promise<void> {
const { driverId } = params;
const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([
@@ -137,7 +134,8 @@ export class GetDashboardOverviewUseCase
friends: friendsSummary,
};
this.presenter.present(viewModel);
presenter.reset();
presenter.present(viewModel);
}
private async getDriverLeagues(allLeagues: any[], driverId: string): Promise<any[]> {

View File

@@ -2,30 +2,36 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { IRankingService } from '../../domain/services/IRankingService';
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { IDriversLeaderboardPresenter } from '../presenters/IDriversLeaderboardPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type {
IDriversLeaderboardPresenter,
DriversLeaderboardResultDTO,
DriversLeaderboardViewModel,
} from '../presenters/IDriversLeaderboardPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case for retrieving driver leaderboard data.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetDriversLeaderboardUseCase
implements AsyncUseCase<void, void> {
implements UseCase<void, DriversLeaderboardResultDTO, DriversLeaderboardViewModel, IDriversLeaderboardPresenter>
{
constructor(
private readonly driverRepository: IDriverRepository,
private readonly rankingService: IRankingService,
private readonly driverStatsService: IDriverStatsService,
private readonly imageService: IImageServicePort,
public readonly presenter: IDriversLeaderboardPresenter,
) {}
async execute(): Promise<void> {
async execute(_input: void, presenter: IDriversLeaderboardPresenter): Promise<void> {
presenter.reset();
const drivers = await this.driverRepository.findAll();
const rankings = this.rankingService.getAllDriverRankings();
const stats: Record<string, any> = {};
const avatarUrls: Record<string, string> = {};
const stats: DriversLeaderboardResultDTO['stats'] = {};
const avatarUrls: DriversLeaderboardResultDTO['avatarUrls'] = {};
for (const driver of drivers) {
const driverStats = this.driverStatsService.getDriverStats(driver.id);
if (driverStats) {
@@ -33,7 +39,14 @@ export class GetDriversLeaderboardUseCase
}
avatarUrls[driver.id] = this.imageService.getDriverAvatar(driver.id);
}
this.presenter.present(drivers, rankings, stats, avatarUrls);
const dto: DriversLeaderboardResultDTO = {
drivers,
rankings,
stats,
avatarUrls,
};
presenter.present(dto);
}
}

View File

@@ -2,8 +2,12 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type {
ILeagueDriverSeasonStatsPresenter,
LeagueDriverSeasonStatsResultDTO,
LeagueDriverSeasonStatsViewModel,
} from '../presenters/ILeagueDriverSeasonStatsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface DriverRatingPort {
getRating(driverId: string): { rating: number | null; ratingChange: number | null };
@@ -18,17 +22,27 @@ export interface GetLeagueDriverSeasonStatsUseCaseParams {
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetLeagueDriverSeasonStatsUseCase
implements AsyncUseCase<GetLeagueDriverSeasonStatsUseCaseParams, void> {
implements
UseCase<
GetLeagueDriverSeasonStatsUseCaseParams,
LeagueDriverSeasonStatsResultDTO,
LeagueDriverSeasonStatsViewModel,
ILeagueDriverSeasonStatsPresenter
>
{
constructor(
private readonly standingRepository: IStandingRepository,
private readonly resultRepository: IResultRepository,
private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository,
private readonly driverRatingPort: DriverRatingPort,
public readonly presenter: ILeagueDriverSeasonStatsPresenter,
) {}
async execute(params: GetLeagueDriverSeasonStatsUseCaseParams): Promise<void> {
async execute(
params: GetLeagueDriverSeasonStatsUseCaseParams,
presenter: ILeagueDriverSeasonStatsPresenter,
): Promise<void> {
presenter.reset();
const { leagueId } = params;
// Get standings and races for the league
@@ -70,16 +84,26 @@ export class GetLeagueDriverSeasonStatsUseCase
// Collect driver results
const driverResults = new Map<string, Array<{ position: number }>>();
for (const standing of standings) {
const results = await this.resultRepository.findByDriverIdAndLeagueId(standing.driverId, leagueId);
const results = await this.resultRepository.findByDriverIdAndLeagueId(
standing.driverId,
leagueId,
);
driverResults.set(standing.driverId, results);
}
this.presenter.present(
const dto: LeagueDriverSeasonStatsResultDTO = {
leagueId,
standings,
penaltiesByDriver,
standings: standings.map(standing => ({
driverId: standing.driverId,
position: standing.position,
points: standing.points,
racesCompleted: standing.racesCompleted,
})),
penalties: penaltiesByDriver,
driverResults,
driverRatings
);
driverRatings,
};
presenter.present(dto);
}
}

View File

@@ -49,9 +49,9 @@ export class GetLeagueFullConfigUseCase
const data: LeagueFullConfigData = {
league,
activeSeason,
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
...(game ?? undefined ? { game } : {}),
...(activeSeason ? { activeSeason } : {}),
...(scoringConfig ? { scoringConfig } : {}),
...(game ? { game } : {}),
};
presenter.reset();

View File

@@ -3,25 +3,29 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type {
ILeagueScoringConfigPresenter,
LeagueScoringConfigData,
LeagueScoringConfigViewModel,
} from '../presenters/ILeagueScoringConfigPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case for retrieving a league's scoring configuration for its active season.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetLeagueScoringConfigUseCase
implements AsyncUseCase<{ leagueId: string }, void> {
implements UseCase<{ leagueId: string }, LeagueScoringConfigData, LeagueScoringConfigViewModel, ILeagueScoringConfigPresenter>
{
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
private readonly gameRepository: IGameRepository,
private readonly presetProvider: LeagueScoringPresetProvider,
public readonly presenter: ILeagueScoringConfigPresenter,
) {}
async execute(params: { leagueId: string }): Promise<void> {
async execute(params: { leagueId: string }, presenter: ILeagueScoringConfigPresenter): Promise<void> {
const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId);
@@ -65,6 +69,7 @@ export class GetLeagueScoringConfigUseCase
championships: scoringConfig.championships,
};
this.presenter.present(data);
presenter.reset();
presenter.present(data);
}
}

View File

@@ -14,6 +14,7 @@ import type {
RaceDetailEntryViewModel,
RaceDetailUserResultViewModel,
} from '../presenters/IRaceDetailPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case: GetRaceDetailUseCase
@@ -30,7 +31,9 @@ export interface GetRaceDetailQueryParams {
driverId: string;
}
export class GetRaceDetailUseCase {
export class GetRaceDetailUseCase
implements UseCase<GetRaceDetailQueryParams, RaceDetailViewModel, RaceDetailViewModel, IRaceDetailPresenter>
{
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
@@ -40,10 +43,11 @@ export class GetRaceDetailUseCase {
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort,
public readonly presenter: IRaceDetailPresenter,
) {}
async execute(params: GetRaceDetailQueryParams): Promise<void> {
async execute(params: GetRaceDetailQueryParams, presenter: IRaceDetailPresenter): Promise<void> {
presenter.reset();
const { raceId, driverId } = params;
const race = await this.raceRepository.findById(raceId);
@@ -59,7 +63,7 @@ export class GetRaceDetailUseCase {
userResult: null,
error: 'Race not found',
};
this.presenter.present(emptyViewModel);
presenter.present(emptyViewModel);
return;
}
@@ -121,8 +125,8 @@ export class GetRaceDetailUseCase {
sessionType: race.sessionType,
status: race.status,
strengthOfField: race.strengthOfField ?? null,
registeredCount: race.registeredCount,
maxParticipants: race.maxParticipants,
...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}),
...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}),
};
const leagueView: RaceDetailLeagueViewModel | null = league
@@ -131,8 +135,12 @@ export class GetRaceDetailUseCase {
name: league.name,
description: league.description,
settings: {
maxDrivers: league.settings.maxDrivers,
qualifyingFormat: league.settings.qualifyingFormat,
...(league.settings.maxDrivers !== undefined
? { maxDrivers: league.settings.maxDrivers }
: {}),
...(league.settings.qualifyingFormat !== undefined
? { qualifyingFormat: league.settings.qualifyingFormat }
: {}),
},
}
: null;
@@ -148,7 +156,7 @@ export class GetRaceDetailUseCase {
userResult: userResultView,
};
this.presenter.present(viewModel);
presenter.present(viewModel);
}
private calculateRatingChange(position: number): number {

View File

@@ -1,6 +1,11 @@
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistrationsPresenter';
import type {
IRaceRegistrationsPresenter,
RaceRegistrationsResultDTO,
RaceRegistrationsViewModel,
} from '../presenters/IRaceRegistrationsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case: GetRaceRegistrationsUseCase
@@ -8,15 +13,26 @@ import type { IRaceRegistrationsPresenter } from '../presenters/IRaceRegistratio
* Returns registered driver IDs for a race.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetRaceRegistrationsUseCase {
export class GetRaceRegistrationsUseCase
implements UseCase<GetRaceRegistrationsQueryParamsDTO, RaceRegistrationsResultDTO, RaceRegistrationsViewModel, IRaceRegistrationsPresenter>
{
constructor(
private readonly registrationRepository: IRaceRegistrationRepository,
public readonly presenter: IRaceRegistrationsPresenter,
) {}
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<void> {
async execute(
params: GetRaceRegistrationsQueryParamsDTO,
presenter: IRaceRegistrationsPresenter,
): Promise<void> {
presenter.reset();
const { raceId } = params;
const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId);
this.presenter.present(registeredDriverIds);
const dto: RaceRegistrationsResultDTO = {
registeredDriverIds,
};
presenter.present(dto);
}
}

View File

@@ -8,6 +8,7 @@ import type {
RaceResultsDetailViewModel,
RaceResultsPenaltySummaryViewModel,
} from '../presenters/IRaceResultsDetailPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
import type { League } from '../../domain/entities/League';
import type { Result } from '../../domain/entities/Result';
import type { Driver } from '../../domain/entities/Driver';
@@ -18,8 +19,8 @@ export interface GetRaceResultsDetailParams {
driverId?: string;
}
function buildPointsSystem(league: League | null): Record<number, number> {
if (!league) return {};
function buildPointsSystem(league: League | null): Record<number, number> | undefined {
if (!league) return undefined;
const pointsSystems: Record<string, Record<number, number>> = {
'f1-2024': {
@@ -53,11 +54,17 @@ function buildPointsSystem(league: League | null): Record<number, number> {
},
};
return (
league.settings.customPoints ||
pointsSystems[league.settings.pointsSystem] ||
pointsSystems['f1-2024']
);
const customPoints = league.settings.customPoints;
if (customPoints) {
return customPoints;
}
const preset = pointsSystems[league.settings.pointsSystem];
if (preset) {
return preset;
}
return pointsSystems['f1-2024'];
}
function getFastestLapTime(results: Result[]): number | undefined {
@@ -73,17 +80,28 @@ function mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewM
}));
}
export class GetRaceResultsDetailUseCase {
export class GetRaceResultsDetailUseCase
implements
UseCase<
GetRaceResultsDetailParams,
RaceResultsDetailViewModel,
RaceResultsDetailViewModel,
IRaceResultsDetailPresenter
>
{
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
private readonly resultRepository: IResultRepository,
private readonly driverRepository: IDriverRepository,
private readonly penaltyRepository: IPenaltyRepository,
public readonly presenter: IRaceResultsDetailPresenter,
) {}
async execute(params: GetRaceResultsDetailParams): Promise<void> {
async execute(
params: GetRaceResultsDetailParams,
presenter: IRaceResultsDetailPresenter,
): Promise<void> {
presenter.reset();
const { raceId, driverId } = params;
const race = await this.raceRepository.findById(raceId);
@@ -95,11 +113,10 @@ export class GetRaceResultsDetailUseCase {
results: [],
drivers: [],
penalties: [],
pointsSystem: {},
currentDriverId: driverId,
...(driverId ? { currentDriverId: driverId } : {}),
error: 'Race not found',
};
this.presenter.present(errorViewModel);
presenter.present(errorViewModel);
return;
}
@@ -111,12 +128,12 @@ export class GetRaceResultsDetailUseCase {
]);
const effectiveCurrentDriverId =
driverId || (drivers.length > 0 ? drivers[0]!.id : undefined);
driverId ?? (drivers.length > 0 ? drivers[0]!.id : undefined);
const pointsSystem = buildPointsSystem(league as League | null);
const fastestLapTime = getFastestLapTime(results);
const penaltySummary = mapPenaltySummary(penalties);
const viewModel: RaceResultsDetailViewModel = {
race: {
id: race.id,
@@ -134,11 +151,11 @@ export class GetRaceResultsDetailUseCase {
results,
drivers,
penalties: penaltySummary,
pointsSystem,
...(pointsSystem ? { pointsSystem } : {}),
...(fastestLapTime !== undefined ? { fastestLapTime } : {}),
currentDriverId: effectiveCurrentDriverId,
...(effectiveCurrentDriverId ? { currentDriverId: effectiveCurrentDriverId } : {}),
};
this.presenter.present(viewModel);
presenter.present(viewModel);
}
}

View File

@@ -14,13 +14,16 @@ import {
AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator,
} from '../../domain/services/StrengthOfFieldCalculator';
import type { IRaceWithSOFPresenter } from '../presenters/IRaceWithSOFPresenter';
import type { IRaceWithSOFPresenter, RaceWithSOFResultDTO } from '../presenters/IRaceWithSOFPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetRaceWithSOFQueryParams {
raceId: string;
}
export class GetRaceWithSOFUseCase {
export class GetRaceWithSOFUseCase
implements UseCase<GetRaceWithSOFQueryParams, RaceWithSOFResultDTO, import('../presenters/IRaceWithSOFPresenter').RaceWithSOFViewModel, IRaceWithSOFPresenter>
{
private readonly sofCalculator: StrengthOfFieldCalculator;
constructor(
@@ -28,18 +31,19 @@ export class GetRaceWithSOFUseCase {
private readonly registrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: IRaceWithSOFPresenter,
sofCalculator?: StrengthOfFieldCalculator,
) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
}
async execute(params: GetRaceWithSOFQueryParams): Promise<void> {
async execute(params: GetRaceWithSOFQueryParams, presenter: IRaceWithSOFPresenter): Promise<void> {
presenter.reset();
const { raceId } = params;
const race = await this.raceRepository.findById(raceId);
if (!race) {
return null;
return;
}
// Get participant IDs based on race status
@@ -56,30 +60,34 @@ export class GetRaceWithSOFUseCase {
// Use stored SOF if available, otherwise calculate
let strengthOfField = race.strengthOfField ?? null;
if (strengthOfField === null && participantIds.length > 0) {
const ratings = this.driverRatingProvider.getRatings(participantIds);
const driverRatings = participantIds
.filter(id => ratings.has(id))
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
strengthOfField = this.sofCalculator.calculate(driverRatings);
}
this.presenter.present(
race.id,
race.leagueId,
race.scheduledAt,
race.track,
race.trackId,
race.car,
race.carId,
race.sessionType,
race.status,
presenter.reset();
const dto: RaceWithSOFResultDTO = {
raceId: race.id,
leagueId: race.leagueId,
scheduledAt: race.scheduledAt,
track: race.track ?? '',
trackId: race.trackId ?? '',
car: race.car ?? '',
carId: race.carId ?? '',
sessionType: race.sessionType,
status: race.status,
strengthOfField,
race.registeredCount ?? participantIds.length,
race.maxParticipants,
participantIds.length
);
registeredCount: race.registeredCount ?? participantIds.length,
maxParticipants: race.maxParticipants ?? participantIds.length,
participantCount: participantIds.length,
};
presenter.present(dto);
}
}

View File

@@ -1,15 +1,23 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRacesPagePresenter } from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
import type {
IRacesPagePresenter,
RacesPageResultDTO,
RacesPageViewModel,
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export class GetRacesPageDataUseCase {
export class GetRacesPageDataUseCase
implements UseCase<void, RacesPageResultDTO, RacesPageViewModel, IRacesPagePresenter>
{
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
public readonly presenter: IRacesPagePresenter,
) {}
async execute(): Promise<void> {
async execute(_input: void, presenter: IRacesPagePresenter): Promise<void> {
presenter.reset();
const [allRaces, allLeagues] = await Promise.all([
this.raceRepository.findAll(),
this.leagueRepository.findAll(),
@@ -33,6 +41,10 @@ export class GetRacesPageDataUseCase {
isPast: race.isPast(),
}));
this.presenter.present(races);
const dto: RacesPageResultDTO = {
races,
};
presenter.present(dto);
}
}

View File

@@ -10,7 +10,11 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ISponsorDashboardPresenter } from '../presenters/ISponsorDashboardPresenter';
import type {
ISponsorDashboardPresenter,
SponsorDashboardViewModel,
} from '../presenters/ISponsorDashboardPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetSponsorDashboardQueryParams {
sponsorId: string;
@@ -47,7 +51,9 @@ export interface SponsorDashboardDTO {
};
}
export class GetSponsorDashboardUseCase {
export class GetSponsorDashboardUseCase
implements UseCase<GetSponsorDashboardQueryParams, SponsorDashboardDTO | null, SponsorDashboardViewModel, ISponsorDashboardPresenter>
{
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -55,15 +61,19 @@ export class GetSponsorDashboardUseCase {
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository,
private readonly presenter: ISponsorDashboardPresenter,
) {}
async execute(params: GetSponsorDashboardQueryParams): Promise<void> {
async execute(
params: GetSponsorDashboardQueryParams,
presenter: ISponsorDashboardPresenter,
): Promise<void> {
presenter.reset();
const { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) {
this.presenter.present(null);
presenter.present(null);
return;
}
@@ -139,11 +149,11 @@ export class GetSponsorDashboardUseCase {
// Calculate exposure score (0-100 based on tier distribution)
const mainSponsorships = sponsorships.filter(s => s.tier === 'main').length;
const exposure = sponsorships.length > 0
const exposure = sponsorships.length > 0
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
: 0;
this.presenter.present({
const dto: SponsorDashboardDTO = {
sponsorId,
sponsorName: sponsor.name,
metrics: {
@@ -162,6 +172,8 @@ export class GetSponsorDashboardUseCase {
totalInvestment,
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
},
});
};
presenter.present(dto);
}
}

View File

@@ -11,7 +11,11 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship';
import type { ISponsorSponsorshipsPresenter } from '../presenters/ISponsorSponsorshipsPresenter';
import type {
ISponsorSponsorshipsPresenter,
SponsorSponsorshipsViewModel,
} from '../presenters/ISponsorSponsorshipsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetSponsorSponsorshipsQueryParams {
sponsorId: string;
@@ -62,7 +66,9 @@ export interface SponsorSponsorshipsDTO {
};
}
export class GetSponsorSponsorshipsUseCase {
export class GetSponsorSponsorshipsUseCase
implements UseCase<GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO | null, SponsorSponsorshipsViewModel, ISponsorSponsorshipsPresenter>
{
constructor(
private readonly sponsorRepository: ISponsorRepository,
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
@@ -70,15 +76,19 @@ export class GetSponsorSponsorshipsUseCase {
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository,
private readonly presenter: ISponsorSponsorshipsPresenter,
) {}
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<void> {
async execute(
params: GetSponsorSponsorshipsQueryParams,
presenter: ISponsorSponsorshipsPresenter,
): Promise<void> {
presenter.reset();
const { sponsorId } = params;
const sponsor = await this.sponsorRepository.findById(sponsorId);
if (!sponsor) {
this.presenter.present(null);
presenter.present(null);
return;
}
@@ -150,7 +160,7 @@ export class GetSponsorSponsorshipsUseCase {
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
this.presenter.present({
const dto: SponsorSponsorshipsDTO = {
sponsorId,
sponsorName: sponsor.name,
sponsorships: sponsorshipDetails,
@@ -161,6 +171,8 @@ export class GetSponsorSponsorshipsUseCase {
totalPlatformFees,
currency: 'USD',
},
});
};
presenter.present(dto);
}
}

View File

@@ -1,19 +1,31 @@
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { ITeamDetailsPresenter } from '../presenters/ITeamDetailsPresenter';
import type {
ITeamDetailsPresenter,
TeamDetailsResultDTO,
TeamDetailsViewModel,
} from '../presenters/ITeamDetailsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
/**
* Use Case for retrieving team details.
* Orchestrates domain logic and delegates presentation to the presenter.
*/
export class GetTeamDetailsUseCase {
export class GetTeamDetailsUseCase
implements UseCase<{ teamId: string; driverId: string }, TeamDetailsResultDTO, TeamDetailsViewModel, ITeamDetailsPresenter>
{
constructor(
private readonly teamRepository: ITeamRepository,
private readonly membershipRepository: ITeamMembershipRepository,
public readonly presenter: ITeamDetailsPresenter,
) {}
async execute(teamId: string, driverId: string): Promise<void> {
async execute(
params: { teamId: string; driverId: string },
presenter: ITeamDetailsPresenter,
): Promise<void> {
presenter.reset();
const { teamId, driverId } = params;
const team = await this.teamRepository.findById(teamId);
if (!team) {
throw new Error('Team not found');
@@ -21,6 +33,12 @@ export class GetTeamDetailsUseCase {
const membership = await this.membershipRepository.getMembership(teamId, driverId);
this.presenter.present(team, membership, driverId);
const dto: TeamDetailsResultDTO = {
team,
membership,
driverId,
};
presenter.present(dto);
}
}

View File

@@ -17,6 +17,10 @@ export class PreviewLeagueScheduleUseCase {
execute(params: PreviewLeagueScheduleQueryParams): void {
const seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule);
if (!seasonSchedule) {
throw new Error('Invalid schedule data');
}
const maxRounds =
params.maxRounds && params.maxRounds > 0
? Math.min(params.maxRounds, seasonSchedule.plannedRounds)
@@ -46,8 +50,11 @@ export class PreviewLeagueScheduleUseCase {
return 'No rounds scheduled.';
}
const first = new Date(rounds[0].scheduledAt);
const last = new Date(rounds[rounds.length - 1].scheduledAt);
const firstRound = rounds[0]!;
const lastRound = rounds[rounds.length - 1]!;
const first = new Date(firstRound.scheduledAt);
const last = new Date(lastRound.scheduledAt);
const firstDate = first.toISOString().slice(0, 10);
const lastDate = last.toISOString().slice(0, 10);

View File

@@ -22,10 +22,12 @@ export class UpdateTeamUseCase {
throw new Error('Team not found');
}
const updated: Team = {
...existing,
...updates,
};
const updated = existing.update({
...(updates.name !== undefined && { name: updates.name }),
...(updates.tag !== undefined && { tag: updates.tag }),
...(updates.description !== undefined && { description: updates.description }),
...(updates.leagues !== undefined && { leagues: updates.leagues }),
});
await this.teamRepository.update(updated);
}