refactor dtos to ports

This commit is contained in:
2025-12-19 14:08:27 +01:00
parent 2ab86ec9bd
commit 499562c456
106 changed files with 386 additions and 1009 deletions

View File

@@ -1,4 +1,4 @@
import { IApproveLeagueJoinRequestPresenter, ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter';
import { IApproveLeagueJoinRequestPresenter, ApproveLeagueJoinRequestResultPort, ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter';
export class ApproveLeagueJoinRequestPresenter implements IApproveLeagueJoinRequestPresenter {
private result: ApproveLeagueJoinRequestViewModel | null = null;
@@ -7,7 +7,7 @@ export class ApproveLeagueJoinRequestPresenter implements IApproveLeagueJoinRequ
this.result = null;
}
present(dto: ApproveLeagueJoinRequestResultDTO) {
present(dto: ApproveLeagueJoinRequestResultPort) {
this.result = dto;
}

View File

@@ -13,7 +13,7 @@ import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO';
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO';
import type { AcceptSponsorshipRequestResultDTO } from '@core/racing/application/dtos/AcceptSponsorshipRequestResultDTO';
import type { AcceptSponsorshipRequestResultPort } from '@core/racing/application/ports/output/AcceptSponsorshipRequestResultPort';
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@ApiTags('sponsors')
@@ -80,7 +80,7 @@ export class SponsorController {
@ApiResponse({ status: 200, description: 'Sponsorship request accepted' })
@ApiResponse({ status: 400, description: 'Invalid request' })
@ApiResponse({ status: 404, description: 'Request not found' })
async acceptSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO): Promise<AcceptSponsorshipRequestResultDTO | null> {
async acceptSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO): Promise<AcceptSponsorshipRequestResultPort | null> {
return this.sponsorService.acceptSponsorshipRequest(requestId, input.respondedBy);
}

View File

@@ -23,7 +23,7 @@ import { GetPendingSponsorshipRequestsUseCase, GetPendingSponsorshipRequestsDTO
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest';
import type { AcceptSponsorshipRequestResultDTO } from '@core/racing/application/dtos/AcceptSponsorshipRequestResultDTO';
import type { AcceptSponsorshipRequestResultPort } from '@core/racing/application/ports/output/AcceptSponsorshipRequestResultPort';
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@@ -123,7 +123,7 @@ export class SponsorService {
return result.value as GetPendingSponsorshipRequestsOutputDTO;
}
async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<AcceptSponsorshipRequestResultDTO | null> {
async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<AcceptSponsorshipRequestResultPort | null> {
this.logger.debug('[SponsorService] Accepting sponsorship request.', { requestId, respondedBy });
const result = await this.acceptSponsorshipRequestUseCase.execute({ requestId, respondedBy });

View File

@@ -1,4 +1,4 @@
import { CreateSponsorViewModel, CreateSponsorResultDTO, ICreateSponsorPresenter } from '@core/racing/application/presenters/ICreateSponsorPresenter';
import { CreateSponsorViewModel, CreateSponsorOutputPort, ICreateSponsorPresenter } from '@core/racing/application/presenters/ICreateSponsorPresenter';
export class CreateSponsorPresenter implements ICreateSponsorPresenter {
private result: CreateSponsorViewModel | null = null;
@@ -7,7 +7,7 @@ export class CreateSponsorPresenter implements ICreateSponsorPresenter {
this.result = null;
}
present(dto: CreateSponsorResultDTO) {
present(dto: CreateSponsorOutputPort) {
this.result = dto;
}

View File

@@ -1,13 +0,0 @@
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { Currency } from '../../domain/value-objects/Money';
export interface ApplyForSponsorshipDTO {
sponsorId: string;
entityType: SponsorableEntityType;
entityId: string;
tier: SponsorshipTier;
offeredAmount: number; // in cents
currency?: Currency;
message?: string;
}

View File

@@ -1,12 +0,0 @@
import type { PenaltyType } from '../../domain/entities/Penalty';
export interface ApplyPenaltyCommand {
raceId: string;
driverId: string;
stewardId: string;
type: PenaltyType;
value?: number;
reason: string;
protestId?: string;
notes?: string;
}

View File

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

View File

@@ -1,5 +0,0 @@
import type { Team } from '../../domain/entities/Team';
export interface CreateTeamResultDTO {
team: Team;
}

View File

@@ -1,3 +0,0 @@
import type { Team } from '../../domain/entities/Team';
export type GetAllTeamsQueryResultDTO = Team[];

View File

@@ -1,7 +0,0 @@
import type { Team } from '../../domain/entities/Team';
import type { TeamMembership } from '../../domain/types/TeamMembership';
export interface GetDriverTeamQueryResultDTO {
team: Team;
membership: TeamMembership;
}

View File

@@ -1,6 +0,0 @@
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
export interface GetEntitySponsorshipPricingDTO {
entityType: SponsorableEntityType;
entityId: string;
}

View File

@@ -1,11 +0,0 @@
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipSlotDTO } from './SponsorshipSlotDTO';
export interface GetEntitySponsorshipPricingResultDTO {
entityType: SponsorableEntityType;
entityId: string;
acceptingApplications: boolean;
customRequirements?: string;
mainSlot?: SponsorshipSlotDTO;
secondarySlot?: SponsorshipSlotDTO;
}

View File

@@ -1,6 +0,0 @@
import type { LeagueMembership } from '../../domain/entities/LeagueMembership';
export interface GetLeagueMembershipsResultDTO {
memberships: LeagueMembership[];
drivers: { id: string; name: string }[];
}

View File

@@ -1,26 +0,0 @@
export interface ProtestDTO {
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
submittedAt: Date;
description: string;
status: string;
}
export interface RaceDTO {
id: string;
name: string;
date: string;
}
export interface DriverDTO {
id: string;
name: string;
}
export interface GetLeagueProtestsResultDTO {
protests: ProtestDTO[];
races: RaceDTO[];
drivers: DriverDTO[];
}

View File

@@ -1,7 +0,0 @@
import type { Team } from '../../domain/entities/Team';
import type { TeamMembership } from '../../domain/types/TeamMembership';
export interface GetTeamDetailsQueryResultDTO {
team: Team;
membership: TeamMembership | null;
}

View File

@@ -1,148 +0,0 @@
import type { StewardingDecisionMode } from '../../domain/entities/League';
import type { LeagueVisibilityType } from '../../domain/value-objects/LeagueVisibility';
export type LeagueStructureMode = 'solo' | 'fixedTeams';
// TODO this is way too much for a DTO. it must be pure InputPort or OutputPort
/**
* League visibility determines public visibility and ranking status.
* - 'ranked': Public, competitive, affects driver ratings. Requires min 10 drivers.
* - 'unranked': Private, casual with friends. No rating impact. Any number of drivers.
*
* For backward compatibility, 'public'/'private' are also supported in the form,
* but the domain uses 'ranked'/'unranked'.
*/
export type LeagueVisibilityFormValue = LeagueVisibilityType | 'public' | 'private';
export interface LeagueStructureFormDTO {
mode: LeagueStructureMode;
maxDrivers: number;
maxTeams?: number;
driversPerTeam?: number;
multiClassEnabled?: boolean;
}
export interface LeagueChampionshipsFormDTO {
enableDriverChampionship: boolean;
enableTeamChampionship: boolean;
enableNationsChampionship: boolean;
enableTrophyChampionship: boolean;
}
export interface LeagueScoringFormDTO {
patternId?: string; // e.g. 'sprint-main-driver', 'club-ladder-solo'
// For now, keep customScoring optional and simple:
customScoringEnabled?: boolean;
}
export interface LeagueDropPolicyFormDTO {
strategy: 'none' | 'bestNResults' | 'dropWorstN';
n?: number;
}
export interface LeagueTimingsFormDTO {
practiceMinutes?: number;
qualifyingMinutes: number;
sprintRaceMinutes?: number;
mainRaceMinutes: number;
sessionCount: number;
roundsPlanned?: number;
seasonStartDate?: string; // ISO date YYYY-MM-DD
seasonEndDate?: string; // ISO date YYYY-MM-DD
raceStartTime?: string; // "HH:MM" 24h
timezoneId?: string; // IANA ID, e.g. "Europe/Berlin", or "track" for track local time
recurrenceStrategy?: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
intervalWeeks?: number;
weekdays?: import('../../domain/types/Weekday').Weekday[];
monthlyOrdinal?: 1 | 2 | 3 | 4;
monthlyWeekday?: import('../../domain/types/Weekday').Weekday;
}
/**
* Stewarding configuration for protests and penalties.
*/
export interface LeagueStewardingFormDTO {
/**
* How protest decisions are made
*/
decisionMode: StewardingDecisionMode;
/**
* Number of votes required to uphold/reject a protest
* Used with steward_vote, member_vote, steward_veto, member_veto modes
*/
requiredVotes?: number;
/**
* Whether to require a defense from the accused before deciding
*/
requireDefense: boolean;
/**
* Time limit (hours) for accused to submit defense
*/
defenseTimeLimit: number;
/**
* Time limit (hours) for voting to complete
*/
voteTimeLimit: number;
/**
* Time limit (hours) after race ends when protests can be filed
*/
protestDeadlineHours: number;
/**
* Time limit (hours) after race ends when stewarding is closed
*/
stewardingClosesHours: number;
/**
* Whether to notify the accused when a protest is filed
*/
notifyAccusedOnProtest: boolean;
/**
* Whether to notify eligible voters when a vote is required
*/
notifyOnVoteRequired: boolean;
}
export interface LeagueConfigFormModel {
leagueId?: string; // present for admin, omitted for create
basics: {
name: string;
description?: string;
/**
* League visibility/ranking mode.
* - 'ranked' (or legacy 'public'): Competitive, public, affects ratings. Min 10 drivers.
* - 'unranked' (or legacy 'private'): Casual with friends, no rating impact.
*/
visibility: LeagueVisibilityFormValue;
gameId: string;
/**
* League logo as base64 data URL (optional).
* Format: data:image/png;base64,... or data:image/jpeg;base64,...
*/
logoDataUrl?: string;
};
structure: LeagueStructureFormDTO;
championships: LeagueChampionshipsFormDTO;
scoring: LeagueScoringFormDTO;
dropPolicy: LeagueDropPolicyFormDTO;
timings: LeagueTimingsFormDTO;
stewarding: LeagueStewardingFormDTO;
}
/**
* Helper to normalize visibility values to new terminology.
* Maps 'public' -> 'ranked' and 'private' -> 'unranked'.
*/
export function normalizeVisibility(value: LeagueVisibilityFormValue): LeagueVisibilityType {
if (value === 'public' || value === 'ranked') return 'ranked';
return 'unranked';
}
/**
* Helper to convert new terminology to legacy for backward compatibility.
* Maps 'ranked' -> 'public' and 'unranked' -> 'private'.
*/
export function toLegacyVisibility(value: LeagueVisibilityFormValue): 'public' | 'private' {
if (value === 'ranked' || value === 'public') return 'public';
return 'private';
}

View File

@@ -1,121 +0,0 @@
import type { LeagueTimingsFormDTO } from './LeagueConfigFormDTO';
import type { Weekday } from '../../domain/types/Weekday';
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
import type { RecurrenceStrategy } from '../../domain/value-objects/RecurrenceStrategy';
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
import { BusinessRuleViolationError } from '../errors/RacingApplicationError';
export interface LeagueScheduleDTO {
seasonStartDate: string;
raceStartTime: string;
timezoneId: string;
recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
intervalWeeks?: number | undefined;
weekdays?: Weekday[] | undefined;
monthlyOrdinal?: 1 | 2 | 3 | 4 | undefined;
monthlyWeekday?: Weekday | undefined;
plannedRounds: number;
}
export interface LeagueSchedulePreviewDTO {
rounds: Array<{ roundNumber: number; scheduledAt: string; timezoneId: string }>;
summary: string;
}
export function leagueTimingsToScheduleDTO(
timings: LeagueTimingsFormDTO,
): LeagueScheduleDTO | null {
if (
!timings.seasonStartDate ||
!timings.raceStartTime ||
!timings.timezoneId ||
!timings.recurrenceStrategy ||
!timings.roundsPlanned
) {
return null;
}
return {
seasonStartDate: timings.seasonStartDate,
raceStartTime: timings.raceStartTime,
timezoneId: timings.timezoneId,
recurrenceStrategy: timings.recurrenceStrategy,
intervalWeeks: timings.intervalWeeks,
weekdays: timings.weekdays,
monthlyOrdinal: timings.monthlyOrdinal,
monthlyWeekday: timings.monthlyWeekday,
plannedRounds: timings.roundsPlanned,
};
}
export function scheduleDTOToSeasonSchedule(dto: LeagueScheduleDTO): SeasonSchedule {
if (!dto.seasonStartDate) {
throw new BusinessRuleViolationError('seasonStartDate is required');
}
if (!dto.raceStartTime) {
throw new BusinessRuleViolationError('raceStartTime is required');
}
if (!dto.timezoneId) {
throw new BusinessRuleViolationError('timezoneId is required');
}
if (!dto.recurrenceStrategy) {
throw new BusinessRuleViolationError('recurrenceStrategy is required');
}
if (!Number.isInteger(dto.plannedRounds) || dto.plannedRounds <= 0) {
throw new BusinessRuleViolationError('plannedRounds must be a positive integer');
}
const startDate = new Date(dto.seasonStartDate);
if (Number.isNaN(startDate.getTime())) {
throw new BusinessRuleViolationError(
`seasonStartDate must be a valid date, got "${dto.seasonStartDate}"`,
);
}
const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime);
const timezone = new LeagueTimezone(dto.timezoneId);
let recurrence: RecurrenceStrategy;
if (dto.recurrenceStrategy === 'weekly') {
if (!dto.weekdays || dto.weekdays.length === 0) {
throw new BusinessRuleViolationError('weekdays are required for weekly recurrence');
}
recurrence = RecurrenceStrategyFactory.weekly(new WeekdaySet(dto.weekdays));
} else if (dto.recurrenceStrategy === 'everyNWeeks') {
if (!dto.weekdays || dto.weekdays.length === 0) {
throw new BusinessRuleViolationError('weekdays are required for everyNWeeks recurrence');
}
if (dto.intervalWeeks == null) {
throw new BusinessRuleViolationError(
'intervalWeeks is required for everyNWeeks recurrence',
);
}
recurrence = RecurrenceStrategyFactory.everyNWeeks(
dto.intervalWeeks,
new WeekdaySet(dto.weekdays),
);
} else if (dto.recurrenceStrategy === 'monthlyNthWeekday') {
if (!dto.monthlyOrdinal || !dto.monthlyWeekday) {
throw new BusinessRuleViolationError(
'monthlyOrdinal and monthlyWeekday are required for monthlyNthWeekday',
);
}
const pattern = new MonthlyRecurrencePattern(dto.monthlyOrdinal, dto.monthlyWeekday);
recurrence = RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
} else {
throw new BusinessRuleViolationError(`Unknown recurrenceStrategy "${dto.recurrenceStrategy}"`);
}
return new SeasonSchedule({
startDate,
timeOfDay,
timezone,
recurrence,
plannedRounds: dto.plannedRounds,
});
}

View File

@@ -1,41 +0,0 @@
export interface LeagueSummaryScoringDTO {
gameId: string;
gameName: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
/**
* Human-readable scoring pattern summary combining preset name and drop policy,
* e.g. "Sprint + Main • Best 6 results of 8 count towards the championship."
*/
scoringPatternSummary: string;
}
export interface LeagueSummaryDTO {
id: string;
name: string;
description?: string;
createdAt: Date;
ownerId: string;
maxDrivers?: number;
usedDriverSlots?: number;
maxTeams?: number;
usedTeamSlots?: number;
/**
* Human-readable structure summary derived from capacity and (future) team settings,
* e.g. "Solo • 24 drivers" or "Teams • 12 × 2 drivers".
*/
structureSummary?: string;
/**
* Human-readable scoring pattern summary for list views,
* e.g. "Sprint + Main • Best 6 results of 8 count towards the championship."
*/
scoringPatternSummary?: string;
/**
* Human-readable timing summary for list views,
* e.g. "30 min Quali • 40 min Race".
*/
timingSummary?: string;
scoring?: LeagueSummaryScoringDTO;
}

View File

@@ -1,8 +0,0 @@
export interface IsDriverRegisteredForRaceQueryParamsDTO {
raceId: string;
driverId: string;
}
export interface GetRaceRegistrationsQueryParamsDTO {
raceId: string;
}

View File

@@ -66,7 +66,6 @@ export type {
TeamMembershipStatus,
} from '../domain/types/TeamMembership';
export type { DriverDTO } from './dto/DriverDTO';
export type { LeagueDTO } from './dto/LeagueDTO';
export type { RaceDTO } from './dto/RaceDTO';
export type { ResultDTO } from './dto/ResultDTO';
@@ -76,10 +75,8 @@ export type {
LeagueScheduleDTO,
LeagueSchedulePreviewDTO,
} from './dto/LeagueScheduleDTO';
export type {
ChampionshipStandingsDTO,
ChampionshipStandingsRowDTO,
} from './dto/ChampionshipStandingsDTO';
export type { ChampionshipStandingsOutputPort } from './ports/output/ChampionshipStandingsOutputPort';
export type { ChampionshipStandingsRowOutputPort } from './ports/output/ChampionshipStandingsRowOutputPort';
export type {
LeagueConfigFormModel,
LeagueStructureFormDTO,

View File

@@ -1,207 +0,0 @@
/**
* Application Layer: Entity to DTO Mappers
*
* Transforms domain entities to plain objects for crossing architectural boundaries.
* These mappers handle the Server Component -> Client Component boundary in Next.js 15.
*/
import { Driver } from '../../domain/entities/Driver';
import { League } from '../../domain/entities/League';
import { Race } from '../../domain/entities/Race';
import { Result } from '../../domain/entities/Result';
import { Standing } from '../../domain/entities/Standing';
import type { DriverDTO } from '../dto/DriverDTO';
import type { LeagueDTO } from '../dto/LeagueDTO';
import type { RaceDTO } from '../dto/RaceDTO';
import type { ResultDTO } from '../dto/ResultDTO';
import type { StandingDTO } from '../dto/StandingDTO';
export class EntityMappers {
static toDriverDTO(driver: Driver | null): DriverDTO | null {
if (!driver) return null;
return {
id: driver.id,
iracingId: driver.iracingId,
name: driver.name,
country: driver.country,
bio: driver.bio ?? '',
joinedAt: driver.joinedAt.toISOString(),
};
}
static toLeagueDTO(league: League | null): LeagueDTO | null {
if (!league) return null;
const socialLinks =
league.socialLinks !== undefined
? {
...(league.socialLinks.discordUrl !== undefined
? { discordUrl: league.socialLinks.discordUrl }
: {}),
...(league.socialLinks.youtubeUrl !== undefined
? { youtubeUrl: league.socialLinks.youtubeUrl }
: {}),
...(league.socialLinks.websiteUrl !== undefined
? { websiteUrl: league.socialLinks.websiteUrl }
: {}),
}
: undefined;
return {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: league.settings,
createdAt: league.createdAt.toISOString(),
...(socialLinks !== undefined ? { socialLinks } : {}),
};
}
static toLeagueDTOs(leagues: League[]): LeagueDTO[] {
return leagues.map((league) => {
const socialLinks =
league.socialLinks !== undefined
? {
...(league.socialLinks.discordUrl !== undefined
? { discordUrl: league.socialLinks.discordUrl }
: {}),
...(league.socialLinks.youtubeUrl !== undefined
? { youtubeUrl: league.socialLinks.youtubeUrl }
: {}),
...(league.socialLinks.websiteUrl !== undefined
? { websiteUrl: league.socialLinks.websiteUrl }
: {}),
}
: undefined;
return {
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: league.settings,
createdAt: league.createdAt.toISOString(),
...(socialLinks !== undefined ? { socialLinks } : {}),
};
});
}
static toRaceDTO(race: Race | null): RaceDTO | null {
if (!race) return null;
const sessionTypeMap = {
practice: 'practice' as const,
qualifying: 'qualifying' as const,
q1: 'qualifying' as const,
q2: 'qualifying' as const,
q3: 'qualifying' as const,
sprint: 'race' as const,
main: 'race' as const,
timeTrial: 'practice' as const,
};
return {
id: race.id,
leagueId: race.leagueId,
scheduledAt: race.scheduledAt.toISOString(),
track: race.track,
trackId: race.trackId ?? '',
car: race.car,
carId: race.carId ?? '',
sessionType: sessionTypeMap[race.sessionType.value],
status: race.status,
...(race.strengthOfField !== undefined
? { strengthOfField: race.strengthOfField }
: {}),
...(race.registeredCount !== undefined
? { registeredCount: race.registeredCount }
: {}),
...(race.maxParticipants !== undefined
? { maxParticipants: race.maxParticipants }
: {}),
};
}
static toRaceDTOs(races: Race[]): RaceDTO[] {
const sessionTypeMap = {
practice: 'practice' as const,
qualifying: 'qualifying' as const,
q1: 'qualifying' as const,
q2: 'qualifying' as const,
q3: 'qualifying' as const,
sprint: 'race' as const,
main: 'race' as const,
timeTrial: 'practice' as const,
};
return races.map((race) => ({
id: race.id,
leagueId: race.leagueId,
scheduledAt: race.scheduledAt.toISOString(),
track: race.track,
trackId: race.trackId ?? '',
car: race.car,
carId: race.carId ?? '',
sessionType: sessionTypeMap[race.sessionType.value],
status: race.status,
...(race.strengthOfField !== undefined
? { strengthOfField: race.strengthOfField }
: {}),
...(race.registeredCount !== undefined
? { registeredCount: race.registeredCount }
: {}),
...(race.maxParticipants !== undefined
? { maxParticipants: race.maxParticipants }
: {}),
}));
}
static toResultDTO(result: Result | null): ResultDTO | null {
if (!result) return null;
return {
id: result.id,
raceId: result.raceId,
driverId: result.driverId,
position: result.position,
fastestLap: result.fastestLap,
incidents: result.incidents,
startPosition: result.startPosition,
};
}
static toResultDTOs(results: Result[]): ResultDTO[] {
return results.map(result => ({
id: result.id,
raceId: result.raceId,
driverId: result.driverId,
position: result.position,
fastestLap: result.fastestLap,
incidents: result.incidents,
startPosition: result.startPosition,
}));
}
static toStandingDTO(standing: Standing | null): StandingDTO | null {
if (!standing) return null;
return {
leagueId: standing.leagueId,
driverId: standing.driverId,
points: standing.points,
wins: standing.wins,
position: standing.position,
racesCompleted: standing.racesCompleted,
};
}
static toStandingDTOs(standings: Standing[]): StandingDTO[] {
return standings.map(standing => ({
leagueId: standing.leagueId,
driverId: standing.driverId,
points: standing.points,
wins: standing.wins,
position: standing.position,
racesCompleted: standing.racesCompleted,
}));
}
}

View File

@@ -1,2 +0,0 @@
// Mappers for converting between domain entities and DTOs
// Example: driverToDTO, leagueToDTO, etc.

View File

@@ -1,3 +0,0 @@
export interface DriverRatingPort {
getRating(driverId: string): { rating: number | null; ratingChange: number | null };
}

View File

@@ -1,20 +0,0 @@
/**
* Application Port: DriverRatingProvider
*
* Port for looking up driver ratings.
* Implemented by infrastructure adapters that connect to rating systems.
*/
export interface DriverRatingProvider {
/**
* Get the rating for a single driver
* Returns null if driver has no rating
*/
getRating(driverId: string): number | null;
/**
* Get ratings for multiple drivers
* Returns a map of driverId -> rating
*/
getRatings(driverIds: string[]): Map<string, number>;
}

View File

@@ -1,12 +0,0 @@
/**
* Application Port: IImageServicePort
*
* Abstraction used by racing application use cases to obtain image URLs
* for drivers, teams and leagues without depending on UI/media layers.
*/
export interface IImageServicePort {
getDriverAvatar(driverId: string): string;
getTeamLogo(teamId: string): string;
getLeagueCover(leagueId: string): string;
getLeagueLogo(leagueId: string): string;
}

View File

@@ -1,46 +0,0 @@
/**
* Application Port: ILiveryCompositor
*
* Defines interface for livery image composition.
* Infrastructure will provide image processing implementation.
*/
import type { LiveryDecal } from '../../domain/value-objects/LiveryDecal';
export interface CompositionResult {
success: boolean;
composedImageUrl?: string;
error?: string;
timestamp: Date;
}
export interface ILiveryCompositor {
/**
* Composite a livery by layering decals on base image
*/
composeLivery(
baseImageUrl: string,
decals: LiveryDecal[]
): Promise<CompositionResult>;
/**
* Generate a livery pack (.zip) for all drivers in a season
*/
generateLiveryPack(
seasonId: string,
liveryData: Array<{
driverId: string;
driverName: string;
carId: string;
composedImageUrl: string;
}>
): Promise<Buffer>;
/**
* Validate livery image (check for logos/text)
*/
validateLiveryImage(imageUrl: string): Promise<{
isValid: boolean;
violations?: string[];
}>;
}

View File

@@ -1,39 +0,0 @@
/**
* Application Port: ILiveryStorage
*
* Defines interface for livery image storage.
* Infrastructure will provide cloud storage adapter.
*/
export interface UploadResult {
success: boolean;
imageUrl?: string;
error?: string;
timestamp: Date;
}
export interface ILiveryStorage {
/**
* Upload a livery image
*/
upload(
imageData: Buffer | string,
fileName: string,
metadata?: Record<string, unknown>
): Promise<UploadResult>;
/**
* Download a livery image
*/
download(imageUrl: string): Promise<Buffer>;
/**
* Delete a livery image
*/
delete(imageUrl: string): Promise<void>;
/**
* Generate a signed URL for temporary access
*/
generateSignedUrl(imageUrl: string, expiresInSeconds: number): Promise<string>;
}

View File

@@ -1,48 +0,0 @@
/**
* Application Port: IPaymentGateway
*
* Defines interface for payment processing.
* Infrastructure will provide mock or real implementation.
*/
import type { Money } from '../../domain/value-objects/Money';
export interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
timestamp: Date;
}
export interface RefundResult {
success: boolean;
refundId?: string;
error?: string;
timestamp: Date;
}
export interface IPaymentGateway {
/**
* Process a payment
*/
processPayment(
amount: Money,
payerId: string,
description: string,
metadata?: Record<string, unknown>
): Promise<PaymentResult>;
/**
* Refund a payment
*/
refund(
originalTransactionId: string,
amount: Money,
reason: string
): Promise<RefundResult>;
/**
* Verify payment status
*/
verifyPayment(transactionId: string): Promise<PaymentResult>;
}

View File

@@ -1,29 +0,0 @@
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver'
| 'team'
| 'nations'
| 'trophy';
export interface LeagueScoringPresetDTO {
id: string;
name: string;
description: string;
primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType;
sessionSummary: string;
bonusSummary: string;
dropPolicySummary: string;
}
/**
* Provider abstraction for league scoring presets used by application-layer queries.
*
* In-memory implementation is backed by the preset registry in
* InMemoryScoringRepositories.
*/
export interface LeagueScoringPresetProvider {
listPresets(): LeagueScoringPresetDTO[];
getPresetById(id: string): LeagueScoringPresetDTO | undefined;
createScoringConfigFromPreset(presetId: string, seasonId: string): LeagueScoringConfig;
}

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
import type { SponsorableEntityType } from '../../../domain/entities/SponsorshipRequest';
export interface GetEntitySponsorshipPricingInputPort {
entityType: SponsorableEntityType;
entityId: string;
}

View File

@@ -0,0 +1,3 @@
export interface GetLeagueCoverInputPort {
leagueId: string;
}

View File

@@ -0,0 +1,3 @@
export interface GetLeagueLogoInputPort {
leagueId: string;
}

View File

@@ -0,0 +1,3 @@
export interface GetLeagueScoringPresetByIdInputPort {
presetId: string;
}

View File

@@ -0,0 +1,3 @@
export interface GetTeamLogoInputPort {
teamId: string;
}

View File

@@ -3,4 +3,4 @@
* - 'ranked' (or legacy 'public'): Competitive, public, affects driver ratings. Min 10 drivers.
* - 'unranked' (or legacy 'private'): Casual with friends, no rating impact.
*/
export type LeagueVisibilityInput = 'ranked' | 'unranked' | 'public' | 'private';
export type LeagueVisibilityInputPort = 'ranked' | 'unranked' | 'public' | 'private';

View File

@@ -0,0 +1,3 @@
export interface ListLeagueScoringPresetsInputPort {
// Empty interface for query with no parameters
}

View File

@@ -0,0 +1,6 @@
export interface ProcessPaymentInputPort {
amount: number; // in cents
payerId: string;
description: string;
metadata?: Record<string, unknown>;
}

View File

@@ -0,0 +1,8 @@
export interface IsDriverRegisteredForRaceInputPort {
raceId: string;
driverId: string;
}
export interface GetRaceRegistrationsInputPort {
raceId: string;
}

View File

@@ -0,0 +1,7 @@
import type { Money } from '../../domain/value-objects/Money';
export interface RefundPaymentInputPort {
originalTransactionId: string;
amount: Money;
reason: string;
}

View File

@@ -0,0 +1,3 @@
export interface VerifyPaymentInputPort {
transactionId: string;
}

View File

@@ -1,4 +1,4 @@
export interface AcceptSponsorshipRequestResultDTO {
export interface AcceptSponsorshipOutputPort {
requestId: string;
sponsorshipId: string;
status: 'accepted';

View File

@@ -1,4 +1,4 @@
export interface ApplyForSponsorshipResultDTO {
export interface ApplyForSponsorshipResultPort {
requestId: string;
status: 'pending';
createdAt: Date;

View File

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

View File

@@ -0,0 +1,8 @@
import type { ChampionshipStandingsRowOutputPort } from './ChampionshipStandingsRowOutputPort';
export interface ChampionshipStandingsOutputPort {
seasonId: string;
championshipId: string;
championshipName: string;
rows: ChampionshipStandingsRowOutputPort[];
}

View File

@@ -1,16 +1,9 @@
import type { ParticipantRef } from '@core/racing/domain/types/ParticipantRef';
export interface ChampionshipStandingsRowDTO {
export interface ChampionshipStandingsRowOutputPort {
participant: ParticipantRef;
position: number;
totalPoints: number;
resultsCounted: number;
resultsDropped: number;
}
export interface ChampionshipStandingsDTO {
seasonId: string;
championshipId: string;
championshipName: string;
rows: ChampionshipStandingsRowDTO[];
}

View File

@@ -1,4 +1,4 @@
export interface CreateLeagueWithSeasonAndScoringResultDTO {
export interface CreateLeagueWithSeasonAndScoringOutputPort {
leagueId: string;
seasonId: string;
scoringPresetId?: string;

View File

@@ -1,4 +1,4 @@
export interface CreateSponsorResultDTO {
export interface CreateSponsorOutputPort {
sponsor: {
id: string;
name: string;

View File

@@ -0,0 +1,5 @@
import type { Team } from '../../../domain/entities/Team';
export interface CreateTeamOutputPort {
team: Team;
}

View File

@@ -1,8 +1,8 @@
export type DriverDTO = {
export interface DriverOutputPort {
id: string;
iracingId: string;
name: string;
country: string;
bio?: string;
joinedAt: string;
};
}

View File

@@ -0,0 +1,3 @@
import type { Team } from '../../../domain/entities/Team';
export type GetAllTeamsOutputPort = Team[];

View File

@@ -0,0 +1,3 @@
export interface GetDriverAvatarOutputPort {
avatarUrl: string;
}

View File

@@ -0,0 +1,4 @@
export interface GetDriverRatingOutputPort {
rating: number | null;
ratingChange: number | null;
}

View File

@@ -0,0 +1,7 @@
import type { Team } from '../../../domain/entities/Team';
import type { TeamMembership } from '../../../domain/types/TeamMembership';
export interface GetDriverTeamOutputPort {
team: Team;
membership: TeamMembership;
}

View File

@@ -0,0 +1,11 @@
import type { SponsorableEntityType } from '../../../domain/entities/SponsorshipRequest';
import type { SponsorshipSlotDTO } from './SponsorshipSlotOutputPort';
export interface GetEntitySponsorshipPricingOutputPort {
entityType: SponsorableEntityType;
entityId: string;
acceptingApplications: boolean;
customRequirements?: string;
mainSlot?: SponsorshipSlotDTO;
secondarySlot?: SponsorshipSlotDTO;
}

View File

@@ -1,4 +1,4 @@
export interface GetLeagueAdminResultDTO {
export interface GetLeagueAdminOutputPort {
league: {
id: string;
ownerId: string;

View File

@@ -1,4 +1,4 @@
export interface GetLeagueAdminPermissionsResultDTO {
export interface GetLeagueAdminPermissionsOutputPort {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}

View File

@@ -0,0 +1,3 @@
export interface GetLeagueCoverOutputPort {
coverUrl: string;
}

View File

@@ -1,4 +1,4 @@
export interface GetLeagueJoinRequestsResultDTO {
export interface GetLeagueJoinRequestsOutputPort {
joinRequests: Array<{
id: string;
leagueId: string;

View File

@@ -0,0 +1,3 @@
export interface GetLeagueLogoOutputPort {
logoUrl: string;
}

View File

@@ -0,0 +1,6 @@
import type { LeagueMembership } from '../../../domain/entities/LeagueMembership';
export interface GetLeagueMembershipsOutputPort {
memberships: LeagueMembership[];
drivers: { id: string; name: string }[];
}

View File

@@ -1,3 +1,3 @@
export interface GetLeagueOwnerSummaryResultDTO {
export interface GetLeagueOwnerSummaryOutputPort {
summary: { driver: { id: string; name: string }; rating: number; rank: number } | null;
}

View File

@@ -0,0 +1,18 @@
import type { ProtestOutputPort } from './ProtestOutputPort';
export interface RaceOutputPort {
id: string;
name: string;
date: string;
}
export interface DriverOutputPort {
id: string;
name: string;
}
export interface GetLeagueProtestsOutputPort {
protests: ProtestOutputPort[];
races: RaceOutputPort[];
drivers: DriverOutputPort[];
}

View File

@@ -1,4 +1,4 @@
export interface GetLeagueScheduleResultDTO {
export interface GetLeagueScheduleOutputPort {
races: Array<{
id: string;
name: string;

View File

@@ -0,0 +1,7 @@
import type { Team } from '../../../domain/entities/Team';
import type { TeamMembership } from '../../../domain/types/TeamMembership';
export interface GetTeamDetailsOutputPort {
team: Team;
membership: TeamMembership | null;
}

View File

@@ -0,0 +1,3 @@
export interface GetTeamLogoOutputPort {
logoUrl: string;
}

View File

@@ -1,4 +1,4 @@
export type LeagueDriverSeasonStatsDTO = {
export interface LeagueDriverSeasonStatsOutputPort {
leagueId: string;
driverId: string;
position: number;
@@ -17,4 +17,4 @@ export type LeagueDriverSeasonStatsDTO = {
avgFinish: number | null;
rating: number | null;
ratingChange: number | null;
};
}

View File

@@ -1,4 +1,4 @@
export type LeagueDTO = {
export interface LeagueOutputPort {
id: string;
name: string;
description: string;
@@ -16,9 +16,5 @@ export type LeagueDTO = {
youtubeUrl?: string;
websiteUrl?: string;
};
/**
* Number of active driver slots currently used in this league.
* Populated by capacity-aware queries such as GetAllLeaguesWithCapacityQuery.
*/
usedSlots?: number;
};
}

View File

@@ -0,0 +1,18 @@
import type { Weekday } from '../../../domain/types/Weekday';
export interface LeagueScheduleOutputPort {
seasonStartDate: string;
raceStartTime: string;
timezoneId: string;
recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
intervalWeeks?: number;
weekdays?: Weekday[];
monthlyOrdinal?: 1 | 2 | 3 | 4;
monthlyWeekday?: Weekday;
plannedRounds: number;
}
export interface LeagueSchedulePreviewOutputPort {
rounds: Array<{ roundNumber: number; scheduledAt: string; timezoneId: string }>;
summary: string;
}

View File

@@ -1,4 +1,4 @@
export interface LeagueScoringChampionshipDTO {
export interface LeagueScoringChampionshipOutputPort {
id: string;
name: string;
type: 'driver' | 'team' | 'nations' | 'trophy';
@@ -8,7 +8,7 @@ export interface LeagueScoringChampionshipDTO {
dropPolicyDescription: string;
}
export interface LeagueScoringConfigDTO {
export interface LeagueScoringConfigOutputPort {
leagueId: string;
seasonId: string;
gameId: string;
@@ -16,5 +16,5 @@ export interface LeagueScoringConfigDTO {
scoringPresetId?: string;
scoringPresetName?: string;
dropPolicySummary: string;
championships: LeagueScoringChampionshipDTO[];
championships: LeagueScoringChampionshipOutputPort[];
}

View File

@@ -0,0 +1,15 @@
export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver'
| 'team'
| 'nations'
| 'trophy';
export interface LeagueScoringPresetOutputPort {
id: string;
name: string;
description: string;
primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType;
sessionSummary: string;
bonusSummary: string;
dropPolicySummary: string;
}

View File

@@ -0,0 +1,25 @@
export interface LeagueSummaryScoringOutputPort {
gameId: string;
gameName: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
scoringPresetId: string;
scoringPresetName: string;
dropPolicySummary: string;
scoringPatternSummary: string;
}
export interface LeagueSummaryOutputPort {
id: string;
name: string;
description?: string;
createdAt: Date;
ownerId: string;
maxDrivers?: number;
usedDriverSlots?: number;
maxTeams?: number;
usedTeamSlots?: number;
structureSummary?: string;
scoringPatternSummary?: string;
timingSummary?: string;
scoring?: LeagueSummaryScoringOutputPort;
}

View File

@@ -0,0 +1,6 @@
export interface ProcessPaymentOutputPort {
success: boolean;
transactionId?: string;
error?: string;
timestamp: Date;
}

View File

@@ -0,0 +1,9 @@
export interface ProtestOutputPort {
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
submittedAt: Date;
description: string;
status: string;
}

View File

@@ -1,4 +1,4 @@
export type RaceDTO = {
export interface RaceOutputPort {
id: string;
leagueId: string;
scheduledAt: string;
@@ -11,4 +11,4 @@ export type RaceDTO = {
strengthOfField?: number;
registeredCount?: number;
maxParticipants?: number;
};
}

View File

@@ -0,0 +1,6 @@
export interface RefundPaymentOutputPort {
success: boolean;
refundId?: string;
error?: string;
timestamp: Date;
}

View File

@@ -1,4 +1,4 @@
export type ResultDTO = {
export interface ResultOutputPort {
id: string;
raceId: string;
driverId: string;
@@ -6,4 +6,4 @@ export type ResultDTO = {
fastestLap: number;
incidents: number;
startPosition: number;
};
}

View File

@@ -1,6 +1,6 @@
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { SponsorshipTier } from '../../../domain/entities/SeasonSponsorship';
export interface SponsorshipSlotDTO {
export interface SponsorshipSlotOutputPort {
tier: SponsorshipTier;
price: number;
currency: string;

View File

@@ -1,8 +1,8 @@
export type StandingDTO = {
export interface StandingOutputPort {
leagueId: string;
driverId: string;
points: number;
wins: number;
position: number;
racesCompleted: number;
};
}

View File

@@ -0,0 +1,6 @@
export interface VerifyPaymentOutputPort {
success: boolean;
transactionId?: string;
error?: string;
timestamp: Date;
}

View File

@@ -2,7 +2,7 @@ import type { League } from '../../domain/entities/League';
import type { Season } from '../../domain/entities/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../domain/entities/Game';
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
import type { Presenter } from '@core/shared/presentation';
export interface LeagueSummaryViewModel {
@@ -40,7 +40,7 @@ export interface LeagueEnrichedData {
season?: Season;
scoringConfig?: LeagueScoringConfig;
game?: Game;
preset?: LeagueScoringPresetDTO;
preset?: LeagueScoringPresetOutputPort;
}
export interface IAllLeaguesWithCapacityAndScoringPresenter

View File

@@ -5,9 +5,9 @@ export interface ApproveLeagueJoinRequestViewModel {
message: string;
}
export interface ApproveLeagueJoinRequestResultDTO {
export interface ApproveLeagueJoinRequestResultPort {
success: boolean;
message: string;
}
export interface IApproveLeagueJoinRequestPresenter extends Presenter<ApproveLeagueJoinRequestResultDTO, ApproveLeagueJoinRequestViewModel> {}
export interface IApproveLeagueJoinRequestPresenter extends Presenter<ApproveLeagueJoinRequestResultPort, ApproveLeagueJoinRequestViewModel> {}

View File

@@ -13,8 +13,8 @@ export interface CreateSponsorViewModel {
sponsor: SponsorDto;
}
export interface CreateSponsorResultDTO {
export interface CreateSponsorOutputPort {
sponsor: SponsorDto;
}
export interface ICreateSponsorPresenter extends Presenter<CreateSponsorResultDTO, CreateSponsorViewModel> {}
export interface ICreateSponsorPresenter extends Presenter<CreateSponsorOutputPort, CreateSponsorViewModel> {}

View File

@@ -1,5 +1,5 @@
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
import type { Presenter } from '@core/shared/presentation';
export interface LeagueScoringChampionshipViewModel {
@@ -29,7 +29,7 @@ export interface LeagueScoringConfigData {
gameId: string;
gameName: string;
scoringPresetId?: string;
preset?: LeagueScoringPresetDTO;
preset?: LeagueScoringPresetOutputPort;
championships: ChampionshipConfig[];
}

View File

@@ -1,13 +1,13 @@
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
import type { Presenter } from '@core/shared/presentation';
export interface LeagueScoringPresetsViewModel {
presets: LeagueScoringPresetDTO[];
presets: LeagueScoringPresetOutputPort[];
totalCount: number;
}
export interface LeagueScoringPresetsResultDTO {
presets: LeagueScoringPresetDTO[];
presets: LeagueScoringPresetOutputPort[];
}
export interface ILeagueScoringPresetsPresenter

View File

@@ -10,7 +10,6 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { INotificationService } from '@core/notifications/application/ports/INotificationService';
import type { IPaymentGateway } from '../ports/IPaymentGateway';
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
@@ -18,22 +17,24 @@ import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { AcceptSponsorshipRequestDTO } from '../dto/AcceptSponsorshipRequestDTO';
import type { AcceptSponsorshipRequestResultDTO } from '../dto/AcceptSponsorshipRequestResultDTO';
import type { AcceptSponsorshipOutputPort } from '../ports/output/AcceptSponsorshipOutputPort';
import type { ProcessPaymentInputPort } from '../ports/input/ProcessPaymentInputPort';
import type { ProcessPaymentOutputPort } from '../ports/output/ProcessPaymentOutputPort';
export class AcceptSponsorshipRequestUseCase
implements AsyncUseCase<AcceptSponsorshipRequestDTO, AcceptSponsorshipRequestResultDTO, string> {
implements AsyncUseCase<AcceptSponsorshipRequestDTO, AcceptSponsorshipOutputPort, string> {
constructor(
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly notificationService: INotificationService,
private readonly paymentGateway: IPaymentGateway,
private readonly paymentProcessor: (input: ProcessPaymentInputPort) => Promise<ProcessPaymentOutputPort>,
private readonly walletRepository: IWalletRepository,
private readonly leagueWalletRepository: ILeagueWalletRepository,
private readonly logger: Logger,
) {}
async execute(dto: AcceptSponsorshipRequestDTO): Promise<Result<AcceptSponsorshipRequestResultDTO, ApplicationErrorCode<string>>> {
async execute(dto: AcceptSponsorshipRequestDTO): Promise<Result<AcceptSponsorshipOutputPort, ApplicationErrorCode<string>>> {
this.logger.debug(`Attempting to accept sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, respondedBy: dto.respondedBy });
// Find the request
@@ -92,13 +93,15 @@ export class AcceptSponsorshipRequestUseCase
},
});
// Process payment
const paymentResult = await this.paymentGateway.processPayment(
request.offeredAmount,
request.sponsorId,
`Sponsorship payment for ${request.entityType} ${request.entityId}`,
{ requestId: request.id }
);
// Process payment using clean input/output ports with primitive types
const paymentInput: ProcessPaymentInputPort = {
amount: request.offeredAmount.amount, // Extract primitive number from value object
payerId: request.sponsorId,
description: `Sponsorship payment for ${request.entityType} ${request.entityId}`,
metadata: { requestId: request.id }
};
const paymentResult = await this.paymentProcessor(paymentInput);
if (!paymentResult.success) {
this.logger.error(`Payment failed for sponsorship request ${request.id}: ${paymentResult.error}`, undefined, { requestId: request.id });
return Result.err({ code: 'PAYMENT_PROCESSING_FAILED' });
@@ -142,4 +145,4 @@ export class AcceptSponsorshipRequestUseCase
netAmount: acceptedRequest.getNetAmount().amount,
});
}
}
}

View File

@@ -14,11 +14,11 @@ import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ApplyForSponsorshipDTO } from '../dto/ApplyForSponsorshipDTO';
import type { ApplyForSponsorshipResultDTO } from '../dto/ApplyForSponsorshipResultDTO';
import type { ApplyForSponsorshipPort } from '../ports/input/ApplyForSponsorshipPort';
import type { ApplyForSponsorshipResultPort } from '../ports/output/ApplyForSponsorshipResultPort';
export class ApplyForSponsorshipUseCase
implements AsyncUseCase<ApplyForSponsorshipDTO, ApplyForSponsorshipResultDTO, string>
implements AsyncUseCase<ApplyForSponsorshipPort, ApplyForSponsorshipResultPort, string>
{
constructor(
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
@@ -27,7 +27,7 @@ export class ApplyForSponsorshipUseCase
private readonly logger: Logger,
) {}
async execute(dto: ApplyForSponsorshipDTO): Promise<Result<ApplyForSponsorshipResultDTO, ApplicationErrorCode<string>>> {
async execute(dto: ApplyForSponsorshipPort): Promise<Result<ApplyForSponsorshipResultPort, ApplicationErrorCode<string>>> {
this.logger.debug('Attempting to apply for sponsorship', { dto });
// Validate sponsor exists

View File

@@ -15,10 +15,10 @@ import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ApplyPenaltyCommand } from '../dto/ApplyPenaltyCommand';
import type { ApplyPenaltyCommandPort } from '../ports/input/ApplyPenaltyCommandPort';
export class ApplyPenaltyUseCase
implements AsyncUseCase<ApplyPenaltyCommand, { penaltyId: string }, string> {
implements AsyncUseCase<ApplyPenaltyCommandPort, { penaltyId: string }, string> {
constructor(
private readonly penaltyRepository: IPenaltyRepository,
private readonly protestRepository: IProtestRepository,
@@ -27,7 +27,7 @@ export class ApplyPenaltyUseCase
private readonly logger: Logger,
) {}
async execute(command: ApplyPenaltyCommand): Promise<Result<{ penaltyId: string }, ApplicationErrorCode<string>>> {
async execute(command: ApplyPenaltyCommandPort): Promise<Result<{ penaltyId: string }, ApplicationErrorCode<string>>> {
this.logger.debug('ApplyPenaltyUseCase: Executing with command', command);
// Validate race exists

View File

@@ -4,13 +4,13 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
import type { AsyncUseCase } from '@core/shared/application';
import { randomUUID } from 'crypto';
import type { ApproveLeagueJoinRequestUseCaseParams } from '../dto/ApproveLeagueJoinRequestUseCaseParams';
import type { ApproveLeagueJoinRequestResultDTO } from '../dto/ApproveLeagueJoinRequestResultDTO';
import type { ApproveLeagueJoinRequestResultPort } from '../ports/output/ApproveLeagueJoinRequestResultPort';
import { JoinedAt } from '../../domain/value-objects/JoinedAt';
export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeagueJoinRequestUseCaseParams, ApproveLeagueJoinRequestResultDTO, string> {
export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeagueJoinRequestUseCaseParams, ApproveLeagueJoinRequestResultPort, string> {
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
async execute(params: ApproveLeagueJoinRequestUseCaseParams): Promise<Result<ApproveLeagueJoinRequestResultDTO, ApplicationErrorCode<string>>> {
async execute(params: ApproveLeagueJoinRequestUseCaseParams): Promise<Result<ApproveLeagueJoinRequestResultPort, ApplicationErrorCode<string>>> {
const requests = await this.leagueMembershipRepository.getJoinRequests(params.leagueId);
const request = requests.find(r => r.id === params.requestId);
if (!request) {
@@ -25,7 +25,7 @@ export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeag
status: 'active',
joinedAt: JoinedAt.create(new Date()),
});
const dto: ApproveLeagueJoinRequestResultDTO = { success: true, message: 'Join request approved.' };
const dto: ApproveLeagueJoinRequestResultPort = { success: true, message: 'Join request approved.' };
return Result.ok(dto);
}
}

View File

@@ -17,7 +17,7 @@ import {
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { LeagueVisibilityInput } from '../dto/LeagueVisibilityInput';
import type { CreateLeagueWithSeasonAndScoringResultDTO } from '../dto/CreateLeagueWithSeasonAndScoringResultDTO';
import type { CreateLeagueWithSeasonAndScoringOutputPort } from '../ports/output/CreateLeagueWithSeasonAndScoringOutputPort';
export interface CreateLeagueWithSeasonAndScoringCommand {
name: string;
@@ -40,7 +40,7 @@ export interface CreateLeagueWithSeasonAndScoringCommand {
}
export class CreateLeagueWithSeasonAndScoringUseCase
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, CreateLeagueWithSeasonAndScoringResultDTO, 'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR'> {
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, CreateLeagueWithSeasonAndScoringOutputPort, 'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR'> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
@@ -51,7 +51,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
async execute(
command: CreateLeagueWithSeasonAndScoringCommand,
): Promise<Result<CreateLeagueWithSeasonAndScoringResultDTO, ApplicationErrorCode<'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR', { message: string }>>> {
): Promise<Result<CreateLeagueWithSeasonAndScoringOutputPort, ApplicationErrorCode<'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug('Executing CreateLeagueWithSeasonAndScoringUseCase', { command });
const validation = this.validate(command);
if (validation.isErr()) {
@@ -112,7 +112,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
await this.leagueScoringConfigRepository.save(finalConfig);
this.logger.info(`Scoring configuration saved for season ${seasonId}.`);
const result: CreateLeagueWithSeasonAndScoringResultDTO = {
const result: CreateLeagueWithSeasonAndScoringOutputPort = {
leagueId: league.id.toString(),
seasonId,
scoringPresetId: preset.id,

View File

@@ -10,7 +10,7 @@ import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { CreateSponsorResultDTO } from '../dto/CreateSponsorResultDTO';
import type { CreateSponsorOutputPort } from '../ports/output/CreateSponsorOutputPort';
export interface CreateSponsorCommand {
name: string;
@@ -20,7 +20,7 @@ export interface CreateSponsorCommand {
}
export class CreateSponsorUseCase
implements AsyncUseCase<CreateSponsorCommand, CreateSponsorResultDTO, 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'>
implements AsyncUseCase<CreateSponsorCommand, CreateSponsorOutputPort, 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'>
{
constructor(
private readonly sponsorRepository: ISponsorRepository,
@@ -29,7 +29,7 @@ export class CreateSponsorUseCase
async execute(
command: CreateSponsorCommand,
): Promise<Result<CreateSponsorResultDTO, ApplicationErrorCode<'VALIDATION_ERROR' | 'REPOSITORY_ERROR', { message: string }>>> {
): Promise<Result<CreateSponsorOutputPort, ApplicationErrorCode<'VALIDATION_ERROR' | 'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug('Executing CreateSponsorUseCase', { command });
const validation = this.validate(command);
if (validation.isErr()) {
@@ -51,7 +51,7 @@ export class CreateSponsorUseCase
await this.sponsorRepository.create(sponsor);
this.logger.info(`Sponsor ${sponsor.name} (${sponsor.id}) created successfully.`);
const result: CreateSponsorResultDTO = {
const result: CreateSponsorOutputPort = {
sponsor: {
id: sponsor.id,
name: sponsor.name,

View File

@@ -16,6 +16,7 @@ import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { CreateTeamOutputPort } from '../ports/output/CreateTeamOutputPort';
export interface CreateTeamCommandDTO {
name: string;
@@ -25,12 +26,8 @@ export interface CreateTeamCommandDTO {
leagues: string[];
}
export interface CreateTeamResultDTO {
team: Team;
}
export class CreateTeamUseCase
implements AsyncUseCase<CreateTeamCommandDTO, CreateTeamResultDTO, 'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR'>
implements AsyncUseCase<CreateTeamCommandDTO, CreateTeamOutputPort, 'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR'>
{
constructor(
private readonly teamRepository: ITeamRepository,
@@ -40,7 +37,7 @@ export class CreateTeamUseCase
async execute(
command: CreateTeamCommandDTO,
): Promise<Result<CreateTeamResultDTO, ApplicationErrorCode<'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR', { message: string }>>> {
): Promise<Result<CreateTeamOutputPort, ApplicationErrorCode<'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug('Executing CreateTeamUseCase', { command });
const { name, tag, description, ownerId, leagues } = command;
@@ -80,7 +77,7 @@ export class CreateTeamUseCase
await this.membershipRepository.saveMembership(membership);
this.logger.debug('Team membership created successfully.');
const result: CreateTeamResultDTO = { team: createdTeam };
const result: CreateTeamOutputPort = { team: createdTeam };
this.logger.debug('CreateTeamUseCase completed successfully.', { result });
return Result.ok(result);
} catch (error) {

View File

@@ -11,11 +11,11 @@ import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISe
import type { AsyncUseCase, Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetEntitySponsorshipPricingDTO } from '../dto/GetEntitySponsorshipPricingDTO';
import type { GetEntitySponsorshipPricingResultDTO } from '../dto/GetEntitySponsorshipPricingResultDTO';
import type { GetEntitySponsorshipPricingInputPort } from '../ports/input/GetEntitySponsorshipPricingInputPort';
import type { GetEntitySponsorshipPricingOutputPort } from '../ports/output/GetEntitySponsorshipPricingOutputPort';
export class GetEntitySponsorshipPricingUseCase
implements AsyncUseCase<GetEntitySponsorshipPricingDTO, GetEntitySponsorshipPricingResultDTO | null, 'REPOSITORY_ERROR'>
implements AsyncUseCase<GetEntitySponsorshipPricingInputPort, GetEntitySponsorshipPricingOutputPort | null, 'REPOSITORY_ERROR'>
{
constructor(
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
@@ -24,7 +24,7 @@ export class GetEntitySponsorshipPricingUseCase
private readonly logger: Logger,
) {}
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<Result<GetEntitySponsorshipPricingResultDTO | null, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
async execute(dto: GetEntitySponsorshipPricingInputPort): Promise<Result<GetEntitySponsorshipPricingOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug(`Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`);
try {
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
@@ -53,7 +53,7 @@ export class GetEntitySponsorshipPricingUseCase
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
}
const result: GetEntitySponsorshipPricingResultDTO = {
const result: GetEntitySponsorshipPricingOutputPort = {
entityType: dto.entityType,
entityId: dto.entityId,
acceptingApplications: pricing.acceptingApplications,

View File

@@ -2,15 +2,15 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { GetLeagueAdminPermissionsResultDTO } from '../dto/GetLeagueAdminPermissionsResultDTO';
import type { GetLeagueAdminPermissionsOutputPort } from '../ports/output/GetLeagueAdminPermissionsOutputPort';
export class GetLeagueAdminPermissionsUseCase implements AsyncUseCase<{ leagueId: string; performerDriverId: string }, GetLeagueAdminPermissionsResultDTO, 'NO_ERROR'> {
export class GetLeagueAdminPermissionsUseCase implements AsyncUseCase<{ leagueId: string; performerDriverId: string }, GetLeagueAdminPermissionsOutputPort, 'NO_ERROR'> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
) {}
async execute(params: { leagueId: string; performerDriverId: string }): Promise<Result<GetLeagueAdminPermissionsResultDTO, never>> {
async execute(params: { leagueId: string; performerDriverId: string }): Promise<Result<GetLeagueAdminPermissionsOutputPort, never>> {
const league = await this.leagueRepository.findById(params.leagueId);
if (!league) {
return Result.ok({ canRemoveMember: false, canUpdateRoles: false });

View File

@@ -2,20 +2,20 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueAdminResultDTO } from '../dto/GetLeagueAdminResultDTO';
import type { GetLeagueAdminOutputPort } from '../ports/output/GetLeagueAdminOutputPort';
export class GetLeagueAdminUseCase implements AsyncUseCase<{ leagueId: string }, GetLeagueAdminResultDTO, 'LEAGUE_NOT_FOUND'> {
export class GetLeagueAdminUseCase implements AsyncUseCase<{ leagueId: string }, GetLeagueAdminOutputPort, 'LEAGUE_NOT_FOUND'> {
constructor(
private readonly leagueRepository: ILeagueRepository,
) {}
async execute(params: { leagueId: string }): Promise<Result<GetLeagueAdminResultDTO, ApplicationErrorCode<'LEAGUE_NOT_FOUND', { message: string }>>> {
async execute(params: { leagueId: string }): Promise<Result<GetLeagueAdminOutputPort, ApplicationErrorCode<'LEAGUE_NOT_FOUND', { message: string }>>> {
const league = await this.leagueRepository.findById(params.leagueId);
if (!league) {
return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: 'League not found' } });
}
const dto: GetLeagueAdminResultDTO = {
const dto: GetLeagueAdminOutputPort = {
league: {
id: league.id,
ownerId: league.ownerId,

View File

@@ -3,19 +3,19 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueJoinRequestsResultDTO } from '../dto/GetLeagueJoinRequestsResultDTO';
import type { GetLeagueJoinRequestsOutputPort } from '../ports/output/GetLeagueJoinRequestsOutputPort';
export interface GetLeagueJoinRequestsUseCaseParams {
leagueId: string;
}
export class GetLeagueJoinRequestsUseCase implements AsyncUseCase<GetLeagueJoinRequestsUseCaseParams, GetLeagueJoinRequestsResultDTO, 'NO_ERROR'> {
export class GetLeagueJoinRequestsUseCase implements AsyncUseCase<GetLeagueJoinRequestsUseCaseParams, GetLeagueJoinRequestsOutputPort, 'NO_ERROR'> {
constructor(
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueJoinRequestsUseCaseParams): Promise<Result<GetLeagueJoinRequestsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
async execute(params: GetLeagueJoinRequestsUseCaseParams): Promise<Result<GetLeagueJoinRequestsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
const joinRequests = await this.leagueMembershipRepository.getJoinRequests(params.leagueId);
const driverIds = [...new Set(joinRequests.map(r => r.driverId))];
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));

View File

@@ -3,19 +3,19 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueMembershipsResultDTO } from '../dto/GetLeagueMembershipsResultDTO';
import type { GetLeagueMembershipsOutputPort } from '../ports/output/GetLeagueMembershipsOutputPort';
export interface GetLeagueMembershipsUseCaseParams {
leagueId: string;
}
export class GetLeagueMembershipsUseCase implements AsyncUseCase<GetLeagueMembershipsUseCaseParams, GetLeagueMembershipsResultDTO, 'NO_ERROR'> {
export class GetLeagueMembershipsUseCase implements AsyncUseCase<GetLeagueMembershipsUseCaseParams, GetLeagueMembershipsOutputPort, 'NO_ERROR'> {
constructor(
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueMembershipsUseCaseParams): Promise<Result<GetLeagueMembershipsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
async execute(params: GetLeagueMembershipsUseCaseParams): Promise<Result<GetLeagueMembershipsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
const drivers: { id: string; name: string }[] = [];
@@ -27,7 +27,7 @@ export class GetLeagueMembershipsUseCase implements AsyncUseCase<GetLeagueMember
}
}
const dto: GetLeagueMembershipsResultDTO = {
const dto: GetLeagueMembershipsOutputPort = {
memberships,
drivers,
};

View File

@@ -2,16 +2,16 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueOwnerSummaryResultDTO } from '../dto/GetLeagueOwnerSummaryResultDTO';
import type { GetLeagueOwnerSummaryOutputPort } from '../ports/output/GetLeagueOwnerSummaryOutputPort';
export interface GetLeagueOwnerSummaryUseCaseParams {
ownerId: string;
}
export class GetLeagueOwnerSummaryUseCase implements AsyncUseCase<GetLeagueOwnerSummaryUseCaseParams, GetLeagueOwnerSummaryResultDTO, 'NO_ERROR'> {
export class GetLeagueOwnerSummaryUseCase implements AsyncUseCase<GetLeagueOwnerSummaryUseCaseParams, GetLeagueOwnerSummaryOutputPort, 'NO_ERROR'> {
constructor(private readonly driverRepository: IDriverRepository) {}
async execute(params: GetLeagueOwnerSummaryUseCaseParams): Promise<Result<GetLeagueOwnerSummaryResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
async execute(params: GetLeagueOwnerSummaryUseCaseParams): Promise<Result<GetLeagueOwnerSummaryOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
const driver = await this.driverRepository.findById(params.ownerId);
const summary = driver ? { driver: { id: driver.id, name: driver.name }, rating: 0, rank: 0 } : null;
return Result.ok({ summary });

View File

@@ -4,8 +4,10 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort';
import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort';
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
import type {
RaceDetailViewModel,
RaceDetailRaceViewModel,
@@ -44,8 +46,8 @@ export class GetRaceDetailUseCase
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort,
private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise<GetDriverRatingOutputPort>,
private readonly getDriverAvatar: (input: GetDriverAvatarInputPort) => Promise<GetDriverAvatarOutputPort>,
) {}
async execute(params: GetRaceDetailQueryParams): Promise<Result<RaceDetailViewModel, ApplicationErrorCode<GetRaceDetailErrorCode>>> {
@@ -62,22 +64,26 @@ export class GetRaceDetailUseCase
this.leagueMembershipRepository.getMembership(race.leagueId, driverId),
]);
const ratings = this.driverRatingProvider.getRatings(registeredDriverIds);
const drivers = await Promise.all(
registeredDriverIds.map(id => this.driverRepository.findById(id)),
);
const entryList: RaceDetailEntryViewModel[] = drivers
.filter((d): d is NonNullable<typeof d> => d !== null)
.map(driver => ({
id: driver.id,
name: driver.name,
country: driver.country,
avatarUrl: this.imageService.getDriverAvatar(driver.id),
rating: ratings.get(driver.id) ?? null,
isCurrentUser: driver.id === driverId,
}));
const entryList: RaceDetailEntryViewModel[] = [];
for (const driver of drivers) {
if (driver) {
const ratingResult = await this.getDriverRating({ driverId: driver.id });
const avatarResult = await this.getDriverAvatar({ driverId: driver.id });
entryList.push({
id: driver.id,
name: driver.name,
country: driver.country,
avatarUrl: avatarResult.avatarUrl,
rating: ratingResult.rating,
isCurrentUser: driver.id === driverId,
});
}
}
const isUserRegistered = registeredDriverIds.includes(driverId);
const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date();

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