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,8 +0,0 @@
export interface AcceptSponsorshipRequestResultDTO {
requestId: string;
sponsorshipId: string;
status: 'accepted';
acceptedAt: Date;
platformFee: number;
netAmount: number;
}

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,5 +0,0 @@
export interface ApplyForSponsorshipResultDTO {
requestId: string;
status: 'pending';
createdAt: Date;
}

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,16 +0,0 @@
import type { ParticipantRef } from '@core/racing/domain/types/ParticipantRef';
export interface ChampionshipStandingsRowDTO {
participant: ParticipantRef;
position: number;
totalPoints: number;
resultsCounted: number;
resultsDropped: number;
}
export interface ChampionshipStandingsDTO {
seasonId: string;
championshipId: string;
championshipName: string;
rows: ChampionshipStandingsRowDTO[];
}

View File

@@ -1,6 +0,0 @@
export interface CreateLeagueWithSeasonAndScoringResultDTO {
leagueId: string;
seasonId: string;
scoringPresetId?: string;
scoringPresetName?: string;
}

View File

@@ -1,10 +0,0 @@
export interface CreateSponsorResultDTO {
sponsor: {
id: string;
name: string;
contactEmail: string;
websiteUrl?: string;
logoUrl?: string;
createdAt: Date;
};
}

View File

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

View File

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

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,4 +0,0 @@
export interface GetLeagueAdminPermissionsResultDTO {
canRemoveMember: boolean;
canUpdateRoles: boolean;
}

View File

@@ -1,7 +0,0 @@
export interface GetLeagueAdminResultDTO {
league: {
id: string;
ownerId: string;
};
// Additional data would be populated by combining multiple use cases
}

View File

@@ -1,13 +0,0 @@
export interface GetLeagueJoinRequestsResultDTO {
joinRequests: Array<{
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message?: string;
driver: {
id: string;
name: string;
};
}>;
}

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,3 +0,0 @@
export interface GetLeagueOwnerSummaryResultDTO {
summary: { driver: { id: string; name: string }; rating: number; rank: number } | null;
}

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 @@
export interface GetLeagueScheduleResultDTO {
races: Array<{
id: string;
name: string;
scheduledAt: Date;
}>;
}

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,24 +0,0 @@
export type LeagueDTO = {
id: string;
name: string;
description: string;
ownerId: string;
settings: {
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
sessionDuration?: number;
qualifyingFormat?: 'single-lap' | 'open';
customPoints?: Record<number, number>;
maxDrivers?: number;
};
createdAt: string;
socialLinks?: {
discordUrl?: string;
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

@@ -1,20 +0,0 @@
export type LeagueDriverSeasonStatsDTO = {
leagueId: string;
driverId: string;
position: number;
driverName: string;
teamId?: string;
teamName?: string;
totalPoints: number;
basePoints: number;
penaltyPoints: number;
bonusPoints: number;
pointsPerRace: number;
racesStarted: number;
racesFinished: number;
dnfs: number;
noShows: number;
avgFinish: number | null;
rating: number | null;
ratingChange: number | null;
};

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,20 +0,0 @@
export interface LeagueScoringChampionshipDTO {
id: string;
name: string;
type: 'driver' | 'team' | 'nations' | 'trophy';
sessionTypes: string[];
pointsPreview: Array<{ sessionType: string; position: number; points: number }>;
bonusSummary: string[];
dropPolicyDescription: string;
}
export interface LeagueScoringConfigDTO {
leagueId: string;
seasonId: string;
gameId: string;
gameName: string;
scoringPresetId?: string;
scoringPresetName?: string;
dropPolicySummary: string;
championships: LeagueScoringChampionshipDTO[];
}

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,6 +0,0 @@
/**
* League visibility/ranking mode.
* - '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';

View File

@@ -1,14 +0,0 @@
export type RaceDTO = {
id: string;
leagueId: string;
scheduledAt: string;
track: string;
trackId?: string;
car: string;
carId?: string;
sessionType: 'practice' | 'qualifying' | 'race';
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
strengthOfField?: number;
registeredCount?: number;
maxParticipants?: number;
};

View File

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

View File

@@ -1,9 +0,0 @@
export type ResultDTO = {
id: string;
raceId: string;
driverId: string;
position: number;
fastestLap: number;
incidents: number;
startPosition: number;
};

View File

@@ -1,13 +0,0 @@
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
export interface SponsorshipSlotDTO {
tier: SponsorshipTier;
price: number;
currency: string;
formattedPrice: string;
benefits: string[];
available: boolean;
maxSlots: number;
filledSlots: number;
pendingRequests: number;
}

View File

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