From 120d3bb1a10b28de7517c2c3640fe414a96a96ce Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 23 Dec 2025 15:38:50 +0100 Subject: [PATCH] fix issues in core --- .../use-cases/GetEntityAnalyticsQuery.test.ts | 4 +- .../use-cases/RecordEngagementUseCase.test.ts | 2 +- .../use-cases/RecordPageViewUseCase.test.ts | 2 +- .../use-cases/RecordPageViewUseCase.ts | 23 +-- .../GetCurrentSessionUseCase.test.ts | 6 +- .../GetCurrentUserSessionUseCase.test.ts | 2 +- .../use-cases/GetUserUseCase.test.ts | 11 +- .../HandleAuthCallbackUseCase.test.ts | 2 +- .../use-cases/LoginUseCase.test.ts | 12 +- .../use-cases/LoginWithEmailUseCase.ts | 30 ++- .../use-cases/SignupUseCase.test.ts | 4 +- .../use-cases/SignupWithEmailUseCase.test.ts | 4 +- .../use-cases/StartAuthUseCase.test.ts | 4 +- .../CreateAchievementUseCase.test.ts | 6 +- core/identity/domain/entities/User.ts | 21 +- .../application/ports/MediaStoragePort.ts | 2 +- .../use-cases/DeleteMediaUseCase.test.ts | 4 +- .../use-cases/GetAvatarUseCase.test.ts | 2 +- .../use-cases/GetMediaUseCase.test.ts | 2 +- .../application/use-cases/GetMediaUseCase.ts | 2 +- .../use-cases/UploadMediaUseCase.ts | 8 +- core/media/domain/entities/Media.ts | 6 +- .../GetUnreadNotificationsUseCase.test.ts | 2 +- .../MarkNotificationReadUseCase.test.ts | 2 +- .../NotificationPreferencesUseCases.test.ts | 16 +- .../use-cases/CreatePaymentUseCase.test.ts | 4 +- .../GetMembershipFeesUseCase.test.ts | 6 +- .../use-cases/GetPaymentsUseCase.test.ts | 4 +- .../ProcessWalletTransactionUseCase.test.ts | 6 +- .../application/dto/LeagueScheduleDTO.ts | 62 ++++++ core/racing/application/dto/index.ts | 6 +- .../ports/output/AllRacesPageOutputPort.ts | 4 +- .../output/ChampionshipStandingsOutputPort.ts | 4 +- .../ChampionshipStandingsRowOutputPort.ts | 4 +- .../DriverRegistrationStatusOutputPort.ts | 4 +- .../services/SeasonApplicationService.ts | 28 ++- .../ApplyForSponsorshipUseCase.test.ts | 14 +- .../use-cases/ApplyForSponsorshipUseCase.ts | 6 +- .../use-cases/ApplyPenaltyUseCase.test.ts | 82 +++++--- .../ApproveLeagueJoinRequestUseCase.test.ts | 15 +- .../use-cases/CancelRaceUseCase.test.ts | 14 +- .../CloseRaceEventStewardingUseCase.test.ts | 7 +- ...eLeagueWithSeasonAndScoringUseCase.test.ts | 7 +- .../use-cases/CreateSeasonForLeagueUseCase.ts | 28 ++- .../use-cases/CreateSponsorUseCase.test.ts | 2 +- .../DashboardOverviewUseCase.test.ts | 38 ++-- .../use-cases/FileProtestUseCase.ts | 10 +- .../GetAllRacesPageDataUseCase.test.ts | 36 +++- .../use-cases/GetAllRacesUseCase.test.ts | 33 ++- .../GetDriversLeaderboardUseCase.test.ts | 12 +- .../GetLeagueAdminPermissionsUseCase.test.ts | 9 +- .../GetLeagueDriverSeasonStatsUseCase.test.ts | 38 ++-- .../GetLeagueDriverSeasonStatsUseCase.ts | 51 ++--- .../use-cases/GetLeagueJoinRequestsUseCase.ts | 5 +- .../use-cases/GetLeagueMembershipsUseCase.ts | 5 +- .../use-cases/GetLeagueProtestsUseCase.ts | 4 +- .../use-cases/GetLeagueSeasonsUseCase.test.ts | 4 +- .../GetPendingSponsorshipRequestsUseCase.ts | 3 +- .../GetProfileOverviewUseCase.test.ts | 12 +- .../use-cases/GetProfileOverviewUseCase.ts | 24 ++- .../use-cases/GetRaceDetailUseCase.test.ts | 4 +- .../use-cases/GetRaceDetailUseCase.ts | 20 +- .../use-cases/GetRacePenaltiesUseCase.ts | 16 +- .../use-cases/GetRaceProtestsUseCase.test.ts | 8 +- .../use-cases/GetRaceProtestsUseCase.ts | 4 +- .../GetRaceRegistrationsUseCase.test.ts | 8 +- .../use-cases/GetRacesPageDataUseCase.test.ts | 35 +++- .../use-cases/GetRacesPageDataUseCase.ts | 4 +- .../use-cases/GetSeasonDetailsUseCase.ts | 2 +- .../use-cases/GetSeasonSponsorshipsUseCase.ts | 27 ++- .../GetSponsorDashboardUseCase.test.ts | 4 +- .../GetSponsorSponsorshipsUseCase.test.ts | 4 +- .../use-cases/GetSponsorsUseCase.test.ts | 2 +- .../use-cases/GetTeamDetailsUseCase.test.ts | 8 +- .../GetTeamJoinRequestsUseCase.test.ts | 8 +- .../use-cases/GetTeamJoinRequestsUseCase.ts | 4 +- .../GetTeamsLeaderboardUseCase.test.ts | 17 +- .../use-cases/GetTeamsLeaderboardUseCase.ts | 2 - .../use-cases/GetTotalDriversUseCase.test.ts | 20 +- .../use-cases/GetTotalRacesUseCase.test.ts | 4 +- .../ImportRaceResultsApiUseCase.test.ts | 5 +- .../use-cases/ImportRaceResultsApiUseCase.ts | 6 +- .../IsDriverRegisteredForRaceUseCase.test.ts | 22 +- .../use-cases/JoinLeagueUseCase.test.ts | 3 +- .../use-cases/JoinLeagueUseCase.ts | 3 +- .../ListSeasonsForLeagueUseCase.test.ts | 1 - .../ManageSeasonLifecycleUseCase.test.ts | 7 +- .../use-cases/ManageSeasonLifecycleUseCase.ts | 6 +- .../PreviewLeagueScheduleUseCase.test.ts | 5 +- .../use-cases/PreviewLeagueScheduleUseCase.ts | 29 +-- .../use-cases/QuickPenaltyUseCase.test.ts | 1 - .../use-cases/QuickPenaltyUseCase.ts | 21 +- ...culateChampionshipStandingsUseCase.test.ts | 7 +- ...RecalculateChampionshipStandingsUseCase.ts | 2 +- .../use-cases/RegisterForRaceUseCase.test.ts | 4 +- .../RejectLeagueJoinRequestUseCase.test.ts | 9 +- .../RejectLeagueJoinRequestUseCase.ts | 7 +- .../RejectTeamJoinRequestUseCase.test.ts | 5 +- .../use-cases/RejectTeamJoinRequestUseCase.ts | 7 +- .../RemoveLeagueMemberUseCase.test.ts | 5 +- .../RequestProtestDefenseUseCase.test.ts | 4 +- .../use-cases/RequestProtestDefenseUseCase.ts | 10 +- .../use-cases/ReviewProtestUseCase.test.ts | 1 - .../use-cases/ReviewProtestUseCase.ts | 16 +- .../use-cases/SeasonUseCases.test.ts | 109 ++++++---- .../application/use-cases/SeasonUseCases.ts | 99 +++++---- .../use-cases/SendFinalResultsUseCase.test.ts | 4 +- .../use-cases/SendFinalResultsUseCase.ts | 19 +- .../SendPerformanceSummaryUseCase.test.ts | 4 +- .../SendPerformanceSummaryUseCase.ts | 7 +- .../UpdateDriverProfileUseCase.test.ts | 6 +- .../use-cases/UpdateDriverProfileUseCase.ts | 14 +- .../use-cases/WithdrawFromRaceUseCase.test.ts | 58 ++++-- .../application/utils/RaceResultGenerator.ts | 9 +- .../utils/RaceResultGeneratorWithIncidents.ts | 22 +- .../domain/entities/DriverLivery.test.ts | 4 +- core/racing/domain/entities/Penalty.ts | 190 +----------------- .../entities/SponsorshipRequest.test.ts | 2 +- .../domain/entities/season/Season.test.ts | 5 +- .../services/EventScoringService.test.ts | 7 - core/racing/domain/value-objects/Money.ts | 6 +- .../domain/value-objects/RaceIncidents.ts | 46 ++++- .../domain/value-objects/SessionType.test.ts | 6 +- .../factories/racing/DriverRefFactory.ts | 1 - .../testing/factories/racing/SeasonFactory.ts | 23 +++ 125 files changed, 1005 insertions(+), 793 deletions(-) create mode 100644 core/testing/factories/racing/SeasonFactory.ts diff --git a/core/analytics/application/use-cases/GetEntityAnalyticsQuery.test.ts b/core/analytics/application/use-cases/GetEntityAnalyticsQuery.test.ts index f68995c3d..07b2ef0dd 100644 --- a/core/analytics/application/use-cases/GetEntityAnalyticsQuery.test.ts +++ b/core/analytics/application/use-cases/GetEntityAnalyticsQuery.test.ts @@ -20,11 +20,11 @@ describe('GetEntityAnalyticsQuery', () => { pageViewRepository = { countByEntityId: vi.fn(), countUniqueVisitors: vi.fn(), - } as unknown as IPageViewRepository as any; + }; engagementRepository = { getSponsorClicksForEntity: vi.fn(), - } as unknown as IEngagementRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/analytics/application/use-cases/RecordEngagementUseCase.test.ts b/core/analytics/application/use-cases/RecordEngagementUseCase.test.ts index f79f99a4d..c38d68523 100644 --- a/core/analytics/application/use-cases/RecordEngagementUseCase.test.ts +++ b/core/analytics/application/use-cases/RecordEngagementUseCase.test.ts @@ -16,7 +16,7 @@ describe('RecordEngagementUseCase', () => { beforeEach(() => { engagementRepository = { save: vi.fn(), - } as unknown as IEngagementRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/analytics/application/use-cases/RecordPageViewUseCase.test.ts b/core/analytics/application/use-cases/RecordPageViewUseCase.test.ts index 35872cb2c..af0bfc295 100644 --- a/core/analytics/application/use-cases/RecordPageViewUseCase.test.ts +++ b/core/analytics/application/use-cases/RecordPageViewUseCase.test.ts @@ -16,7 +16,7 @@ describe('RecordPageViewUseCase', () => { beforeEach(() => { pageViewRepository = { save: vi.fn(), - } as unknown as IPageViewRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/analytics/application/use-cases/RecordPageViewUseCase.ts b/core/analytics/application/use-cases/RecordPageViewUseCase.ts index cd232a995..e8554eb66 100644 --- a/core/analytics/application/use-cases/RecordPageViewUseCase.ts +++ b/core/analytics/application/use-cases/RecordPageViewUseCase.ts @@ -31,26 +31,19 @@ export class RecordPageViewUseCase implements UseCase>> { try { - const props = { + type PageViewCreateProps = Parameters<(typeof PageView)['create']>[0]; + + const props: PageViewCreateProps = { id: crypto.randomUUID(), entityType: input.entityType, entityId: input.entityId, visitorType: input.visitorType, sessionId: input.sessionId, - } as any; - - if (input.visitorId !== undefined) { - props.visitorId = input.visitorId; - } - if (input.referrer !== undefined) { - props.referrer = input.referrer; - } - if (input.userAgent !== undefined) { - props.userAgent = input.userAgent; - } - if (input.country !== undefined) { - props.country = input.country; - } + ...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}), + ...(input.referrer !== undefined ? { referrer: input.referrer } : {}), + ...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}), + ...(input.country !== undefined ? { country: input.country } : {}), + }; const pageView = PageView.create(props); diff --git a/core/identity/application/use-cases/GetCurrentSessionUseCase.test.ts b/core/identity/application/use-cases/GetCurrentSessionUseCase.test.ts index 995d8c80b..bdc6ec864 100644 --- a/core/identity/application/use-cases/GetCurrentSessionUseCase.test.ts +++ b/core/identity/application/use-cases/GetCurrentSessionUseCase.test.ts @@ -4,6 +4,10 @@ import { User } from '../../domain/entities/User'; import { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; +type GetCurrentSessionOutput = { + user: User; +}; + describe('GetCurrentSessionUseCase', () => { let useCase: GetCurrentSessionUseCase; let mockUserRepo: { @@ -14,7 +18,7 @@ describe('GetCurrentSessionUseCase', () => { emailExists: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; beforeEach(() => { mockUserRepo = { diff --git a/core/identity/application/use-cases/GetCurrentUserSessionUseCase.test.ts b/core/identity/application/use-cases/GetCurrentUserSessionUseCase.test.ts index b959088f0..c70356628 100644 --- a/core/identity/application/use-cases/GetCurrentUserSessionUseCase.test.ts +++ b/core/identity/application/use-cases/GetCurrentUserSessionUseCase.test.ts @@ -11,7 +11,7 @@ describe('GetCurrentUserSessionUseCase', () => { clearSession: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; let useCase: GetCurrentUserSessionUseCase; beforeEach(() => { diff --git a/core/identity/application/use-cases/GetUserUseCase.test.ts b/core/identity/application/use-cases/GetUserUseCase.test.ts index 8e6361853..0bbd12600 100644 --- a/core/identity/application/use-cases/GetUserUseCase.test.ts +++ b/core/identity/application/use-cases/GetUserUseCase.test.ts @@ -3,14 +3,16 @@ import { GetUserUseCase } from './GetUserUseCase'; import { User } from '../../domain/entities/User'; import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; -import type { Result } from '@core/shared/application/Result'; +import { Result } from '@core/shared/application/Result'; + +type GetUserOutput = Result<{ user: User }, unknown>; describe('GetUserUseCase', () => { let userRepository: { findById: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; let useCase: GetUserUseCase; beforeEach(() => { @@ -54,8 +56,9 @@ describe('GetUserUseCase', () => { expect(userRepository.findById).toHaveBeenCalledWith('user-1'); expect(result.isOk()).toBe(true); expect(output.present).toHaveBeenCalled(); - const callArgs = output.present.mock.calls?.[0]?.[0] as Result; - const user = callArgs.unwrap().user; + const callArgs = output.present.mock.calls?.[0]?.[0]; + expect(callArgs).toBeInstanceOf(Result); + const user = (callArgs as GetUserOutput).unwrap().user; expect(user).toBeInstanceOf(User); expect(user.getId().value).toBe('user-1'); expect(user.getDisplayName()).toBe('Test User'); diff --git a/core/identity/application/use-cases/HandleAuthCallbackUseCase.test.ts b/core/identity/application/use-cases/HandleAuthCallbackUseCase.test.ts index f12cc7609..b71e5065f 100644 --- a/core/identity/application/use-cases/HandleAuthCallbackUseCase.test.ts +++ b/core/identity/application/use-cases/HandleAuthCallbackUseCase.test.ts @@ -17,7 +17,7 @@ describe('HandleAuthCallbackUseCase', () => { clearSession: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; let useCase: HandleAuthCallbackUseCase; beforeEach(() => { diff --git a/core/identity/application/use-cases/LoginUseCase.test.ts b/core/identity/application/use-cases/LoginUseCase.test.ts index deea00b37..75a2b2231 100644 --- a/core/identity/application/use-cases/LoginUseCase.test.ts +++ b/core/identity/application/use-cases/LoginUseCase.test.ts @@ -6,6 +6,8 @@ import { type LoginErrorCode, } from './LoginUseCase'; import { EmailAddress } from '../../domain/value-objects/EmailAddress'; +import { UserId } from '../../domain/value-objects/UserId'; +import { PasswordHash } from '../../domain/value-objects/PasswordHash'; import type { IAuthRepository } from '../../domain/repositories/IAuthRepository'; import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService'; import { User } from '../../domain/entities/User'; @@ -56,13 +58,12 @@ describe('LoginUseCase', () => { const emailVO = EmailAddress.create(input.email); const user = User.create({ - id: { value: 'user-1' } as any, + id: UserId.fromString('user-1'), displayName: 'Test User', email: emailVO.value, + passwordHash: PasswordHash.fromHash('stored-hash'), }); - (user as any).getPasswordHash = () => ({ value: 'stored-hash' }); - authRepo.findByEmail.mockResolvedValue(user); passwordService.verify.mockResolvedValue(true); @@ -107,13 +108,12 @@ describe('LoginUseCase', () => { const emailVO = EmailAddress.create(input.email); const user = User.create({ - id: { value: 'user-1' } as any, + id: UserId.fromString('user-1'), displayName: 'Test User', email: emailVO.value, + passwordHash: PasswordHash.fromHash('stored-hash'), }); - (user as any).getPasswordHash = () => ({ value: 'stored-hash' }); - authRepo.findByEmail.mockResolvedValue(user); passwordService.verify.mockResolvedValue(false); diff --git a/core/identity/application/use-cases/LoginWithEmailUseCase.ts b/core/identity/application/use-cases/LoginWithEmailUseCase.ts index 7b2bc2b63..ca00da63d 100644 --- a/core/identity/application/use-cases/LoginWithEmailUseCase.ts +++ b/core/identity/application/use-cases/LoginWithEmailUseCase.ts @@ -70,21 +70,29 @@ export class LoginWithEmailUseCase { } as LoginWithEmailApplicationError); } - const session = await this.sessionPort.createSession({ + type CreateSessionInput = Parameters[0]; + + const createSessionInput = { id: user.id, displayName: user.displayName, - email: user.email, - primaryDriverId: user.primaryDriverId, - } as any); + ...(user.email !== undefined ? { email: user.email } : {}), + ...(user.primaryDriverId !== undefined + ? { primaryDriverId: user.primaryDriverId } + : {}), + } satisfies CreateSessionInput; + + const session = await this.sessionPort.createSession(createSessionInput); const result: LoginWithEmailResult = { - sessionToken: (session as any).token, - userId: (session as any).user.id, - displayName: (session as any).user.displayName, - email: (session as any).user.email, - primaryDriverId: (session as any).user.primaryDriverId, - issuedAt: (session as any).issuedAt, - expiresAt: (session as any).expiresAt, + sessionToken: session.token, + userId: session.user.id, + displayName: session.user.displayName, + ...(session.user.email !== undefined ? { email: session.user.email } : {}), + ...(session.user.primaryDriverId !== undefined + ? { primaryDriverId: session.user.primaryDriverId } + : {}), + issuedAt: session.issuedAt, + expiresAt: session.expiresAt, }; this.output.present(result); diff --git a/core/identity/application/use-cases/SignupUseCase.test.ts b/core/identity/application/use-cases/SignupUseCase.test.ts index 45db6e6f7..b0ee51a6b 100644 --- a/core/identity/application/use-cases/SignupUseCase.test.ts +++ b/core/identity/application/use-cases/SignupUseCase.test.ts @@ -13,6 +13,8 @@ vi.mock('../../domain/value-objects/PasswordHash', () => ({ }, })); +type SignupOutput = unknown; + describe('SignupUseCase', () => { let authRepo: { findByEmail: Mock; @@ -22,7 +24,7 @@ describe('SignupUseCase', () => { hash: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; let useCase: SignupUseCase; beforeEach(() => { diff --git a/core/identity/application/use-cases/SignupWithEmailUseCase.test.ts b/core/identity/application/use-cases/SignupWithEmailUseCase.test.ts index 00522faad..0b0610423 100644 --- a/core/identity/application/use-cases/SignupWithEmailUseCase.test.ts +++ b/core/identity/application/use-cases/SignupWithEmailUseCase.test.ts @@ -6,6 +6,8 @@ import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; +type SignupWithEmailOutput = unknown; + describe('SignupWithEmailUseCase', () => { let userRepository: { findByEmail: Mock; @@ -17,7 +19,7 @@ describe('SignupWithEmailUseCase', () => { clearSession: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; let useCase: SignupWithEmailUseCase; beforeEach(() => { diff --git a/core/identity/application/use-cases/StartAuthUseCase.test.ts b/core/identity/application/use-cases/StartAuthUseCase.test.ts index ee377bc2f..4f33df4bb 100644 --- a/core/identity/application/use-cases/StartAuthUseCase.test.ts +++ b/core/identity/application/use-cases/StartAuthUseCase.test.ts @@ -43,7 +43,7 @@ describe('StartAuthUseCase', () => { it('returns ok and presents redirect when provider call succeeds', async () => { const input: StartAuthInput = { - provider: 'IRACING_DEMO' as any, + provider: 'IRACING_DEMO', returnTo: 'https://app/callback', }; @@ -69,7 +69,7 @@ describe('StartAuthUseCase', () => { it('wraps unexpected errors as REPOSITORY_ERROR and logs them', async () => { const input: StartAuthInput = { - provider: 'IRACING_DEMO' as any, + provider: 'IRACING_DEMO', returnTo: 'https://app/callback', }; diff --git a/core/identity/application/use-cases/achievement/CreateAchievementUseCase.test.ts b/core/identity/application/use-cases/achievement/CreateAchievementUseCase.test.ts index 52870dd97..503e0c3d6 100644 --- a/core/identity/application/use-cases/achievement/CreateAchievementUseCase.test.ts +++ b/core/identity/application/use-cases/achievement/CreateAchievementUseCase.test.ts @@ -3,13 +3,17 @@ import { CreateAchievementUseCase, type IAchievementRepository } from './CreateA import { Achievement } from '@core/identity/domain/entities/Achievement'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; +type CreateAchievementOutput = { + achievement: Achievement; +}; + describe('CreateAchievementUseCase', () => { let achievementRepository: { save: Mock; findById: Mock; }; let logger: Logger; - let output: UseCaseOutputPort & { present: Mock }; + let output: UseCaseOutputPort & { present: Mock }; let useCase: CreateAchievementUseCase; beforeEach(() => { diff --git a/core/identity/domain/entities/User.ts b/core/identity/domain/entities/User.ts index 10fe7a776..e52690a67 100644 --- a/core/identity/domain/entities/User.ts +++ b/core/identity/domain/entities/User.ts @@ -31,6 +31,7 @@ export class User { this.id = props.id; this.displayName = props.displayName.trim(); this.email = props.email; + this.passwordHash = props.passwordHash; this.iracingCustomerId = props.iracingCustomerId; this.primaryDriverId = props.primaryDriverId; this.avatarUrl = props.avatarUrl; @@ -52,18 +53,20 @@ export class User { } public static fromStored(stored: StoredUser): User { - const passwordHash = stored.passwordHash ? PasswordHash.fromHash(stored.passwordHash) : undefined; - const userProps: any = { + const passwordHash = stored.passwordHash + ? PasswordHash.fromHash(stored.passwordHash) + : undefined; + + const userProps: UserProps = { id: UserId.fromString(stored.id), displayName: stored.displayName, - email: stored.email, + ...(stored.email !== undefined ? { email: stored.email } : {}), + ...(passwordHash !== undefined ? { passwordHash } : {}), + ...(stored.primaryDriverId !== undefined + ? { primaryDriverId: stored.primaryDriverId } + : {}), }; - if (passwordHash) { - userProps.passwordHash = passwordHash; - } - if (stored.primaryDriverId) { - userProps.primaryDriverId = stored.primaryDriverId; - } + return new User(userProps); } diff --git a/core/media/application/ports/MediaStoragePort.ts b/core/media/application/ports/MediaStoragePort.ts index e698c4ead..cb236c2b5 100644 --- a/core/media/application/ports/MediaStoragePort.ts +++ b/core/media/application/ports/MediaStoragePort.ts @@ -7,7 +7,7 @@ export interface UploadOptions { filename: string; mimeType: string; - metadata?: Record; + metadata?: Record; } export interface UploadResult { diff --git a/core/media/application/use-cases/DeleteMediaUseCase.test.ts b/core/media/application/use-cases/DeleteMediaUseCase.test.ts index 68a86e472..2ec9943e3 100644 --- a/core/media/application/use-cases/DeleteMediaUseCase.test.ts +++ b/core/media/application/use-cases/DeleteMediaUseCase.test.ts @@ -33,11 +33,11 @@ describe('DeleteMediaUseCase', () => { mediaRepo = { findById: vi.fn(), delete: vi.fn(), - } as unknown as IMediaRepository as any; + }; mediaStorage = { deleteMedia: vi.fn(), - } as unknown as MediaStoragePort as any; + }; logger = { debug: vi.fn(), diff --git a/core/media/application/use-cases/GetAvatarUseCase.test.ts b/core/media/application/use-cases/GetAvatarUseCase.test.ts index 33c4e6fef..cd6796d48 100644 --- a/core/media/application/use-cases/GetAvatarUseCase.test.ts +++ b/core/media/application/use-cases/GetAvatarUseCase.test.ts @@ -29,7 +29,7 @@ describe('GetAvatarUseCase', () => { avatarRepo = { findActiveByDriverId: vi.fn(), save: vi.fn(), - } as unknown as IAvatarRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/media/application/use-cases/GetMediaUseCase.test.ts b/core/media/application/use-cases/GetMediaUseCase.test.ts index a15a5754a..b4a1150af 100644 --- a/core/media/application/use-cases/GetMediaUseCase.test.ts +++ b/core/media/application/use-cases/GetMediaUseCase.test.ts @@ -27,7 +27,7 @@ describe('GetMediaUseCase', () => { beforeEach(() => { mediaRepo = { findById: vi.fn(), - } as unknown as IMediaRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/media/application/use-cases/GetMediaUseCase.ts b/core/media/application/use-cases/GetMediaUseCase.ts index b22213a9b..659427be4 100644 --- a/core/media/application/use-cases/GetMediaUseCase.ts +++ b/core/media/application/use-cases/GetMediaUseCase.ts @@ -24,7 +24,7 @@ export interface GetMediaResult { type: string; uploadedBy: string; uploadedAt: Date; - metadata?: Record; + metadata?: Record; }; } diff --git a/core/media/application/use-cases/UploadMediaUseCase.ts b/core/media/application/use-cases/UploadMediaUseCase.ts index cee92a3a6..6b71f8622 100644 --- a/core/media/application/use-cases/UploadMediaUseCase.ts +++ b/core/media/application/use-cases/UploadMediaUseCase.ts @@ -29,7 +29,7 @@ export interface MulterFile { export interface UploadMediaInput { file: MulterFile; uploadedBy: string; - metadata?: Record; + metadata?: Record; } export interface UploadMediaResult { @@ -60,7 +60,11 @@ export class UploadMediaUseCase { try { // Upload file to storage service - const uploadOptions: { filename: string; mimeType: string; metadata?: Record } = { + const uploadOptions: { + filename: string; + mimeType: string; + metadata?: Record; + } = { filename: input.file.originalname, mimeType: input.file.mimetype, }; diff --git a/core/media/domain/entities/Media.ts b/core/media/domain/entities/Media.ts index 841a16a78..55f10ec56 100644 --- a/core/media/domain/entities/Media.ts +++ b/core/media/domain/entities/Media.ts @@ -19,7 +19,7 @@ export interface MediaProps { type: MediaType; uploadedBy: string; uploadedAt: Date; - metadata?: Record | undefined; + metadata?: Record | undefined; } export class Media implements IEntity { @@ -32,7 +32,7 @@ export class Media implements IEntity { readonly type: MediaType; readonly uploadedBy: string; readonly uploadedAt: Date; - readonly metadata?: Record | undefined; + readonly metadata?: Record | undefined; private constructor(props: MediaProps) { this.id = props.id; @@ -56,7 +56,7 @@ export class Media implements IEntity { url: string; type: MediaType; uploadedBy: string; - metadata?: Record; + metadata?: Record; }): Media { if (!props.filename) { throw new Error('Filename is required'); diff --git a/core/notifications/application/use-cases/GetUnreadNotificationsUseCase.test.ts b/core/notifications/application/use-cases/GetUnreadNotificationsUseCase.test.ts index dfccf844c..802a7d3c7 100644 --- a/core/notifications/application/use-cases/GetUnreadNotificationsUseCase.test.ts +++ b/core/notifications/application/use-cases/GetUnreadNotificationsUseCase.test.ts @@ -27,7 +27,7 @@ describe('GetUnreadNotificationsUseCase', () => { beforeEach(() => { notificationRepository = { findUnreadByRecipientId: vi.fn(), - } as unknown as INotificationRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/notifications/application/use-cases/MarkNotificationReadUseCase.test.ts b/core/notifications/application/use-cases/MarkNotificationReadUseCase.test.ts index 5ff36bbf0..3da92abda 100644 --- a/core/notifications/application/use-cases/MarkNotificationReadUseCase.test.ts +++ b/core/notifications/application/use-cases/MarkNotificationReadUseCase.test.ts @@ -31,7 +31,7 @@ describe('MarkNotificationReadUseCase', () => { findById: vi.fn(), update: vi.fn(), markAllAsReadByRecipientId: vi.fn(), - } as unknown as INotificationRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/notifications/application/use-cases/NotificationPreferencesUseCases.test.ts b/core/notifications/application/use-cases/NotificationPreferencesUseCases.test.ts index f7464ea21..228e1beb2 100644 --- a/core/notifications/application/use-cases/NotificationPreferencesUseCases.test.ts +++ b/core/notifications/application/use-cases/NotificationPreferencesUseCases.test.ts @@ -35,7 +35,7 @@ describe('NotificationPreferencesUseCases', () => { preferenceRepository = { getOrCreateDefault: vi.fn(), save: vi.fn(), - } as unknown as INotificationPreferenceRepository as any; + }; logger = { debug: vi.fn(), @@ -54,7 +54,7 @@ describe('NotificationPreferencesUseCases', () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new GetNotificationPreferencesQuery( preferenceRepository as unknown as INotificationPreferenceRepository, @@ -79,7 +79,7 @@ describe('NotificationPreferencesUseCases', () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new UpdateChannelPreferenceUseCase( preferenceRepository as unknown as INotificationPreferenceRepository, @@ -110,7 +110,7 @@ describe('NotificationPreferencesUseCases', () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new UpdateTypePreferenceUseCase( preferenceRepository as unknown as INotificationPreferenceRepository, @@ -141,7 +141,7 @@ describe('NotificationPreferencesUseCases', () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new UpdateQuietHoursUseCase( preferenceRepository as unknown as INotificationPreferenceRepository, @@ -170,7 +170,7 @@ describe('NotificationPreferencesUseCases', () => { it('UpdateQuietHoursUseCase returns error on invalid hours', async () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new UpdateQuietHoursUseCase( preferenceRepository as unknown as INotificationPreferenceRepository, @@ -200,7 +200,7 @@ describe('NotificationPreferencesUseCases', () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new SetDigestModeUseCase( preferenceRepository as unknown as INotificationPreferenceRepository, @@ -228,7 +228,7 @@ describe('NotificationPreferencesUseCases', () => { it('SetDigestModeUseCase returns error on invalid frequency', async () => { const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; const useCase = new SetDigestModeUseCase( preferenceRepository as unknown as INotificationPreferenceRepository, diff --git a/core/payments/application/use-cases/CreatePaymentUseCase.test.ts b/core/payments/application/use-cases/CreatePaymentUseCase.test.ts index 6fdf6ac3e..662a4efab 100644 --- a/core/payments/application/use-cases/CreatePaymentUseCase.test.ts +++ b/core/payments/application/use-cases/CreatePaymentUseCase.test.ts @@ -16,7 +16,7 @@ describe('CreatePaymentUseCase', () => { beforeEach(() => { paymentRepository = { create: vi.fn(), - } as unknown as IPaymentRepository as any; + }; output = { present: vi.fn(), @@ -24,7 +24,7 @@ describe('CreatePaymentUseCase', () => { useCase = new CreatePaymentUseCase( paymentRepository as unknown as IPaymentRepository, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); }); diff --git a/core/payments/application/use-cases/GetMembershipFeesUseCase.test.ts b/core/payments/application/use-cases/GetMembershipFeesUseCase.test.ts index b768c82e5..9aeb2ba4e 100644 --- a/core/payments/application/use-cases/GetMembershipFeesUseCase.test.ts +++ b/core/payments/application/use-cases/GetMembershipFeesUseCase.test.ts @@ -18,11 +18,11 @@ describe('GetMembershipFeesUseCase', () => { beforeEach(() => { membershipFeeRepository = { findByLeagueId: vi.fn(), - } as unknown as IMembershipFeeRepository as any; + }; memberPaymentRepository = { findByLeagueIdAndDriverId: vi.fn(), - } as unknown as IMemberPaymentRepository as any; + }; output = { present: vi.fn(), @@ -31,7 +31,7 @@ describe('GetMembershipFeesUseCase', () => { useCase = new GetMembershipFeesUseCase( membershipFeeRepository as unknown as IMembershipFeeRepository, memberPaymentRepository as unknown as IMemberPaymentRepository, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); }); diff --git a/core/payments/application/use-cases/GetPaymentsUseCase.test.ts b/core/payments/application/use-cases/GetPaymentsUseCase.test.ts index c2e5a640d..4c770eb8f 100644 --- a/core/payments/application/use-cases/GetPaymentsUseCase.test.ts +++ b/core/payments/application/use-cases/GetPaymentsUseCase.test.ts @@ -16,7 +16,7 @@ describe('GetPaymentsUseCase', () => { beforeEach(() => { paymentRepository = { findByFilters: vi.fn(), - } as unknown as IPaymentRepository as any; + }; output = { present: vi.fn(), @@ -24,7 +24,7 @@ describe('GetPaymentsUseCase', () => { useCase = new GetPaymentsUseCase( paymentRepository as unknown as IPaymentRepository, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); }); diff --git a/core/payments/application/use-cases/ProcessWalletTransactionUseCase.test.ts b/core/payments/application/use-cases/ProcessWalletTransactionUseCase.test.ts index eb3b96145..54febe4ae 100644 --- a/core/payments/application/use-cases/ProcessWalletTransactionUseCase.test.ts +++ b/core/payments/application/use-cases/ProcessWalletTransactionUseCase.test.ts @@ -23,11 +23,11 @@ describe('ProcessWalletTransactionUseCase', () => { findByLeagueId: vi.fn(), create: vi.fn(), update: vi.fn(), - } as unknown as IWalletRepository as any; + }; transactionRepository = { create: vi.fn(), - } as unknown as ITransactionRepository as any; + }; output = { present: vi.fn(), @@ -36,7 +36,7 @@ describe('ProcessWalletTransactionUseCase', () => { useCase = new ProcessWalletTransactionUseCase( walletRepository as unknown as IWalletRepository, transactionRepository as unknown as ITransactionRepository, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); }); diff --git a/core/racing/application/dto/LeagueScheduleDTO.ts b/core/racing/application/dto/LeagueScheduleDTO.ts index c19199023..6f6a8c0f9 100644 --- a/core/racing/application/dto/LeagueScheduleDTO.ts +++ b/core/racing/application/dto/LeagueScheduleDTO.ts @@ -18,4 +18,66 @@ export interface LeagueSchedulePreviewDTO { 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/index.ts b/core/racing/application/dto/index.ts index 6b7557024..4c86e6c76 100644 --- a/core/racing/application/dto/index.ts +++ b/core/racing/application/dto/index.ts @@ -1,7 +1,9 @@ export * from './LeagueConfigFormDTO'; -export * from './LeagueDTO'; export * from './LeagueDriverSeasonStatsDTO'; +export * from './LeagueDTO'; export * from './LeagueScheduleDTO'; export * from './RaceDTO'; export * from './ResultDTO'; -export * from './StandingDTO'; \ No newline at end of file +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/ports/output/AllRacesPageOutputPort.ts b/core/racing/application/ports/output/AllRacesPageOutputPort.ts index 13bf10c3d..553c360bb 100644 --- a/core/racing/application/ports/output/AllRacesPageOutputPort.ts +++ b/core/racing/application/ports/output/AllRacesPageOutputPort.ts @@ -12,4 +12,6 @@ export interface AllRacesPageOutputPort { total: number; page: number; limit: number; -} \ No newline at end of file +} + +// 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 index 2d945d142..6b6e6e38e 100644 --- a/core/racing/application/ports/output/ChampionshipStandingsOutputPort.ts +++ b/core/racing/application/ports/output/ChampionshipStandingsOutputPort.ts @@ -9,4 +9,6 @@ export interface ChampionshipStandingsOutputPort { teamId?: string; teamName?: string; }>; -} \ No newline at end of file +} + +// 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 index 976d5b4a1..38e9095d8 100644 --- a/core/racing/application/ports/output/ChampionshipStandingsRowOutputPort.ts +++ b/core/racing/application/ports/output/ChampionshipStandingsRowOutputPort.ts @@ -5,4 +5,6 @@ export interface ChampionshipStandingsRowOutputPort { driverName: string; teamId?: string; teamName?: string; -} \ No newline at end of file +} + +// 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 index 9f9032122..175be2f9f 100644 --- a/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts +++ b/core/racing/application/ports/output/DriverRegistrationStatusOutputPort.ts @@ -4,4 +4,6 @@ export interface DriverRegistrationStatusOutputPort { leagueId: string; registered: boolean; status: 'registered' | 'withdrawn' | 'pending' | 'not_registered'; -} \ No newline at end of file +} + +// 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 index 725179833..e77780fe9 100644 --- a/core/racing/application/services/SeasonApplicationService.ts +++ b/core/racing/application/services/SeasonApplicationService.ts @@ -7,12 +7,13 @@ 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 } from '../../domain/value-objects/SeasonDropPolicy'; +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? @@ -280,6 +281,27 @@ export class SeasonApplicationService { }; } + 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; @@ -298,14 +320,14 @@ export class SeasonApplicationService { const dropPolicy = config.dropPolicy ? new SeasonDropPolicy({ - strategy: config.dropPolicy.strategy as any, + strategy: this.parseDropStrategy(config.dropPolicy.strategy), ...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}), }) : undefined; const stewardingConfig = config.stewarding ? new SeasonStewardingConfig({ - decisionMode: config.stewarding.decisionMode as any, + decisionMode: this.parseDecisionMode(config.stewarding.decisionMode), ...(config.stewarding.requiredVotes !== undefined ? { requiredVotes: config.stewarding.requiredVotes } : {}), diff --git a/core/racing/application/use-cases/ApplyForSponsorshipUseCase.test.ts b/core/racing/application/use-cases/ApplyForSponsorshipUseCase.test.ts index 2a7913667..b27dfd84e 100644 --- a/core/racing/application/use-cases/ApplyForSponsorshipUseCase.test.ts +++ b/core/racing/application/use-cases/ApplyForSponsorshipUseCase.test.ts @@ -52,7 +52,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue(null); @@ -80,7 +80,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' }); @@ -109,7 +109,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' }); @@ -142,7 +142,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' }); @@ -175,7 +175,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' }); @@ -209,7 +209,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' }); @@ -242,7 +242,7 @@ describe('ApplyForSponsorshipUseCase', () => { mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository, mockSponsorRepo as unknown as ISponsorRepository, mockLogger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' }); diff --git a/core/racing/application/use-cases/ApplyForSponsorshipUseCase.ts b/core/racing/application/use-cases/ApplyForSponsorshipUseCase.ts index 8ed89ccdf..5f4bcfc2a 100644 --- a/core/racing/application/use-cases/ApplyForSponsorshipUseCase.ts +++ b/core/racing/application/use-cases/ApplyForSponsorshipUseCase.ts @@ -9,7 +9,7 @@ import { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest'; import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository'; import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository'; import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository'; -import { Money } from '../../domain/value-objects/Money'; +import { Money, isCurrency } from '../../domain/value-objects/Money'; import type { Logger } from '@core/shared/application'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -123,7 +123,9 @@ export class ApplyForSponsorshipUseCase { // Create the sponsorship request const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const offeredAmount = Money.create(input.offeredAmount, (input.currency as any) || 'USD'); + const currency = + input.currency !== undefined && isCurrency(input.currency) ? input.currency : 'USD'; + const offeredAmount = Money.create(input.offeredAmount, currency); const request = SponsorshipRequest.create({ id: requestId, diff --git a/core/racing/application/use-cases/ApplyPenaltyUseCase.test.ts b/core/racing/application/use-cases/ApplyPenaltyUseCase.test.ts index f9ffbfc05..1d88ae66c 100644 --- a/core/racing/application/use-cases/ApplyPenaltyUseCase.test.ts +++ b/core/racing/application/use-cases/ApplyPenaltyUseCase.test.ts @@ -1,10 +1,11 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; -import { ApplyPenaltyUseCase } from './ApplyPenaltyUseCase'; +import { ApplyPenaltyUseCase, type ApplyPenaltyResult } from './ApplyPenaltyUseCase'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IProtestRepository } from '../../domain/repositories/IProtestRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { Logger } from '@core/shared/application'; +import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; describe('ApplyPenaltyUseCase', () => { let mockPenaltyRepo: { @@ -48,7 +49,7 @@ describe('ApplyPenaltyUseCase', () => { }); it('should return error when race does not exist', async () => { - const output = { + const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), }; @@ -58,7 +59,7 @@ describe('ApplyPenaltyUseCase', () => { mockRaceRepo as unknown as IRaceRepository, mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository, mockLogger as unknown as Logger, - output as any, + output, ); mockRaceRepo.findById.mockResolvedValue(null); @@ -77,7 +78,7 @@ describe('ApplyPenaltyUseCase', () => { }); it('should return error when steward does not have authority', async () => { - const output = { + const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), }; @@ -87,13 +88,18 @@ describe('ApplyPenaltyUseCase', () => { mockRaceRepo as unknown as IRaceRepository, mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository, mockLogger as unknown as Logger, - output as any, + output, ); mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' }); - mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([ - { driverId: 'steward1', role: 'member', status: 'active' }, - ]); + + const membership = { + driverId: { toString: () => 'steward1' }, + role: { toString: () => 'member' }, + status: { toString: () => 'active' }, + }; + + mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]); const result = await useCase.execute({ raceId: 'race1', @@ -109,7 +115,7 @@ describe('ApplyPenaltyUseCase', () => { }); it('should return error when protest does not exist', async () => { - const output = { + const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), }; @@ -119,13 +125,18 @@ describe('ApplyPenaltyUseCase', () => { mockRaceRepo as unknown as IRaceRepository, mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository, mockLogger as unknown as Logger, - output as any, + output, ); mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' }); - mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([ - { driverId: 'steward1', role: 'owner', status: 'active' }, - ]); + + const membership = { + driverId: { toString: () => 'steward1' }, + role: { toString: () => 'owner' }, + status: { toString: () => 'active' }, + }; + + mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]); mockProtestRepo.findById.mockResolvedValue(null); const result = await useCase.execute({ @@ -143,7 +154,7 @@ describe('ApplyPenaltyUseCase', () => { }); it('should return error when protest is not upheld', async () => { - const output = { + const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), }; @@ -153,13 +164,18 @@ describe('ApplyPenaltyUseCase', () => { mockRaceRepo as unknown as IRaceRepository, mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository, mockLogger as unknown as Logger, - output as any, + output, ); mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' }); - mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([ - { driverId: 'steward1', role: 'owner', status: 'active' }, - ]); + + const membership = { + driverId: { toString: () => 'steward1' }, + role: { toString: () => 'owner' }, + status: { toString: () => 'active' }, + }; + + mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]); mockProtestRepo.findById.mockResolvedValue({ id: 'protest1', status: 'pending', raceId: 'race1' }); const result = await useCase.execute({ @@ -177,7 +193,7 @@ describe('ApplyPenaltyUseCase', () => { }); it('should return error when protest is not for this race', async () => { - const output = { + const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), }; @@ -187,13 +203,18 @@ describe('ApplyPenaltyUseCase', () => { mockRaceRepo as unknown as IRaceRepository, mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository, mockLogger as unknown as Logger, - output as any, + output, ); mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' }); - mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([ - { driverId: 'steward1', role: 'owner', status: 'active' }, - ]); + + const membership = { + driverId: { toString: () => 'steward1' }, + role: { toString: () => 'owner' }, + status: { toString: () => 'active' }, + }; + + mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]); mockProtestRepo.findById.mockResolvedValue({ id: 'protest1', status: 'upheld', raceId: 'race2' }); const result = await useCase.execute({ @@ -211,7 +232,7 @@ describe('ApplyPenaltyUseCase', () => { }); it('should create penalty and return result on success', async () => { - const output = { + const output: UseCaseOutputPort & { present: Mock } = { present: vi.fn(), }; @@ -221,13 +242,18 @@ describe('ApplyPenaltyUseCase', () => { mockRaceRepo as unknown as IRaceRepository, mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository, mockLogger as unknown as Logger, - output as any, + output, ); mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' }); - mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([ - { driverId: 'steward1', role: 'admin', status: 'active' }, - ]); + + const membership = { + driverId: { toString: () => 'steward1' }, + role: { toString: () => 'admin' }, + status: { toString: () => 'active' }, + }; + + mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]); mockPenaltyRepo.create.mockResolvedValue(undefined); const result = await useCase.execute({ diff --git a/core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase.test.ts b/core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase.test.ts index 269769008..3abd6dd81 100644 --- a/core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase.test.ts +++ b/core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; -import { ApproveLeagueJoinRequestUseCase } from './ApproveLeagueJoinRequestUseCase'; +import { + ApproveLeagueJoinRequestUseCase, + type ApproveLeagueJoinRequestResult, +} from './ApproveLeagueJoinRequestUseCase'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; @@ -33,7 +36,10 @@ describe('ApproveLeagueJoinRequestUseCase', () => { mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue(joinRequests); - const result = await useCase.execute({ leagueId, requestId }, output as unknown as UseCaseOutputPort); + const result = await useCase.execute( + { leagueId, requestId }, + output as unknown as UseCaseOutputPort, + ); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); @@ -62,7 +68,10 @@ describe('ApproveLeagueJoinRequestUseCase', () => { mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue([]); - const result = await useCase.execute({ leagueId: 'league-1', requestId: 'req-1' }, output as unknown as UseCaseOutputPort); + const result = await useCase.execute( + { leagueId: 'league-1', requestId: 'req-1' }, + output as unknown as UseCaseOutputPort, + ); expect(result.isOk()).toBe(false); expect(result.error!.code).toBe('JOIN_REQUEST_NOT_FOUND'); diff --git a/core/racing/application/use-cases/CancelRaceUseCase.test.ts b/core/racing/application/use-cases/CancelRaceUseCase.test.ts index 234b46de1..309047bfe 100644 --- a/core/racing/application/use-cases/CancelRaceUseCase.test.ts +++ b/core/racing/application/use-cases/CancelRaceUseCase.test.ts @@ -91,8 +91,11 @@ describe('CancelRaceUseCase', () => { const result = await useCase.execute({ raceId, cancelledById: 'admin-1' }); expect(result.isErr()).toBe(true); - expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED'); - expect((result.unwrapErr() as any).details.message).toContain('already cancelled'); + const err = result.unwrapErr(); + expect(err.code).toBe('NOT_AUTHORIZED'); + if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) { + expect(err.details.message).toContain('already cancelled'); + } expect(output.present).not.toHaveBeenCalled(); }); @@ -113,8 +116,11 @@ describe('CancelRaceUseCase', () => { const result = await useCase.execute({ raceId, cancelledById: 'admin-1' }); expect(result.isErr()).toBe(true); - expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED'); - expect((result.unwrapErr() as any).details.message).toContain('completed race'); + const err = result.unwrapErr(); + expect(err.code).toBe('NOT_AUTHORIZED'); + if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) { + expect(err.details.message).toContain('completed race'); + } expect(output.present).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/CloseRaceEventStewardingUseCase.test.ts b/core/racing/application/use-cases/CloseRaceEventStewardingUseCase.test.ts index 09a9c6ab6..34a946223 100644 --- a/core/racing/application/use-cases/CloseRaceEventStewardingUseCase.test.ts +++ b/core/racing/application/use-cases/CloseRaceEventStewardingUseCase.test.ts @@ -117,8 +117,11 @@ describe('CloseRaceEventStewardingUseCase', () => { const result = await useCase.execute({ raceId: 'event-1', closedById: 'admin-1' }); expect(result.isErr()).toBe(true); - expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR'); - expect((result.unwrapErr() as any).details.message).toContain('DB error'); + const err = result.unwrapErr(); + expect(err.code).toBe('REPOSITORY_ERROR'); + if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) { + expect(err.details.message).toContain('DB error'); + } expect(output.present).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.test.ts b/core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.test.ts index 33c3deb17..8d9e822d6 100644 --- a/core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.test.ts +++ b/core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.test.ts @@ -162,8 +162,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => { const result = await useCase.execute(command); expect(result.isErr()).toBe(true); - expect(result.unwrapErr().code).toBe('VALIDATION_ERROR'); - expect((result.unwrapErr() as any).details.message).toBe('gameId is required'); + const err = result.unwrapErr(); + expect(err.code).toBe('VALIDATION_ERROR'); + if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) { + expect(err.details.message).toBe('gameId is required'); + } expect(output.present).not.toHaveBeenCalled(); }); diff --git a/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts b/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts index 86d6f989d..83238a070 100644 --- a/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts +++ b/core/racing/application/use-cases/CreateSeasonForLeagueUseCase.ts @@ -5,8 +5,9 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit 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 } from '../../domain/value-objects/SeasonDropPolicy'; +import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy'; import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig'; +import type { StewardingDecisionMode } from '../../domain/entities/League'; import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay'; import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone'; import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy'; @@ -120,6 +121,27 @@ export class CreateSeasonForLeagueUseCase { } } + 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; @@ -133,11 +155,11 @@ export class CreateSeasonForLeagueUseCase { customScoringEnabled: config.scoring?.customScoringEnabled ?? false, }); const dropPolicy = new SeasonDropPolicy({ - strategy: (config.dropPolicy?.strategy as any) ?? 'none', + strategy: this.parseDropStrategy(config.dropPolicy?.strategy), ...(config.dropPolicy?.n !== undefined ? { n: config.dropPolicy.n } : {}), }); const stewardingConfig = new SeasonStewardingConfig({ - decisionMode: (config.stewarding?.decisionMode as any) ?? 'auto', + decisionMode: this.parseDecisionMode(config.stewarding?.decisionMode), ...(config.stewarding?.requiredVotes !== undefined ? { requiredVotes: config.stewarding.requiredVotes } : {}), diff --git a/core/racing/application/use-cases/CreateSponsorUseCase.test.ts b/core/racing/application/use-cases/CreateSponsorUseCase.test.ts index d27480bbc..7d0f11822 100644 --- a/core/racing/application/use-cases/CreateSponsorUseCase.test.ts +++ b/core/racing/application/use-cases/CreateSponsorUseCase.test.ts @@ -35,7 +35,7 @@ describe('CreateSponsorUseCase', () => { useCase = new CreateSponsorUseCase( sponsorRepository as unknown as ISponsorRepository, logger as unknown as Logger, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); }); diff --git a/core/racing/application/use-cases/DashboardOverviewUseCase.test.ts b/core/racing/application/use-cases/DashboardOverviewUseCase.test.ts index d65f2b9b2..b3d62774e 100644 --- a/core/racing/application/use-cases/DashboardOverviewUseCase.test.ts +++ b/core/racing/application/use-cases/DashboardOverviewUseCase.test.ts @@ -16,6 +16,8 @@ import type { FeedItem } from '@core/social/domain/types/FeedItem'; import { Result as UseCaseResult } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import { JoinRequest } from '@core/racing/domain/entities/JoinRequest'; +import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration'; describe('DashboardOverviewUseCase', () => { it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => { @@ -195,14 +197,14 @@ describe('DashboardOverviewUseCase', () => { ); }, getLeagueMembers: async (): Promise => [], - getJoinRequests: async (): Promise => [], + getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, - saveJoinRequest: async (): Promise => { + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { @@ -227,7 +229,7 @@ describe('DashboardOverviewUseCase', () => { clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, - findByRaceId: async (): Promise => [], + findByRaceId: async (): Promise => [], }; const feedRepository = { @@ -289,9 +291,9 @@ describe('DashboardOverviewUseCase', () => { expect(_presentedData).not.toBeNull(); const vm = _presentedData!; - expect(vm.myUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-1', 'race-3']); + expect(vm.myUpcomingRaces.map(r => r.race.id)).toEqual(['race-1', 'race-3']); - expect(vm.otherUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-2', 'race-4']); + expect(vm.otherUpcomingRaces.map(r => r.race.id)).toEqual(['race-2', 'race-4']); expect(vm.nextRace).not.toBeNull(); expect(vm.nextRace!.race.id).toBe('race-1'); @@ -482,14 +484,14 @@ describe('DashboardOverviewUseCase', () => { ); }, getLeagueMembers: async (): Promise => [], - getJoinRequests: async (): Promise => [], + getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, - saveJoinRequest: async (): Promise => { + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { @@ -511,7 +513,7 @@ describe('DashboardOverviewUseCase', () => { clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, - findByRaceId: async (): Promise => [], + findByRaceId: async (): Promise => [], }; const feedRepository = { @@ -578,7 +580,7 @@ describe('DashboardOverviewUseCase', () => { expect(vm.recentResults[1]!.race.id).toBe('race-old'); const summariesByLeague = new Map( - vm.leagueStandingsSummaries.map((s: any) => [s.league.id.toString(), s]), + vm.leagueStandingsSummaries.map(s => [s.league.id.toString(), s] as const), ); const summaryA = summariesByLeague.get('league-A'); @@ -702,14 +704,14 @@ describe('DashboardOverviewUseCase', () => { const leagueMembershipRepository = { getMembership: async (): Promise => null, getLeagueMembers: async (): Promise => [], - getJoinRequests: async (): Promise => [], + getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, - saveJoinRequest: async (): Promise => { + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { @@ -731,7 +733,7 @@ describe('DashboardOverviewUseCase', () => { clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, - findByRaceId: async (): Promise => [], + findByRaceId: async (): Promise => [], }; const feedRepository = { @@ -898,14 +900,14 @@ describe('DashboardOverviewUseCase', () => { const leagueMembershipRepository = { getMembership: async (): Promise => null, getLeagueMembers: async (): Promise => [], - getJoinRequests: async (): Promise => [], + getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, - saveJoinRequest: async (): Promise => { + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { @@ -927,7 +929,7 @@ describe('DashboardOverviewUseCase', () => { clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, - findByRaceId: async (): Promise => [], + findByRaceId: async (): Promise => [], }; const feedRepository = { @@ -1089,14 +1091,14 @@ describe('DashboardOverviewUseCase', () => { const leagueMembershipRepository = { getMembership: async (): Promise => null, getLeagueMembers: async (): Promise => [], - getJoinRequests: async (): Promise => [], + getJoinRequests: async (): Promise => [], saveMembership: async (): Promise => { throw new Error('Not implemented'); }, removeMembership: async (): Promise => { throw new Error('Not implemented'); }, - saveJoinRequest: async (): Promise => { + saveJoinRequest: async (): Promise => { throw new Error('Not implemented'); }, removeJoinRequest: async (): Promise => { @@ -1118,7 +1120,7 @@ describe('DashboardOverviewUseCase', () => { clearRaceRegistrations: async (): Promise => { throw new Error('Not implemented'); }, - findByRaceId: async (): Promise => [], + findByRaceId: async (): Promise => [], }; const feedRepository = { diff --git a/core/racing/application/use-cases/FileProtestUseCase.ts b/core/racing/application/use-cases/FileProtestUseCase.ts index 5b9a5f921..44254bddc 100644 --- a/core/racing/application/use-cases/FileProtestUseCase.ts +++ b/core/racing/application/use-cases/FileProtestUseCase.ts @@ -55,11 +55,11 @@ export class FileProtestUseCase { // Validate protesting driver is a member of the league const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); - const protestingDriverMembership = memberships.find(m => { - const driverId = (m as any).driverId; - const status = (m as any).status; - return driverId === command.protestingDriverId && status === 'active'; - }); + const protestingDriverMembership = memberships.find( + m => + m.driverId.toString() === command.protestingDriverId && + m.status.toString() === 'active', + ); if (!protestingDriverMembership) { return Result.err({ code: 'NOT_MEMBER', details: { message: 'Protesting driver is not an active member of this league' } }); diff --git a/core/racing/application/use-cases/GetAllRacesPageDataUseCase.test.ts b/core/racing/application/use-cases/GetAllRacesPageDataUseCase.test.ts index fde91a325..e44cd7325 100644 --- a/core/racing/application/use-cases/GetAllRacesPageDataUseCase.test.ts +++ b/core/racing/application/use-cases/GetAllRacesPageDataUseCase.test.ts @@ -8,6 +8,8 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository' import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { Logger } from '@core/shared/application'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import { Race } from '../../domain/entities/Race'; +import { League } from '../../domain/entities/League'; describe('GetAllRacesPageDataUseCase', () => { const mockRaceFindAll = vi.fn(); @@ -61,26 +63,38 @@ describe('GetAllRacesPageDataUseCase', () => { output, ); - const race1 = { + const race1 = Race.create({ id: 'race1', + leagueId: 'league1', track: 'Track A', car: 'Car A', scheduledAt: new Date('2023-01-01T10:00:00Z'), - status: 'scheduled' as const, - leagueId: 'league1', + status: 'scheduled', strengthOfField: 5, - } as any; - const race2 = { + }); + + const race2 = Race.create({ id: 'race2', + leagueId: 'league2', track: 'Track B', car: 'Car B', scheduledAt: new Date('2023-01-02T10:00:00Z'), - status: 'completed' as const, - leagueId: 'league2', - strengthOfField: null, - } as any; - const league1 = { id: 'league1', name: 'League One' } as any; - const league2 = { id: 'league2', name: 'League Two' } as any; + status: 'completed', + }); + + const league1 = League.create({ + id: 'league1', + name: 'League One', + description: 'League One', + ownerId: 'owner-1', + }); + + const league2 = League.create({ + id: 'league2', + name: 'League Two', + description: 'League Two', + ownerId: 'owner-2', + }); mockRaceFindAll.mockResolvedValue([race1, race2]); mockLeagueFindAll.mockResolvedValue([league1, league2]); diff --git a/core/racing/application/use-cases/GetAllRacesUseCase.test.ts b/core/racing/application/use-cases/GetAllRacesUseCase.test.ts index 93357abac..9bee3d9b9 100644 --- a/core/racing/application/use-cases/GetAllRacesUseCase.test.ts +++ b/core/racing/application/use-cases/GetAllRacesUseCase.test.ts @@ -8,6 +8,8 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository' import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { Logger } from '@core/shared/application'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import { Race } from '../../domain/entities/Race'; +import { League } from '../../domain/entities/League'; describe('GetAllRacesUseCase', () => { const mockRaceFindAll = vi.fn(); @@ -61,22 +63,37 @@ describe('GetAllRacesUseCase', () => { ); useCase.setOutput(output); - const race1 = { + const race1 = Race.create({ id: 'race1', + leagueId: 'league1', track: 'Track A', car: 'Car A', scheduledAt: new Date('2023-01-01T10:00:00Z'), - leagueId: 'league1', - } as any; - const race2 = { + status: 'scheduled', + }); + + const race2 = Race.create({ id: 'race2', + leagueId: 'league2', track: 'Track B', car: 'Car B', scheduledAt: new Date('2023-01-02T10:00:00Z'), - leagueId: 'league2', - } as any; - const league1 = { id: 'league1' } as any; - const league2 = { id: 'league2' } as any; + status: 'scheduled', + }); + + const league1 = League.create({ + id: 'league1', + name: 'League One', + description: 'League One', + ownerId: 'owner-1', + }); + + const league2 = League.create({ + id: 'league2', + name: 'League Two', + description: 'League Two', + ownerId: 'owner-2', + }); mockRaceFindAll.mockResolvedValue([race1, race2]); mockLeagueFindAll.mockResolvedValue([league1, league2]); diff --git a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts index 6bf185100..4c332a271 100644 --- a/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts +++ b/core/racing/application/use-cases/GetDriversLeaderboardUseCase.test.ts @@ -70,7 +70,7 @@ describe('GetDriversLeaderboardUseCase', () => { mockDriverFindAll.mockResolvedValue([driver1, driver2]); mockRankingGetAllDriverRankings.mockReturnValue(rankings); - mockDriverStatsGetDriverStats.mockImplementation((id) => { + mockDriverStatsGetDriverStats.mockImplementation((id: string) => { if (id === 'driver1') return stats1; if (id === 'driver2') return stats2; return null; @@ -89,7 +89,7 @@ describe('GetDriversLeaderboardUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult; + const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult; expect(presented).toEqual({ items: [ @@ -142,7 +142,7 @@ describe('GetDriversLeaderboardUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult; + const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult; expect(presented).toEqual({ items: [], @@ -177,7 +177,7 @@ describe('GetDriversLeaderboardUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult; + const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult; expect(presented).toEqual({ items: [ @@ -218,7 +218,9 @@ describe('GetDriversLeaderboardUseCase', () => { expect(result.isErr()).toBe(true); const err = result.unwrapErr(); expect(err.code).toBe('REPOSITORY_ERROR'); - expect((err as any).details?.message).toBe('Repository error'); + if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) { + expect(err.details.message).toBe('Repository error'); + } expect(output.present).not.toHaveBeenCalled(); }); }); diff --git a/core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase.test.ts b/core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase.test.ts index 019f835a4..0686f9392 100644 --- a/core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase.test.ts +++ b/core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase.test.ts @@ -9,6 +9,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import type { Logger } from '@core/shared/application'; describe('GetLeagueAdminPermissionsUseCase', () => { let mockLeagueRepo: ILeagueRepository; @@ -16,12 +17,12 @@ describe('GetLeagueAdminPermissionsUseCase', () => { let mockFindById: Mock; let mockGetMembership: Mock; let output: UseCaseOutputPort & { present: Mock }; - const logger = { + const logger: Logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), - } as any; + }; beforeEach(() => { mockFindById = vi.fn(); @@ -122,7 +123,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => { }); it('returns admin permissions for admin role and calls output once', async () => { - const league = { id: 'league1' } as any; + const league = { id: 'league1' } as unknown as { id: string }; mockFindById.mockResolvedValue(league); mockGetMembership.mockResolvedValue({ status: 'active', role: 'admin' }); @@ -144,7 +145,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => { }); it('returns admin permissions for owner role and calls output once', async () => { - const league = { id: 'league1' } as any; + const league = { id: 'league1' } as unknown as { id: string }; mockFindById.mockResolvedValue(league); mockGetMembership.mockResolvedValue({ status: 'active', role: 'owner' }); diff --git a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts index ff390632f..37d351a69 100644 --- a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts +++ b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.test.ts @@ -1,19 +1,18 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { - GetLeagueDriverSeasonStatsUseCase, - type GetLeagueDriverSeasonStatsResult, - type GetLeagueDriverSeasonStatsInput, - type GetLeagueDriverSeasonStatsErrorCode, -} from './GetLeagueDriverSeasonStatsUseCase'; -import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; -import type { IResultRepository } from '../../domain/repositories/IResultRepository'; -import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; -import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; -import type { DriverRatingPort } from '../ports/DriverRatingPort'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; +import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; +import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; +import type { IResultRepository } from '../../domain/repositories/IResultRepository'; +import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; +import type { DriverRatingPort } from '../ports/DriverRatingPort'; +import { + GetLeagueDriverSeasonStatsUseCase, + type GetLeagueDriverSeasonStatsErrorCode, + type GetLeagueDriverSeasonStatsInput, + type GetLeagueDriverSeasonStatsResult, +} from './GetLeagueDriverSeasonStatsUseCase'; describe('GetLeagueDriverSeasonStatsUseCase', () => { const mockStandingFindByLeagueId = vi.fn(); @@ -30,7 +29,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { let penaltyRepository: IPenaltyRepository; let raceRepository: IRaceRepository; let driverRepository: IDriverRepository; - let teamRepository: ITeamRepository; let driverRatingPort: DriverRatingPort; let output: UseCaseOutputPort & { present: ReturnType }; @@ -102,15 +100,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { exists: vi.fn(), existsByIRacingId: vi.fn(), }; - teamRepository = { - findById: mockTeamFindById, - findAll: vi.fn(), - findByLeagueId: vi.fn(), - create: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - exists: vi.fn(), - }; driverRatingPort = { getDriverRating: mockDriverRatingGetRating, calculateRatingChange: vi.fn(), @@ -129,7 +118,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => { penaltyRepository, raceRepository, driverRepository, - teamRepository, driverRatingPort, output, ); diff --git a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts index ae358a06f..3cd0585c0 100644 --- a/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase.ts @@ -1,12 +1,11 @@ -import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; -import type { IResultRepository } from '../../domain/repositories/IResultRepository'; +import { Result } from '@core/shared/application/Result'; +import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; +import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; -import { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; +import type { IResultRepository } from '../../domain/repositories/IResultRepository'; +import type { IStandingRepository } from '../../domain/repositories/IStandingRepository'; import type { DriverRatingPort } from '../ports/DriverRatingPort'; export type DriverSeasonStats = { @@ -56,7 +55,6 @@ export class GetLeagueDriverSeasonStatsUseCase { private readonly penaltyRepository: IPenaltyRepository, private readonly raceRepository: IRaceRepository, private readonly driverRepository: IDriverRepository, - private readonly teamRepository: ITeamRepository, private readonly driverRatingPort: DriverRatingPort, private readonly output: UseCaseOutputPort, ) {} @@ -101,39 +99,32 @@ export class GetLeagueDriverSeasonStatsUseCase { const driverRatings = new Map(); for (const standing of standings) { - const driverId = String(standing.driverId); + const driverId = standing.driverId.toString(); const rating = await this.driverRatingPort.getDriverRating(driverId); driverRatings.set(driverId, { rating, ratingChange: null }); } const driverResults = new Map>(); for (const standing of standings) { - const driverId = String(standing.driverId); + const driverId = standing.driverId.toString(); const results = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId); driverResults.set( driverId, - results.map(result => ({ position: Number((result as any).position) })), + results.map(result => ({ position: result.position.toNumber() })), ); } - const driverIds = standings.map(s => String(s.driverId)); + const driverIds = standings.map(s => s.driverId.toString()); const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id))); - const driversMap = new Map(drivers.filter(d => d).map(d => [String(d!.id), d!])); - const teamIds = Array.from( - new Set( - drivers - .filter(d => (d as any)?.teamId) - .map(d => (d as any).teamId as string), - ), + const driversMap = new Map( + drivers + .filter((driver): driver is NonNullable => driver !== null) + .map(driver => [driver.id, driver]), ); - const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id))); - const teamsMap = new Map(teams.filter(t => t).map(t => [String(t!.id), t!])); const stats: DriverSeasonStats[] = standings.map(standing => { - const driverId = String(standing.driverId); - const driver = driversMap.get(driverId) as any; - const teamId = driver?.teamId as string | undefined; - const team = teamId ? teamsMap.get(String(teamId)) : undefined; + const driverId = standing.driverId.toString(); + const driver = driversMap.get(driverId); const penalties = penaltiesByDriver.get(driverId) ?? { baseDelta: 0, bonusDelta: 0 }; const results = driverResults.get(driverId) ?? []; const rating = driverRatings.get(driverId); @@ -146,16 +137,16 @@ export class GetLeagueDriverSeasonStatsUseCase { results.length > 0 ? results.reduce((sum, r) => sum + r.position, 0) / results.length : null; - const totalPoints = Number(standing.points); + const totalPoints = standing.points.toNumber(); const pointsPerRace = racesStarted > 0 ? totalPoints / racesStarted : 0; return { leagueId, driverId, - position: Number(standing.position), - driverName: String(driver?.name ?? ''), - teamId, - teamName: (team as any)?.name as string | undefined, + position: standing.position.toNumber(), + driverName: driver ? driver.name.toString() : '', + teamId: undefined, + teamName: undefined, totalPoints, basePoints: totalPoints - penalties.baseDelta, penaltyPoints: penalties.baseDelta, diff --git a/core/racing/application/use-cases/GetLeagueJoinRequestsUseCase.ts b/core/racing/application/use-cases/GetLeagueJoinRequestsUseCase.ts index 262db3e40..02eef201d 100644 --- a/core/racing/application/use-cases/GetLeagueJoinRequestsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueJoinRequestsUseCase.ts @@ -73,10 +73,7 @@ export class GetLeagueJoinRequestsUseCase { return Result.ok(undefined); } catch (error: unknown) { - const message = - error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string' - ? (error as Error).message - : 'Failed to load league join requests'; + const message = error instanceof Error ? error.message : 'Failed to load league join requests'; return Result.err({ code: 'REPOSITORY_ERROR', diff --git a/core/racing/application/use-cases/GetLeagueMembershipsUseCase.ts b/core/racing/application/use-cases/GetLeagueMembershipsUseCase.ts index bc9aab7d5..7ea6c1950 100644 --- a/core/racing/application/use-cases/GetLeagueMembershipsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueMembershipsUseCase.ts @@ -67,10 +67,7 @@ export class GetLeagueMembershipsUseCase { return Result.ok(undefined); } catch (error: unknown) { - const message = - error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string' - ? (error as any).message - : 'Failed to load league memberships'; + const message = error instanceof Error ? error.message : 'Failed to load league memberships'; return Result.err({ code: 'REPOSITORY_ERROR', diff --git a/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts b/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts index fda9ba0fd..26e4b7937 100644 --- a/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueProtestsUseCase.ts @@ -94,8 +94,8 @@ export class GetLeagueProtestsUseCase { return Result.ok(undefined); } catch (error: unknown) { const message = - error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string' - ? (error as any).message + error instanceof Error && error.message + ? error.message : 'Failed to load league protests'; return Result.err({ diff --git a/core/racing/application/use-cases/GetLeagueSeasonsUseCase.test.ts b/core/racing/application/use-cases/GetLeagueSeasonsUseCase.test.ts index 547e98c43..27bd54b2b 100644 --- a/core/racing/application/use-cases/GetLeagueSeasonsUseCase.test.ts +++ b/core/racing/application/use-cases/GetLeagueSeasonsUseCase.test.ts @@ -27,11 +27,11 @@ describe('GetLeagueSeasonsUseCase', () => { beforeEach(() => { seasonRepository = { findByLeagueId: vi.fn(), - } as unknown as ISeasonRepository as any; + }; leagueRepository = { findById: vi.fn(), - } as unknown as ILeagueRepository as any; + }; output = { present: vi.fn(), diff --git a/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts b/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts index ae0846755..13382a11d 100644 --- a/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts +++ b/core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase.ts @@ -6,11 +6,10 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository'; import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository'; -import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; +import type { SponsorableEntityType, SponsorshipRequest } from '../../domain/entities/SponsorshipRequest'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -import type { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest'; import type { Sponsor } from '../../domain/entities/sponsor/Sponsor'; import { Money } from '../../domain/value-objects/Money'; diff --git a/core/racing/application/use-cases/GetProfileOverviewUseCase.test.ts b/core/racing/application/use-cases/GetProfileOverviewUseCase.test.ts index b57b1c117..406e9686d 100644 --- a/core/racing/application/use-cases/GetProfileOverviewUseCase.test.ts +++ b/core/racing/application/use-cases/GetProfileOverviewUseCase.test.ts @@ -9,7 +9,6 @@ import { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository'; -import type { IImageServicePort } from '../ports/IImageServicePort'; import { Driver } from '../../domain/entities/Driver'; import { Team } from '../../domain/entities/Team'; import type { UseCaseOutputPort } from '@core/shared/application'; @@ -29,9 +28,6 @@ describe('GetProfileOverviewUseCase', () => { let socialRepository: { getFriends: Mock; }; - let imageService: { - getDriverAvatar: Mock; - }; let getDriverStats: Mock; let getAllDriverRankings: Mock; let driverExtendedProfileProvider: { @@ -52,9 +48,6 @@ describe('GetProfileOverviewUseCase', () => { socialRepository = { getFriends: vi.fn(), }; - imageService = { - getDriverAvatar: vi.fn(), - }; getDriverStats = vi.fn(); getAllDriverRankings = vi.fn(); driverExtendedProfileProvider = { @@ -69,7 +62,6 @@ describe('GetProfileOverviewUseCase', () => { teamRepository as unknown as ITeamRepository, teamMembershipRepository as unknown as ITeamMembershipRepository, socialRepository as unknown as ISocialGraphRepository, - imageService as unknown as IImageServicePort, driverExtendedProfileProvider, getDriverStats, getAllDriverRankings, @@ -117,7 +109,6 @@ describe('GetProfileOverviewUseCase', () => { teamRepository.findAll.mockResolvedValue(teams); teamMembershipRepository.getMembership.mockResolvedValue(null); socialRepository.getFriends.mockResolvedValue(friends); - imageService.getDriverAvatar.mockReturnValue('avatar-url'); getDriverStats.mockReturnValue(statsAdapter); getAllDriverRankings.mockReturnValue(rankings); driverExtendedProfileProvider.getExtendedProfile.mockReturnValue(null); @@ -127,8 +118,7 @@ describe('GetProfileOverviewUseCase', () => { expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = (output.present as unknown as Mock).mock - .calls[0][0] as GetProfileOverviewResult; + const presented = (output.present as unknown as Mock).mock.calls[0]?.[0] as GetProfileOverviewResult; expect(presented.driverInfo.driver.id).toBe(driverId); expect(presented.extendedProfile).toBeNull(); }); diff --git a/core/racing/application/use-cases/GetProfileOverviewUseCase.ts b/core/racing/application/use-cases/GetProfileOverviewUseCase.ts index 2d6efebe1..04ae0f82e 100644 --- a/core/racing/application/use-cases/GetProfileOverviewUseCase.ts +++ b/core/racing/application/use-cases/GetProfileOverviewUseCase.ts @@ -1,7 +1,6 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; -import type { IImageServicePort } from '../ports/IImageServicePort'; import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository'; import type { DriverExtendedProfileProvider } from '../ports/DriverExtendedProfileProvider'; import type { Driver } from '../../domain/entities/Driver'; @@ -9,7 +8,7 @@ import type { Team } from '../../domain/entities/Team'; import type { TeamMembership } from '../../domain/types/TeamMembership'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { UseCaseOutputPort, UseCase } from '@core/shared/application'; +import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; interface ProfileDriverStatsAdapter { rating: number | null; @@ -85,29 +84,29 @@ export type GetProfileOverviewResult = { finishDistribution: ProfileOverviewFinishDistribution | null; teamMemberships: ProfileOverviewTeamMembership[]; socialSummary: ProfileOverviewSocialSummary; - extendedProfile: unknown; + extendedProfile: ReturnType; }; export type GetProfileOverviewErrorCode = | 'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR'; -export class GetProfileOverviewUseCase implements UseCase { +export class GetProfileOverviewUseCase { constructor( private readonly driverRepository: IDriverRepository, private readonly teamRepository: ITeamRepository, private readonly teamMembershipRepository: ITeamMembershipRepository, private readonly socialRepository: ISocialGraphRepository, - private readonly imageService: IImageServicePort, private readonly driverExtendedProfileProvider: DriverExtendedProfileProvider, private readonly getDriverStats: (driverId: string) => ProfileDriverStatsAdapter | null, private readonly getAllDriverRankings: () => DriverRankingEntry[], + private readonly output: UseCaseOutputPort, ) {} async execute( input: GetProfileOverviewInput, ): Promise< - Result> + Result> > { try { const { driverId } = input; @@ -130,10 +129,11 @@ export class GetProfileOverviewUseCase implements UseCase { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetRaceDetailResult; + const presented = output.present.mock.calls[0]?.[0] as GetRaceDetailResult; expect(presented.race).toEqual(race); expect(presented.league).toEqual(league); expect(presented.registrations).toEqual(registrations); @@ -145,7 +145,7 @@ describe('GetRaceDetailUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetRaceDetailResult; + const presented = output.present.mock.calls[0]?.[0] as GetRaceDetailResult; expect(presented.userResult).toBe(userDomainResult); expect(presented.race).toEqual(race); expect(presented.league).toBeNull(); diff --git a/core/racing/application/use-cases/GetRaceDetailUseCase.ts b/core/racing/application/use-cases/GetRaceDetailUseCase.ts index 3992fd75b..bd19511e4 100644 --- a/core/racing/application/use-cases/GetRaceDetailUseCase.ts +++ b/core/racing/application/use-cases/GetRaceDetailUseCase.ts @@ -1,9 +1,10 @@ -import { Result as DomainResult, Result } from '@core/shared/application/Result'; +import { Result } from '@core/shared/application/Result'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { League } from '../../domain/entities/League'; import type { Race } from '../../domain/entities/Race'; import type { RaceRegistration } from '../../domain/entities/RaceRegistration'; +import type { Result as RaceResult } from '../../domain/entities/result/Result'; import type { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; @@ -26,14 +27,12 @@ export type GetRaceDetailResult = { league: League | null; registrations: RaceRegistration[]; drivers: NonNullable>>[]; - userResult: DomainResult | null; + userResult: RaceResult | null; isUserRegistered: boolean; canRegister: boolean; }; export class GetRaceDetailUseCase { - private output: UseCaseOutputPort | null = null; // TODO wtf this must be injected via constructor - constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, @@ -41,12 +40,9 @@ export class GetRaceDetailUseCase { private readonly raceRegistrationRepository: IRaceRegistrationRepository, private readonly resultRepository: IResultRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, + private readonly output: UseCaseOutputPort, ) {} - setOutput(output: UseCaseOutputPort) { // TODO must be removed - this.output = output; - } - async execute( input: GetRaceDetailInput, ): Promise>> { @@ -75,9 +71,10 @@ export class GetRaceDetailUseCase { const isUserRegistered = registrations.some(reg => reg.driverId.toString() === driverId); const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date(); - const canRegister = !!membership && membership.status === 'active' && isUpcoming; + const canRegister = + !!membership && membership.status.toString() === 'active' && isUpcoming; - let userResult: DomainResult | null = null; + let userResult: RaceResult | null = null; if (race.status === 'completed') { const results = await this.resultRepository.findByRaceId(race.id); @@ -94,9 +91,6 @@ export class GetRaceDetailUseCase { canRegister, }; - if (!this.output) { - throw new Error('Output not set'); - } this.output.present(result); return Result.ok(undefined); diff --git a/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts b/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts index f4284e9b2..7a5ec1446 100644 --- a/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts +++ b/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts @@ -38,10 +38,10 @@ export class GetRacePenaltiesUseCase { const penalties = await this.penaltyRepository.findByRaceId(input.raceId); const driverIds = new Set(); - penalties.forEach((penalty: any) => { + for (const penalty of penalties) { driverIds.add(penalty.driverId); driverIds.add(penalty.issuedBy); - }); + } const drivers = await Promise.all( Array.from(driverIds).map((id) => this.driverRepository.findById(id)), @@ -52,16 +52,16 @@ export class GetRacePenaltiesUseCase { this.output.present({ penalties, drivers: validDrivers }); return Result.ok(undefined); - } catch (error) { + } catch (error: unknown) { const message = - error instanceof Error && error.message ? error.message : 'Failed to load race penalties'; + error instanceof Error && error.message + ? error.message + : 'Failed to load race penalties'; return Result.err({ code: 'REPOSITORY_ERROR', - details: { - message, - }, - } as ApplicationErrorCode); + details: { message }, + }); } } } \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRaceProtestsUseCase.test.ts b/core/racing/application/use-cases/GetRaceProtestsUseCase.test.ts index ca5f437ee..d5659e7ff 100644 --- a/core/racing/application/use-cases/GetRaceProtestsUseCase.test.ts +++ b/core/racing/application/use-cases/GetRaceProtestsUseCase.test.ts @@ -77,7 +77,9 @@ describe('GetRaceProtestsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetRaceProtestsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetRaceProtestsResult; expect(presented.protests).toHaveLength(1); expect(presented.protests[0]).toEqual(protest); @@ -96,7 +98,9 @@ describe('GetRaceProtestsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetRaceProtestsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetRaceProtestsResult; expect(presented.protests).toEqual([]); expect(presented.drivers).toEqual([]); diff --git a/core/racing/application/use-cases/GetRaceProtestsUseCase.ts b/core/racing/application/use-cases/GetRaceProtestsUseCase.ts index f5607b284..722db20ba 100644 --- a/core/racing/application/use-cases/GetRaceProtestsUseCase.ts +++ b/core/racing/application/use-cases/GetRaceProtestsUseCase.ts @@ -62,8 +62,8 @@ export class GetRaceProtestsUseCase { return Result.ok(undefined); } catch (error: unknown) { const message = - error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string' - ? (error as any).message + error instanceof Error && error.message + ? error.message : 'Failed to load race protests'; return Result.err({ diff --git a/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts b/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts index 60e4eade7..8ac84e26c 100644 --- a/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts +++ b/core/racing/application/use-cases/GetRaceRegistrationsUseCase.test.ts @@ -57,12 +57,14 @@ describe('GetRaceRegistrationsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetRaceRegistrationsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetRaceRegistrationsResult; expect(presented.race).toEqual(race); expect(presented.registrations).toHaveLength(2); - expect(presented.registrations[0].registration).toEqual(registrations[0]); - expect(presented.registrations[1].registration).toEqual(registrations[1]); + expect(presented.registrations[0]!.registration).toEqual(registrations[0]); + expect(presented.registrations[1]!.registration).toEqual(registrations[1]); }); it('should return RACE_NOT_FOUND error when race does not exist', async () => { diff --git a/core/racing/application/use-cases/GetRacesPageDataUseCase.test.ts b/core/racing/application/use-cases/GetRacesPageDataUseCase.test.ts index a90e13e12..9532034d5 100644 --- a/core/racing/application/use-cases/GetRacesPageDataUseCase.test.ts +++ b/core/racing/application/use-cases/GetRacesPageDataUseCase.test.ts @@ -62,13 +62,28 @@ describe('GetRacesPageDataUseCase', () => { }); it('should present races page data for a league', async () => { - const races = [ + type RaceRow = { + id: string; + track: string; + car: string; + scheduledAt: Date; + status: 'scheduled' | 'completed'; + leagueId: string; + strengthOfField: number; + isUpcoming: () => boolean; + isLive: () => boolean; + isPast: () => boolean; + }; + + type LeagueRow = { id: string; name: string }; + + const races: RaceRow[] = [ { id: 'race-1', track: 'Track 1', car: 'Car 1', scheduledAt: new Date('2023-01-01T10:00:00Z'), - status: 'scheduled' as const, + status: 'scheduled', leagueId: 'league-1', strengthOfField: 1500, isUpcoming: () => true, @@ -80,16 +95,16 @@ describe('GetRacesPageDataUseCase', () => { track: 'Track 2', car: 'Car 2', scheduledAt: new Date('2023-01-02T10:00:00Z'), - status: 'completed' as const, + status: 'completed', leagueId: 'league-1', strengthOfField: 1600, isUpcoming: () => false, isLive: () => false, isPast: () => true, }, - ] as any[]; + ]; - const leagues = [{ id: 'league-1', name: 'League 1' }] as any[]; + const leagues: LeagueRow[] = [{ id: 'league-1', name: 'League 1' }]; (raceRepository.findAll as Mock).mockResolvedValue(races); (leagueRepository.findAll as Mock).mockResolvedValue(leagues); @@ -103,14 +118,16 @@ describe('GetRacesPageDataUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0]! as GetRacesPageDataResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetRacesPageDataResult; expect(presented.leagueId).toBe('league-1'); expect(presented.races).toHaveLength(2); - expect(presented.races[0].race.id).toBe('race-1'); - expect(presented.races[0].leagueName).toBe('League 1'); - expect(presented.races[1].race.id).toBe('race-2'); + expect(presented.races[0]!.race.id).toBe('race-1'); + expect(presented.races[0]!.leagueName).toBe('League 1'); + expect(presented.races[1]!.race.id).toBe('race-2'); }); it('should return repository error when repositories throw and not present data', async () => { diff --git a/core/racing/application/use-cases/GetRacesPageDataUseCase.ts b/core/racing/application/use-cases/GetRacesPageDataUseCase.ts index 2aa450b2b..82ddc2376 100644 --- a/core/racing/application/use-cases/GetRacesPageDataUseCase.ts +++ b/core/racing/application/use-cases/GetRacesPageDataUseCase.ts @@ -41,7 +41,9 @@ export class GetRacesPageDataUseCase { this.leagueRepository.findAll(), ]); - const leagueMap = new Map(allLeagues.map(league => [league.id, league.name])); + const leagueMap = new Map( + allLeagues.map(league => [league.id.toString(), league.name.toString()]), + ); const filteredRaces = allRaces .filter(race => race.leagueId === input.leagueId) diff --git a/core/racing/application/use-cases/GetSeasonDetailsUseCase.ts b/core/racing/application/use-cases/GetSeasonDetailsUseCase.ts index 871c33b24..27ca9aeef 100644 --- a/core/racing/application/use-cases/GetSeasonDetailsUseCase.ts +++ b/core/racing/application/use-cases/GetSeasonDetailsUseCase.ts @@ -55,7 +55,7 @@ export class GetSeasonDetailsUseCase { } const result: GetSeasonDetailsResult = { - leagueId: league.id, + leagueId: league.id.toString(), season, }; diff --git a/core/racing/application/use-cases/GetSeasonSponsorshipsUseCase.ts b/core/racing/application/use-cases/GetSeasonSponsorshipsUseCase.ts index 4b7f5ef5d..f5f0edc43 100644 --- a/core/racing/application/use-cases/GetSeasonSponsorshipsUseCase.ts +++ b/core/racing/application/use-cases/GetSeasonSponsorshipsUseCase.ts @@ -23,13 +23,10 @@ export type SeasonSponsorshipFinancials = { currency: string; }; -import type { LeagueId } from '../../domain/entities/LeagueId'; -import type { LeagueName } from '../../domain/entities/LeagueName'; - export type SeasonSponsorshipDetail = { id: string; - leagueId: LeagueId; - leagueName: LeagueName; + leagueId: string; + leagueName: string; seasonId: string; seasonName: string; seasonStartDate?: Date; @@ -101,20 +98,18 @@ export class GetSeasonSponsorshipsUseCase { const completedRaces = races.filter(r => r.status === 'completed').length; const impressions = completedRaces * driverCount * 100; - const sponsorshipDetails: SeasonSponsorshipDetail[] = sponsorships.map(sponsorship => { + const sponsorshipDetails: SeasonSponsorshipDetail[] = sponsorships.map((sponsorship) => { const platformFee = sponsorship.getPlatformFee(); const netAmount = sponsorship.getNetAmount(); - return { + const detail: SeasonSponsorshipDetail = { id: sponsorship.id, - leagueId: league.id, - leagueName: league.name, + leagueId: league.id.toString(), + leagueName: league.name.toString(), seasonId: season.id, seasonName: season.name, - seasonStartDate: season.startDate, - seasonEndDate: season.endDate, - tier: sponsorship.tier, - status: sponsorship.status, + tier: sponsorship.tier.toString(), + status: sponsorship.status.toString(), pricing: { amount: sponsorship.pricing.amount, currency: sponsorship.pricing.currency, @@ -134,8 +129,12 @@ export class GetSeasonSponsorshipsUseCase { impressions, }, createdAt: sponsorship.createdAt, - activatedAt: sponsorship.activatedAt, + ...(season.startDate ? { seasonStartDate: season.startDate } : {}), + ...(season.endDate ? { seasonEndDate: season.endDate } : {}), + ...(sponsorship.activatedAt ? { activatedAt: sponsorship.activatedAt } : {}), }; + + return detail; }); this.output.present({ diff --git a/core/racing/application/use-cases/GetSponsorDashboardUseCase.test.ts b/core/racing/application/use-cases/GetSponsorDashboardUseCase.test.ts index e1338ec01..7475ebfec 100644 --- a/core/racing/application/use-cases/GetSponsorDashboardUseCase.test.ts +++ b/core/racing/application/use-cases/GetSponsorDashboardUseCase.test.ts @@ -119,7 +119,9 @@ describe('GetSponsorDashboardUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const dashboard = (output.present as Mock).mock.calls[0][0] as GetSponsorDashboardResult; + const dashboardRaw = (output.present as Mock).mock.calls[0]?.[0]; + expect(dashboardRaw).toBeDefined(); + const dashboard = dashboardRaw as GetSponsorDashboardResult; expect(dashboard).toBeDefined(); expect(dashboard.sponsorId).toBe(sponsorId); diff --git a/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.test.ts b/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.test.ts index ea014f8a0..2c6d611d7 100644 --- a/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.test.ts +++ b/core/racing/application/use-cases/GetSponsorSponsorshipsUseCase.test.ts @@ -119,7 +119,9 @@ describe('GetSponsorSponsorshipsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = (output.present as Mock).mock.calls[0][0] as GetSponsorSponsorshipsResult; + const presentedRaw = (output.present as Mock).mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetSponsorSponsorshipsResult; expect(presented.sponsor).toBe(sponsor); expect(presented.sponsorships).toHaveLength(1); diff --git a/core/racing/application/use-cases/GetSponsorsUseCase.test.ts b/core/racing/application/use-cases/GetSponsorsUseCase.test.ts index 36d14bea8..6f2752ed2 100644 --- a/core/racing/application/use-cases/GetSponsorsUseCase.test.ts +++ b/core/racing/application/use-cases/GetSponsorsUseCase.test.ts @@ -22,7 +22,7 @@ describe('GetSponsorsUseCase', () => { }; useCase = new GetSponsorsUseCase( sponsorRepository as unknown as ISponsorRepository, - output as unknown as UseCaseOutputPort, + output as unknown as UseCaseOutputPort, ); }); diff --git a/core/racing/application/use-cases/GetTeamDetailsUseCase.test.ts b/core/racing/application/use-cases/GetTeamDetailsUseCase.test.ts index 4d1e0686d..c88e7d219 100644 --- a/core/racing/application/use-cases/GetTeamDetailsUseCase.test.ts +++ b/core/racing/application/use-cases/GetTeamDetailsUseCase.test.ts @@ -68,7 +68,9 @@ describe('GetTeamDetailsUseCase', () => { expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetTeamDetailsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetTeamDetailsResult; expect(presented.team).toBe(team); expect(presented.membership).toEqual(membership); expect(presented.canManage).toBe(false); @@ -103,7 +105,9 @@ describe('GetTeamDetailsUseCase', () => { expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetTeamDetailsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetTeamDetailsResult; expect(presented.canManage).toBe(true); }); diff --git a/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.test.ts b/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.test.ts index a36878b6f..473ac343a 100644 --- a/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.test.ts +++ b/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.test.ts @@ -82,17 +82,19 @@ describe('GetTeamJoinRequestsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as GetTeamJoinRequestsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetTeamJoinRequestsResult; expect(presented.team).toBe(team); expect(presented.joinRequests).toHaveLength(1); - expect(presented.joinRequests[0]).toMatchObject({ + expect(presented.joinRequests[0]!).toMatchObject({ id: 'req-1', teamId, driverId: 'driver-1', message: 'msg', }); - expect(presented.joinRequests[0].driver).toBe(driver); + expect(presented.joinRequests[0]!.driver).toBe(driver); }); it('should return TEAM_NOT_FOUND error when team does not exist', async () => { diff --git a/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts b/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts index d1c2f19e3..0b0a9dcda 100644 --- a/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts +++ b/core/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts @@ -69,8 +69,8 @@ export class GetTeamJoinRequestsUseCase { return Result.ok(undefined); } catch (error: unknown) { const message = - error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string' - ? (error as any).message + error instanceof Error && error.message + ? error.message : 'Failed to load team join requests'; return Result.err({ diff --git a/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.test.ts b/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.test.ts index f705e8836..936cfa6b9 100644 --- a/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.test.ts +++ b/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.test.ts @@ -7,7 +7,6 @@ import { } from './GetTeamsLeaderboardUseCase'; import { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; -import { IDriverRepository } from '../../domain/repositories/IDriverRepository'; import { Team } from '../../domain/entities/Team'; import type { Logger } from '@core/shared/application'; import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; @@ -21,9 +20,6 @@ describe('GetTeamsLeaderboardUseCase', () => { let teamMembershipRepository: { getTeamMembers: Mock; }; - let driverRepository: { - findById: Mock; - }; let getDriverStats: Mock; let logger: { debug: Mock; @@ -40,9 +36,6 @@ describe('GetTeamsLeaderboardUseCase', () => { teamMembershipRepository = { getTeamMembers: vi.fn(), }; - driverRepository = { - findById: vi.fn(), - }; getDriverStats = vi.fn(); logger = { debug: vi.fn(), @@ -52,12 +45,12 @@ describe('GetTeamsLeaderboardUseCase', () => { }; output = { present: vi.fn(), - } as any; + } as unknown as UseCaseOutputPort & { present: Mock }; + useCase = new GetTeamsLeaderboardUseCase( teamRepository as unknown as ITeamRepository, teamMembershipRepository as unknown as ITeamMembershipRepository, - driverRepository as unknown as IDriverRepository, - getDriverStats, + getDriverStats as unknown as (driverId: string) => { rating: number | null; wins: number; totalRaces: number } | null, logger as unknown as Logger, output, ); @@ -109,7 +102,9 @@ describe('GetTeamsLeaderboardUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = (output.present as unknown as Mock).mock.calls[0][0] as GetTeamsLeaderboardResult; + const presentedRaw = (output.present as unknown as Mock).mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as GetTeamsLeaderboardResult; expect(presented.recruitingCount).toBe(2); // both teams are recruiting expect(presented.items).toHaveLength(2); diff --git a/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts b/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts index d303a9a08..196b58051 100644 --- a/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts +++ b/core/racing/application/use-cases/GetTeamsLeaderboardUseCase.ts @@ -1,6 +1,5 @@ import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; -import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import { SkillLevelService, type SkillLevel } from '@core/racing/domain/services/SkillLevelService'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -46,7 +45,6 @@ export class GetTeamsLeaderboardUseCase { constructor( private readonly teamRepository: ITeamRepository, private readonly teamMembershipRepository: ITeamMembershipRepository, - private readonly driverRepository: IDriverRepository, private readonly getDriverStats: (driverId: string) => DriverStatsAdapter | null, private readonly logger: Logger, private readonly output: UseCaseOutputPort, diff --git a/core/racing/application/use-cases/GetTotalDriversUseCase.test.ts b/core/racing/application/use-cases/GetTotalDriversUseCase.test.ts index 2abdc5563..e34b7c106 100644 --- a/core/racing/application/use-cases/GetTotalDriversUseCase.test.ts +++ b/core/racing/application/use-cases/GetTotalDriversUseCase.test.ts @@ -2,11 +2,9 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { GetTotalDriversUseCase, GetTotalDriversInput, - GetTotalDriversResult, GetTotalDriversErrorCode, } from './GetTotalDriversUseCase'; import { IDriverRepository } from '../../domain/repositories/IDriverRepository'; -import type { UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; describe('GetTotalDriversUseCase', () => { @@ -14,21 +12,12 @@ describe('GetTotalDriversUseCase', () => { let driverRepository: { findAll: Mock; }; - let output: UseCaseOutputPort & { present: Mock }; - beforeEach(() => { driverRepository = { findAll: vi.fn(), }; - output = { - present: vi.fn(), - } as unknown as UseCaseOutputPort & { present: Mock }; - - useCase = new GetTotalDriversUseCase( - driverRepository as unknown as IDriverRepository, - output, - ); + useCase = new GetTotalDriversUseCase(driverRepository as unknown as IDriverRepository); }); it('should return total number of drivers', async () => { @@ -41,11 +30,7 @@ describe('GetTotalDriversUseCase', () => { const result = await useCase.execute(input); expect(result.isOk()).toBe(true); - expect(result.unwrap()).toBeUndefined(); - expect(output.present).toHaveBeenCalledTimes(1); - expect(output.present).toHaveBeenCalledWith<[{ totalDrivers: number }]>( - expect.objectContaining({ totalDrivers: 2 }), - ); + expect(result.unwrap()).toEqual({ totalDrivers: 2 }); }); it('should return error on repository failure', async () => { @@ -66,6 +51,5 @@ describe('GetTotalDriversUseCase', () => { expect(unwrappedError.code).toBe('REPOSITORY_ERROR'); expect(unwrappedError.details.message).toBe(error.message); - expect(output.present).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/GetTotalRacesUseCase.test.ts b/core/racing/application/use-cases/GetTotalRacesUseCase.test.ts index 60967619d..e2cc2616f 100644 --- a/core/racing/application/use-cases/GetTotalRacesUseCase.test.ts +++ b/core/racing/application/use-cases/GetTotalRacesUseCase.test.ts @@ -59,7 +59,9 @@ describe('GetTotalRacesUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const payload = output.present.mock.calls[0][0] as GetTotalRacesResult; + const payloadRaw = output.present.mock.calls[0]?.[0]; + expect(payloadRaw).toBeDefined(); + const payload = payloadRaw as GetTotalRacesResult; expect(payload.totalRaces).toBe(2); }); diff --git a/core/racing/application/use-cases/ImportRaceResultsApiUseCase.test.ts b/core/racing/application/use-cases/ImportRaceResultsApiUseCase.test.ts index b879d5c8a..d7d8a04b1 100644 --- a/core/racing/application/use-cases/ImportRaceResultsApiUseCase.test.ts +++ b/core/racing/application/use-cases/ImportRaceResultsApiUseCase.test.ts @@ -188,8 +188,9 @@ describe('ImportRaceResultsApiUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = - output.present.mock.calls[0][0] as ImportRaceResultsApiResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as ImportRaceResultsApiResult; expect(presented.success).toBe(true); expect(presented.raceId).toBe('race-1'); diff --git a/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts b/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts index a97679b6c..770ac30cd 100644 --- a/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts +++ b/core/racing/application/use-cases/ImportRaceResultsApiUseCase.ts @@ -172,16 +172,16 @@ export class ImportRaceResultsApiUseCase { this.logger.info('ImportRaceResultsApiUseCase:race results created', { raceId }); - await this.standingRepository.recalculate(league.id); + await this.standingRepository.recalculate(league.id.toString()); this.logger.info('ImportRaceResultsApiUseCase:standings recalculated', { - leagueId: league.id, + leagueId: league.id.toString(), }); const result: ImportRaceResultsApiResult = { success: true, raceId, - leagueId: league.id, + leagueId: league.id.toString(), driversProcessed: results.length, resultsRecorded: validEntities.length, errors: [], diff --git a/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.test.ts b/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.test.ts index 63545d56a..dec778ada 100644 --- a/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.test.ts +++ b/core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase.test.ts @@ -2,11 +2,10 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { IsDriverRegisteredForRaceUseCase, type IsDriverRegisteredForRaceInput, - type IsDriverRegisteredForRaceResult, type IsDriverRegisteredForRaceErrorCode, } from './IsDriverRegisteredForRaceUseCase'; import { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository'; -import type { Logger, UseCaseOutputPort } from '@core/shared/application'; +import type { Logger } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; describe('IsDriverRegisteredForRaceUseCase', () => { @@ -20,10 +19,6 @@ describe('IsDriverRegisteredForRaceUseCase', () => { warn: Mock; error: Mock; }; - let output: UseCaseOutputPort & { - present: Mock; - }; - beforeEach(() => { registrationRepository = { isRegistered: vi.fn(), @@ -34,13 +29,9 @@ describe('IsDriverRegisteredForRaceUseCase', () => { warn: vi.fn(), error: vi.fn(), }; - output = { - present: vi.fn(), - } as unknown as UseCaseOutputPort & { present: Mock }; useCase = new IsDriverRegisteredForRaceUseCase( registrationRepository as unknown as IRaceRegistrationRepository, logger as unknown as Logger, - output as UseCaseOutputPort, ); }); @@ -52,10 +43,7 @@ describe('IsDriverRegisteredForRaceUseCase', () => { const result = await useCase.execute(params); expect(result.isOk()).toBe(true); - expect(result.unwrap()).toBeUndefined(); - expect(output.present).toHaveBeenCalledTimes(1); - const [[presented]] = (output.present as Mock).mock.calls as [[IsDriverRegisteredForRaceResult]]; - expect(presented).toEqual({ + expect(result.unwrap()).toEqual({ raceId: params.raceId, driverId: params.driverId, isRegistered: true, @@ -70,10 +58,7 @@ describe('IsDriverRegisteredForRaceUseCase', () => { const result = await useCase.execute(params); expect(result.isOk()).toBe(true); - expect(result.unwrap()).toBeUndefined(); - expect(output.present).toHaveBeenCalledTimes(1); - const [[presented]] = (output.present as Mock).mock.calls as [[IsDriverRegisteredForRaceResult]]; - expect(presented).toEqual({ + expect(result.unwrap()).toEqual({ raceId: params.raceId, driverId: params.driverId, isRegistered: false, @@ -95,6 +80,5 @@ describe('IsDriverRegisteredForRaceUseCase', () => { >; expect(errorResult.code).toBe('REPOSITORY_ERROR'); expect(errorResult.details?.message).toBe('Repository error'); - expect(output.present).not.toHaveBeenCalled(); }); }); \ No newline at end of file diff --git a/core/racing/application/use-cases/JoinLeagueUseCase.test.ts b/core/racing/application/use-cases/JoinLeagueUseCase.test.ts index fb73e1415..08f1fb410 100644 --- a/core/racing/application/use-cases/JoinLeagueUseCase.test.ts +++ b/core/racing/application/use-cases/JoinLeagueUseCase.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import { JoinLeagueUseCase, type JoinLeagueResult, type JoinLeagueInput, type JoinLeagueErrorCode } from './JoinLeagueUseCase'; import { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; -import type { Logger } from '@core/shared/application'; -import type { UseCaseOutputPort } from '@core/shared/application'; +import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; describe('JoinLeagueUseCase', () => { diff --git a/core/racing/application/use-cases/JoinLeagueUseCase.ts b/core/racing/application/use-cases/JoinLeagueUseCase.ts index a4e740b50..76f31cd77 100644 --- a/core/racing/application/use-cases/JoinLeagueUseCase.ts +++ b/core/racing/application/use-cases/JoinLeagueUseCase.ts @@ -1,9 +1,8 @@ -import type { Logger } from '@core/shared/application'; +import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import { LeagueMembership } from '../../domain/entities/LeagueMembership'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { UseCaseOutputPort } from '@core/shared/application'; export type JoinLeagueErrorCode = 'ALREADY_MEMBER' | 'REPOSITORY_ERROR'; diff --git a/core/racing/application/use-cases/ListSeasonsForLeagueUseCase.test.ts b/core/racing/application/use-cases/ListSeasonsForLeagueUseCase.test.ts index 7667d94ea..95ba0a925 100644 --- a/core/racing/application/use-cases/ListSeasonsForLeagueUseCase.test.ts +++ b/core/racing/application/use-cases/ListSeasonsForLeagueUseCase.test.ts @@ -10,7 +10,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import { Season } from '../../domain/entities/season/Season'; import type { UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import { Result } from '@core/shared/application/Result'; describe('ListSeasonsForLeagueUseCase', () => { diff --git a/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.test.ts b/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.test.ts index 5c0738cb5..1f1f9f07e 100644 --- a/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.test.ts +++ b/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.test.ts @@ -10,7 +10,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import { Season } from '../../domain/entities/season/Season'; import type { UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import { Result } from '@core/shared/application/Result'; describe('ManageSeasonLifecycleUseCase', () => { let useCase: ManageSeasonLifecycleUseCase; @@ -107,7 +106,11 @@ describe('ManageSeasonLifecycleUseCase', () => { expect(archived.isOk()).toBe(true); expect(archived.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - presented = output.present.mock.calls[0][0] as ManageSeasonLifecycleResult; + { + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + presented = presentedRaw as ManageSeasonLifecycleResult; + } expect(presented.season.status).toBe('archived'); }); diff --git a/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.ts b/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.ts index 639aa8098..a0ec3f2ec 100644 --- a/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.ts +++ b/core/racing/application/use-cases/ManageSeasonLifecycleUseCase.ts @@ -58,10 +58,12 @@ export class ManageSeasonLifecycleUseCase { } const season = await this.seasonRepository.findById(input.seasonId); - if (!season || season.leagueId !== league.id) { + if (!season || season.leagueId !== league.id.toString()) { return Result.err({ code: 'SEASON_NOT_FOUND', - details: { message: `Season ${input.seasonId} does not belong to league ${league.id}` }, + details: { + message: `Season ${input.seasonId} does not belong to league ${league.id.toString()}`, + }, }); } diff --git a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.test.ts b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.test.ts index 792e5e38e..5a68d7114 100644 --- a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.test.ts +++ b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.test.ts @@ -55,8 +55,9 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = - output.present.mock.calls[0][0] as PreviewLeagueScheduleResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as PreviewLeagueScheduleResult; expect(presented.rounds.length).toBeGreaterThan(0); expect(presented.summary).toContain('Every Mon'); }); diff --git a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts index 73e8c0c3c..3fdbcc1da 100644 --- a/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts +++ b/core/racing/application/use-cases/PreviewLeagueScheduleUseCase.ts @@ -1,22 +1,15 @@ import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator'; -import { scheduleDTOToSeasonSchedule } from '../dto/LeagueScheduleDTO'; +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 type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; -export type PreviewLeagueScheduleSeasonConfig = { - seasonStartDate: string; - recurrenceStrategy: string; - weekdays?: string[]; - raceStartTime: string; - timezoneId: string; - plannedRounds: number; - intervalWeeks?: number; - monthlyOrdinal?: 1 | 2 | 3 | 4; - monthlyWeekday?: string; -}; +export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfigDTO; export type PreviewLeagueScheduleInput = { schedule: PreviewLeagueScheduleSeasonConfig; @@ -61,7 +54,7 @@ export class PreviewLeagueScheduleUseCase { try { let seasonSchedule: SeasonSchedule; try { - seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule as any); + seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule); } catch (error) { this.logger.warn('Invalid schedule data provided', { schedule: params.schedule, @@ -83,11 +76,11 @@ export class PreviewLeagueScheduleUseCase { maxRounds, ); - const rounds: PreviewLeagueScheduleRound[] = slots.map((slot) => ({ - roundNumber: slot.roundNumber, - scheduledAt: slot.scheduledAt.toISOString(), - timezoneId: slot.timezone.id, - })); + const rounds: PreviewLeagueScheduleRound[] = slots.map(slot => ({ + roundNumber: slot.roundNumber, + scheduledAt: slot.scheduledAt.toISOString(), + timezoneId: slot.timezone.id, + })); const summary = this.buildSummary(params.schedule, rounds); diff --git a/core/racing/application/use-cases/QuickPenaltyUseCase.test.ts b/core/racing/application/use-cases/QuickPenaltyUseCase.test.ts index 51eaedd03..b181ae62d 100644 --- a/core/racing/application/use-cases/QuickPenaltyUseCase.test.ts +++ b/core/racing/application/use-cases/QuickPenaltyUseCase.test.ts @@ -4,7 +4,6 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; -import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; diff --git a/core/racing/application/use-cases/QuickPenaltyUseCase.ts b/core/racing/application/use-cases/QuickPenaltyUseCase.ts index 024f240ae..db26b4e14 100644 --- a/core/racing/application/use-cases/QuickPenaltyUseCase.ts +++ b/core/racing/application/use-cases/QuickPenaltyUseCase.ts @@ -5,7 +5,7 @@ * Designed for fast, common penalty scenarios like track limits, warnings, etc. */ -import { Penalty } from '../../domain/entities/Penalty'; +import { Penalty } from '../../domain/entities/penalty/Penalty'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; @@ -74,7 +74,11 @@ export class QuickPenaltyUseCase { ); if (!penaltyMapping) { - this.logger.error('Unknown infraction type', { infractionType: input.infractionType, severity: input.severity }); + this.logger.error( + 'Unknown infraction type', + undefined, + { infractionType: input.infractionType, severity: input.severity }, + ); return Result.err({ code: 'UNKNOWN_INFRACTION', details: { message: 'Unknown infraction type' } }); } @@ -111,9 +115,16 @@ export class QuickPenaltyUseCase { this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: input.raceId, driverId: input.driverId }); return Result.ok(undefined); - } catch (error) { - this.logger.error('Failed to apply quick penalty', { error: error instanceof Error ? error.message : 'Unknown error' }); - return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } }); + } catch (error: unknown) { + const err = + error instanceof Error ? error : new Error('Failed to apply quick penalty'); + + this.logger.error('Failed to apply quick penalty', err); + + return Result.err({ + code: 'REPOSITORY_ERROR', + details: { message: err.message }, + }); } } diff --git a/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts b/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts index adedf7c23..c0b463ebd 100644 --- a/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts +++ b/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts @@ -13,6 +13,7 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos import type { IChampionshipStandingRepository } from '../../domain/repositories/IChampionshipStandingRepository'; import type { Penalty } from '../../domain/entities/Penalty'; import { EventScoringService } from '../../domain/services/EventScoringService'; +import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import { ChampionshipAggregator } from '../../domain/services/ChampionshipAggregator'; import type { UseCaseOutputPort, Logger } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -53,7 +54,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => { output = { present: vi.fn() } as unknown as typeof output; useCase = new RecalculateChampionshipStandingsUseCase( - leagueRepository as unknown as ISeasonRepository, + leagueRepository as unknown as ILeagueRepository, seasonRepository as unknown as ISeasonRepository, leagueScoringConfigRepository as unknown as ILeagueScoringConfigRepository, raceRepository as unknown as IRaceRepository, @@ -172,7 +173,9 @@ describe('RecalculateChampionshipStandingsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as RecalculateChampionshipStandingsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as RecalculateChampionshipStandingsResult; expect(presented.leagueId).toBe('league-1'); expect(presented.seasonId).toBe('season-1'); expect(presented.entries).toHaveLength(1); diff --git a/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts b/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts index 8f4da793e..1749af9ce 100644 --- a/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts +++ b/core/racing/application/use-cases/RecalculateChampionshipStandingsUseCase.ts @@ -97,7 +97,7 @@ export class RecalculateChampionshipStandingsUseCase { {}; for (const race of races) { - const sessionType = this.mapRaceSessionType(race.sessionType); + const sessionType = this.mapRaceSessionType(String(race.sessionType)); if (!championship.sessionTypes.includes(sessionType)) { continue; } diff --git a/core/racing/application/use-cases/RegisterForRaceUseCase.test.ts b/core/racing/application/use-cases/RegisterForRaceUseCase.test.ts index fb38eb70c..06848fe91 100644 --- a/core/racing/application/use-cases/RegisterForRaceUseCase.test.ts +++ b/core/racing/application/use-cases/RegisterForRaceUseCase.test.ts @@ -100,7 +100,9 @@ describe('RegisterForRaceUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as RegisterForRaceResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as RegisterForRaceResult; expect(presented).toEqual({ raceId: 'race-1', driverId: 'driver-1', diff --git a/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.test.ts b/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.test.ts index 1979e1af2..56fff6731 100644 --- a/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.test.ts +++ b/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.test.ts @@ -9,7 +9,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import { Result } from '@core/shared/application/Result'; interface LeagueRepositoryMock { findById: Mock; @@ -31,13 +30,13 @@ describe('RejectLeagueJoinRequestUseCase', () => { beforeEach(() => { leagueRepository = { findById: vi.fn(), - } as unknown as ILeagueRepository as any; + }; leagueMembershipRepository = { getMembership: vi.fn(), getJoinRequests: vi.fn(), removeJoinRequest: vi.fn(), - } as unknown as ILeagueMembershipRepository as any; + }; logger = { debug: vi.fn(), @@ -87,7 +86,9 @@ describe('RejectLeagueJoinRequestUseCase', () => { expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as RejectLeagueJoinRequestResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as RejectLeagueJoinRequestResult; expect(presented.leagueId).toBe('league-1'); expect(presented.requestId).toBe('req-1'); expect(presented.status).toBe('rejected'); diff --git a/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.ts b/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.ts index e42ac9e10..2f9aff0c9 100644 --- a/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.ts +++ b/core/racing/application/use-cases/RejectLeagueJoinRequestUseCase.ts @@ -74,7 +74,12 @@ export class RejectLeagueJoinRequestUseCase { }); } - const currentStatus = (joinRequest as any).status ?? 'pending'; + const currentStatus = (() => { + const rawStatus = (joinRequest as unknown as { status?: unknown }).status; + return rawStatus === 'pending' || rawStatus === 'approved' || rawStatus === 'rejected' + ? rawStatus + : 'pending'; + })(); if (currentStatus !== 'pending') { this.logger.warn('Join request is in invalid state for rejection', { leagueId, diff --git a/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.test.ts b/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.test.ts index 67b0ec8ed..847f6d2ff 100644 --- a/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.test.ts +++ b/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.test.ts @@ -9,7 +9,6 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM import type { ITeamRepository } from '../../domain/repositories/ITeamRepository'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import { Result } from '@core/shared/application/Result'; interface TeamRepositoryMock { findById: Mock; @@ -31,13 +30,13 @@ describe('RejectTeamJoinRequestUseCase', () => { beforeEach(() => { teamRepository = { findById: vi.fn(), - } as unknown as ITeamRepository as any; + }; membershipRepository = { getMembership: vi.fn(), getJoinRequests: vi.fn(), removeJoinRequest: vi.fn(), - } as unknown as ITeamMembershipRepository as any; + }; logger = { debug: vi.fn(), diff --git a/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts b/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts index 2d5cb0868..b8ef8545d 100644 --- a/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts +++ b/core/racing/application/use-cases/RejectTeamJoinRequestUseCase.ts @@ -70,7 +70,12 @@ export class RejectTeamJoinRequestUseCase { }); } - const currentStatus = (joinRequest as any).status ?? 'pending'; + const currentStatus = (() => { + const rawStatus = (joinRequest as unknown as { status?: unknown }).status; + return rawStatus === 'pending' || rawStatus === 'approved' || rawStatus === 'rejected' + ? rawStatus + : 'pending'; + })(); if (currentStatus !== 'pending') { this.logger.warn('Join request is in invalid state for rejection', { teamId, diff --git a/core/racing/application/use-cases/RemoveLeagueMemberUseCase.test.ts b/core/racing/application/use-cases/RemoveLeagueMemberUseCase.test.ts index 045e1c267..76b2398d9 100644 --- a/core/racing/application/use-cases/RemoveLeagueMemberUseCase.test.ts +++ b/core/racing/application/use-cases/RemoveLeagueMemberUseCase.test.ts @@ -50,8 +50,9 @@ describe('RemoveLeagueMemberUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(leagueMembershipRepository.saveMembership).toHaveBeenCalledTimes(1); - const savedMembership = leagueMembershipRepository.saveMembership.mock.calls[0][0]; - expect(savedMembership.status.toString()).toBe('inactive'); + const savedMembership = leagueMembershipRepository.saveMembership.mock.calls[0]?.[0]; + expect(savedMembership).toBeDefined(); + expect(savedMembership!.status.toString()).toBe('inactive'); expect(output.present).toHaveBeenCalledTimes(1); expect(output.present).toHaveBeenCalledWith({ diff --git a/core/racing/application/use-cases/RequestProtestDefenseUseCase.test.ts b/core/racing/application/use-cases/RequestProtestDefenseUseCase.test.ts index e480ef18a..e7c25caab 100644 --- a/core/racing/application/use-cases/RequestProtestDefenseUseCase.test.ts +++ b/core/racing/application/use-cases/RequestProtestDefenseUseCase.test.ts @@ -149,7 +149,9 @@ describe('RequestProtestDefenseUseCase', () => { expect(protestRepository.update).toHaveBeenCalledWith(updatedProtest); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as RequestProtestDefenseResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as RequestProtestDefenseResult; expect(presented).toEqual({ leagueId: 'league-1', protestId: 'protest-1', diff --git a/core/racing/application/use-cases/RequestProtestDefenseUseCase.ts b/core/racing/application/use-cases/RequestProtestDefenseUseCase.ts index 0a47da544..51c5ee875 100644 --- a/core/racing/application/use-cases/RequestProtestDefenseUseCase.ts +++ b/core/racing/application/use-cases/RequestProtestDefenseUseCase.ts @@ -55,8 +55,14 @@ export class RequestProtestDefenseUseCase { return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race not found' } }); } - const membership = await this.membershipRepository.getMembership(race.leagueId, input.stewardId); - if (!membership || !isLeagueStewardOrHigherRole(membership.role)) { + const membership = await this.membershipRepository.getMembership( + race.leagueId, + input.stewardId, + ); + if ( + !membership || + !isLeagueStewardOrHigherRole(membership.role.toString()) + ) { return Result.err({ code: 'INSUFFICIENT_PERMISSIONS', details: { message: 'Insufficient permissions to request defense' }, diff --git a/core/racing/application/use-cases/ReviewProtestUseCase.test.ts b/core/racing/application/use-cases/ReviewProtestUseCase.test.ts index 2069fa4a1..a84c2b790 100644 --- a/core/racing/application/use-cases/ReviewProtestUseCase.test.ts +++ b/core/racing/application/use-cases/ReviewProtestUseCase.test.ts @@ -5,7 +5,6 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository' import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; -import { Result } from '@core/shared/application/Result'; describe('ReviewProtestUseCase', () => { let useCase: ReviewProtestUseCase; diff --git a/core/racing/application/use-cases/ReviewProtestUseCase.ts b/core/racing/application/use-cases/ReviewProtestUseCase.ts index 201c6bbcc..367d07732 100644 --- a/core/racing/application/use-cases/ReviewProtestUseCase.ts +++ b/core/racing/application/use-cases/ReviewProtestUseCase.ts @@ -73,9 +73,23 @@ export class ReviewProtestUseCase { await this.protestRepository.update(updatedProtest); + const protestId = (() => { + const unknownId = (protest as unknown as { id: unknown }).id; + if (typeof unknownId === 'string') return unknownId; + if ( + unknownId && + typeof unknownId === 'object' && + 'toString' in unknownId && + typeof (unknownId as { toString: unknown }).toString === 'function' + ) { + return (unknownId as { toString: () => string }).toString(); + } + return String(unknownId); + })(); + const result: ReviewProtestResult = { leagueId: race.leagueId, - protestId: typeof protest.id === 'string' ? protest.id : (protest as any).id, + protestId, status: input.decision === 'uphold' ? 'upheld' : 'dismissed', }; diff --git a/core/racing/application/use-cases/SeasonUseCases.test.ts b/core/racing/application/use-cases/SeasonUseCases.test.ts index 3c7212a69..1970c1c99 100644 --- a/core/racing/application/use-cases/SeasonUseCases.test.ts +++ b/core/racing/application/use-cases/SeasonUseCases.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { Season } from '@core/racing/domain/entities/season/Season'; +import { League } from '@core/racing/domain/entities/League'; import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository'; import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { @@ -23,13 +24,49 @@ import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueC import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -function createFakeLeagueRepository(seed: Array<{ id: string }>): ILeagueRepository { +type MockOutputPort = UseCaseOutputPort & { present: ReturnType }; + +function createOutputPort(): MockOutputPort { return { - findById: async (id: string) => seed.find((l) => l.id === id) ?? null, - findAll: async () => seed, - create: async (league: any) => league, - update: async (league: any) => league, - } as unknown as ILeagueRepository; + present: vi.fn<(data: T) => void>(), + }; +} + +function getUnknownString(value: unknown): string | null { + if (typeof value === 'string') return value; + if ( + value && + typeof value === 'object' && + 'toString' in value && + typeof (value as { toString: unknown }).toString === 'function' + ) { + return (value as { toString: () => string }).toString(); + } + return null; +} + +function createFakeLeagueRepository(seed: Array<{ id: string }>): ILeagueRepository { + const leagues: League[] = seed.map(({ id }) => + League.create({ + id, + name: `League ${id}`, + description: 'Test league', + ownerId: 'owner-1', + }), + ); + + return { + findById: async (id: string) => + leagues.find((league) => league.id.toString() === id) ?? null, + findAll: async () => leagues, + findByOwnerId: async (ownerId: string) => + leagues.filter((league) => getUnknownString((league as unknown as { ownerId: unknown }).ownerId) === ownerId), + create: async (league: League) => league, + update: async (league: League) => league, + delete: async () => undefined, + exists: async (id: string) => leagues.some((league) => league.id.toString() === id), + searchByName: async () => [], + }; } function createLeagueConfigFormModel(overrides?: Partial): LeagueConfigFormModel { @@ -100,9 +137,7 @@ describe('CreateSeasonForLeagueUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo, output); @@ -150,7 +185,9 @@ describe('CreateSeasonForLeagueUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const payload = (output.present as ReturnType).mock.calls[0][0] as CreateSeasonForLeagueResult; + const payloadRaw = (output.present as ReturnType).mock.calls[0]?.[0]; + expect(payloadRaw).toBeDefined(); + const payload = payloadRaw as CreateSeasonForLeagueResult; const season = payload.season; expect(season.leagueId).toBe('league-1'); @@ -199,7 +236,9 @@ describe('CreateSeasonForLeagueUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const payload = (output.present as ReturnType).mock.calls[0][0] as CreateSeasonForLeagueResult; + const payloadRaw = (output.present as ReturnType).mock.calls[0]?.[0]; + expect(payloadRaw).toBeDefined(); + const payload = payloadRaw as CreateSeasonForLeagueResult; const season = payload.season; expect(season.id).not.toBe(sourceSeason.id); @@ -223,9 +262,7 @@ describe('CreateSeasonForLeagueUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo, output); @@ -257,9 +294,7 @@ describe('ListSeasonsForLeagueUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo, output); @@ -303,7 +338,9 @@ describe('ListSeasonsForLeagueUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const payload = (output.present as ReturnType).mock.calls[0][0] as ListSeasonsForLeagueResult; + const payloadRaw = (output.present as ReturnType).mock.calls[0]?.[0]; + expect(payloadRaw).toBeDefined(); + const payload = payloadRaw as ListSeasonsForLeagueResult; const league1Seasons = payload.seasons.filter((s) => s.leagueId === 'league-1'); expect(league1Seasons.map((s) => s.id).sort()).toEqual(['season-1', 'season-2']); @@ -319,9 +356,7 @@ describe('ListSeasonsForLeagueUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo, output); @@ -347,9 +382,7 @@ describe('GetSeasonDetailsUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new GetSeasonDetailsUseCase(leagueRepo, seasonRepo, output); @@ -378,7 +411,9 @@ describe('GetSeasonDetailsUseCase', () => { expect(result.unwrap()).toBeUndefined(); expect(output.present).toHaveBeenCalledTimes(1); - const payload = (output.present as ReturnType).mock.calls[0][0] as GetSeasonDetailsResult; + const payloadRaw = (output.present as ReturnType).mock.calls[0]?.[0]; + expect(payloadRaw).toBeDefined(); + const payload = payloadRaw as GetSeasonDetailsResult; expect(payload.season.id).toBe('season-1'); expect(payload.season.leagueId).toBe('league-1'); @@ -441,9 +476,7 @@ describe('ManageSeasonLifecycleUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output); @@ -476,7 +509,9 @@ describe('ManageSeasonLifecycleUseCase', () => { const activated = await useCase.execute(activateCommand); expect(activated.isOk()).toBe(true); - const activatePayload = (output.present as ReturnType).mock.calls[0][0] as ManageSeasonLifecycleResult; + const activatePayloadRaw = (output.present as ReturnType).mock.calls[0]?.[0]; + expect(activatePayloadRaw).toBeDefined(); + const activatePayload = activatePayloadRaw as ManageSeasonLifecycleResult; expect(activatePayload.season.status).toBe('active'); const completeCommand: ManageSeasonLifecycleCommand = { @@ -488,7 +523,9 @@ describe('ManageSeasonLifecycleUseCase', () => { const completed = await useCase.execute(completeCommand); expect(completed.isOk()).toBe(true); - const completePayload = (output.present as ReturnType).mock.calls[1][0] as ManageSeasonLifecycleResult; + const completePayloadRaw = (output.present as ReturnType).mock.calls[1]?.[0]; + expect(completePayloadRaw).toBeDefined(); + const completePayload = completePayloadRaw as ManageSeasonLifecycleResult; expect(completePayload.season.status).toBe('completed'); const archiveCommand: ManageSeasonLifecycleCommand = { @@ -500,7 +537,9 @@ describe('ManageSeasonLifecycleUseCase', () => { const archived = await useCase.execute(archiveCommand); expect(archived.isOk()).toBe(true); - const archivePayload = (output.present as ReturnType).mock.calls[2][0] as ManageSeasonLifecycleResult; + const archivePayloadRaw = (output.present as ReturnType).mock.calls[2]?.[0]; + expect(archivePayloadRaw).toBeDefined(); + const archivePayload = archivePayloadRaw as ManageSeasonLifecycleResult; expect(archivePayload.season.status).toBe('archived'); expect(currentSeason.status).toBe('archived'); @@ -544,9 +583,7 @@ describe('ManageSeasonLifecycleUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output); @@ -575,9 +612,7 @@ describe('ManageSeasonLifecycleUseCase', () => { listActiveByLeague: vi.fn(), } as unknown as ISeasonRepository; - const output: UseCaseOutputPort & { present: ReturnType } = { - present: vi.fn(), - } as any; + const output = createOutputPort(); const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output); diff --git a/core/racing/application/use-cases/SeasonUseCases.ts b/core/racing/application/use-cases/SeasonUseCases.ts index 0e5664b14..f3c54b283 100644 --- a/core/racing/application/use-cases/SeasonUseCases.ts +++ b/core/racing/application/use-cases/SeasonUseCases.ts @@ -190,7 +190,7 @@ export class CreateSeasonForLeagueUseCase { const season = Season.create({ id: seasonId, - leagueId: league.id, + leagueId: league.id.toString(), gameId: command.gameId, name: command.name, year: new Date().getFullYear(), @@ -235,31 +235,52 @@ export class CreateSeasonForLeagueUseCase { maxDrivers?: number; } { const schedule = this.buildScheduleFromTimings(config); + + const scoring = config.scoring ?? {}; const scoringConfig = new SeasonScoringConfig({ - scoringPresetId: config.scoring.patternId ?? 'custom', - customScoringEnabled: config.scoring.customScoringEnabled ?? false, + scoringPresetId: scoring.patternId ?? 'custom', + customScoringEnabled: scoring.customScoringEnabled ?? false, }); + + const dropPolicyInput = config.dropPolicy ?? {}; + const dropStrategy = dropPolicyInput.strategy; const dropPolicy = new SeasonDropPolicy({ - strategy: config.dropPolicy.strategy, - ...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}), + strategy: + dropStrategy === 'none' || + dropStrategy === 'bestNResults' || + dropStrategy === 'dropWorstN' + ? dropStrategy + : 'none', + ...(dropPolicyInput.n !== undefined ? { n: dropPolicyInput.n } : {}), }); + + const stewardingInput = config.stewarding ?? {}; + const decisionMode = stewardingInput.decisionMode; const stewardingConfig = new SeasonStewardingConfig({ - decisionMode: config.stewarding.decisionMode, - ...(config.stewarding.requiredVotes !== undefined - ? { requiredVotes: config.stewarding.requiredVotes } + decisionMode: + decisionMode === 'admin_only' || + decisionMode === 'steward_decides' || + decisionMode === 'steward_vote' || + decisionMode === 'member_vote' || + decisionMode === 'steward_veto' || + decisionMode === 'member_veto' + ? decisionMode + : 'admin_only', + ...(stewardingInput.requiredVotes !== undefined + ? { requiredVotes: stewardingInput.requiredVotes } : {}), - requireDefense: config.stewarding.requireDefense, - defenseTimeLimit: config.stewarding.defenseTimeLimit, - voteTimeLimit: config.stewarding.voteTimeLimit, - protestDeadlineHours: config.stewarding.protestDeadlineHours, - stewardingClosesHours: config.stewarding.stewardingClosesHours, - notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest, - notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired, + requireDefense: stewardingInput.requireDefense ?? false, + defenseTimeLimit: stewardingInput.defenseTimeLimit ?? 48, + voteTimeLimit: stewardingInput.voteTimeLimit ?? 72, + protestDeadlineHours: stewardingInput.protestDeadlineHours ?? 48, + stewardingClosesHours: stewardingInput.stewardingClosesHours ?? 168, + notifyAccusedOnProtest: stewardingInput.notifyAccusedOnProtest ?? true, + notifyOnVoteRequired: stewardingInput.notifyOnVoteRequired ?? true, }); const structure = config.structure; const maxDrivers = - typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0 + typeof structure?.maxDrivers === 'number' && structure.maxDrivers > 0 ? structure.maxDrivers : undefined; @@ -275,44 +296,50 @@ export class CreateSeasonForLeagueUseCase { private buildScheduleFromTimings( config: LeagueConfigFormModel, ): SeasonSchedule | undefined { - const { timings } = config; - if (!timings.seasonStartDate || !timings.raceStartTime) { + const timings = config.timings; + if (!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 = new LeagueTimezone(timezoneId); + const timezone = LeagueTimezone.create(timings.timezoneId ?? 'UTC'); const plannedRounds = typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0 ? timings.roundsPlanned - : timings.sessionCount; + : timings.sessionCount ?? 0; + + if (!Number.isInteger(plannedRounds) || plannedRounds <= 0) { + return undefined; + } + + const weekdaysRaw = timings.weekdays ?? []; + const weekdays = WeekdaySet.fromArray( + weekdaysRaw + .filter((d): d is Weekday => (['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const).includes(d as Weekday)) + .slice(0), + ); + + const safeWeekdays = weekdays.getAll().length > 0 ? weekdays : WeekdaySet.fromArray(['Mon']); 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, + safeWeekdays, ); case 'monthlyNthWeekday': { - const pattern = new MonthlyRecurrencePattern({ - ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4, - weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday, - }); + 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 RecurrenceStrategyFactory.weekly(safeWeekdays); } })(); @@ -350,7 +377,7 @@ export class ListSeasonsForLeagueUseCase { }); } - const seasons = await this.seasonRepository.listByLeague(league.id); + const seasons = await this.seasonRepository.listByLeague(league.id.toString()); this.output.present({ seasons }); @@ -391,7 +418,7 @@ export class GetSeasonDetailsUseCase { } const season = await this.seasonRepository.findById(query.seasonId); - if (!season || season.leagueId !== league.id) { + if (!season || season.leagueId !== league.id.toString()) { return Result.err({ code: 'SEASON_NOT_FOUND', details: { @@ -439,7 +466,7 @@ export class ManageSeasonLifecycleUseCase { } const season = await this.seasonRepository.findById(command.seasonId); - if (!season || season.leagueId !== league.id) { + if (!season || season.leagueId !== league.id.toString()) { return Result.err({ code: 'SEASON_NOT_FOUND', details: { diff --git a/core/racing/application/use-cases/SendFinalResultsUseCase.test.ts b/core/racing/application/use-cases/SendFinalResultsUseCase.test.ts index b3e84f771..7bc37d28a 100644 --- a/core/racing/application/use-cases/SendFinalResultsUseCase.test.ts +++ b/core/racing/application/use-cases/SendFinalResultsUseCase.test.ts @@ -110,7 +110,9 @@ describe('SendFinalResultsUseCase', () => { expect(notificationService.sendNotification).toHaveBeenCalledTimes(2); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as SendFinalResultsResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as SendFinalResultsResult; expect(presented).toEqual({ leagueId: 'league-1', raceId: 'race-1', diff --git a/core/racing/application/use-cases/SendFinalResultsUseCase.ts b/core/racing/application/use-cases/SendFinalResultsUseCase.ts index a50776f27..e34fdf14a 100644 --- a/core/racing/application/use-cases/SendFinalResultsUseCase.ts +++ b/core/racing/application/use-cases/SendFinalResultsUseCase.ts @@ -10,6 +10,8 @@ import type { IResultRepository } from '../../domain/repositories/IResultReposit import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import { isLeagueStewardOrHigherRole } from '../../domain/types/LeagueRoles'; +import { Position } from '../../domain/entities/result/Position'; +import { IncidentCount } from '../../domain/entities/result/IncidentCount'; export type SendFinalResultsInput = { leagueId: string; @@ -61,8 +63,11 @@ export class SendFinalResultsUseCase { return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race event not found' } }); } - const membership = await this.membershipRepository.getMembership(league.id.toString(), input.triggeredById); - if (!membership || !isLeagueStewardOrHigherRole(membership.role)) { + const membership = await this.membershipRepository.getMembership( + league.id.toString(), + input.triggeredById, + ); + if (!membership || !isLeagueStewardOrHigherRole(membership.role.toString())) { return Result.err({ code: 'INSUFFICIENT_PERMISSIONS', details: { message: 'Insufficient permissions to send final results' }, @@ -133,10 +138,12 @@ export class SendFinalResultsUseCase { ); const title = `Final Results: ${raceEvent.name}`; + const positionValue = position instanceof Position ? position.toNumber() : position; + const incidentValue = incidents instanceof IncidentCount ? incidents.toNumber() : incidents; const body = this.buildFinalResultsBody( - position, + positionValue, positionChange, - incidents, + incidentValue, finalRatingChange, hadPenaltiesApplied, ); @@ -152,9 +159,9 @@ export class SendFinalResultsUseCase { raceEventId: raceEvent.id, sessionId: raceEvent.getMainRaceSession()?.id ?? '', leagueId, - position, + position: positionValue, positionChange, - incidents, + incidents: incidentValue, finalRatingChange, hadPenaltiesApplied, }, diff --git a/core/racing/application/use-cases/SendPerformanceSummaryUseCase.test.ts b/core/racing/application/use-cases/SendPerformanceSummaryUseCase.test.ts index 83c140aa8..500ecf74e 100644 --- a/core/racing/application/use-cases/SendPerformanceSummaryUseCase.test.ts +++ b/core/racing/application/use-cases/SendPerformanceSummaryUseCase.test.ts @@ -117,7 +117,9 @@ describe('SendPerformanceSummaryUseCase', () => { ); expect(output.present).toHaveBeenCalledTimes(1); - const presented = output.present.mock.calls[0][0] as SendPerformanceSummaryResult; + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + const presented = presentedRaw as SendPerformanceSummaryResult; expect(presented).toEqual({ leagueId: 'league-1', raceId: 'race-1', diff --git a/core/racing/application/use-cases/SendPerformanceSummaryUseCase.ts b/core/racing/application/use-cases/SendPerformanceSummaryUseCase.ts index e2426136f..a7bd3a0b8 100644 --- a/core/racing/application/use-cases/SendPerformanceSummaryUseCase.ts +++ b/core/racing/application/use-cases/SendPerformanceSummaryUseCase.ts @@ -74,8 +74,11 @@ export class SendPerformanceSummaryUseCase { } if (input.triggeredById !== input.driverId) { - const membership = await this.membershipRepository.getMembership(league.id.toString(), input.triggeredById); - if (!membership || !isLeagueStewardOrHigherRole(membership.role)) { + const membership = await this.membershipRepository.getMembership( + league.id.toString(), + input.triggeredById, + ); + if (!membership || !isLeagueStewardOrHigherRole(membership.role.toString())) { return Result.err({ code: 'INSUFFICIENT_PERMISSIONS', details: { message: 'Insufficient permissions to send performance summary' }, diff --git a/core/racing/application/use-cases/UpdateDriverProfileUseCase.test.ts b/core/racing/application/use-cases/UpdateDriverProfileUseCase.test.ts index 52e459dbe..2ddd61a73 100644 --- a/core/racing/application/use-cases/UpdateDriverProfileUseCase.test.ts +++ b/core/racing/application/use-cases/UpdateDriverProfileUseCase.test.ts @@ -35,7 +35,7 @@ describe('UpdateDriverProfileUseCase', () => { error: vi.fn(), } as unknown as Logger & { error: ReturnType }; - useCase = new UpdateDriverProfileUseCase(driverRepository, output, logger); + useCase = new UpdateDriverProfileUseCase(driverRepository, logger, output); }); it('updates driver profile successfully', async () => { @@ -66,7 +66,9 @@ describe('UpdateDriverProfileUseCase', () => { expect(driverRepository.update).toHaveBeenCalled(); expect(output.present).toHaveBeenCalledTimes(1); - expect(output.present).toHaveBeenCalledWith({ driverId: 'driver-1' }); + const presentedRaw = output.present.mock.calls[0]?.[0]; + expect(presentedRaw).toBeDefined(); + expect(presentedRaw).toEqual({ id: 'driver-1' }); }); it('returns error when driver not found', async () => { diff --git a/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts b/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts index 04943051d..2984f5489 100644 --- a/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts +++ b/core/racing/application/use-cases/UpdateDriverProfileUseCase.ts @@ -1,3 +1,4 @@ +import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort'; import { Result } from '@core/shared/application/Result'; import type { UseCase } from '@core/shared/application'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; @@ -23,15 +24,20 @@ export type UpdateDriverProfileErrorCode = * Encapsulates domain entity mutation. Mapping to DTOs is handled by presenters * in the presentation layer through the output port. */ -export class UpdateDriverProfileUseCase implements UseCase { +export class UpdateDriverProfileUseCase + implements UseCase +{ constructor( private readonly driverRepository: IDriverRepository, private readonly logger: Logger, + private readonly output: UseCaseOutputPort, ) {} async execute( input: UpdateDriverProfileInput, - ): Promise>> { + ): Promise< + Result> + > { const { driverId, bio, country } = input; if ((bio !== undefined && bio.trim().length === 0) || (country !== undefined && country.trim().length === 0)) { @@ -61,7 +67,9 @@ export class UpdateDriverProfileUseCase implements UseCase { - let raceRepository: IRaceRepository; - let registrationRepository: IRaceRegistrationRepository; + let raceRepository: { findById: ReturnType }; + let registrationRepository: { isRegistered: ReturnType; withdraw: ReturnType }; let logger: Logger; let output: UseCaseOutputPort & { present: ReturnType }; beforeEach(() => { raceRepository = { findById: vi.fn(), - } as unknown as IRaceRepository; + }; registrationRepository = { isRegistered: vi.fn(), withdraw: vi.fn(), - } as unknown as IRaceRegistrationRepository; + }; logger = { debug: vi.fn(), @@ -35,21 +34,30 @@ describe('WithdrawFromRaceUseCase', () => { error: vi.fn(), }; - output = { present: vi.fn() } as any; + output = { present: vi.fn() } as unknown as UseCaseOutputPort & { + present: ReturnType; + }; }); const createUseCase = () => - new WithdrawFromRaceUseCase(raceRepository, registrationRepository, logger, output); + new WithdrawFromRaceUseCase( + raceRepository as unknown as IRaceRepository, + registrationRepository as unknown as IRaceRegistrationRepository, + logger, + output, + ); it('withdraws from race successfully', async () => { const race = { id: 'race-1', isUpcoming: vi.fn().mockReturnValue(true), - } as any; + }; - (raceRepository.findById as any).mockResolvedValue(race); - (registrationRepository.isRegistered as any).mockResolvedValue(true); - (registrationRepository.withdraw as any).mockResolvedValue(undefined); + raceRepository.findById.mockResolvedValue( + race as unknown as Awaited>, + ); + registrationRepository.isRegistered.mockResolvedValue(true); + registrationRepository.withdraw.mockResolvedValue(undefined); const useCase = createUseCase(); @@ -72,7 +80,7 @@ describe('WithdrawFromRaceUseCase', () => { }); it('returns error when race is not found', async () => { - (raceRepository.findById as any).mockResolvedValue(null); + raceRepository.findById.mockResolvedValue(null); const useCase = createUseCase(); @@ -94,10 +102,12 @@ describe('WithdrawFromRaceUseCase', () => { const race = { id: 'race-1', isUpcoming: vi.fn().mockReturnValue(true), - } as any; + }; - (raceRepository.findById as any).mockResolvedValue(race); - (registrationRepository.isRegistered as any).mockResolvedValue(false); + raceRepository.findById.mockResolvedValue( + race as unknown as Awaited>, + ); + registrationRepository.isRegistered.mockResolvedValue(false); const useCase = createUseCase(); @@ -119,10 +129,12 @@ describe('WithdrawFromRaceUseCase', () => { const race = { id: 'race-1', isUpcoming: vi.fn().mockReturnValue(false), - } as any; + }; - (raceRepository.findById as any).mockResolvedValue(race); - (registrationRepository.isRegistered as any).mockResolvedValue(true); + raceRepository.findById.mockResolvedValue( + race as unknown as Awaited>, + ); + registrationRepository.isRegistered.mockResolvedValue(true); const useCase = createUseCase(); @@ -144,11 +156,13 @@ describe('WithdrawFromRaceUseCase', () => { const race = { id: 'race-1', isUpcoming: vi.fn().mockReturnValue(true), - } as any; + }; - (raceRepository.findById as any).mockResolvedValue(race); - (registrationRepository.isRegistered as any).mockResolvedValue(true); - (registrationRepository.withdraw as any).mockRejectedValue(new Error('DB failure')); + raceRepository.findById.mockResolvedValue( + race as unknown as Awaited>, + ); + registrationRepository.isRegistered.mockResolvedValue(true); + registrationRepository.withdraw.mockRejectedValue(new Error('DB failure')); const useCase = createUseCase(); diff --git a/core/racing/application/utils/RaceResultGenerator.ts b/core/racing/application/utils/RaceResultGenerator.ts index b42c56bd0..fe900b121 100644 --- a/core/racing/application/utils/RaceResultGenerator.ts +++ b/core/racing/application/utils/RaceResultGenerator.ts @@ -39,10 +39,11 @@ export class RaceResultGenerator { // Generate results const results: Result[] = []; - for (let i = 0; i < driverPerformances.length; i++) { - const { driverId } = driverPerformances[i]; - const position = i + 1; - const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1; + for (const [index, performance] of driverPerformances.entries()) { + const driverId = performance.driverId; + const position = index + 1; + const startPosition = + qualiPerformances.findIndex(p => p.driverId === driverId) + 1; // Generate realistic lap times (90-120 seconds for a lap) const baseLapTime = 90000 + Math.random() * 30000; diff --git a/core/racing/application/utils/RaceResultGeneratorWithIncidents.ts b/core/racing/application/utils/RaceResultGeneratorWithIncidents.ts index e9d304bf3..aac16e443 100644 --- a/core/racing/application/utils/RaceResultGeneratorWithIncidents.ts +++ b/core/racing/application/utils/RaceResultGeneratorWithIncidents.ts @@ -40,10 +40,11 @@ export class RaceResultGeneratorWithIncidents { // Generate results const results: ResultWithIncidents[] = []; - for (let i = 0; i < driverPerformances.length; i++) { - const { driverId } = driverPerformances[i]; - const position = i + 1; - const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1; + for (const [index, performance] of driverPerformances.entries()) { + const driverId = performance.driverId; + const position = index + 1; + const startPosition = + qualiPerformances.findIndex(p => p.driverId === driverId) + 1; // Generate realistic lap times (90-120 seconds for a lap) const baseLapTime = 90000 + Math.random() * 30000; @@ -118,7 +119,7 @@ export class RaceResultGeneratorWithIncidents { /** * Select appropriate incident type based on context */ - private static selectIncidentType(position: number, totalDrivers: number, incidentIndex: number): IncidentType { + private static selectIncidentType(position: number, totalDrivers: number, _incidentIndex: number): IncidentType { // Different incident types have different probabilities const incidentProbabilities: Array<{ type: IncidentType; weight: number }> = [ { type: 'track_limits', weight: 40 }, // Most common @@ -153,9 +154,8 @@ export class RaceResultGeneratorWithIncidents { /** * Select appropriate lap for incident */ - private static selectIncidentLap(incidentNumber: number, totalIncidents: number): number { + private static selectIncidentLap(incidentNumber: number, _totalIncidents: number): number { // Spread incidents throughout the race - const raceLaps = 20; // Assume 20 lap race const lapRanges = [ { min: 1, max: 5 }, // Early race { min: 6, max: 12 }, // Mid race @@ -164,8 +164,7 @@ export class RaceResultGeneratorWithIncidents { // Distribute incidents across race phases const phaseIndex = Math.min(incidentNumber - 1, lapRanges.length - 1); - const range = lapRanges[phaseIndex]; - + const range = lapRanges[phaseIndex] ?? lapRanges[0]!; return Math.floor(Math.random() * (range.max - range.min + 1)) + range.min; } @@ -228,8 +227,9 @@ export class RaceResultGeneratorWithIncidents { ], }; - const options = descriptions[type] || descriptions.other; - return options[Math.floor(Math.random() * options.length)]; + const options = descriptions[type] ?? descriptions.other; + const selected = options[Math.floor(Math.random() * options.length)]; + return selected ?? descriptions.other[0]!; } /** diff --git a/core/racing/domain/entities/DriverLivery.test.ts b/core/racing/domain/entities/DriverLivery.test.ts index fabaf978b..0328c50b3 100644 --- a/core/racing/domain/entities/DriverLivery.test.ts +++ b/core/racing/domain/entities/DriverLivery.test.ts @@ -156,7 +156,7 @@ describe('DriverLivery', () => { const updatedLivery = livery.setLeagueOverride('league-1', 'season-1', 'decal-1', 0.5, 0.5); expect(updatedLivery.leagueOverrides).toHaveLength(1); - expect(updatedLivery.leagueOverrides[0].leagueId).toBe('league-1'); + expect(updatedLivery.leagueOverrides[0]!.leagueId).toBe('league-1'); }); it('should update existing override', () => { @@ -172,7 +172,7 @@ describe('DriverLivery', () => { const updatedLivery = livery.setLeagueOverride('league-1', 'season-1', 'decal-1', 0.6, 0.6); expect(updatedLivery.leagueOverrides).toHaveLength(1); - expect(updatedLivery.leagueOverrides[0].newX).toBe(0.6); + expect(updatedLivery.leagueOverrides[0]!.newX).toBe(0.6); }); }); diff --git a/core/racing/domain/entities/Penalty.ts b/core/racing/domain/entities/Penalty.ts index b3a24ca06..2abfddd11 100644 --- a/core/racing/domain/entities/Penalty.ts +++ b/core/racing/domain/entities/Penalty.ts @@ -1,192 +1,14 @@ /** - * Domain Entity: Penalty + * Compatibility re-export. * - * Represents a penalty applied to a driver for an incident during a race. - * Penalties can be applied as a result of an upheld protest or directly by stewards. + * `Penalty` moved to `entities/penalty/Penalty` but some code still imports + * from `entities/Penalty`. Re-exporting avoids having two distinct classes + * (which breaks assignability due to private fields). */ -import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; -import type { IEntity } from '@core/shared/domain'; -import { PenaltyId } from './penalty/PenaltyId'; -import { LeagueId } from './LeagueId'; -import { RaceId } from './RaceId'; -import { DriverId } from './DriverId'; -import { PenaltyType } from './penalty/PenaltyType'; -import { PenaltyValue } from './penalty/PenaltyValue'; -import { PenaltyReason } from './penalty/PenaltyReason'; -import { ProtestId } from './ProtestId'; -import { StewardId } from './StewardId'; -import { PenaltyStatus } from './penalty/PenaltyStatus'; -import { IssuedAt } from './IssuedAt'; -import { AppliedAt } from './AppliedAt'; -import { PenaltyNotes } from './penalty/PenaltyNotes'; +export { Penalty } from './penalty/Penalty'; +export type { PenaltyProps } from './penalty/Penalty'; -export interface PenaltyProps { - id: PenaltyId; - leagueId: LeagueId; - raceId: RaceId; - /** The driver receiving the penalty */ - driverId: DriverId; - /** Type of penalty */ - type: PenaltyType; - /** Value depends on type: seconds for time_penalty, positions for grid_penalty, points for points_deduction */ - value?: PenaltyValue; - /** Reason for the penalty */ - reason: PenaltyReason; - /** ID of the protest that led to this penalty (if applicable) */ - protestId?: ProtestId; - /** ID of the steward who issued the penalty */ - issuedBy: StewardId; - /** Current status of the penalty */ - status: PenaltyStatus; - /** Timestamp when the penalty was issued */ - issuedAt: IssuedAt; - /** Timestamp when the penalty was applied to results */ - appliedAt?: AppliedAt; - /** Notes about the penalty application */ - notes?: PenaltyNotes; -} - -export class Penalty implements IEntity { - private constructor(private readonly props: PenaltyProps) {} - - static create(props: { - id: string; - leagueId: string; - raceId: string; - driverId: string; - type: string; - value?: number; - reason: string; - protestId?: string; - issuedBy: string; - status?: string; - issuedAt?: Date; - appliedAt?: Date; - notes?: string; - }): Penalty { - if (!props.id) throw new RacingDomainValidationError('Penalty ID is required'); - if (!props.leagueId) throw new RacingDomainValidationError('League ID is required'); - if (!props.raceId) throw new RacingDomainValidationError('Race ID is required'); - if (!props.driverId) throw new RacingDomainValidationError('Driver ID is required'); - if (!props.type) throw new RacingDomainValidationError('Penalty type is required'); - if (!props.reason?.trim()) throw new RacingDomainValidationError('Penalty reason is required'); - if (!props.issuedBy) throw new RacingDomainValidationError('Penalty must be issued by a steward'); - - // Validate value based on type - if (['time_penalty', 'grid_penalty', 'points_deduction', 'license_points', 'fine', 'race_ban'].includes(props.type)) { - if (props.value === undefined || props.value <= 0) { - throw new RacingDomainValidationError(`${props.type} requires a positive value`); - } - } - - const penaltyProps: PenaltyProps = { - id: PenaltyId.create(props.id), - leagueId: LeagueId.create(props.leagueId), - raceId: RaceId.create(props.raceId), - driverId: DriverId.create(props.driverId), - type: PenaltyType.create(props.type), - reason: PenaltyReason.create(props.reason), - issuedBy: StewardId.create(props.issuedBy), - status: PenaltyStatus.create(props.status || 'pending'), - issuedAt: IssuedAt.create(props.issuedAt || new Date()), - ...(props.value !== undefined && { value: PenaltyValue.create(props.value) }), - ...(props.protestId !== undefined && { protestId: ProtestId.create(props.protestId) }), - ...(props.appliedAt !== undefined && { appliedAt: AppliedAt.create(props.appliedAt) }), - ...(props.notes !== undefined && { notes: PenaltyNotes.create(props.notes) }), - }; - - return new Penalty(penaltyProps); - } - - get id(): string { return this.props.id.toString(); } - get leagueId(): string { return this.props.leagueId.toString(); } - get raceId(): string { return this.props.raceId.toString(); } - get driverId(): string { return this.props.driverId.toString(); } - get type(): string { return this.props.type.toString(); } - get value(): number | undefined { return this.props.value?.toNumber(); } - get reason(): string { return this.props.reason.toString(); } - get protestId(): string | undefined { return this.props.protestId?.toString(); } - get issuedBy(): string { return this.props.issuedBy.toString(); } - get status(): string { return this.props.status.toString(); } - get issuedAt(): Date { return this.props.issuedAt.toDate(); } - get appliedAt(): Date | undefined { return this.props.appliedAt?.toDate(); } - get notes(): string | undefined { return this.props.notes?.toString(); } - - isPending(): boolean { - return this.props.status.toString() === 'pending'; - } - - isApplied(): boolean { - return this.props.status.toString() === 'applied'; - } - - /** - * Mark penalty as applied (after recalculating results) - */ - markAsApplied(notes?: string): Penalty { - if (this.isApplied()) { - throw new RacingDomainInvariantError('Penalty is already applied'); - } - if (this.props.status.toString() === 'overturned') { - throw new RacingDomainInvariantError('Cannot apply an overturned penalty'); - } - const base: PenaltyProps = { - ...this.props, - status: PenaltyStatus.create('applied'), - appliedAt: AppliedAt.create(new Date()), - }; - - const next: PenaltyProps = - notes !== undefined ? { ...base, notes: PenaltyNotes.create(notes) } : base; - - return new Penalty(next); - } - - /** - * Overturn the penalty (e.g., after successful appeal) - */ - overturn(reason: string): Penalty { - if (this.props.status.toString() === 'overturned') { - throw new RacingDomainInvariantError('Penalty is already overturned'); - } - return new Penalty({ - ...this.props, - status: PenaltyStatus.create('overturned'), - notes: PenaltyNotes.create(reason), - }); - } - - /** - * Get a human-readable description of the penalty - */ - getDescription(): string { - switch (this.props.type.toString()) { - case 'time_penalty': - return `+${this.props.value?.toNumber()}s time penalty`; - case 'grid_penalty': - return `${this.props.value?.toNumber()} place grid penalty (next race)`; - case 'points_deduction': - return `${this.props.value?.toNumber()} championship points deducted`; - case 'disqualification': - return 'Disqualified from race'; - case 'warning': - return 'Official warning'; - case 'license_points': - return `${this.props.value?.toNumber()} license penalty points`; - case 'probation': - return 'Probationary period'; - case 'fine': - return `${this.props.value?.toNumber()} points fine`; - case 'race_ban': - return `${this.props.value?.toNumber()} race suspension`; - default: - return 'Penalty'; - } - } -} - -// Export types for external use export { PenaltyType } from './penalty/PenaltyType'; export { PenaltyStatus } from './penalty/PenaltyStatus'; export { PenaltyValue } from './penalty/PenaltyValue'; diff --git a/core/racing/domain/entities/SponsorshipRequest.test.ts b/core/racing/domain/entities/SponsorshipRequest.test.ts index cc1d7b711..f9f31e53d 100644 --- a/core/racing/domain/entities/SponsorshipRequest.test.ts +++ b/core/racing/domain/entities/SponsorshipRequest.test.ts @@ -1,4 +1,4 @@ -import { SponsorshipRequest, SponsorableEntityType, SponsorshipRequestProps } from './SponsorshipRequest'; +import { SponsorshipRequest, SponsorableEntityType } from './SponsorshipRequest'; import { SponsorshipTier } from './season/SeasonSponsorship'; import { Money } from '../value-objects/Money'; import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; diff --git a/core/racing/domain/entities/season/Season.test.ts b/core/racing/domain/entities/season/Season.test.ts index 29251c4ac..0790f7676 100644 --- a/core/racing/domain/entities/season/Season.test.ts +++ b/core/racing/domain/entities/season/Season.test.ts @@ -10,7 +10,10 @@ import { SeasonDropPolicy, } from '@core/racing/domain/value-objects/SeasonDropPolicy'; import { SeasonStewardingConfig } from '@core/racing/domain/value-objects/SeasonStewardingConfig'; -import { createMinimalSeason, createBaseSeason } from '../../../../../testing/factories/racing/SeasonFactory'; +import { + createMinimalSeason, + createBaseSeason, +} from '@core/testing/factories/racing/SeasonFactory'; describe('Season aggregate lifecycle', () => { diff --git a/core/racing/domain/services/EventScoringService.test.ts b/core/racing/domain/services/EventScoringService.test.ts index 56489cffe..1ec5f71c0 100644 --- a/core/racing/domain/services/EventScoringService.test.ts +++ b/core/racing/domain/services/EventScoringService.test.ts @@ -1,16 +1,9 @@ import { describe, it, expect } from 'vitest'; import { EventScoringService } from '@core/racing/domain/services/EventScoringService'; -import type { ParticipantRef } from '@core/racing/domain/types/ParticipantRef'; -import type { SessionType } from '@core/racing/domain/types/SessionType'; -import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; import type { BonusRule } from '@core/racing/domain/types/BonusRule'; -import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; import { Result } from '@core/racing/domain/entities/result/Result'; import type { Penalty } from '@core/racing/domain/entities/Penalty'; -import type { ChampionshipType } from '@core/racing/domain/types/ChampionshipType'; -import { makeDriverRef } from '../../../testing/factories/racing/DriverRefFactory'; -import { makePointsTable } from '../../../testing/factories/racing/PointsTableFactory'; import { makeChampionshipConfig } from '../../../testing/factories/racing/ChampionshipConfigFactory'; diff --git a/core/racing/domain/value-objects/Money.ts b/core/racing/domain/value-objects/Money.ts index 88aa80dbd..846bd4f62 100644 --- a/core/racing/domain/value-objects/Money.ts +++ b/core/racing/domain/value-objects/Money.ts @@ -3,11 +3,14 @@ * Represents a monetary amount with currency and platform fee calculation */ -import { RacingDomainValidationError } from '../errors/RacingDomainError'; import type { IValueObject } from '@core/shared/domain'; +import { RacingDomainValidationError } from '../errors/RacingDomainError'; export type Currency = 'USD' | 'EUR' | 'GBP'; +export const isCurrency = (value: string): value is Currency => + value === 'USD' || value === 'EUR' || value === 'GBP'; + export interface MoneyProps { amount: number; currency: Currency; @@ -34,6 +37,7 @@ export class Money implements IValueObject { return new Money(amount, currency); } + // TODO i dont think platform fee must be coupled /** * Calculate platform fee (10%) */ diff --git a/core/racing/domain/value-objects/RaceIncidents.ts b/core/racing/domain/value-objects/RaceIncidents.ts index f27cbf25b..eaedb44ea 100644 --- a/core/racing/domain/value-objects/RaceIncidents.ts +++ b/core/racing/domain/value-objects/RaceIncidents.ts @@ -90,7 +90,51 @@ export class RaceIncidents implements IValueObject { return this.incidents.length === 0; } - // Removed getSeverityScore, getSummary, and getIncidentTypeLabel to eliminate static data in core + /** + * Backwards-compatible helper for legacy "incident count" fields. + * Creates `count` placeholder incidents of type `other`. + */ + static fromLegacyIncidentsCount(count: number): RaceIncidents { + if (!Number.isFinite(count) || count <= 0) { + return new RaceIncidents(); + } + + const incidents: IncidentRecord[] = Array.from({ length: Math.floor(count) }, (_, index) => ({ + type: 'other', + lap: index + 1, + penaltyPoints: 0, + })); + + return new RaceIncidents(incidents); + } + + /** + * A coarse severity score for incidents. + * Kept intentionally data-light: derived only from `penaltyPoints`. + */ + getSeverityScore(): number { + return this.getTotalPenaltyPoints(); + } + + /** + * Human-readable summary without hardcoded incident labels. + */ + getSummary(): string { + const total = this.getTotalCount(); + if (total === 0) return 'Clean race'; + + const countsByType = new Map(); + for (const incident of this.incidents) { + countsByType.set(incident.type, (countsByType.get(incident.type) ?? 0) + 1); + } + + const typeSummary = Array.from(countsByType.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([type, n]) => `${type}:${n}`) + .join(', '); + + return typeSummary.length > 0 ? `${total} incidents (${typeSummary})` : `${total} incidents`; + } equals(other: IValueObject): boolean { const otherIncidents = other.props; diff --git a/core/racing/domain/value-objects/SessionType.test.ts b/core/racing/domain/value-objects/SessionType.test.ts index 5ce1c4b3b..f1b2e2ec0 100644 --- a/core/racing/domain/value-objects/SessionType.test.ts +++ b/core/racing/domain/value-objects/SessionType.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { SessionType } from './SessionType'; +import { SessionType, type SessionTypeValue } from './SessionType'; describe('SessionType', () => { it('should create session type', () => { @@ -9,8 +9,8 @@ describe('SessionType', () => { }); it('should throw for invalid session type', () => { - expect(() => new SessionType('invalid' as any)).toThrow(); - expect(() => new SessionType('' as any)).toThrow(); + expect(() => new SessionType('invalid' as unknown as SessionTypeValue)).toThrow(); + expect(() => new SessionType('' as unknown as SessionTypeValue)).toThrow(); }); it('should have static factory methods', () => { diff --git a/core/testing/factories/racing/DriverRefFactory.ts b/core/testing/factories/racing/DriverRefFactory.ts index cef3fbe14..f4e05c864 100644 --- a/core/testing/factories/racing/DriverRefFactory.ts +++ b/core/testing/factories/racing/DriverRefFactory.ts @@ -1,4 +1,3 @@ -import type { DriverId } from '../../../racing/domain/entities/DriverId'; import type { ParticipantRef } from '../../../racing/domain/types/ParticipantRef'; export function makeDriverRef(driverId: string): ParticipantRef { diff --git a/core/testing/factories/racing/SeasonFactory.ts b/core/testing/factories/racing/SeasonFactory.ts new file mode 100644 index 000000000..0437ccbcd --- /dev/null +++ b/core/testing/factories/racing/SeasonFactory.ts @@ -0,0 +1,23 @@ +import { Season } from '@core/racing/domain/entities/season/Season'; +import type { SeasonStatus } from '@core/racing/domain/entities/season/Season'; + +export const createMinimalSeason = (overrides?: { status?: SeasonStatus }) => + Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Test Season', + status: overrides?.status ?? 'planned', + }); + +export const createBaseSeason = () => + Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Config Season', + status: 'planned', + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: undefined, + maxDrivers: 24, + }); \ No newline at end of file