diff --git a/core/analytics/application/use-cases/GetAnalyticsMetricsUseCase.ts b/core/analytics/application/use-cases/GetAnalyticsMetricsUseCase.ts index 5dfd5b327..8a07fc2f2 100644 --- a/core/analytics/application/use-cases/GetAnalyticsMetricsUseCase.ts +++ b/core/analytics/application/use-cases/GetAnalyticsMetricsUseCase.ts @@ -1,6 +1,7 @@ import type { Logger, UseCase, UseCaseOutputPort } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import type { IPageViewRepository } from '../repositories/IPageViewRepository'; export interface GetAnalyticsMetricsInput { startDate?: Date; @@ -18,19 +19,23 @@ export type GetAnalyticsMetricsErrorCode = 'REPOSITORY_ERROR'; export class GetAnalyticsMetricsUseCase implements UseCase { constructor( - // private readonly pageViewRepository: IPageViewRepository, // TODO: Use when implementation is ready private readonly logger: Logger, private readonly output: UseCaseOutputPort, + private readonly pageViewRepository?: IPageViewRepository, ) {} - async execute(input: GetAnalyticsMetricsInput = {}): Promise>> { + async execute( + input: GetAnalyticsMetricsInput = {}, + ): Promise>> { try { const startDate = input.startDate ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago const endDate = input.endDate ?? new Date(); - // TODO: Use pageViewRepository when implemented - // const pageViews = await this.pageViewRepository.countByDateRange(startDate, endDate); - const pageViews = 0; + const pageViews = this.pageViewRepository + ? (await this.pageViewRepository.findByDateRange(startDate, endDate)).length + : 0; + + // Note: additional metrics require dedicated repositories / event sources. const uniqueVisitors = 0; const averageSessionDuration = 0; const bounceRate = 0; diff --git a/core/analytics/application/use-cases/GetEntityAnalyticsQuery.ts b/core/analytics/application/use-cases/GetEntityAnalyticsQuery.ts index 11acf2147..ee27c7e07 100644 --- a/core/analytics/application/use-cases/GetEntityAnalyticsQuery.ts +++ b/core/analytics/application/use-cases/GetEntityAnalyticsQuery.ts @@ -50,7 +50,6 @@ export class GetEntityAnalyticsQuery constructor( private readonly pageViewRepository: IPageViewRepository, private readonly engagementRepository: IEngagementRepository, - // private readonly snapshotRepository: IAnalyticsSnapshotRepository, // TODO: Use when implementation is ready private readonly logger: Logger ) {} diff --git a/core/identity/application/dto/AuthCallbackCommandDTO.ts b/core/identity/application/dto/AuthCallbackCommandDTO.ts deleted file mode 100644 index df8fda1d5..000000000 --- a/core/identity/application/dto/AuthCallbackCommandDTO.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { AuthProviderDTO } from './AuthProviderDTO'; - -export interface AuthCallbackCommandDTO { - provider: AuthProviderDTO; - code: string; - state: string; - returnTo?: string; -} \ No newline at end of file diff --git a/core/identity/application/dto/AuthProviderDTO.ts b/core/identity/application/dto/AuthProviderDTO.ts deleted file mode 100644 index 64080a8c2..000000000 --- a/core/identity/application/dto/AuthProviderDTO.ts +++ /dev/null @@ -1 +0,0 @@ -export type AuthProviderDTO = 'IRACING_DEMO'; \ No newline at end of file diff --git a/core/identity/application/dto/AuthSessionDTO.ts b/core/identity/application/dto/AuthSessionDTO.ts deleted file mode 100644 index 71c8b0aac..000000000 --- a/core/identity/application/dto/AuthSessionDTO.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { AuthenticatedUserDTO } from './AuthenticatedUserDTO'; - -export interface AuthSessionDTO { - user: AuthenticatedUserDTO; - issuedAt: number; - expiresAt: number; - token: string; -} \ No newline at end of file diff --git a/core/identity/application/dto/AuthenticatedUserDTO.ts b/core/identity/application/dto/AuthenticatedUserDTO.ts deleted file mode 100644 index 708988575..000000000 --- a/core/identity/application/dto/AuthenticatedUserDTO.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AuthenticatedUserDTO { - id: string; - displayName: string; - email?: string; - iracingCustomerId?: string; - primaryDriverId?: string; - avatarUrl?: string; -} \ No newline at end of file diff --git a/core/identity/application/dto/IracingAuthStateDTO.ts b/core/identity/application/dto/IracingAuthStateDTO.ts deleted file mode 100644 index f06d28e2a..000000000 --- a/core/identity/application/dto/IracingAuthStateDTO.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IracingAuthStateDTO { - state: string; - returnTo?: string; -} \ No newline at end of file diff --git a/core/identity/application/dto/StartAuthCommandDTO.ts b/core/identity/application/dto/StartAuthCommandDTO.ts deleted file mode 100644 index 071adb8c9..000000000 --- a/core/identity/application/dto/StartAuthCommandDTO.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { AuthProviderDTO } from './AuthProviderDTO'; - -export interface StartAuthCommandDTO { - provider: AuthProviderDTO; - returnTo?: string; -} \ No newline at end of file diff --git a/core/identity/application/ports/IdentityProviderPort.ts b/core/identity/application/ports/IdentityProviderPort.ts index d4d955f8f..f8173d634 100644 --- a/core/identity/application/ports/IdentityProviderPort.ts +++ b/core/identity/application/ports/IdentityProviderPort.ts @@ -1,8 +1,27 @@ -import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; -import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO'; -import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO'; +export type AuthProvider = 'IRACING_DEMO'; + +export interface StartAuthCommand { + provider: AuthProvider; + returnTo?: string; +} + +export interface AuthCallbackCommand { + provider: AuthProvider; + code: string; + state: string; + returnTo?: string; +} + +export interface AuthenticatedUser { + id: string; + displayName: string; + email?: string; + iracingCustomerId?: string; + primaryDriverId?: string; + avatarUrl?: string; +} export interface IdentityProviderPort { - startAuth(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }>; - completeAuth(command: AuthCallbackCommandDTO): Promise; + startAuth(command: StartAuthCommand): Promise<{ redirectUrl: string; state: string }>; + completeAuth(command: AuthCallbackCommand): Promise; } \ No newline at end of file diff --git a/core/identity/application/ports/IdentitySessionPort.ts b/core/identity/application/ports/IdentitySessionPort.ts index c44374e56..47d864523 100644 --- a/core/identity/application/ports/IdentitySessionPort.ts +++ b/core/identity/application/ports/IdentitySessionPort.ts @@ -1,10 +1,15 @@ -import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; -import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; +import type { AuthenticatedUser } from './IdentityProviderPort'; -// TODO not so sure if this here is proper clean architecture +export interface AuthSession { + user: AuthenticatedUser; + issuedAt: number; + expiresAt: number; + token: string; +} +// Application port for session access/persistence (implemented by adapters). export interface IdentitySessionPort { - getCurrentSession(): Promise; - createSession(user: AuthenticatedUserDTO): Promise; + getCurrentSession(): Promise; + createSession(user: AuthenticatedUser): Promise; clearSession(): Promise; } \ No newline at end of file diff --git a/core/identity/application/use-cases/GetCurrentUserSessionUseCase.ts b/core/identity/application/use-cases/GetCurrentUserSessionUseCase.ts index 9148b172c..07f1ec7e1 100644 --- a/core/identity/application/use-cases/GetCurrentUserSessionUseCase.ts +++ b/core/identity/application/use-cases/GetCurrentUserSessionUseCase.ts @@ -1,12 +1,11 @@ -import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; -import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; +import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort, Logger } from '@core/shared/application'; export type GetCurrentUserSessionInput = void; -export type GetCurrentUserSessionResult = AuthSessionDTO | null; +export type GetCurrentUserSessionResult = AuthSession | null; export type GetCurrentUserSessionErrorCode = 'REPOSITORY_ERROR'; diff --git a/core/identity/application/use-cases/HandleAuthCallbackUseCase.ts b/core/identity/application/use-cases/HandleAuthCallbackUseCase.ts index b350ea040..a3c6ceba7 100644 --- a/core/identity/application/use-cases/HandleAuthCallbackUseCase.ts +++ b/core/identity/application/use-cases/HandleAuthCallbackUseCase.ts @@ -1,15 +1,12 @@ -import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO'; -import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; -import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; -import type { IdentityProviderPort } from '../ports/IdentityProviderPort'; -import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; +import type { AuthCallbackCommand, AuthenticatedUser, IdentityProviderPort } from '../ports/IdentityProviderPort'; +import type { AuthSession, IdentitySessionPort } from '../ports/IdentitySessionPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort, Logger } from '@core/shared/application'; -export type HandleAuthCallbackInput = AuthCallbackCommandDTO; +export type HandleAuthCallbackInput = AuthCallbackCommand; -export type HandleAuthCallbackResult = AuthSessionDTO; +export type HandleAuthCallbackResult = AuthSession; export type HandleAuthCallbackErrorCode = 'REPOSITORY_ERROR'; @@ -30,7 +27,7 @@ export class HandleAuthCallbackUseCase { Result > { try { - const user: AuthenticatedUserDTO = await this.provider.completeAuth(input); + const user: AuthenticatedUser = await this.provider.completeAuth(input); const session = await this.sessionPort.createSession(user); this.output.present(session); diff --git a/core/identity/application/use-cases/StartAuthUseCase.ts b/core/identity/application/use-cases/StartAuthUseCase.ts index ef283daff..e1609ba9f 100644 --- a/core/identity/application/use-cases/StartAuthUseCase.ts +++ b/core/identity/application/use-cases/StartAuthUseCase.ts @@ -1,12 +1,11 @@ -import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO'; -import type { IdentityProviderPort } from '../ports/IdentityProviderPort'; +import type { IdentityProviderPort, AuthProvider, StartAuthCommand } from '../ports/IdentityProviderPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort, Logger } from '@core/shared/application'; export type StartAuthInput = { - provider: StartAuthCommandDTO['provider']; - returnTo?: StartAuthCommandDTO['returnTo']; + provider: AuthProvider; + returnTo?: StartAuthCommand['returnTo']; }; export type StartAuthResult = { @@ -27,7 +26,7 @@ export class StartAuthUseCase { async execute(input: StartAuthInput): Promise> { try { - const command: StartAuthCommandDTO = input.returnTo + const command: StartAuthCommand = input.returnTo ? { provider: input.provider, returnTo: input.returnTo, diff --git a/core/identity/index.ts b/core/identity/index.ts index e20076b2a..b9b5c2bd2 100644 --- a/core/identity/index.ts +++ b/core/identity/index.ts @@ -11,12 +11,8 @@ export * from './domain/repositories/ISponsorAccountRepository'; export * from './domain/repositories/IUserRatingRepository'; export * from './domain/repositories/IAchievementRepository'; -export * from './application/dto/AuthenticatedUserDTO'; -export * from './application/dto/AuthSessionDTO'; -export * from './application/dto/AuthCallbackCommandDTO'; -export * from './application/dto/StartAuthCommandDTO'; -export * from './application/dto/AuthProviderDTO'; -export * from './application/dto/IracingAuthStateDTO'; +export * from './application/ports/IdentityProviderPort'; +export * from './application/ports/IdentitySessionPort'; export * from './application/use-cases/StartAuthUseCase'; export * from './application/use-cases/HandleAuthCallbackUseCase'; diff --git a/core/media/application/use-cases/UpdateAvatarUseCase.ts b/core/media/application/use-cases/UpdateAvatarUseCase.ts index 731cfa61b..213c27c4d 100644 --- a/core/media/application/use-cases/UpdateAvatarUseCase.ts +++ b/core/media/application/use-cases/UpdateAvatarUseCase.ts @@ -10,6 +10,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC import { v4 as uuidv4 } from 'uuid'; import { Avatar } from '../../domain/entities/Avatar'; import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository'; +import { AvatarId } from '../../domain/value-objects/AvatarId'; export interface UpdateAvatarInput { driverId: string; @@ -48,9 +49,9 @@ export class UpdateAvatarUseCase { await this.avatarRepo.save(currentAvatar); } - const avatarId = uuidv4(); // TODO this ID should be a value object + const avatarId = AvatarId.create(uuidv4()); const newAvatar = Avatar.create({ - id: avatarId, + id: avatarId.toString(), driverId: input.driverId, mediaUrl: input.mediaUrl, }); @@ -58,13 +59,13 @@ export class UpdateAvatarUseCase { await this.avatarRepo.save(newAvatar); this.output.present({ - avatarId, + avatarId: avatarId.toString(), driverId: input.driverId, }); this.logger.info('[UpdateAvatarUseCase] Avatar updated successfully', { driverId: input.driverId, - avatarId, + avatarId: avatarId.toString(), }); return Result.ok(undefined); diff --git a/core/media/domain/value-objects/AvatarId.ts b/core/media/domain/value-objects/AvatarId.ts new file mode 100644 index 000000000..b0a9ae77e --- /dev/null +++ b/core/media/domain/value-objects/AvatarId.ts @@ -0,0 +1,31 @@ +import type { IValueObject } from '@core/shared/domain'; + +export interface AvatarIdProps { + value: string; +} + +export class AvatarId implements IValueObject { + public readonly props: AvatarIdProps; + + private constructor(value: string) { + this.props = { value }; + } + + static create(raw: string): AvatarId { + const value = raw?.trim(); + + if (!value) { + throw new Error('Avatar ID cannot be empty'); + } + + return new AvatarId(value); + } + + toString(): string { + return this.props.value; + } + + equals(other: IValueObject): boolean { + return this.props.value === other.props.value; + } +} \ No newline at end of file diff --git a/core/racing/application/dto/LeagueConfigFormDTO.ts b/core/racing/application/dto/LeagueConfigFormDTO.ts deleted file mode 100644 index bc9ab32fd..000000000 --- a/core/racing/application/dto/LeagueConfigFormDTO.ts +++ /dev/null @@ -1,93 +0,0 @@ -export interface LeagueConfigFormModel { - basics?: { - name?: string; - description?: string; - visibility?: string; - gameId?: string; - }; - structure?: { - mode?: string; - maxDrivers?: number; - }; - championships?: { - enableDriverChampionship?: boolean; - enableTeamChampionship?: boolean; - enableNationsChampionship?: boolean; - enableTrophyChampionship?: boolean; - }; - scoring?: { - patternId?: string; - customScoringEnabled?: boolean; - }; - dropPolicy?: { - strategy?: string; - n?: number; - }; - timings?: { - qualifyingMinutes?: number; - mainRaceMinutes?: number; - sessionCount?: number; - roundsPlanned?: number; - seasonStartDate?: string; - raceStartTime?: string; - timezoneId?: string; - recurrenceStrategy?: string; - weekdays?: string[]; - intervalWeeks?: number; - monthlyOrdinal?: number; - monthlyWeekday?: string; - }; - stewarding?: { - decisionMode?: string; - requiredVotes?: number; - requireDefense?: boolean; - defenseTimeLimit?: number; - voteTimeLimit?: number; - protestDeadlineHours?: number; - stewardingClosesHours?: number; - notifyAccusedOnProtest?: boolean; - notifyOnVoteRequired?: boolean; - }; -} - -export interface LeagueStructureFormDTO { - name: string; - description: string; - ownerId: string; -} - -export interface LeagueChampionshipsFormDTO { - pointsSystem: string; - customPoints?: Record; -} - -export interface LeagueScoringFormDTO { - pointsSystem: string; - customPoints?: Record; -} - -export interface LeagueDropPolicyFormDTO { - dropWeeks?: number; - bestResults?: number; -} - -export interface LeagueStructureMode { - mode: 'simple' | 'advanced'; -} - -export interface LeagueTimingsFormDTO { - sessionDuration?: number; - qualifyingFormat?: string; -} - -export interface LeagueStewardingFormDTO { - decisionMode: string; - requiredVotes?: number; - requireDefense?: boolean; - defenseTimeLimit?: number; - voteTimeLimit?: number; - protestDeadlineHours?: number; - stewardingClosesHours?: number; - notifyAccusedOnProtest?: boolean; - notifyOnVoteRequired?: boolean; -} \ No newline at end of file diff --git a/core/racing/application/dto/LeagueDTO.ts b/core/racing/application/dto/LeagueDTO.ts deleted file mode 100644 index 32477db34..000000000 --- a/core/racing/application/dto/LeagueDTO.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface LeagueDTO { - id: string; - name: string; - description: string; - ownerId: string; - settings: { - pointsSystem: string; - sessionDuration?: number; - qualifyingFormat?: string; - customPoints?: Record; - maxDrivers?: number; - stewarding?: { - decisionMode: string; - requiredVotes?: number; - requireDefense?: boolean; - defenseTimeLimit?: number; - voteTimeLimit?: number; - protestDeadlineHours?: number; - stewardingClosesHours?: number; - notifyAccusedOnProtest?: boolean; - notifyOnVoteRequired?: boolean; - }; - }; - createdAt: Date; - socialLinks?: { - discordUrl?: string; - youtubeUrl?: string; - websiteUrl?: string; - }; -} \ No newline at end of file diff --git a/core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts b/core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts deleted file mode 100644 index 258de006d..000000000 --- a/core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface LeagueDriverSeasonStatsDTO { - driverId: string; - leagueId: string; - seasonId: string; - totalPoints: number; - averagePoints: number; - bestFinish: number; - podiums: number; - races: number; - wins: number; -} \ No newline at end of file diff --git a/core/racing/application/dto/LeagueScheduleDTO.ts b/core/racing/application/dto/LeagueScheduleDTO.ts deleted file mode 100644 index 6f6a8c0f9..000000000 --- a/core/racing/application/dto/LeagueScheduleDTO.ts +++ /dev/null @@ -1,83 +0,0 @@ -export interface LeagueScheduleDTO { - leagueId: string; - seasonId: string; - races: Array<{ - id: string; - name: string; - scheduledTime: Date; - trackId: string; - status: string; - }>; -} - -export interface LeagueSchedulePreviewDTO { - leagueId: string; - preview: Array<{ - id: string; - name: string; - scheduledTime: Date; - trackId: string; - }>; -} - -export type SeasonScheduleConfigDTO = { - seasonStartDate: string; - recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday'; - weekdays?: string[]; - raceStartTime: string; - timezoneId: string; - plannedRounds: number; - intervalWeeks?: number; - monthlyOrdinal?: 1 | 2 | 3 | 4; - monthlyWeekday?: string; -}; - -import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; -import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; -import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; -import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; -import { WeekdaySet } from '../../domain/value-objects/WeekdaySet'; -import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern'; -import { ALL_WEEKDAYS, type Weekday } from '../../domain/types/Weekday'; - -function toWeekdaySet(values: string[] | undefined): WeekdaySet { - const weekdays = (values ?? []).filter((v): v is Weekday => - ALL_WEEKDAYS.includes(v as Weekday), - ); - - return WeekdaySet.fromArray(weekdays.length > 0 ? weekdays : ['Mon']); -} - -export function scheduleDTOToSeasonSchedule(dto: SeasonScheduleConfigDTO): SeasonSchedule { - const startDate = new Date(dto.seasonStartDate); - const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime); - const timezone = LeagueTimezone.create(dto.timezoneId); - - const recurrence = (() => { - switch (dto.recurrenceStrategy) { - case 'everyNWeeks': - return RecurrenceStrategyFactory.everyNWeeks( - dto.intervalWeeks ?? 2, - toWeekdaySet(dto.weekdays), - ); - case 'monthlyNthWeekday': { - const pattern = MonthlyRecurrencePattern.create( - dto.monthlyOrdinal ?? 1, - ((dto.monthlyWeekday ?? 'Mon') as Weekday), - ); - return RecurrenceStrategyFactory.monthlyNthWeekday(pattern); - } - case 'weekly': - default: - return RecurrenceStrategyFactory.weekly(toWeekdaySet(dto.weekdays)); - } - })(); - - return new SeasonSchedule({ - startDate, - timeOfDay, - timezone, - recurrence, - plannedRounds: dto.plannedRounds, - }); -} \ No newline at end of file diff --git a/core/racing/application/dto/RaceDTO.ts b/core/racing/application/dto/RaceDTO.ts deleted file mode 100644 index acd79eee1..000000000 --- a/core/racing/application/dto/RaceDTO.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface RaceDTO { - id: string; - leagueId: string; - name: string; - scheduledTime: Date; - trackId: string; - status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled'; - results?: string[]; -} \ No newline at end of file diff --git a/core/racing/application/dto/ResultDTO.ts b/core/racing/application/dto/ResultDTO.ts deleted file mode 100644 index c58b07f8e..000000000 --- a/core/racing/application/dto/ResultDTO.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ResultDTO { - id: string; - raceId: string; - driverId: string; - position: number; - points: number; - time?: string; - incidents?: number; -} \ No newline at end of file diff --git a/core/racing/application/dto/StandingDTO.ts b/core/racing/application/dto/StandingDTO.ts deleted file mode 100644 index 24b964076..000000000 --- a/core/racing/application/dto/StandingDTO.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface StandingDTO { - id: string; - leagueId: string; - driverId: string; - position: number; - points: number; - races: number; - wins: number; - podiums: number; -} \ No newline at end of file diff --git a/core/racing/application/dto/index.ts b/core/racing/application/dto/index.ts deleted file mode 100644 index 4c86e6c76..000000000 --- a/core/racing/application/dto/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './LeagueConfigFormDTO'; -export * from './LeagueDriverSeasonStatsDTO'; -export * from './LeagueDTO'; -export * from './LeagueScheduleDTO'; -export * from './RaceDTO'; -export * from './ResultDTO'; -export * from './StandingDTO'; - -// TODO DTOs dont belong into core. We use Results in UseCases and DTOs in apps/api. \ No newline at end of file diff --git a/core/racing/application/index.ts b/core/racing/application/index.ts index ca145b6e7..1be517c8f 100644 --- a/core/racing/application/index.ts +++ b/core/racing/application/index.ts @@ -64,27 +64,3 @@ export type { TeamRole, TeamMembershipStatus, } from '../domain/types/TeamMembership'; - -export type { LeagueDTO } from './dto/LeagueDTO'; -export type { RaceDTO } from './dto/RaceDTO'; -export type { ResultDTO } from './dto/ResultDTO'; -export type { StandingDTO } from './dto/StandingDTO'; -export type { LeagueDriverSeasonStatsDTO } from './dto/LeagueDriverSeasonStatsDTO'; -export type { - LeagueScheduleDTO, - LeagueSchedulePreviewDTO, -} from './dto/LeagueScheduleDTO'; -export type { ChampionshipStandingsOutputPort } from './ports/output/ChampionshipStandingsOutputPort'; -export type { ChampionshipStandingsRowOutputPort } from './ports/output/ChampionshipStandingsRowOutputPort'; -export type { AllRacesPageOutputPort } from './ports/output/AllRacesPageOutputPort'; -export type { DriverRegistrationStatusOutputPort } from './ports/output/DriverRegistrationStatusOutputPort'; -export type { - LeagueConfigFormModel, - LeagueStructureFormDTO, - LeagueChampionshipsFormDTO, - LeagueScoringFormDTO, - LeagueDropPolicyFormDTO, - LeagueStructureMode, - LeagueTimingsFormDTO, - LeagueStewardingFormDTO, -} from './dto/LeagueConfigFormDTO'; \ No newline at end of file diff --git a/core/racing/application/ports/output/AllRacesPageOutputPort.ts b/core/racing/application/ports/output/AllRacesPageOutputPort.ts deleted file mode 100644 index 553c360bb..000000000 --- a/core/racing/application/ports/output/AllRacesPageOutputPort.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface AllRacesPageOutputPort { - races: Array<{ - id: string; - name: string; - leagueId: string; - leagueName: string; - scheduledTime: Date; - trackId: string; - status: string; - participants: number; - }>; - total: number; - page: number; - limit: number; -} - -// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort \ No newline at end of file diff --git a/core/racing/application/ports/output/ChampionshipStandingsOutputPort.ts b/core/racing/application/ports/output/ChampionshipStandingsOutputPort.ts deleted file mode 100644 index 6b6e6e38e..000000000 --- a/core/racing/application/ports/output/ChampionshipStandingsOutputPort.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface ChampionshipStandingsOutputPort { - leagueId: string; - seasonId: string; - standings: Array<{ - driverId: string; - position: number; - points: number; - driverName: string; - teamId?: string; - teamName?: string; - }>; -} - -// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort \ No newline at end of file diff --git a/core/racing/application/ports/output/ChampionshipStandingsRowOutputPort.ts b/core/racing/application/ports/output/ChampionshipStandingsRowOutputPort.ts deleted file mode 100644 index 38e9095d8..000000000 --- a/core/racing/application/ports/output/ChampionshipStandingsRowOutputPort.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface ChampionshipStandingsRowOutputPort { - driverId: string; - position: number; - points: number; - driverName: string; - teamId?: string; - teamName?: string; -} - -// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort \ No newline at end of file diff --git a/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts b/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts deleted file mode 100644 index 175be2f9f..000000000 --- a/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface DriverRegistrationStatusOutputPort { - driverId: string; - raceId: string; - leagueId: string; - registered: boolean; - status: 'registered' | 'withdrawn' | 'pending' | 'not_registered'; -} - -// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort \ No newline at end of file diff --git a/core/racing/application/services/SeasonApplicationService.ts b/core/racing/application/services/SeasonApplicationService.ts deleted file mode 100644 index e77780fe9..000000000 --- a/core/racing/application/services/SeasonApplicationService.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { Season } from '../../domain/entities/season/Season'; -import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; -import type { Weekday } from '../../domain/types/Weekday'; -import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; -import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern'; -import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; -import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; -import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy'; -import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; -import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig'; -import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig'; -import { WeekdaySet } from '../../domain/value-objects/WeekdaySet'; -import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO'; -import type { StewardingDecisionMode } from '../../domain/entities/League'; - -// TODO The whole file mixes a lot of concerns...should be resolved into use cases or is it obsolet? - -export interface CreateSeasonForLeagueCommand { - leagueId: string; - name: string; - gameId: string; - sourceSeasonId?: string; - config?: LeagueConfigFormModel; -} - -export interface CreateSeasonForLeagueResultDTO { - seasonId: string; -} - -export interface SeasonSummaryDTO { - seasonId: string; - leagueId: string; - name: string; - status: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled'; - startDate?: Date; - endDate?: Date; - isPrimary: boolean; -} - -export interface ListSeasonsForLeagueQuery { - leagueId: string; -} - -export interface ListSeasonsForLeagueResultDTO { - items: SeasonSummaryDTO[]; -} - -export interface GetSeasonDetailsQuery { - leagueId: string; - seasonId: string; -} - -export interface SeasonDetailsDTO { - seasonId: string; - leagueId: string; - gameId: string; - name: string; - status: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled'; - startDate?: Date; - endDate?: Date; - maxDrivers?: number; - schedule?: { - startDate: Date; - plannedRounds: number; - }; - scoring?: { - scoringPresetId: string; - customScoringEnabled: boolean; - }; - dropPolicy?: { - strategy: string; - n?: number; - }; - stewarding?: { - decisionMode: string; - requiredVotes?: number; - requireDefense: boolean; - defenseTimeLimit: number; - voteTimeLimit: number; - protestDeadlineHours: number; - stewardingClosesHours: number; - notifyAccusedOnProtest: boolean; - notifyOnVoteRequired: boolean; - }; -} - -export type SeasonLifecycleTransition = 'activate' | 'complete' | 'archive' | 'cancel'; - -export interface ManageSeasonLifecycleCommand { - leagueId: string; - seasonId: string; - transition: SeasonLifecycleTransition; -} - -export interface ManageSeasonLifecycleResultDTO { - seasonId: string; - status: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled'; - startDate?: Date; - endDate?: Date; -} - -export class SeasonApplicationService { - constructor( - private readonly leagueRepository: ILeagueRepository, - private readonly seasonRepository: ISeasonRepository, - ) {} - - async createSeasonForLeague(command: CreateSeasonForLeagueCommand): Promise { - const league = await this.leagueRepository.findById(command.leagueId); - if (!league) { - throw new Error(`League not found: ${command.leagueId}`); - } - - let baseSeasonProps: { - schedule?: SeasonSchedule; - scoringConfig?: SeasonScoringConfig; - dropPolicy?: SeasonDropPolicy; - stewardingConfig?: SeasonStewardingConfig; - maxDrivers?: number; - } = {}; - - if (command.sourceSeasonId) { - const source = await this.seasonRepository.findById(command.sourceSeasonId); - if (!source) { - throw new Error(`Source Season not found: ${command.sourceSeasonId}`); - } - baseSeasonProps = { - ...(source.schedule !== undefined ? { schedule: source.schedule } : {}), - ...(source.scoringConfig !== undefined ? { scoringConfig: source.scoringConfig } : {}), - ...(source.dropPolicy !== undefined ? { dropPolicy: source.dropPolicy } : {}), - ...(source.stewardingConfig !== undefined ? { stewardingConfig: source.stewardingConfig } : {}), - ...(source.maxDrivers !== undefined ? { maxDrivers: source.maxDrivers } : {}), - }; - } else if (command.config) { - baseSeasonProps = this.deriveSeasonPropsFromConfig(command.config); - } - - const seasonId = uuidv4(); - - const season = Season.create({ - id: seasonId, - leagueId: league.id.toString(), - gameId: command.gameId, - name: command.name, - year: new Date().getFullYear(), - status: 'planned', - ...(baseSeasonProps?.schedule ? { schedule: baseSeasonProps.schedule } : {}), - ...(baseSeasonProps?.scoringConfig ? { scoringConfig: baseSeasonProps.scoringConfig } : {}), - ...(baseSeasonProps?.dropPolicy ? { dropPolicy: baseSeasonProps.dropPolicy } : {}), - ...(baseSeasonProps?.stewardingConfig ? { stewardingConfig: baseSeasonProps.stewardingConfig } : {}), - ...(baseSeasonProps?.maxDrivers !== undefined ? { maxDrivers: baseSeasonProps.maxDrivers } : {}), - }); - - await this.seasonRepository.add(season); - - return { seasonId }; - } - - async listSeasonsForLeague(query: ListSeasonsForLeagueQuery): Promise { - const league = await this.leagueRepository.findById(query.leagueId); - if (!league) { - throw new Error(`League not found: ${query.leagueId}`); - } - - const seasons = await this.seasonRepository.listByLeague(league.id.toString()); - const items: SeasonSummaryDTO[] = seasons.map((s) => ({ - seasonId: s.id, - leagueId: s.leagueId, - name: s.name, - status: s.status, - ...(s.startDate !== undefined ? { startDate: s.startDate } : {}), - ...(s.endDate !== undefined ? { endDate: s.endDate } : {}), - isPrimary: false, - })); - - return { items }; - } - - async getSeasonDetails(query: GetSeasonDetailsQuery): Promise { - const league = await this.leagueRepository.findById(query.leagueId); - if (!league) { - throw new Error(`League not found: ${query.leagueId}`); - } - - const season = await this.seasonRepository.findById(query.seasonId); - if (!season || season.leagueId !== league.id.toString()) { - throw new Error(`Season ${query.seasonId} does not belong to league ${league.id}`); - } - - return { - seasonId: season.id, - leagueId: season.leagueId, - gameId: season.gameId, - name: season.name, - status: season.status, - ...(season.startDate !== undefined ? { startDate: season.startDate } : {}), - ...(season.endDate !== undefined ? { endDate: season.endDate } : {}), - ...(season.maxDrivers !== undefined ? { maxDrivers: season.maxDrivers } : {}), - ...(season.schedule - ? { - schedule: { - startDate: season.schedule.startDate, - plannedRounds: season.schedule.plannedRounds, - }, - } - : {}), - ...(season.scoringConfig - ? { - scoring: { - scoringPresetId: season.scoringConfig.scoringPresetId, - customScoringEnabled: season.scoringConfig.customScoringEnabled ?? false, - }, - } - : {}), - ...(season.dropPolicy - ? { - dropPolicy: { - strategy: season.dropPolicy.strategy, - ...(season.dropPolicy.n !== undefined ? { n: season.dropPolicy.n } : {}), - }, - } - : {}), - ...(season.stewardingConfig - ? { - stewarding: { - decisionMode: season.stewardingConfig.decisionMode, - ...(season.stewardingConfig.requiredVotes !== undefined - ? { requiredVotes: season.stewardingConfig.requiredVotes } - : {}), - requireDefense: season.stewardingConfig.requireDefense, - defenseTimeLimit: season.stewardingConfig.defenseTimeLimit, - voteTimeLimit: season.stewardingConfig.voteTimeLimit, - protestDeadlineHours: season.stewardingConfig.protestDeadlineHours, - stewardingClosesHours: season.stewardingConfig.stewardingClosesHours, - notifyAccusedOnProtest: season.stewardingConfig.notifyAccusedOnProtest, - notifyOnVoteRequired: season.stewardingConfig.notifyOnVoteRequired, - }, - } - : {}), - }; - } - - async manageSeasonLifecycle(command: ManageSeasonLifecycleCommand): Promise { - const league = await this.leagueRepository.findById(command.leagueId); - if (!league) { - throw new Error(`League not found: ${command.leagueId}`); - } - - const season = await this.seasonRepository.findById(command.seasonId); - if (!season || season.leagueId !== league.id.toString()) { - throw new Error(`Season ${command.seasonId} does not belong to league ${league.id}`); - } - - let updated: Season; - switch (command.transition) { - case 'activate': - updated = season.activate(); - break; - case 'complete': - updated = season.complete(); - break; - case 'archive': - updated = season.archive(); - break; - case 'cancel': - updated = season.cancel(); - break; - default: - throw new Error('Unsupported Season lifecycle transition'); - } - - await this.seasonRepository.update(updated); - - return { - seasonId: updated.id, - status: updated.status, - ...(updated.startDate !== undefined ? { startDate: updated.startDate } : {}), - ...(updated.endDate !== undefined ? { endDate: updated.endDate } : {}), - }; - } - - private parseDropStrategy(value: unknown): SeasonDropStrategy { - if (value === 'none' || value === 'bestNResults' || value === 'dropWorstN') { - return value; - } - return 'none'; - } - - private parseDecisionMode(value: unknown): StewardingDecisionMode { - if ( - value === 'admin_only' || - value === 'steward_decides' || - value === 'steward_vote' || - value === 'member_vote' || - value === 'steward_veto' || - value === 'member_veto' - ) { - return value; - } - return 'admin_only'; - } - - private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): { - schedule?: SeasonSchedule; - scoringConfig?: SeasonScoringConfig; - dropPolicy?: SeasonDropPolicy; - stewardingConfig?: SeasonStewardingConfig; - maxDrivers?: number; - } { - const schedule = this.buildScheduleFromTimings(config); - - const scoringConfig = config.scoring - ? new SeasonScoringConfig({ - scoringPresetId: config.scoring.patternId ?? 'custom', - customScoringEnabled: config.scoring.customScoringEnabled ?? false, - }) - : undefined; - - const dropPolicy = config.dropPolicy - ? new SeasonDropPolicy({ - strategy: this.parseDropStrategy(config.dropPolicy.strategy), - ...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}), - }) - : undefined; - - const stewardingConfig = config.stewarding - ? new SeasonStewardingConfig({ - decisionMode: this.parseDecisionMode(config.stewarding.decisionMode), - ...(config.stewarding.requiredVotes !== undefined - ? { requiredVotes: config.stewarding.requiredVotes } - : {}), - requireDefense: config.stewarding.requireDefense ?? false, - defenseTimeLimit: config.stewarding.defenseTimeLimit ?? 48, - voteTimeLimit: config.stewarding.voteTimeLimit ?? 72, - protestDeadlineHours: config.stewarding.protestDeadlineHours ?? 48, - stewardingClosesHours: config.stewarding.stewardingClosesHours ?? 168, - notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest ?? true, - notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired ?? true, - }) - : undefined; - - const structure = config.structure ?? {}; - const maxDrivers = - typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0 - ? structure.maxDrivers - : undefined; - - return { - ...(schedule !== undefined ? { schedule } : {}), - ...(scoringConfig !== undefined ? { scoringConfig } : {}), - ...(dropPolicy !== undefined ? { dropPolicy } : {}), - ...(stewardingConfig !== undefined ? { stewardingConfig } : {}), - ...(maxDrivers !== undefined ? { maxDrivers } : {}), - }; - } - - private buildScheduleFromTimings(config: LeagueConfigFormModel): SeasonSchedule | undefined { - const { timings } = config; - if (!timings || !timings.seasonStartDate || !timings.raceStartTime) { - return undefined; - } - - const startDate = new Date(timings.seasonStartDate); - const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime); - const timezoneId = timings.timezoneId ?? 'UTC'; - const timezone = LeagueTimezone.create(timezoneId); - - const plannedRounds = - typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0 - ? timings.roundsPlanned - : timings.sessionCount ?? 0; - - const recurrence = (() => { - const weekdays: WeekdaySet = - timings.weekdays && timings.weekdays.length > 0 - ? WeekdaySet.fromArray(timings.weekdays as unknown as Weekday[]) - : WeekdaySet.fromArray(['Mon']); - switch (timings.recurrenceStrategy) { - case 'everyNWeeks': - return RecurrenceStrategyFactory.everyNWeeks( - timings.intervalWeeks ?? 2, - weekdays, - ); - case 'monthlyNthWeekday': { - const pattern = MonthlyRecurrencePattern.create( - (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4, - (timings.monthlyWeekday ?? 'Mon') as Weekday, - ); - return RecurrenceStrategyFactory.monthlyNthWeekday(pattern); - } - case 'weekly': - default: - return RecurrenceStrategyFactory.weekly(weekdays); - } - })(); - - return new SeasonSchedule({ - startDate, - timeOfDay, - timezone, - recurrence, - plannedRounds, - }); - } -} diff --git a/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.test.ts b/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.test.ts index fa0afcaab..835fd12f7 100644 --- a/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.test.ts +++ b/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.test.ts @@ -7,8 +7,8 @@ import { CreateSeasonForLeagueUseCase, type CreateSeasonForLeagueInput, type CreateSeasonForLeagueResult, + type LeagueConfigFormModel, } from '@core/racing/application/use-cases/CreateSeasonForLeagueUseCase'; -import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO'; import type { UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { Result } from '@core/shared/application/Result'; diff --git a/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts b/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts index 83238a070..8e15a054b 100644 --- a/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts +++ b/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts @@ -2,7 +2,6 @@ import { Season } from '../../domain/entities/season/Season'; import { League } from '../../domain/entities/League'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO'; import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig'; import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy'; @@ -19,6 +18,58 @@ import type { UseCaseOutputPort } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +export type LeagueConfigFormModel = { + basics?: { + name?: string; + description?: string; + visibility?: string; + gameId?: string; + }; + structure?: { + mode?: string; + maxDrivers?: number; + }; + championships?: { + enableDriverChampionship?: boolean; + enableTeamChampionship?: boolean; + enableNationsChampionship?: boolean; + enableTrophyChampionship?: boolean; + }; + scoring?: { + patternId?: string; + customScoringEnabled?: boolean; + }; + dropPolicy?: { + strategy?: string; + n?: number; + }; + timings?: { + qualifyingMinutes?: number; + mainRaceMinutes?: number; + sessionCount?: number; + roundsPlanned?: number; + seasonStartDate?: string; + raceStartTime?: string; + timezoneId?: string; + recurrenceStrategy?: string; + weekdays?: string[]; + intervalWeeks?: number; + monthlyOrdinal?: number; + monthlyWeekday?: string; + }; + stewarding?: { + decisionMode?: string; + requiredVotes?: number; + requireDefense?: boolean; + defenseTimeLimit?: number; + voteTimeLimit?: number; + protestDeadlineHours?: number; + stewardingClosesHours?: number; + notifyAccusedOnProtest?: boolean; + notifyOnVoteRequired?: boolean; + }; +}; + export type CreateSeasonForLeagueInput = { leagueId: string; name: string; diff --git a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts index 5f0035833..5d756c212 100644 --- a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts +++ b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.ts @@ -71,10 +71,8 @@ export class GetDriversLeaderboardUseCase implements UseCase { - const ranking = rankings.find(r => r.driverId === driver.id); + const items: DriverLeaderboardItem[] = drivers.map((driver) => { + const ranking = rankings.find((r) => r.driverId === driver.id); const stats = this.driverStatsService.getDriverStats(driver.id); const rating = ranking?.rating ?? 0; const racesCompleted = stats?.totalRaces ?? 0; diff --git a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts index 3fdbcc1da..114b9f9e3 100644 --- a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts +++ b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts @@ -1,15 +1,71 @@ import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator'; -import { - scheduleDTOToSeasonSchedule, - type SeasonScheduleConfigDTO, -} from '../dto/LeagueScheduleDTO'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Logger } from '@core/shared/application'; -import type { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; +import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; +import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; +import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; +import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; +import { WeekdaySet } from '../../domain/value-objects/WeekdaySet'; +import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern'; +import { ALL_WEEKDAYS, type Weekday } from '../../domain/types/Weekday'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfigDTO; +export type SeasonScheduleConfig = { + seasonStartDate: string; + recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday'; + weekdays?: string[]; + raceStartTime: string; + timezoneId: string; + plannedRounds: number; + intervalWeeks?: number; + monthlyOrdinal?: 1 | 2 | 3 | 4; + monthlyWeekday?: string; +}; + +function toWeekdaySet(values: string[] | undefined): WeekdaySet { + const weekdays = (values ?? []).filter((v): v is Weekday => + ALL_WEEKDAYS.includes(v as Weekday), + ); + + return WeekdaySet.fromArray(weekdays.length > 0 ? weekdays : ['Mon']); +} + +function scheduleConfigToSeasonSchedule(dto: SeasonScheduleConfig): SeasonSchedule { + const startDate = new Date(dto.seasonStartDate); + const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime); + const timezone = LeagueTimezone.create(dto.timezoneId); + + const recurrence = (() => { + switch (dto.recurrenceStrategy) { + case 'everyNWeeks': + return RecurrenceStrategyFactory.everyNWeeks( + dto.intervalWeeks ?? 2, + toWeekdaySet(dto.weekdays), + ); + case 'monthlyNthWeekday': { + const pattern = MonthlyRecurrencePattern.create( + dto.monthlyOrdinal ?? 1, + ((dto.monthlyWeekday ?? 'Mon') as Weekday), + ); + return RecurrenceStrategyFactory.monthlyNthWeekday(pattern); + } + case 'weekly': + default: + return RecurrenceStrategyFactory.weekly(toWeekdaySet(dto.weekdays)); + } + })(); + + return new SeasonSchedule({ + startDate, + timeOfDay, + timezone, + recurrence, + plannedRounds: dto.plannedRounds, + }); +} + +export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfig; export type PreviewLeagueScheduleInput = { schedule: PreviewLeagueScheduleSeasonConfig; @@ -54,7 +110,7 @@ export class PreviewLeagueScheduleUseCase { try { let seasonSchedule: SeasonSchedule; try { - seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule); + seasonSchedule = scheduleConfigToSeasonSchedule(params.schedule); } catch (error) { this.logger.warn('Invalid schedule data provided', { schedule: params.schedule, diff --git a/core/racing/application/use-cases/SeasonUseCases.test.ts b/core/racing/application/use-cases/SeasonUseCases.test.ts index 1970c1c99..d1c09dff2 100644 --- a/core/racing/application/use-cases/SeasonUseCases.test.ts +++ b/core/racing/application/use-cases/SeasonUseCases.test.ts @@ -19,8 +19,8 @@ import { type ListSeasonsForLeagueErrorCode, type GetSeasonDetailsErrorCode, type ManageSeasonLifecycleErrorCode, + type LeagueConfigFormModel, } from '@core/racing/application/use-cases/SeasonUseCases'; -import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; diff --git a/core/racing/application/use-cases/SeasonUseCases.ts b/core/racing/application/use-cases/SeasonUseCases.ts index f3c54b283..6f91d52b4 100644 --- a/core/racing/application/use-cases/SeasonUseCases.ts +++ b/core/racing/application/use-cases/SeasonUseCases.ts @@ -1,7 +1,6 @@ import { Season } from '../../domain/entities/season/Season'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; -import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO'; import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule'; import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig'; import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy'; @@ -21,6 +20,58 @@ import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPo * Input, result and error models shared across Season-focused use cases. */ +export type LeagueConfigFormModel = { + basics?: { + name?: string; + description?: string; + visibility?: string; + gameId?: string; + }; + structure?: { + mode?: string; + maxDrivers?: number; + }; + championships?: { + enableDriverChampionship?: boolean; + enableTeamChampionship?: boolean; + enableNationsChampionship?: boolean; + enableTrophyChampionship?: boolean; + }; + scoring?: { + patternId?: string; + customScoringEnabled?: boolean; + }; + dropPolicy?: { + strategy?: string; + n?: number; + }; + timings?: { + qualifyingMinutes?: number; + mainRaceMinutes?: number; + sessionCount?: number; + roundsPlanned?: number; + seasonStartDate?: string; + raceStartTime?: string; + timezoneId?: string; + recurrenceStrategy?: string; + weekdays?: string[]; + intervalWeeks?: number; + monthlyOrdinal?: number; + monthlyWeekday?: string; + }; + stewarding?: { + decisionMode?: string; + requiredVotes?: number; + requireDefense?: boolean; + defenseTimeLimit?: number; + voteTimeLimit?: number; + protestDeadlineHours?: number; + stewardingClosesHours?: number; + notifyAccusedOnProtest?: boolean; + notifyOnVoteRequired?: boolean; + }; +}; + export interface CreateSeasonForLeagueInput { leagueId: string; name: string; diff --git a/core/racing/domain/entities/ResultWithIncidents.ts b/core/racing/domain/entities/ResultWithIncidents.ts index f71a8f1a8..7fe5b814d 100644 --- a/core/racing/domain/entities/ResultWithIncidents.ts +++ b/core/racing/domain/entities/ResultWithIncidents.ts @@ -66,9 +66,8 @@ export class ResultWithIncidents implements IEntity { }); } - // TODO WE DONT NEED ANY LEGACY CODE WE ARE NOT EVEN LIVE /** - * Create from legacy Result data (with incidents as number) + * Legacy interop: create from Result data where incidents are stored as a number. */ static fromLegacy(props: { id: string; diff --git a/core/racing/domain/value-objects/Money.ts b/core/racing/domain/value-objects/Money.ts index 846bd4f62..d17d41ebf 100644 --- a/core/racing/domain/value-objects/Money.ts +++ b/core/racing/domain/value-objects/Money.ts @@ -17,7 +17,7 @@ export interface MoneyProps { } export class Money implements IValueObject { - private static readonly PLATFORM_FEE_PERCENTAGE = 0.10; + static readonly DEFAULT_PLATFORM_FEE_PERCENTAGE = 0.10; readonly amount: number; readonly currency: Currency; @@ -37,20 +37,24 @@ export class Money implements IValueObject { return new Money(amount, currency); } - // TODO i dont think platform fee must be coupled /** - * Calculate platform fee (10%) + * Calculate a fee amount for a given percentage. + * Defaults to the current platform fee percentage. */ - calculatePlatformFee(): Money { - const feeAmount = this.amount * Money.PLATFORM_FEE_PERCENTAGE; + calculatePlatformFee(platformFeePercentage: number = Money.DEFAULT_PLATFORM_FEE_PERCENTAGE): Money { + if (!Number.isFinite(platformFeePercentage) || platformFeePercentage < 0) { + throw new RacingDomainValidationError('Platform fee percentage must be a non-negative finite number'); + } + const feeAmount = this.amount * platformFeePercentage; return new Money(feeAmount, this.currency); } /** - * Calculate net amount after platform fee + * Calculate net amount after subtracting a fee. + * Defaults to subtracting the current platform fee percentage. */ - calculateNetAmount(): Money { - const platformFee = this.calculatePlatformFee(); + calculateNetAmount(platformFeePercentage: number = Money.DEFAULT_PLATFORM_FEE_PERCENTAGE): Money { + const platformFee = this.calculatePlatformFee(platformFeePercentage); return new Money(this.amount - platformFee.amount, this.currency); } diff --git a/core/social/application/use-cases/GetCurrentUserSocialUseCase.ts b/core/social/application/use-cases/GetCurrentUserSocialUseCase.ts index 622e4ed04..2800dde3f 100644 --- a/core/social/application/use-cases/GetCurrentUserSocialUseCase.ts +++ b/core/social/application/use-cases/GetCurrentUserSocialUseCase.ts @@ -59,9 +59,9 @@ export class GetCurrentUserSocialUseCase { ); } - // TODO looks like this must still be implemented? - - const friends: FriendDTO[] = friendsDomain.map(friend => ({ + // The social graph context currently only knows about relationships. + // Profile fields for the current user are expected to be enriched by identity/profile contexts. + const friends: FriendDTO[] = friendsDomain.map((friend) => ({ driverId: friend.id, displayName: friend.name.toString(), avatarUrl: '', diff --git a/docs/architecture/SERVICES.md b/docs/architecture/SERVICES.md index 45e95764b..5fbe3b7f9 100644 --- a/docs/architecture/SERVICES.md +++ b/docs/architecture/SERVICES.md @@ -102,7 +102,6 @@ apps/api/**/ApplicationService.ts Example • LeagueApplicationService - • SeasonApplicationService API services are delivery-layer coordinators.