wip
This commit is contained in:
@@ -56,29 +56,37 @@ export class ApplyForSponsorshipUseCase
|
||||
}
|
||||
|
||||
if (!pricing.acceptingApplications) {
|
||||
throw new RacingApplicationError('This entity is not currently accepting sponsorship applications');
|
||||
throw new BusinessRuleViolationError(
|
||||
'This entity is not currently accepting sponsorship applications',
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the requested tier slot is available
|
||||
const slotAvailable = pricing.isSlotAvailable(dto.tier);
|
||||
if (!slotAvailable) {
|
||||
throw new RacingApplicationError(`No ${dto.tier} sponsorship slots are available`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`No ${dto.tier} sponsorship slots are available`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if sponsor already has a pending request for this entity
|
||||
const hasPending = await this.sponsorshipRequestRepo.hasPendingRequest(
|
||||
dto.sponsorId,
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
dto.entityId,
|
||||
);
|
||||
if (hasPending) {
|
||||
throw new RacingApplicationError('You already have a pending sponsorship request for this entity');
|
||||
throw new BusinessRuleViolationError(
|
||||
'You already have a pending sponsorship request for this entity',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate offered amount meets minimum price
|
||||
const minPrice = pricing.getPrice(dto.tier);
|
||||
if (minPrice && dto.offeredAmount < minPrice.amount) {
|
||||
throw new RacingApplicationError(`Offered amount must be at least ${minPrice.format()}`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`Offered amount must be at least ${minPrice.format()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create the sponsorship request
|
||||
@@ -92,7 +100,7 @@ export class ApplyForSponsorshipUseCase
|
||||
entityId: dto.entityId,
|
||||
tier: dto.tier,
|
||||
offeredAmount,
|
||||
message: dto.message,
|
||||
...(dto.message !== undefined ? { message: dto.message } : {}),
|
||||
});
|
||||
|
||||
await this.sponsorshipRequestRepo.create(request);
|
||||
|
||||
@@ -70,13 +70,13 @@ export class ApplyPenaltyUseCase
|
||||
raceId: command.raceId,
|
||||
driverId: command.driverId,
|
||||
type: command.type,
|
||||
value: command.value,
|
||||
...(command.value !== undefined ? { value: command.value } : {}),
|
||||
reason: command.reason,
|
||||
protestId: command.protestId,
|
||||
...(command.protestId !== undefined ? { protestId: command.protestId } : {}),
|
||||
issuedBy: command.stewardId,
|
||||
status: 'pending',
|
||||
issuedAt: new Date(),
|
||||
notes: command.notes,
|
||||
...(command.notes !== undefined ? { notes: command.notes } : {}),
|
||||
});
|
||||
|
||||
await this.penaltyRepository.create(penalty);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
@@ -70,30 +71,28 @@ export class CreateLeagueWithSeasonAndScoringUseCase
|
||||
description: command.description ?? '',
|
||||
ownerId: command.ownerId,
|
||||
settings: {
|
||||
pointsSystem: (command.scoringPresetId as any) ?? 'custom',
|
||||
maxDrivers: command.maxDrivers,
|
||||
// Presets are attached at scoring-config level; league settings use a stable points system id.
|
||||
pointsSystem: 'custom',
|
||||
...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
await this.leagueRepository.create(league);
|
||||
|
||||
const seasonId = uuidv4();
|
||||
const season = {
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
gameId: command.gameId,
|
||||
name: `${command.name} Season 1`,
|
||||
year: new Date().getFullYear(),
|
||||
order: 1,
|
||||
status: 'active' as const,
|
||||
status: 'active',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
// Season is a domain entity; use the repository's create, but shape matches Season.create expectations.
|
||||
// To keep this use case independent, we rely on repository to persist the plain object.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await this.seasonRepository.create(season as any);
|
||||
await this.seasonRepository.create(season);
|
||||
|
||||
const presetId = command.scoringPresetId ?? 'club-default';
|
||||
const preset: LeagueScoringPresetDTO | undefined =
|
||||
|
||||
@@ -55,8 +55,8 @@ export class FileProtestUseCase {
|
||||
protestingDriverId: command.protestingDriverId,
|
||||
accusedDriverId: command.accusedDriverId,
|
||||
incident: command.incident,
|
||||
comment: command.comment,
|
||||
proofVideoUrl: command.proofVideoUrl,
|
||||
...(command.comment !== undefined ? { comment: command.comment } : {}),
|
||||
...(command.proofVideoUrl !== undefined ? { proofVideoUrl: command.proofVideoUrl } : {}),
|
||||
status: 'pending',
|
||||
filedAt: new Date(),
|
||||
});
|
||||
|
||||
@@ -46,9 +46,9 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
|
||||
let scoringConfig;
|
||||
let game;
|
||||
let preset;
|
||||
let scoringConfig: LeagueEnrichedData['scoringConfig'];
|
||||
let game: LeagueEnrichedData['game'];
|
||||
let preset: LeagueEnrichedData['preset'];
|
||||
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
@@ -65,9 +65,9 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
league,
|
||||
usedDriverSlots,
|
||||
season: activeSeason,
|
||||
scoringConfig,
|
||||
game,
|
||||
preset,
|
||||
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
|
||||
...(game ?? undefined ? { game } : {}),
|
||||
...(preset ?? undefined ? { preset } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IAllTeamsPresenter } from '../presenters/IAllTeamsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IAllTeamsPresenter,
|
||||
AllTeamsResultDTO,
|
||||
} from '../presenters/IAllTeamsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllTeamsUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
implements UseCase<void, AllTeamsResultDTO, import('../presenters/IAllTeamsPresenter').AllTeamsViewModel, IAllTeamsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
public readonly presenter: IAllTeamsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(_input: void, presenter: IAllTeamsPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const teams = await this.teamRepository.findAll();
|
||||
|
||||
// Enrich teams with member counts
|
||||
const enrichedTeams = await Promise.all(
|
||||
|
||||
const enrichedTeams: Array<Team & { memberCount: number }> = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const memberships = await this.teamMembershipRepository.findByTeamId(team.id);
|
||||
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
|
||||
return {
|
||||
...team,
|
||||
memberCount: memberships.length,
|
||||
memberCount,
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
this.presenter.present(enrichedTeams as any);
|
||||
|
||||
const dto: AllTeamsResultDTO = {
|
||||
teams: enrichedTeams,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,46 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverTeamPresenter } from '../presenters/IDriverTeamPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IDriverTeamPresenter,
|
||||
DriverTeamResultDTO,
|
||||
DriverTeamViewModel,
|
||||
} from '../presenters/IDriverTeamPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a driver's team.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetDriverTeamUseCase
|
||||
implements AsyncUseCase<string, boolean> {
|
||||
implements UseCase<{ driverId: string }, DriverTeamResultDTO, DriverTeamViewModel, IDriverTeamPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: IDriverTeamPresenter,
|
||||
) {}
|
||||
|
||||
async execute(driverId: string): Promise<boolean> {
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
|
||||
async execute(input: { driverId: string }, presenter: IDriverTeamPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId);
|
||||
if (!membership) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(membership.teamId);
|
||||
if (!team) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.presenter.present(team, membership, driverId);
|
||||
return true;
|
||||
const dto: DriverTeamResultDTO = {
|
||||
team,
|
||||
membership,
|
||||
driverId: input.driverId,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,9 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
acceptingApplications: pricing.acceptingApplications,
|
||||
customRequirements: pricing.customRequirements,
|
||||
...(pricing.customRequirements !== undefined
|
||||
? { customRequirements: pricing.customRequirements }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (pricing.mainSlot) {
|
||||
|
||||
@@ -2,8 +2,12 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILeagueFullConfigPresenter, LeagueFullConfigData } from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
ILeagueFullConfigPresenter,
|
||||
LeagueFullConfigData,
|
||||
LeagueConfigFormViewModel,
|
||||
} from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
import { EntityNotFoundError } from '../errors/RacingApplicationError';
|
||||
|
||||
/**
|
||||
@@ -11,17 +15,16 @@ import { EntityNotFoundError } from '../errors/RacingApplicationError';
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueFullConfigUseCase
|
||||
implements AsyncUseCase<{ leagueId: string }, void>
|
||||
implements UseCase<{ leagueId: string }, LeagueFullConfigData, LeagueConfigFormViewModel, ILeagueFullConfigPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
public readonly presenter: ILeagueFullConfigPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<void> {
|
||||
async execute(params: { leagueId: string }, presenter: ILeagueFullConfigPresenter): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
@@ -35,23 +38,23 @@ export class GetLeagueFullConfigUseCase
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
|
||||
let scoringConfig;
|
||||
let game;
|
||||
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (activeSeason.gameId) {
|
||||
game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
}
|
||||
}
|
||||
let scoringConfig = await (async () => {
|
||||
if (!activeSeason) return undefined;
|
||||
return this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
})();
|
||||
let game = await (async () => {
|
||||
if (!activeSeason || !activeSeason.gameId) return undefined;
|
||||
return this.gameRepository.findById(activeSeason.gameId);
|
||||
})();
|
||||
|
||||
const data: LeagueFullConfigData = {
|
||||
league,
|
||||
activeSeason,
|
||||
scoringConfig,
|
||||
game,
|
||||
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
|
||||
...(game ?? undefined ? { game } : {}),
|
||||
};
|
||||
|
||||
this.presenter.present(data);
|
||||
presenter.reset();
|
||||
presenter.present(data);
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,14 @@ export class GetLeagueScoringConfigUseCase
|
||||
if (!seasons || seasons.length === 0) {
|
||||
throw new Error(`No seasons found for league ${leagueId}`);
|
||||
}
|
||||
|
||||
|
||||
const activeSeason =
|
||||
seasons.find((s) => s.status === 'active') ?? seasons[0];
|
||||
|
||||
|
||||
if (!activeSeason) {
|
||||
throw new Error(`No active season could be determined for league ${leagueId}`);
|
||||
}
|
||||
|
||||
const scoringConfig =
|
||||
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (!scoringConfig) {
|
||||
@@ -50,14 +54,14 @@ export class GetLeagueScoringConfigUseCase
|
||||
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
const preset = presetId ? this.presetProvider.getPresetById(presetId) : undefined;
|
||||
|
||||
|
||||
const data: LeagueScoringConfigData = {
|
||||
leagueId: league.id,
|
||||
seasonId: activeSeason.id,
|
||||
gameId: game.id,
|
||||
gameName: game.name,
|
||||
scoringPresetId: presetId,
|
||||
preset,
|
||||
...(presetId !== undefined ? { scoringPresetId: presetId } : {}),
|
||||
...(preset !== undefined ? { preset } : {}),
|
||||
championships: scoringConfig.championships,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { ILeagueStandingsPresenter } from '../presenters/ILeagueStandingsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
ILeagueStandingsPresenter,
|
||||
LeagueStandingsResultDTO,
|
||||
LeagueStandingsViewModel,
|
||||
} from '../presenters/ILeagueStandingsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetLeagueStandingsUseCaseParams {
|
||||
leagueId: string;
|
||||
@@ -11,14 +15,20 @@ export interface GetLeagueStandingsUseCaseParams {
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueStandingsUseCase
|
||||
implements AsyncUseCase<GetLeagueStandingsUseCaseParams, void> {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
public readonly presenter: ILeagueStandingsPresenter,
|
||||
) {}
|
||||
implements
|
||||
UseCase<GetLeagueStandingsUseCaseParams, LeagueStandingsResultDTO, LeagueStandingsViewModel, ILeagueStandingsPresenter>
|
||||
{
|
||||
constructor(private readonly standingRepository: IStandingRepository) {}
|
||||
|
||||
async execute(params: GetLeagueStandingsUseCaseParams): Promise<void> {
|
||||
async execute(
|
||||
params: GetLeagueStandingsUseCaseParams,
|
||||
presenter: ILeagueStandingsPresenter,
|
||||
): Promise<void> {
|
||||
const standings = await this.standingRepository.findByLeagueId(params.leagueId);
|
||||
this.presenter.present(standings);
|
||||
const dto: LeagueStandingsResultDTO = {
|
||||
standings,
|
||||
};
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,11 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IPendingSponsorshipRequestsPresenter } from '../presenters/IPendingSponsorshipRequestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
import type {
|
||||
IPendingSponsorshipRequestsPresenter,
|
||||
PendingSponsorshipRequestsViewModel,
|
||||
} from '../presenters/IPendingSponsorshipRequestsPresenter';
|
||||
|
||||
export interface GetPendingSponsorshipRequestsDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -37,14 +41,23 @@ export interface GetPendingSponsorshipRequestsResultDTO {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class GetPendingSponsorshipRequestsUseCase {
|
||||
export class GetPendingSponsorshipRequestsUseCase
|
||||
implements UseCase<
|
||||
GetPendingSponsorshipRequestsDTO,
|
||||
GetPendingSponsorshipRequestsResultDTO,
|
||||
PendingSponsorshipRequestsViewModel,
|
||||
IPendingSponsorshipRequestsPresenter
|
||||
> {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorRepo: ISponsorRepository,
|
||||
private readonly presenter: IPendingSponsorshipRequestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<void> {
|
||||
async execute(
|
||||
dto: GetPendingSponsorshipRequestsDTO,
|
||||
presenter: IPendingSponsorshipRequestsPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
@@ -59,12 +72,12 @@ export class GetPendingSponsorshipRequestsUseCase {
|
||||
id: request.id,
|
||||
sponsorId: request.sponsorId,
|
||||
sponsorName: sponsor?.name ?? 'Unknown Sponsor',
|
||||
sponsorLogo: sponsor?.logoUrl,
|
||||
...(sponsor?.logoUrl !== undefined ? { sponsorLogo: sponsor.logoUrl } : {}),
|
||||
tier: request.tier,
|
||||
offeredAmount: request.offeredAmount.amount,
|
||||
currency: request.offeredAmount.currency,
|
||||
formattedAmount: request.offeredAmount.format(),
|
||||
message: request.message,
|
||||
...(request.message !== undefined ? { message: request.message } : {}),
|
||||
createdAt: request.createdAt,
|
||||
platformFee: request.getPlatformFee().amount,
|
||||
netAmount: request.getNetAmount().amount,
|
||||
@@ -74,7 +87,7 @@ export class GetPendingSponsorshipRequestsUseCase {
|
||||
// Sort by creation date (newest first)
|
||||
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
this.presenter.present({
|
||||
presenter.present({
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
requests: requestDTOs,
|
||||
|
||||
@@ -50,7 +50,7 @@ export class GetProfileOverviewUseCase {
|
||||
public readonly presenter: IProfileOverviewPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetProfileOverviewParams): Promise<void> {
|
||||
async execute(params: GetProfileOverviewParams): Promise<ProfileOverviewViewModel | null> {
|
||||
const { driverId } = params;
|
||||
|
||||
const driver = await this.driverRepository.findById(driverId);
|
||||
@@ -69,7 +69,7 @@ export class GetProfileOverviewUseCase {
|
||||
};
|
||||
|
||||
this.presenter.present(emptyViewModel);
|
||||
return;
|
||||
return emptyViewModel;
|
||||
}
|
||||
|
||||
const [statsAdapter, teams, friends] = await Promise.all([
|
||||
@@ -95,6 +95,7 @@ export class GetProfileOverviewUseCase {
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private buildDriverSummary(
|
||||
@@ -103,6 +104,7 @@ export class GetProfileOverviewUseCase {
|
||||
): ProfileOverviewDriverSummaryViewModel {
|
||||
const rankings = this.getAllDriverRankings();
|
||||
const fallbackRank = this.computeFallbackRank(driver.id, rankings);
|
||||
const totalDrivers = rankings.length;
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
@@ -110,13 +112,15 @@ export class GetProfileOverviewUseCase {
|
||||
country: driver.country,
|
||||
avatarUrl: this.imageService.getDriverAvatar(driver.id),
|
||||
iracingId: driver.iracingId ?? null,
|
||||
joinedAt: driver.joinedAt instanceof Date
|
||||
? driver.joinedAt.toISOString()
|
||||
: new Date(driver.joinedAt).toISOString(),
|
||||
joinedAt:
|
||||
driver.joinedAt instanceof Date
|
||||
? driver.joinedAt.toISOString()
|
||||
: new Date(driver.joinedAt).toISOString(),
|
||||
rating: stats?.rating ?? null,
|
||||
globalRank: stats?.overallRank ?? fallbackRank,
|
||||
consistency: stats?.consistency ?? null,
|
||||
bio: driver.bio ?? null,
|
||||
totalDrivers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,6 +165,9 @@ export class GetProfileOverviewUseCase {
|
||||
winRate,
|
||||
podiumRate,
|
||||
percentile: stats.percentile,
|
||||
rating: stats.rating,
|
||||
consistency: stats.consistency,
|
||||
overallRank: stats.overallRank,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,8 +424,10 @@ export class GetProfileOverviewUseCase {
|
||||
'Flexible schedule',
|
||||
];
|
||||
|
||||
const socialHandles = socialOptions[hash % socialOptions.length];
|
||||
const achievementsSource = achievementSets[hash % achievementSets.length];
|
||||
const socialHandles =
|
||||
socialOptions[hash % socialOptions.length] ?? [];
|
||||
const achievementsSource =
|
||||
achievementSets[hash % achievementSets.length] ?? [];
|
||||
|
||||
return {
|
||||
socialHandles,
|
||||
@@ -430,11 +439,11 @@ export class GetProfileOverviewUseCase {
|
||||
rarity: achievement.rarity,
|
||||
earnedAt: achievement.earnedAt.toISOString(),
|
||||
})),
|
||||
racingStyle: styles[hash % styles.length],
|
||||
favoriteTrack: tracks[hash % tracks.length],
|
||||
favoriteCar: cars[hash % cars.length],
|
||||
timezone: timezones[hash % timezones.length],
|
||||
availableHours: hours[hash % hours.length],
|
||||
racingStyle: styles[hash % styles.length] ?? 'Consistent Pacer',
|
||||
favoriteTrack: tracks[hash % tracks.length] ?? 'Unknown Track',
|
||||
favoriteCar: cars[hash % cars.length] ?? 'Unknown Car',
|
||||
timezone: timezones[hash % timezones.length] ?? 'UTC',
|
||||
availableHours: hours[hash % hours.length] ?? 'Flexible schedule',
|
||||
lookingForTeam: hash % 3 === 0,
|
||||
openToRequests: hash % 2 === 0,
|
||||
};
|
||||
|
||||
@@ -7,36 +7,51 @@
|
||||
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IRacePenaltiesPresenter } from '../presenters/IRacePenaltiesPresenter';
|
||||
import type {
|
||||
IRacePenaltiesPresenter,
|
||||
RacePenaltiesResultDTO,
|
||||
RacePenaltiesViewModel,
|
||||
} from '../presenters/IRacePenaltiesPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetRacePenaltiesUseCase {
|
||||
export interface GetRacePenaltiesInput {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRacePenaltiesUseCase
|
||||
implements
|
||||
UseCase<GetRacePenaltiesInput, RacePenaltiesResultDTO, RacePenaltiesViewModel, IRacePenaltiesPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
public readonly presenter: IRacePenaltiesPresenter,
|
||||
) {}
|
||||
|
||||
async execute(raceId: string): Promise<void> {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(raceId);
|
||||
|
||||
// Load all driver details in parallel
|
||||
async execute(input: GetRacePenaltiesInput, presenter: IRacePenaltiesPresenter): Promise<void> {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
penalties.forEach(penalty => {
|
||||
penalties.forEach((penalty) => {
|
||||
driverIds.add(penalty.driverId);
|
||||
driverIds.add(penalty.issuedBy);
|
||||
});
|
||||
|
||||
const drivers = await Promise.all(
|
||||
Array.from(driverIds).map(id => this.driverRepository.findById(id))
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
);
|
||||
|
||||
const driverMap = new Map<string, string>();
|
||||
drivers.forEach(driver => {
|
||||
drivers.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap.set(driver.id, driver.name);
|
||||
}
|
||||
});
|
||||
|
||||
this.presenter.present(penalties, driverMap);
|
||||
presenter.reset();
|
||||
const dto: RacePenaltiesResultDTO = {
|
||||
penalties,
|
||||
driverMap,
|
||||
};
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,31 @@
|
||||
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IRaceProtestsPresenter } from '../presenters/IRaceProtestsPresenter';
|
||||
import type {
|
||||
IRaceProtestsPresenter,
|
||||
RaceProtestsResultDTO,
|
||||
RaceProtestsViewModel,
|
||||
} from '../presenters/IRaceProtestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetRaceProtestsUseCase {
|
||||
export interface GetRaceProtestsInput {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRaceProtestsUseCase
|
||||
implements
|
||||
UseCase<GetRaceProtestsInput, RaceProtestsResultDTO, RaceProtestsViewModel, IRaceProtestsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
public readonly presenter: IRaceProtestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(raceId: string): Promise<void> {
|
||||
const protests = await this.protestRepository.findByRaceId(raceId);
|
||||
|
||||
// Load all driver details in parallel
|
||||
async execute(input: GetRaceProtestsInput, presenter: IRaceProtestsPresenter): Promise<void> {
|
||||
const protests = await this.protestRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
protests.forEach(protest => {
|
||||
protests.forEach((protest) => {
|
||||
driverIds.add(protest.protestingDriverId);
|
||||
driverIds.add(protest.accusedDriverId);
|
||||
if (protest.reviewedBy) {
|
||||
@@ -30,16 +40,21 @@ export class GetRaceProtestsUseCase {
|
||||
});
|
||||
|
||||
const drivers = await Promise.all(
|
||||
Array.from(driverIds).map(id => this.driverRepository.findById(id))
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
);
|
||||
|
||||
const driverMap = new Map<string, string>();
|
||||
drivers.forEach(driver => {
|
||||
drivers.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap.set(driver.id, driver.name);
|
||||
}
|
||||
});
|
||||
|
||||
this.presenter.present(protests, driverMap);
|
||||
presenter.reset();
|
||||
const dto: RaceProtestsResultDTO = {
|
||||
protests,
|
||||
driverMap,
|
||||
};
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ function mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewM
|
||||
return penalties.map((p) => ({
|
||||
driverId: p.driverId,
|
||||
type: p.type,
|
||||
value: p.value,
|
||||
...(p.value !== undefined ? { value: p.value } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ export class GetRaceResultsDetailUseCase {
|
||||
drivers: [],
|
||||
penalties: [],
|
||||
pointsSystem: {},
|
||||
fastestLapTime: undefined,
|
||||
currentDriverId: driverId,
|
||||
error: 'Race not found',
|
||||
};
|
||||
@@ -117,7 +116,7 @@ export class GetRaceResultsDetailUseCase {
|
||||
const pointsSystem = buildPointsSystem(league as League | null);
|
||||
const fastestLapTime = getFastestLapTime(results);
|
||||
const penaltySummary = mapPenaltySummary(penalties);
|
||||
|
||||
|
||||
const viewModel: RaceResultsDetailViewModel = {
|
||||
race: {
|
||||
id: race.id,
|
||||
@@ -136,7 +135,7 @@ export class GetRaceResultsDetailUseCase {
|
||||
drivers,
|
||||
penalties: penaltySummary,
|
||||
pointsSystem,
|
||||
fastestLapTime,
|
||||
...(fastestLapTime !== undefined ? { fastestLapTime } : {}),
|
||||
currentDriverId: effectiveCurrentDriverId,
|
||||
};
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
leagueName: league.name,
|
||||
seasonId: season.id,
|
||||
seasonName: season.name,
|
||||
seasonStartDate: season.startDate,
|
||||
seasonEndDate: season.endDate,
|
||||
...(season.startDate !== undefined ? { seasonStartDate: season.startDate } : {}),
|
||||
...(season.endDate !== undefined ? { seasonEndDate: season.endDate } : {}),
|
||||
tier: sponsorship.tier,
|
||||
status: sponsorship.status,
|
||||
pricing: {
|
||||
@@ -144,7 +144,7 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
impressions,
|
||||
},
|
||||
createdAt: sponsorship.createdAt,
|
||||
activatedAt: sponsorship.activatedAt,
|
||||
...(sponsorship.activatedAt !== undefined ? { activatedAt: sponsorship.activatedAt } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ITeamJoinRequestsPresenter } from '../presenters/ITeamJoinRequestsPresenter';
|
||||
import type {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestsResultDTO,
|
||||
TeamJoinRequestsViewModel,
|
||||
} from '../presenters/ITeamJoinRequestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team join requests.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetTeamJoinRequestsUseCase {
|
||||
export class GetTeamJoinRequestsUseCase
|
||||
implements UseCase<{ teamId: string }, TeamJoinRequestsResultDTO, TeamJoinRequestsViewModel, ITeamJoinRequestsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: ITeamJoinRequestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(teamId: string): Promise<void> {
|
||||
const requests = await this.membershipRepository.getJoinRequests(teamId);
|
||||
|
||||
async execute(input: { teamId: string }, presenter: ITeamJoinRequestsPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const requests = await this.membershipRepository.getJoinRequests(input.teamId);
|
||||
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
|
||||
for (const request of requests) {
|
||||
const driver = await this.driverRepository.findById(request.driverId);
|
||||
if (driver) {
|
||||
@@ -28,7 +39,13 @@ export class GetTeamJoinRequestsUseCase {
|
||||
}
|
||||
avatarUrls[request.driverId] = this.imageService.getDriverAvatar(request.driverId);
|
||||
}
|
||||
|
||||
this.presenter.present(requests, driverNames, avatarUrls);
|
||||
|
||||
const dto: TeamJoinRequestsResultDTO = {
|
||||
requests,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ITeamMembersPresenter } from '../presenters/ITeamMembersPresenter';
|
||||
import type {
|
||||
ITeamMembersPresenter,
|
||||
TeamMembersResultDTO,
|
||||
TeamMembersViewModel,
|
||||
} from '../presenters/ITeamMembersPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team members.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetTeamMembersUseCase {
|
||||
export class GetTeamMembersUseCase
|
||||
implements UseCase<{ teamId: string }, TeamMembersResultDTO, TeamMembersViewModel, ITeamMembersPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: ITeamMembersPresenter,
|
||||
) {}
|
||||
|
||||
async execute(teamId: string): Promise<void> {
|
||||
const memberships = await this.membershipRepository.getTeamMembers(teamId);
|
||||
|
||||
async execute(input: { teamId: string }, presenter: ITeamMembersPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const memberships = await this.membershipRepository.getTeamMembers(input.teamId);
|
||||
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
|
||||
for (const membership of memberships) {
|
||||
const driver = await this.driverRepository.findById(membership.driverId);
|
||||
if (driver) {
|
||||
@@ -28,7 +39,13 @@ export class GetTeamMembersUseCase {
|
||||
}
|
||||
avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId);
|
||||
}
|
||||
|
||||
this.presenter.present(memberships, driverNames, avatarUrls);
|
||||
|
||||
const dto: TeamMembersResultDTO = {
|
||||
memberships,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ITeamsLeaderboardPresenter } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import type {
|
||||
ITeamsLeaderboardPresenter,
|
||||
TeamsLeaderboardResultDTO,
|
||||
TeamsLeaderboardViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
interface DriverStatsAdapter {
|
||||
rating: number | null;
|
||||
@@ -16,22 +21,22 @@ interface DriverStatsAdapter {
|
||||
* Plain constructor-injected dependencies (no decorators) to keep the
|
||||
* application layer framework-agnostic and compatible with test tooling.
|
||||
*/
|
||||
export class GetTeamsLeaderboardUseCase {
|
||||
export class GetTeamsLeaderboardUseCase
|
||||
implements UseCase<void, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel, ITeamsLeaderboardPresenter> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly getDriverStats: (driverId: string) => DriverStatsAdapter | null,
|
||||
public readonly presenter: ITeamsLeaderboardPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(_input: void, presenter: ITeamsLeaderboardPresenter): Promise<void> {
|
||||
const allTeams = await this.teamRepository.findAll();
|
||||
const teams: any[] = [];
|
||||
|
||||
await Promise.all(
|
||||
allTeams.map(async (team) => {
|
||||
const memberships = await this.teamMembershipRepository.findByTeamId(team.id);
|
||||
const memberships = await this.teamMembershipRepository.getTeamMembers(team.id);
|
||||
const memberCount = memberships.length;
|
||||
|
||||
let ratingSum = 0;
|
||||
@@ -66,15 +71,18 @@ export class GetTeamsLeaderboardUseCase {
|
||||
isRecruiting: true,
|
||||
createdAt: new Date(),
|
||||
description: team.description,
|
||||
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const recruitingCount = teams.filter((t) => t.isRecruiting).length;
|
||||
|
||||
this.presenter.present(teams, recruitingCount);
|
||||
const result: TeamsLeaderboardResultDTO = {
|
||||
teams,
|
||||
recruitingCount,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(result);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { ILeagueScoringPresetsPresenter } from '../presenters/ILeagueScoringPresetsPresenter';
|
||||
import type {
|
||||
ILeagueScoringPresetsPresenter,
|
||||
LeagueScoringPresetsResultDTO,
|
||||
LeagueScoringPresetsViewModel,
|
||||
} from '../presenters/ILeagueScoringPresetsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for listing league scoring presets.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class ListLeagueScoringPresetsUseCase {
|
||||
constructor(
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
public readonly presenter: ILeagueScoringPresetsPresenter,
|
||||
) {}
|
||||
export class ListLeagueScoringPresetsUseCase
|
||||
implements UseCase<void, LeagueScoringPresetsResultDTO, LeagueScoringPresetsViewModel, ILeagueScoringPresetsPresenter>
|
||||
{
|
||||
constructor(private readonly presetProvider: LeagueScoringPresetProvider) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(_input: void, presenter: ILeagueScoringPresetsPresenter): Promise<void> {
|
||||
const presets = await this.presetProvider.listPresets();
|
||||
this.presenter.present(presets);
|
||||
|
||||
const dto: LeagueScoringPresetsResultDTO = {
|
||||
presets,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -38,14 +38,16 @@ export class RejectSponsorshipRequestUseCase {
|
||||
// Reject the request
|
||||
const rejectedRequest = request.reject(dto.respondedBy, dto.reason);
|
||||
await this.sponsorshipRequestRepo.update(rejectedRequest);
|
||||
|
||||
|
||||
// TODO: In a real implementation, notify the sponsor
|
||||
|
||||
|
||||
return {
|
||||
requestId: rejectedRequest.id,
|
||||
status: 'rejected',
|
||||
rejectedAt: rejectedRequest.respondedAt!,
|
||||
reason: rejectedRequest.rejectionReason,
|
||||
...(rejectedRequest.rejectionReason !== undefined
|
||||
? { reason: rejectedRequest.rejectionReason }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,8 @@ export class UpdateDriverProfileUseCase {
|
||||
}
|
||||
|
||||
const updated = existing.update({
|
||||
bio: bio ?? existing.bio,
|
||||
country: country ?? existing.country,
|
||||
...(bio !== undefined ? { bio } : {}),
|
||||
...(country !== undefined ? { country } : {}),
|
||||
});
|
||||
|
||||
const persisted = await this.driverRepository.update(updated);
|
||||
|
||||
Reference in New Issue
Block a user