diff --git a/apps/website/lib/api/analytics/AnalyticsApiClient.ts b/apps/website/lib/api/analytics/AnalyticsApiClient.ts index 9de69a6c8..41fdd02d3 100644 --- a/apps/website/lib/api/analytics/AnalyticsApiClient.ts +++ b/apps/website/lib/api/analytics/AnalyticsApiClient.ts @@ -1,12 +1,24 @@ import { BaseApiClient } from '../base/BaseApiClient'; -import type { - RecordPageViewInputDto, - RecordPageViewOutputDto, - RecordEngagementInputDto, - RecordEngagementOutputDto, - AnalyticsDashboardDto, - AnalyticsMetricsDto, -} from '../../dtos'; +import { RecordPageViewOutputDTO } from '../../types/generated/RecordPageViewOutputDTO'; +import { RecordEngagementOutputDTO } from '../../types/generated/RecordEngagementOutputDTO'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type RecordPageViewInputDto = { path: string; userId?: string }; +type RecordEngagementInputDto = { eventType: string; userId?: string; metadata?: Record }; + +// TODO: Move these types to apps/website/lib/types/generated when available +type AnalyticsDashboardDto = { + totalUsers: number; + activeUsers: number; + totalRaces: number; + totalLeagues: number; +}; +type AnalyticsMetricsDto = { + pageViews: number; + uniqueVisitors: number; + averageSessionDuration: number; + bounceRate: number; +}; /** * Analytics API Client @@ -15,13 +27,13 @@ import type { */ export class AnalyticsApiClient extends BaseApiClient { /** Record a page view */ - recordPageView(input: RecordPageViewInputDto): Promise { - return this.post('/analytics/page-view', input); + recordPageView(input: RecordPageViewInputDto): Promise { + return this.post('/analytics/page-view', input); } /** Record an engagement event */ - recordEngagement(input: RecordEngagementInputDto): Promise { - return this.post('/analytics/engagement', input); + recordEngagement(input: RecordEngagementInputDto): Promise { + return this.post('/analytics/engagement', input); } /** Get analytics dashboard data */ diff --git a/apps/website/lib/dtos/AllLeaguesWithCapacityDto.ts b/apps/website/lib/dtos/AllLeaguesWithCapacityDto.ts deleted file mode 100644 index 9102a4c43..000000000 --- a/apps/website/lib/dtos/AllLeaguesWithCapacityDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { LeagueSummaryDto } from './LeagueSummaryDto'; - -/** - * All leagues with capacity transport object - * Contains a list of leagues with their capacity information - */ -export interface AllLeaguesWithCapacityDto { - leagues: LeagueSummaryDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/AllRacesPageDto.ts b/apps/website/lib/dtos/AllRacesPageDto.ts deleted file mode 100644 index f1401ebd3..000000000 --- a/apps/website/lib/dtos/AllRacesPageDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { RaceListItemDto } from './RaceListItemDto'; - -/** - * All races page data transfer object - * List of all races for the races page - */ -export interface AllRacesPageDto { - races: RaceListItemDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/AllTeamsDto.ts b/apps/website/lib/dtos/AllTeamsDto.ts deleted file mode 100644 index 7e015f060..000000000 --- a/apps/website/lib/dtos/AllTeamsDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TeamSummaryDto } from './TeamSummaryDto'; - -/** - * All teams data transfer object - * List of all teams - */ -export interface AllTeamsDto { - teams: TeamSummaryDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/AnalyticsDashboardDto.ts b/apps/website/lib/dtos/AnalyticsDashboardDto.ts deleted file mode 100644 index 4cabc95f3..000000000 --- a/apps/website/lib/dtos/AnalyticsDashboardDto.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface AnalyticsDashboardDto { - totalUsers: number; - activeUsers: number; - totalRaces: number; - totalLeagues: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/AnalyticsMetricsDto.ts b/apps/website/lib/dtos/AnalyticsMetricsDto.ts deleted file mode 100644 index 50e2f301e..000000000 --- a/apps/website/lib/dtos/AnalyticsMetricsDto.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface AnalyticsMetricsDto { - pageViews: number; - uniqueVisitors: number; - averageSessionDuration: number; - bounceRate: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CompleteOnboardingInputDto.ts b/apps/website/lib/dtos/CompleteOnboardingInputDto.ts deleted file mode 100644 index 9ff384dc8..000000000 --- a/apps/website/lib/dtos/CompleteOnboardingInputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Complete onboarding input data transfer object - * Input for completing driver onboarding - */ -export interface CompleteOnboardingInputDto { - iracingId: string; - displayName: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CompleteOnboardingOutputDto.ts b/apps/website/lib/dtos/CompleteOnboardingOutputDto.ts deleted file mode 100644 index 9283755e4..000000000 --- a/apps/website/lib/dtos/CompleteOnboardingOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Complete onboarding output data transfer object - * Output from completing driver onboarding - */ -export interface CompleteOnboardingOutputDto { - driverId: string; - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateLeagueInputDto.ts b/apps/website/lib/dtos/CreateLeagueInputDto.ts deleted file mode 100644 index 4398f9fc9..000000000 --- a/apps/website/lib/dtos/CreateLeagueInputDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Create league input transport object - * Input data for creating a new league - */ -export interface CreateLeagueInputDto { - name: string; - description?: string; - isPublic: boolean; - maxMembers: number; - ownerId: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateLeagueOutputDto.ts b/apps/website/lib/dtos/CreateLeagueOutputDto.ts deleted file mode 100644 index 003ca14c0..000000000 --- a/apps/website/lib/dtos/CreateLeagueOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Create league output transport object - * Output data after creating a new league - */ -export interface CreateLeagueOutputDto { - leagueId: string; - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreatePaymentInputDto.ts b/apps/website/lib/dtos/CreatePaymentInputDto.ts deleted file mode 100644 index 66483a032..000000000 --- a/apps/website/lib/dtos/CreatePaymentInputDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Create payment input data transfer object - * Input for creating a payment - */ -export interface CreatePaymentInputDto { - amount: number; - currency: string; - leagueId: string; - driverId: string; - description?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreatePaymentOutputDto.ts b/apps/website/lib/dtos/CreatePaymentOutputDto.ts deleted file mode 100644 index 4ab8feabf..000000000 --- a/apps/website/lib/dtos/CreatePaymentOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Create payment output data transfer object - * Output from creating a payment - */ -export interface CreatePaymentOutputDto { - paymentId: string; - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateSponsorInputDto.ts b/apps/website/lib/dtos/CreateSponsorInputDto.ts deleted file mode 100644 index cbb0ea185..000000000 --- a/apps/website/lib/dtos/CreateSponsorInputDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Create sponsor input data transfer object - * Input for creating a new sponsor - */ -export interface CreateSponsorInputDto { - name: string; - logoUrl?: string; - websiteUrl?: string; - userId: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateSponsorOutputDto.ts b/apps/website/lib/dtos/CreateSponsorOutputDto.ts deleted file mode 100644 index 1a4b74234..000000000 --- a/apps/website/lib/dtos/CreateSponsorOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Create sponsor output data transfer object - * Output from creating a sponsor - */ -export interface CreateSponsorOutputDto { - sponsorId: string; - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateTeamInputDto.ts b/apps/website/lib/dtos/CreateTeamInputDto.ts deleted file mode 100644 index f887bd0cf..000000000 --- a/apps/website/lib/dtos/CreateTeamInputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Create team input data transfer object - * Input for creating a new team - */ -export interface CreateTeamInputDto { - name: string; - description?: string; - ownerId: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/CreateTeamOutputDto.ts b/apps/website/lib/dtos/CreateTeamOutputDto.ts deleted file mode 100644 index f437ad144..000000000 --- a/apps/website/lib/dtos/CreateTeamOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Create team output data transfer object - * Output from creating a team - */ -export interface CreateTeamOutputDto { - teamId: string; - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DeleteMediaOutputDto.ts b/apps/website/lib/dtos/DeleteMediaOutputDto.ts deleted file mode 100644 index fba43476f..000000000 --- a/apps/website/lib/dtos/DeleteMediaOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Delete media output data transfer object - * Output from deleting media - */ -export interface DeleteMediaOutputDto { - success: boolean; - error?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverDto.ts b/apps/website/lib/dtos/DriverDto.ts deleted file mode 100644 index 3e167cf1d..000000000 --- a/apps/website/lib/dtos/DriverDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Driver transport object - * Represents a driver as received from the API - */ -export interface DriverDto { - id: string; - name: string; - avatarUrl?: string; - iracingId?: string; - rating?: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverLeaderboardItemDto.ts b/apps/website/lib/dtos/DriverLeaderboardItemDto.ts deleted file mode 100644 index 78e786820..000000000 --- a/apps/website/lib/dtos/DriverLeaderboardItemDto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Driver leaderboard item data transfer object - * Represents a driver in the global leaderboard - */ -export interface DriverLeaderboardItemDto { - id: string; - name: string; - avatarUrl?: string; - rating: number; - wins: number; - races: number; - skillLevel: string; - isActive: boolean; - nationality: string; - podiums: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverRegistrationStatusDto.ts b/apps/website/lib/dtos/DriverRegistrationStatusDto.ts deleted file mode 100644 index 0d5faab49..000000000 --- a/apps/website/lib/dtos/DriverRegistrationStatusDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Driver registration status data transfer object - * Represents a driver's registration status for a race - */ -export interface DriverRegistrationStatusDto { - isRegistered: boolean; - raceId: string; - driverId: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverRowDto.ts b/apps/website/lib/dtos/DriverRowDto.ts deleted file mode 100644 index 8996b4e4f..000000000 --- a/apps/website/lib/dtos/DriverRowDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Driver row data transfer object - * Represents a driver in a table row - */ -export interface DriverRowDto { - id: string; - name: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverStatsDto.ts b/apps/website/lib/dtos/DriverStatsDto.ts deleted file mode 100644 index de1dbc72c..000000000 --- a/apps/website/lib/dtos/DriverStatsDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Driver stats data transfer object - * Global driver statistics - */ -export interface DriverStatsDto { - totalDrivers: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriverTeamDto.ts b/apps/website/lib/dtos/DriverTeamDto.ts deleted file mode 100644 index 4480520af..000000000 --- a/apps/website/lib/dtos/DriverTeamDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Driver team data transfer object - * Represents a driver's team membership - */ -export interface DriverTeamDto { - teamId: string; - teamName: string; - role: string; - joinedAt: Date; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/DriversLeaderboardDto.ts b/apps/website/lib/dtos/DriversLeaderboardDto.ts deleted file mode 100644 index 4f7e6d39b..000000000 --- a/apps/website/lib/dtos/DriversLeaderboardDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { DriverLeaderboardItemDto } from './DriverLeaderboardItemDto'; - -/** - * Drivers leaderboard data transfer object - * Contains the list of top drivers - */ -export interface DriversLeaderboardDto { - drivers: DriverLeaderboardItemDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetAvatarOutputDto.ts b/apps/website/lib/dtos/GetAvatarOutputDto.ts deleted file mode 100644 index ef5d25834..000000000 --- a/apps/website/lib/dtos/GetAvatarOutputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Get avatar output data transfer object - * Output from getting avatar information - */ -export interface GetAvatarOutputDto { - driverId: string; - avatarUrl?: string; - hasAvatar: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts b/apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts deleted file mode 100644 index 06f4671bf..000000000 --- a/apps/website/lib/dtos/GetEntitySponsorshipPricingResultDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Get entity sponsorship pricing result data transfer object - * Pricing information for sponsorship slots - */ -export interface GetEntitySponsorshipPricingResultDto { - mainSlotPrice: number; - secondarySlotPrice: number; - currency: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetMediaOutputDto.ts b/apps/website/lib/dtos/GetMediaOutputDto.ts deleted file mode 100644 index 1f00809d2..000000000 --- a/apps/website/lib/dtos/GetMediaOutputDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Get media output data transfer object - * Output from getting media information - */ -export interface GetMediaOutputDto { - id: string; - url: string; - type: 'image' | 'video' | 'document'; - category?: 'avatar' | 'team-logo' | 'league-cover' | 'race-result'; - uploadedAt: string; - size?: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetMembershipFeesOutputDto.ts b/apps/website/lib/dtos/GetMembershipFeesOutputDto.ts deleted file mode 100644 index 57f153850..000000000 --- a/apps/website/lib/dtos/GetMembershipFeesOutputDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { MembershipFeeDto } from './MembershipFeeDto'; -import type { MemberPaymentDto } from './MemberPaymentDto'; - -/** - * Get membership fees output data transfer object - * Output containing membership fees and payments - */ -export interface GetMembershipFeesOutputDto { - fees: MembershipFeeDto[]; - memberPayments: MemberPaymentDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetPaymentsOutputDto.ts b/apps/website/lib/dtos/GetPaymentsOutputDto.ts deleted file mode 100644 index 9df9d5d28..000000000 --- a/apps/website/lib/dtos/GetPaymentsOutputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PaymentDto } from './PaymentDto'; - -/** - * Get payments output data transfer object - * Output containing list of payments - */ -export interface GetPaymentsOutputDto { - payments: PaymentDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetPrizesOutputDto.ts b/apps/website/lib/dtos/GetPrizesOutputDto.ts deleted file mode 100644 index b8471e425..000000000 --- a/apps/website/lib/dtos/GetPrizesOutputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { PrizeDto } from './PrizeDto'; - -/** - * Get prizes output data transfer object - * Output containing list of prizes - */ -export interface GetPrizesOutputDto { - prizes: PrizeDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetSponsorsOutputDto.ts b/apps/website/lib/dtos/GetSponsorsOutputDto.ts deleted file mode 100644 index f5575efed..000000000 --- a/apps/website/lib/dtos/GetSponsorsOutputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { SponsorDto } from './SponsorDto'; - -/** - * Get sponsors output data transfer object - * Output containing list of sponsors - */ -export interface GetSponsorsOutputDto { - sponsors: SponsorDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/GetWalletOutputDto.ts b/apps/website/lib/dtos/GetWalletOutputDto.ts deleted file mode 100644 index 0d6364c22..000000000 --- a/apps/website/lib/dtos/GetWalletOutputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { WalletDto } from './WalletDto'; - -/** - * Get wallet output data transfer object - * Output containing wallet information - */ -export interface GetWalletOutputDto { - wallet: WalletDto; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/ImportRaceResultsInputDto.ts b/apps/website/lib/dtos/ImportRaceResultsInputDto.ts deleted file mode 100644 index 59eb9b949..000000000 --- a/apps/website/lib/dtos/ImportRaceResultsInputDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Import race results input data transfer object - * Input for importing race results - */ -export interface ImportRaceResultsInputDto { - resultsFileContent: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/ImportRaceResultsSummaryDto.ts b/apps/website/lib/dtos/ImportRaceResultsSummaryDto.ts deleted file mode 100644 index d0926f06e..000000000 --- a/apps/website/lib/dtos/ImportRaceResultsSummaryDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Import race results summary data transfer object - * Summary of race results import operation - */ -export interface ImportRaceResultsSummaryDto { - success: boolean; - raceId: string; - driversProcessed: number; - resultsRecorded: number; - errors?: string[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/ImportResultRowDto.ts b/apps/website/lib/dtos/ImportResultRowDto.ts deleted file mode 100644 index 7ab0d6213..000000000 --- a/apps/website/lib/dtos/ImportResultRowDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Import result row data transfer object - * Represents a row in imported race results - */ -export interface ImportResultRowDto { - id: string; - raceId: string; - driverId: string; - position: number; - fastestLap: number; - incidents: number; - startPosition: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueAdminDto.ts b/apps/website/lib/dtos/LeagueAdminDto.ts deleted file mode 100644 index b5452eb8d..000000000 --- a/apps/website/lib/dtos/LeagueAdminDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { LeagueMemberDto } from './LeagueMemberDto'; -import type { LeagueJoinRequestDto } from './LeagueJoinRequestDto'; -import type { LeagueConfigFormModelDto } from './LeagueConfigFormModelDto'; - -/** - * League admin transport object - * Contains all data needed for league administration - */ -export interface LeagueAdminDto { - config: LeagueConfigFormModelDto; - members: LeagueMemberDto[]; - joinRequests: LeagueJoinRequestDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueAdminPermissionsDto.ts b/apps/website/lib/dtos/LeagueAdminPermissionsDto.ts deleted file mode 100644 index 265335a0c..000000000 --- a/apps/website/lib/dtos/LeagueAdminPermissionsDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * League admin permissions transport object - * Defines the administrative permissions for a user in a league - */ -export interface LeagueAdminPermissionsDto { - canManageMembers: boolean; - canManageRaces: boolean; - canManageSettings: boolean; - canManageProtests: boolean; - isOwner: boolean; - isAdmin: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueAdminProtestsDto.ts b/apps/website/lib/dtos/LeagueAdminProtestsDto.ts deleted file mode 100644 index 0676be25b..000000000 --- a/apps/website/lib/dtos/LeagueAdminProtestsDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ProtestViewModel } from '../apiClient'; - -/** - * League admin protests transport object - * Contains protests for league administration - */ -export interface LeagueAdminProtestsDto { - protests: ProtestViewModel[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueConfigFormModelDto.ts b/apps/website/lib/dtos/LeagueConfigFormModelDto.ts deleted file mode 100644 index edc6cee07..000000000 --- a/apps/website/lib/dtos/LeagueConfigFormModelDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * League configuration form model transport object - * Used for league configuration forms - */ -export interface LeagueConfigFormModelDto { - id: string; - name: string; - description?: string; - isPublic: boolean; - maxMembers: number; - // Add other config fields as needed -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueJoinRequestDto.ts b/apps/website/lib/dtos/LeagueJoinRequestDto.ts deleted file mode 100644 index bdad28c51..000000000 --- a/apps/website/lib/dtos/LeagueJoinRequestDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * League join request transport object - * Represents a driver's request to join a league - */ -export interface LeagueJoinRequestDto { - id: string; - leagueId: string; - driverId: string; - requestedAt: Date; - message?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueMemberDto.ts b/apps/website/lib/dtos/LeagueMemberDto.ts deleted file mode 100644 index 92d0ef813..000000000 --- a/apps/website/lib/dtos/LeagueMemberDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { DriverDto } from './DriverDto'; - -/** - * League member data transfer object - * Represents a driver's membership in a league - */ -export interface LeagueMemberDto { - driverId: string; - driver?: DriverDto; - role: string; - joinedAt: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueMembershipsDto.ts b/apps/website/lib/dtos/LeagueMembershipsDto.ts deleted file mode 100644 index b5e9c4933..000000000 --- a/apps/website/lib/dtos/LeagueMembershipsDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { LeagueMemberViewModel } from '../apiClient'; - -/** - * League memberships transport object - * Contains the list of league members - */ -export interface LeagueMembershipsDto { - members: LeagueMemberViewModel[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueOwnerSummaryDto.ts b/apps/website/lib/dtos/LeagueOwnerSummaryDto.ts deleted file mode 100644 index 00a133002..000000000 --- a/apps/website/lib/dtos/LeagueOwnerSummaryDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * League owner summary transport object - * Summary information for league owners - */ -export interface LeagueOwnerSummaryDto { - leagueId: string; - leagueName: string; - memberCount: number; - pendingRequests: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueScheduleDto.ts b/apps/website/lib/dtos/LeagueScheduleDto.ts deleted file mode 100644 index a35ecdad3..000000000 --- a/apps/website/lib/dtos/LeagueScheduleDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ScheduledRaceViewModel } from '../apiClient'; - -/** - * League schedule transport object - * Contains the scheduled races for a league - */ -export interface LeagueScheduleDto { - races: ScheduledRaceViewModel[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueSeasonSummaryDto.ts b/apps/website/lib/dtos/LeagueSeasonSummaryDto.ts deleted file mode 100644 index 43cfa7765..000000000 --- a/apps/website/lib/dtos/LeagueSeasonSummaryDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * League season summary data transfer object - * Represents a season within a league - */ -export interface LeagueSeasonSummaryDto { - id: string; - name: string; - startDate?: string; - endDate?: string; - status: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueStandingsDto.ts b/apps/website/lib/dtos/LeagueStandingsDto.ts deleted file mode 100644 index 5db76dc61..000000000 --- a/apps/website/lib/dtos/LeagueStandingsDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { StandingEntryDto } from './StandingEntryDto'; -import type { DriverDto } from './DriverDto'; -import type { LeagueMembership } from './LeagueMembershipDto'; - -/** - * League standings transport object - * Contains the current league standings - */ -export interface LeagueStandingsDto { - standings: StandingEntryDto[]; - drivers: DriverDto[]; - memberships: LeagueMembership[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueStatsDto.ts b/apps/website/lib/dtos/LeagueStatsDto.ts deleted file mode 100644 index 5f54c1784..000000000 --- a/apps/website/lib/dtos/LeagueStatsDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * League stats DTO - * Contains statistical information about a league's races - */ -export interface LeagueStatsDto { - leagueId: string; - totalRaces: number; - completedRaces: number; - scheduledRaces: number; - averageSOF?: number; - highestSOF?: number; - lowestSOF?: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LeagueSummaryDto.ts b/apps/website/lib/dtos/LeagueSummaryDto.ts deleted file mode 100644 index 6672e8134..000000000 --- a/apps/website/lib/dtos/LeagueSummaryDto.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * League summary transport object - * Contains basic league information for list views - */ -export interface LeagueSummaryDto { - id: string; - name: string; - description?: string; - logoUrl?: string; - coverImage?: string; - memberCount: number; - maxMembers: number; - isPublic: boolean; - ownerId: string; - ownerName?: string; - scoringType?: string; - status?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/LoginParamsDto.ts b/apps/website/lib/dtos/LoginParamsDto.ts deleted file mode 100644 index a0d99eda9..000000000 --- a/apps/website/lib/dtos/LoginParamsDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Login parameters data transfer object - * Parameters for user login - */ -export interface LoginParamsDto { - email: string; - password: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/MemberPaymentDto.ts b/apps/website/lib/dtos/MemberPaymentDto.ts deleted file mode 100644 index 4fa850a18..000000000 --- a/apps/website/lib/dtos/MemberPaymentDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Member payment data transfer object - * Represents a payment made by a league member - */ -export interface MemberPaymentDto { - driverId: string; - amount: number; - paidAt: string; - status: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/MembershipFeeDto.ts b/apps/website/lib/dtos/MembershipFeeDto.ts deleted file mode 100644 index e6f3d31b2..000000000 --- a/apps/website/lib/dtos/MembershipFeeDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Membership fee data transfer object - * Represents a membership fee for a league - */ -export interface MembershipFeeDto { - leagueId: string; - amount: number; - currency: string; - period: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/PaymentDto.ts b/apps/website/lib/dtos/PaymentDto.ts deleted file mode 100644 index 8fe845948..000000000 --- a/apps/website/lib/dtos/PaymentDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Payment data transfer object - * Represents a payment transaction - */ -export interface PaymentDto { - id: string; - amount: number; - currency: string; - status: string; - createdAt: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/PenaltyDataDto.ts b/apps/website/lib/dtos/PenaltyDataDto.ts deleted file mode 100644 index 072565cee..000000000 --- a/apps/website/lib/dtos/PenaltyDataDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { PenaltyTypeDto } from './PenaltyTypeDto'; - -/** - * Penalty data structure - * Used when creating or updating penalties - */ -export interface PenaltyDataDto { - driverId: string; - type: PenaltyTypeDto; - value?: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/PenaltyTypeDto.ts b/apps/website/lib/dtos/PenaltyTypeDto.ts deleted file mode 100644 index b2a48c973..000000000 --- a/apps/website/lib/dtos/PenaltyTypeDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Penalty type enumeration - * Defines all possible penalty types in the system - */ -export type PenaltyTypeDto = - | 'time_penalty' - | 'grid_penalty' - | 'points_deduction' - | 'disqualification' - | 'warning' - | 'license_points'; \ No newline at end of file diff --git a/apps/website/lib/dtos/PrizeDto.ts b/apps/website/lib/dtos/PrizeDto.ts deleted file mode 100644 index 83e4c4082..000000000 --- a/apps/website/lib/dtos/PrizeDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Prize data transfer object - * Represents a prize in a league - */ -export interface PrizeDto { - id: string; - name: string; - amount: number; - currency: string; - position?: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/ProtestDto.ts b/apps/website/lib/dtos/ProtestDto.ts deleted file mode 100644 index 0f6457376..000000000 --- a/apps/website/lib/dtos/ProtestDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Protest data transfer object - * Represents a protest filed in a race - */ -export interface ProtestDto { - id: string; - raceId: string; - complainantId: string; - defendantId: string; - description: string; - status: string; - createdAt: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailDto.ts b/apps/website/lib/dtos/RaceDetailDto.ts deleted file mode 100644 index ca4d45dff..000000000 --- a/apps/website/lib/dtos/RaceDetailDto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RaceDetailRaceDto } from './RaceDetailRaceDto'; -import type { RaceDetailLeagueDto } from './RaceDetailLeagueDto'; -import type { RaceDetailEntryDto } from './RaceDetailEntryDto'; -import type { RaceDetailRegistrationDto } from './RaceDetailRegistrationDto'; -import type { RaceDetailUserResultDto } from './RaceDetailUserResultDto'; - -/** - * Race detail data transfer object - * Complete race details view - */ -export interface RaceDetailDto { - race: RaceDetailRaceDto | null; - league: RaceDetailLeagueDto | null; - entryList: RaceDetailEntryDto[]; - registration: RaceDetailRegistrationDto; - userResult: RaceDetailUserResultDto | null; - error?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailEntryDto.ts b/apps/website/lib/dtos/RaceDetailEntryDto.ts deleted file mode 100644 index 608cb27cc..000000000 --- a/apps/website/lib/dtos/RaceDetailEntryDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Race detail entry data transfer object - * Represents an entry in race details - */ -export interface RaceDetailEntryDto { - id: string; - name: string; - country: string; - avatarUrl: string; - rating: number | null; - isCurrentUser: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailLeagueDto.ts b/apps/website/lib/dtos/RaceDetailLeagueDto.ts deleted file mode 100644 index 4c7c22dff..000000000 --- a/apps/website/lib/dtos/RaceDetailLeagueDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Race detail league data transfer object - * League information in race details - */ -export interface RaceDetailLeagueDto { - id: string; - name: string; - description: string; - settings: { - maxDrivers?: number; - qualifyingFormat?: string; - }; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailRaceDto.ts b/apps/website/lib/dtos/RaceDetailRaceDto.ts deleted file mode 100644 index 5d490845a..000000000 --- a/apps/website/lib/dtos/RaceDetailRaceDto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Race detail race data transfer object - * Race information in race details - */ -export interface RaceDetailRaceDto { - id: string; - leagueId: string; - track: string; - car: string; - scheduledAt: string; - sessionType: string; - status: string; - strengthOfField: number | null; - registeredCount?: number; - maxParticipants?: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailRegistrationDto.ts b/apps/website/lib/dtos/RaceDetailRegistrationDto.ts deleted file mode 100644 index 7fbc8c0b7..000000000 --- a/apps/website/lib/dtos/RaceDetailRegistrationDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Race detail registration data transfer object - * Registration information in race details - */ -export interface RaceDetailRegistrationDto { - isUserRegistered: boolean; - canRegister: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceDetailUserResultDto.ts b/apps/website/lib/dtos/RaceDetailUserResultDto.ts deleted file mode 100644 index cb2218d7b..000000000 --- a/apps/website/lib/dtos/RaceDetailUserResultDto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Race detail user result data transfer object - * Represents the current user's result in race details - */ -export interface RaceDetailUserResultDto { - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; - ratingChange: number | null; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceListItemDto.ts b/apps/website/lib/dtos/RaceListItemDto.ts deleted file mode 100644 index 055d198c5..000000000 --- a/apps/website/lib/dtos/RaceListItemDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Race list item data transfer object - * Represents a race in list views - */ -export interface RaceListItemDto { - id: string; - name: string; - leagueId: string; - leagueName: string; - scheduledTime: string; - status: string; - trackName?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacePenaltiesDto.ts b/apps/website/lib/dtos/RacePenaltiesDto.ts deleted file mode 100644 index f9071389e..000000000 --- a/apps/website/lib/dtos/RacePenaltiesDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RacePenaltyDto } from './RacePenaltyDto'; - -/** - * Race penalties data transfer object - * List of penalties for a race - */ -export interface RacePenaltiesDto { - penalties: RacePenaltyDto[]; - driverMap: Record; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacePenaltyDto.ts b/apps/website/lib/dtos/RacePenaltyDto.ts deleted file mode 100644 index dd96e735b..000000000 --- a/apps/website/lib/dtos/RacePenaltyDto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Race penalty data transfer object - * Represents a penalty issued in a race - */ -export interface RacePenaltyDto { - id: string; - driverId: string; - type: string; - value: number; - reason: string; - issuedBy: string; - issuedAt: string; - notes?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceProtestDto.ts b/apps/website/lib/dtos/RaceProtestDto.ts deleted file mode 100644 index 248313fa2..000000000 --- a/apps/website/lib/dtos/RaceProtestDto.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Race protest data transfer object - * Represents a protest filed for a race - */ -export interface RaceProtestDto { - id: string; - protestingDriverId: string; - accusedDriverId: string; - incident: { - lap: number; - description: string; - }; - status: string; - filedAt: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceProtestsDto.ts b/apps/website/lib/dtos/RaceProtestsDto.ts deleted file mode 100644 index c771898d1..000000000 --- a/apps/website/lib/dtos/RaceProtestsDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RaceProtestDto } from './RaceProtestDto'; - -/** - * Race protests data transfer object - * List of protests for a race - */ -export interface RaceProtestsDto { - protests: RaceProtestDto[]; - driverMap: Record; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceResultDto.ts b/apps/website/lib/dtos/RaceResultDto.ts deleted file mode 100644 index 8ad550790..000000000 --- a/apps/website/lib/dtos/RaceResultDto.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Race result data transfer object - * Represents a driver's result in a race - */ -export interface RaceResultDto { - id: string; - raceId: string; - driverId: string; - driverName: string; - avatarUrl: string; - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceResultRowDto.ts b/apps/website/lib/dtos/RaceResultRowDto.ts deleted file mode 100644 index ce337bdd0..000000000 --- a/apps/website/lib/dtos/RaceResultRowDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Race result row data transfer object - * Represents a row in race results table - */ -export interface RaceResultRowDto { - id: string; - raceId: string; - driverId: string; - position: number; - fastestLap: number; - incidents: number; - startPosition: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceResultsDetailDto.ts b/apps/website/lib/dtos/RaceResultsDetailDto.ts deleted file mode 100644 index d3a44731a..000000000 --- a/apps/website/lib/dtos/RaceResultsDetailDto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RaceResultDto } from './RaceResultDto'; - -/** - * Race results detail data transfer object - * Detailed results for a race - */ -export interface RaceResultsDetailDto { - raceId: string; - track: string; - results: RaceResultDto[]; - league?: { id: string; name: string }; - race?: { id: string; track: string; scheduledAt: string }; - drivers: { id: string; name: string }[]; - pointsSystem: Record; - fastestLapTime: number; - penalties: { driverId: string; type: string; value?: number }[]; - currentDriverId: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceStatsDto.ts b/apps/website/lib/dtos/RaceStatsDto.ts deleted file mode 100644 index 5e3c7666a..000000000 --- a/apps/website/lib/dtos/RaceStatsDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Race stats data transfer object - * Global race statistics - */ -export interface RaceStatsDto { - totalRaces: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RaceWithSOFDto.ts b/apps/website/lib/dtos/RaceWithSOFDto.ts deleted file mode 100644 index aca4cf627..000000000 --- a/apps/website/lib/dtos/RaceWithSOFDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Race with strength of field data transfer object - * Race information including SOF - */ -export interface RaceWithSOFDto { - id: string; - track: string; - strengthOfField: number | null; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacesPageDataDto.ts b/apps/website/lib/dtos/RacesPageDataDto.ts deleted file mode 100644 index 5323a1a7d..000000000 --- a/apps/website/lib/dtos/RacesPageDataDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { RacesPageDataRaceDto } from './RacesPageDataRaceDto'; - -/** - * Races page data data transfer object - * Data for the races page - */ -export interface RacesPageDataDto { - races: RacesPageDataRaceDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RacesPageDataRaceDto.ts b/apps/website/lib/dtos/RacesPageDataRaceDto.ts deleted file mode 100644 index 0c3fc81df..000000000 --- a/apps/website/lib/dtos/RacesPageDataRaceDto.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Races page data race data transfer object - * Race information for the races page - */ -export interface RacesPageDataRaceDto { - id: string; - track: string; - car: string; - scheduledAt: string; - status: string; - leagueId: string; - leagueName: string; - strengthOfField: number | null; - isUpcoming: boolean; - isLive: boolean; - isPast: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordEngagementInputDto.ts b/apps/website/lib/dtos/RecordEngagementInputDto.ts deleted file mode 100644 index 5b12ea52a..000000000 --- a/apps/website/lib/dtos/RecordEngagementInputDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Record engagement input data transfer object - * Input for recording an engagement event - */ -export interface RecordEngagementInputDto { - eventType: string; - eventData?: Record; - userId?: string; - sessionId?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordEngagementOutputDto.ts b/apps/website/lib/dtos/RecordEngagementOutputDto.ts deleted file mode 100644 index 803e85d17..000000000 --- a/apps/website/lib/dtos/RecordEngagementOutputDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Record engagement output data transfer object - * Output from recording an engagement event - */ -export interface RecordEngagementOutputDto { - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordPageViewInputDto.ts b/apps/website/lib/dtos/RecordPageViewInputDto.ts deleted file mode 100644 index 3ad2e5d48..000000000 --- a/apps/website/lib/dtos/RecordPageViewInputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Record page view input data transfer object - * Input for recording a page view event - */ -export interface RecordPageViewInputDto { - path: string; - userId?: string; - sessionId?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RecordPageViewOutputDto.ts b/apps/website/lib/dtos/RecordPageViewOutputDto.ts deleted file mode 100644 index 4dfbacece..000000000 --- a/apps/website/lib/dtos/RecordPageViewOutputDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Record page view output data transfer object - * Output from recording a page view - */ -export interface RecordPageViewOutputDto { - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RegisterForRaceInputDto.ts b/apps/website/lib/dtos/RegisterForRaceInputDto.ts deleted file mode 100644 index fdd4bcd13..000000000 --- a/apps/website/lib/dtos/RegisterForRaceInputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Register for race input data transfer object - * Input for registering a driver for a race - */ -export interface RegisterForRaceInputDto { - leagueId: string; - driverId: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts b/apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts deleted file mode 100644 index 16fadc3b8..000000000 --- a/apps/website/lib/dtos/RequestAvatarGenerationInputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Request avatar generation input data transfer object - * Input for requesting avatar generation - */ -export interface RequestAvatarGenerationInputDto { - driverId: string; - style?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts b/apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts deleted file mode 100644 index 8aec3e3df..000000000 --- a/apps/website/lib/dtos/RequestAvatarGenerationOutputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Request avatar generation output data transfer object - * Output from avatar generation request - */ -export interface RequestAvatarGenerationOutputDto { - success: boolean; - avatarUrl?: string; - error?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/ScheduledRaceDto.ts b/apps/website/lib/dtos/ScheduledRaceDto.ts deleted file mode 100644 index 424b05207..000000000 --- a/apps/website/lib/dtos/ScheduledRaceDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Scheduled race data transfer object - * Represents a race scheduled in a league - */ -export interface ScheduledRaceDto { - id: string; - name: string; - scheduledTime: string; - status: string; - trackName?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/SessionDataDto.ts b/apps/website/lib/dtos/SessionDataDto.ts deleted file mode 100644 index c293471be..000000000 --- a/apps/website/lib/dtos/SessionDataDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Session data data transfer object - * User session information - */ -export interface SessionDataDto { - userId: string; - email: string; - displayName?: string; - driverId?: string; - isAuthenticated: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/SignupParamsDto.ts b/apps/website/lib/dtos/SignupParamsDto.ts deleted file mode 100644 index 4e3263f99..000000000 --- a/apps/website/lib/dtos/SignupParamsDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Signup parameters data transfer object - * Parameters for user signup - */ -export interface SignupParamsDto { - email: string; - password: string; - displayName: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorDashboardDto.ts b/apps/website/lib/dtos/SponsorDashboardDto.ts deleted file mode 100644 index 5249236c6..000000000 --- a/apps/website/lib/dtos/SponsorDashboardDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Sponsor dashboard data transfer object - * Dashboard information for a sponsor - */ -export interface SponsorDashboardDto { - sponsorId: string; - sponsorName: string; - totalSponsorships: number; - activeSponsorships: number; - totalInvestment: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorDto.ts b/apps/website/lib/dtos/SponsorDto.ts deleted file mode 100644 index 1e9a07710..000000000 --- a/apps/website/lib/dtos/SponsorDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Sponsor data transfer object - * Represents a sponsor entity - */ -export interface SponsorDto { - id: string; - name: string; - logoUrl?: string; - websiteUrl?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorSponsorshipsDto.ts b/apps/website/lib/dtos/SponsorSponsorshipsDto.ts deleted file mode 100644 index 56c588fe2..000000000 --- a/apps/website/lib/dtos/SponsorSponsorshipsDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { SponsorshipDetailDto } from './SponsorshipDetailDto'; - -/** - * Sponsor sponsorships data transfer object - * Sponsorships associated with a sponsor - */ -export interface SponsorSponsorshipsDto { - sponsorId: string; - sponsorName: string; - sponsorships: SponsorshipDetailDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/SponsorshipDetailDto.ts b/apps/website/lib/dtos/SponsorshipDetailDto.ts deleted file mode 100644 index 19f3cbd71..000000000 --- a/apps/website/lib/dtos/SponsorshipDetailDto.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Sponsorship detail data transfer object - * Details of a sponsorship - */ -export interface SponsorshipDetailDto { - id: string; - leagueId: string; - leagueName: string; - seasonId: string; - tier: 'main' | 'secondary'; - status: string; - amount: number; - currency: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/StandingEntryDto.ts b/apps/website/lib/dtos/StandingEntryDto.ts deleted file mode 100644 index 805f28b06..000000000 --- a/apps/website/lib/dtos/StandingEntryDto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { DriverDto } from './DriverDto'; - -/** - * Standing entry data transfer object - * Represents a driver's standing in league standings - */ -export interface StandingEntryDto { - driverId: string; - driver?: DriverDto; - position: number; - points: number; - wins: number; - podiums: number; - races: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamDetailsDto.ts b/apps/website/lib/dtos/TeamDetailsDto.ts deleted file mode 100644 index 8ffc6965d..000000000 --- a/apps/website/lib/dtos/TeamDetailsDto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { TeamMemberDto } from './TeamMemberDto'; - -/** - * Team details data transfer object - * Detailed information about a team - */ -export interface TeamDetailsDto { - id: string; - name: string; - description?: string; - logoUrl?: string; - memberCount: number; - ownerId: string; - members: TeamMemberDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamJoinRequestItemDto.ts b/apps/website/lib/dtos/TeamJoinRequestItemDto.ts deleted file mode 100644 index d4a5e7b50..000000000 --- a/apps/website/lib/dtos/TeamJoinRequestItemDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Team join request item data transfer object - * Represents a request to join a team - */ -export interface TeamJoinRequestItemDto { - id: string; - teamId: string; - driverId: string; - requestedAt: string; - message?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamJoinRequestsDto.ts b/apps/website/lib/dtos/TeamJoinRequestsDto.ts deleted file mode 100644 index 14394f531..000000000 --- a/apps/website/lib/dtos/TeamJoinRequestsDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TeamJoinRequestItemDto } from './TeamJoinRequestItemDto'; - -/** - * Team join requests data transfer object - * List of join requests for a team - */ -export interface TeamJoinRequestsDto { - requests: TeamJoinRequestItemDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamMemberDto.ts b/apps/website/lib/dtos/TeamMemberDto.ts deleted file mode 100644 index 086bb482f..000000000 --- a/apps/website/lib/dtos/TeamMemberDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { DriverDto } from './DriverDto'; - -/** - * Team member data transfer object - * Represents a driver's membership in a team - */ -export interface TeamMemberDto { - driverId: string; - driver?: DriverDto; - role: string; - joinedAt: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamMembersDto.ts b/apps/website/lib/dtos/TeamMembersDto.ts deleted file mode 100644 index 8d81d69ff..000000000 --- a/apps/website/lib/dtos/TeamMembersDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TeamMemberDto } from './TeamMemberDto'; - -/** - * Team members data transfer object - * List of team members - */ -export interface TeamMembersDto { - members: TeamMemberDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/TeamSummaryDto.ts b/apps/website/lib/dtos/TeamSummaryDto.ts deleted file mode 100644 index 3c821d65e..000000000 --- a/apps/website/lib/dtos/TeamSummaryDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Team summary data transfer object - * Basic information about a team - */ -export interface TeamSummaryDto { - id: string; - name: string; - logoUrl?: string; - memberCount: number; - rating: number; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/UpdateAvatarInputDto.ts b/apps/website/lib/dtos/UpdateAvatarInputDto.ts deleted file mode 100644 index aea0fe9b3..000000000 --- a/apps/website/lib/dtos/UpdateAvatarInputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Update avatar input data transfer object - * Input for updating driver avatar - */ -export interface UpdateAvatarInputDto { - driverId: string; - avatarUrl: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/UpdateAvatarOutputDto.ts b/apps/website/lib/dtos/UpdateAvatarOutputDto.ts deleted file mode 100644 index 448c5d0b6..000000000 --- a/apps/website/lib/dtos/UpdateAvatarOutputDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Update avatar output data transfer object - * Output from updating avatar - */ -export interface UpdateAvatarOutputDto { - success: boolean; - error?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/UpdateTeamInputDto.ts b/apps/website/lib/dtos/UpdateTeamInputDto.ts deleted file mode 100644 index 7950b590f..000000000 --- a/apps/website/lib/dtos/UpdateTeamInputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Update team input data transfer object - * Input for updating team information - */ -export interface UpdateTeamInputDto { - name?: string; - description?: string; - logoUrl?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/UpdateTeamOutputDto.ts b/apps/website/lib/dtos/UpdateTeamOutputDto.ts deleted file mode 100644 index bd8938372..000000000 --- a/apps/website/lib/dtos/UpdateTeamOutputDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Update team output data transfer object - * Output from updating team information - */ -export interface UpdateTeamOutputDto { - success: boolean; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/UploadMediaInputDto.ts b/apps/website/lib/dtos/UploadMediaInputDto.ts deleted file mode 100644 index 19e283910..000000000 --- a/apps/website/lib/dtos/UploadMediaInputDto.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Upload media input data transfer object - * Input for uploading media files - */ -export interface UploadMediaInputDto { - file: File; - type: 'image' | 'video' | 'document'; - category?: 'avatar' | 'team-logo' | 'league-cover' | 'race-result'; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/UploadMediaOutputDto.ts b/apps/website/lib/dtos/UploadMediaOutputDto.ts deleted file mode 100644 index 5b4699514..000000000 --- a/apps/website/lib/dtos/UploadMediaOutputDto.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Upload media output data transfer object - * Output from media upload operation - */ -export interface UploadMediaOutputDto { - success: boolean; - mediaId?: string; - url?: string; - error?: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/WalletDto.ts b/apps/website/lib/dtos/WalletDto.ts deleted file mode 100644 index 3e6114cfb..000000000 --- a/apps/website/lib/dtos/WalletDto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { WalletTransactionDto } from './WalletTransactionDto'; - -/** - * Wallet data transfer object - * Represents a driver's wallet - */ -export interface WalletDto { - driverId: string; - balance: number; - currency: string; - transactions: WalletTransactionDto[]; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/WalletTransactionDto.ts b/apps/website/lib/dtos/WalletTransactionDto.ts deleted file mode 100644 index 718dc9e9e..000000000 --- a/apps/website/lib/dtos/WalletTransactionDto.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Wallet transaction data transfer object - * Represents a transaction in a driver's wallet - */ -export interface WalletTransactionDto { - id: string; - type: 'deposit' | 'withdrawal'; - amount: number; - description?: string; - createdAt: string; -} \ No newline at end of file diff --git a/apps/website/lib/dtos/WithdrawFromRaceInputDto.ts b/apps/website/lib/dtos/WithdrawFromRaceInputDto.ts deleted file mode 100644 index d64438bfe..000000000 --- a/apps/website/lib/dtos/WithdrawFromRaceInputDto.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Withdraw from race input data transfer object - * Input for withdrawing a driver from a race - */ -export interface WithdrawFromRaceInputDto { - driverId: string; -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts b/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts deleted file mode 100644 index 2463340e3..000000000 --- a/apps/website/lib/presenters/AllLeaguesWithCapacityAndScoringPresenter.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * AllLeaguesWithCapacityAndScoringPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ - -import { apiClient, type AllLeaguesWithCapacityViewModel } from '@/lib/apiClient'; - -export interface LeagueScoringViewModel { - gameId: string; - gameName: string; - primaryChampionshipType: string; - scoringPresetId: string; - scoringPresetName: string; - dropPolicySummary: string; - scoringPatternSummary: string; -} - -export interface LeagueSummaryViewModel { - id: string; - name: string; - description?: string | undefined; - ownerId: string; - createdAt: string; - maxDrivers: number; - usedDriverSlots: number; - maxTeams: number; - usedTeamSlots: number; - structureSummary: string; - scoringPatternSummary: string; - timingSummary: string; - scoring: LeagueScoringViewModel; -} - -export interface AllLeaguesWithCapacityAndScoringViewModel { - leagues: LeagueSummaryViewModel[]; - totalCount: number; -} - -export interface IAllLeaguesWithCapacityAndScoringPresenter { - reset(): void; - getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null; -} - -/** - * Transform API response to view model - */ -function transformApiResponse(apiResponse: AllLeaguesWithCapacityViewModel): AllLeaguesWithCapacityAndScoringViewModel { - const leagueItems: LeagueSummaryViewModel[] = apiResponse.leagues.map((league) => { - const maxDrivers = league.maxMembers; - const usedDriverSlots = league.memberCount; - const structureSummary = `Solo • ${maxDrivers} drivers`; - const timingSummary = '30 min Quali • 40 min Race'; - const scoringPatternSummary = 'Custom • All results count'; - - const scoringSummary: LeagueScoringViewModel = { - gameId: 'unknown', - gameName: 'Unknown', - primaryChampionshipType: 'driver', - scoringPresetId: 'custom', - scoringPresetName: 'Custom', - dropPolicySummary: 'All results count', - scoringPatternSummary, - }; - - return { - id: league.id, - name: league.name, - description: league.description, - ownerId: league.ownerId, - createdAt: new Date().toISOString(), // Would need from API - maxDrivers, - usedDriverSlots, - maxTeams: 0, - usedTeamSlots: 0, - structureSummary, - scoringPatternSummary, - timingSummary, - scoring: scoringSummary, - }; - }); - - return { - leagues: leagueItems, - totalCount: leagueItems.length, - }; -} - -export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter { - private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - async fetchAndPresent(): Promise { - const apiResponse = await apiClient.leagues.getAllWithCapacity(); - this.viewModel = transformApiResponse(apiResponse); - } - - getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null { - return this.viewModel; - } -} - -/** - * Convenience function to fetch and transform all leagues - */ -export async function fetchAllLeaguesWithCapacityAndScoring(): Promise { - const apiResponse = await apiClient.leagues.getAllWithCapacity(); - return transformApiResponse(apiResponse); -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts b/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts deleted file mode 100644 index ea4d0c758..000000000 --- a/apps/website/lib/presenters/AllLeaguesWithCapacityPresenter.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { - IAllLeaguesWithCapacityPresenter, - LeagueWithCapacityViewModel, - AllLeaguesWithCapacityViewModel, - AllLeaguesWithCapacityResultDTO, -} from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter'; - -export class AllLeaguesWithCapacityPresenter implements IAllLeaguesWithCapacityPresenter { - private viewModel: AllLeaguesWithCapacityViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(input: AllLeaguesWithCapacityResultDTO): void { - const { leagues, memberCounts } = input; - const leagueItems: LeagueWithCapacityViewModel[] = leagues.map((league) => { - const usedSlots = memberCounts.get(league.id) ?? 0; - - // Ensure we never expose an impossible state like 26/24: - // clamp maxDrivers to at least usedSlots at the application boundary. - const configuredMax = league.settings.maxDrivers ?? usedSlots; - const safeMaxDrivers = Math.max(configuredMax, usedSlots); - - const base: LeagueWithCapacityViewModel = { - id: league.id, - name: league.name, - description: league.description, - ownerId: league.ownerId, - settings: { - ...league.settings, - maxDrivers: safeMaxDrivers, - }, - createdAt: league.createdAt.toISOString(), - usedSlots, - }; - - if (!league.socialLinks) { - return base; - } - - const socialLinks: NonNullable = {}; - - if (league.socialLinks.discordUrl) { - socialLinks.discordUrl = league.socialLinks.discordUrl; - } - if (league.socialLinks.youtubeUrl) { - socialLinks.youtubeUrl = league.socialLinks.youtubeUrl; - } - if (league.socialLinks.websiteUrl) { - socialLinks.websiteUrl = league.socialLinks.websiteUrl; - } - - if (Object.keys(socialLinks).length === 0) { - return base; - } - - return { - ...base, - socialLinks, - }; - }); - - this.viewModel = { - leagues: leagueItems, - totalCount: leagueItems.length, - }; - } - - getViewModel(): AllLeaguesWithCapacityViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AllRacesPagePresenter.ts b/apps/website/lib/presenters/AllRacesPagePresenter.ts deleted file mode 100644 index a20cdf73e..000000000 --- a/apps/website/lib/presenters/AllRacesPagePresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { - IAllRacesPagePresenter, - AllRacesPageResultDTO, - AllRacesPageViewModel, -} from '@core/racing/application/presenters/IAllRacesPagePresenter'; - -export class AllRacesPagePresenter implements IAllRacesPagePresenter { - private viewModel: AllRacesPageViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(dto: AllRacesPageResultDTO): void { - this.viewModel = dto; - } - - getViewModel(): AllRacesPageViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AllTeamsPresenter.ts b/apps/website/lib/presenters/AllTeamsPresenter.ts deleted file mode 100644 index 1e73c064e..000000000 --- a/apps/website/lib/presenters/AllTeamsPresenter.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * AllTeamsPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ - -import { apiClient, type AllTeamsViewModel as ApiAllTeamsViewModel } from '@/lib/apiClient'; - -export interface TeamListItemViewModel { - id: string; - name: string; - tag?: string | undefined; - description?: string | undefined; - memberCount: number; - logoUrl?: string | undefined; - rating?: number | undefined; -} - -export interface AllTeamsViewModel { - teams: TeamListItemViewModel[]; - totalCount: number; -} - -export interface IAllTeamsPresenter { - reset(): void; - getViewModel(): AllTeamsViewModel | null; -} - -/** - * Transform API response to view model - */ -function transformApiResponse(apiResponse: ApiAllTeamsViewModel): AllTeamsViewModel { - const teamItems: TeamListItemViewModel[] = apiResponse.teams.map((team) => { - const viewModel: TeamListItemViewModel = { - id: team.id, - name: team.name, - memberCount: team.memberCount ?? 0, - }; - - if (team.logoUrl) { - viewModel.logoUrl = team.logoUrl; - } - if (team.rating) { - viewModel.rating = team.rating; - } - - return viewModel; - }); - - return { - teams: teamItems, - totalCount: teamItems.length, - }; -} - -export class AllTeamsPresenter implements IAllTeamsPresenter { - private viewModel: AllTeamsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - async fetchAndPresent(): Promise { - const apiResponse = await apiClient.teams.getAll(); - this.viewModel = transformApiResponse(apiResponse); - } - - getViewModel(): AllTeamsViewModel | null { - return this.viewModel; - } -} - -/** - * Convenience function to fetch and transform all teams - */ -export async function fetchAllTeams(): Promise { - const apiResponse = await apiClient.teams.getAll(); - return transformApiResponse(apiResponse); -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AnalyticsDashboardPresenter.ts b/apps/website/lib/presenters/AnalyticsDashboardPresenter.ts deleted file mode 100644 index 5deb526dc..000000000 --- a/apps/website/lib/presenters/AnalyticsDashboardPresenter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AnalyticsDashboardDto } from '../dtos'; -import { AnalyticsDashboardViewModel } from '../view-models'; - -export class AnalyticsDashboardPresenter { - present(dto: AnalyticsDashboardDto): AnalyticsDashboardViewModel { - return new AnalyticsDashboardViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AnalyticsMetricsPresenter.ts b/apps/website/lib/presenters/AnalyticsMetricsPresenter.ts deleted file mode 100644 index dec0ce818..000000000 --- a/apps/website/lib/presenters/AnalyticsMetricsPresenter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AnalyticsMetricsDto } from '../dtos'; -import { AnalyticsMetricsViewModel } from '../view-models'; - -export class AnalyticsMetricsPresenter { - present(dto: AnalyticsMetricsDto): AnalyticsMetricsViewModel { - return new AnalyticsMetricsViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/AvatarPresenter.ts b/apps/website/lib/presenters/AvatarPresenter.ts deleted file mode 100644 index 6ef1f5796..000000000 --- a/apps/website/lib/presenters/AvatarPresenter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { - GetAvatarOutputDto, - RequestAvatarGenerationOutputDto, - UpdateAvatarOutputDto -} from '../dtos'; -import type { - AvatarViewModel, - RequestAvatarGenerationViewModel, - UpdateAvatarViewModel -} from '../view-models'; - -/** - * Avatar Presenter - * Transforms avatar DTOs to ViewModels - */ -export class AvatarPresenter { - presentAvatar(dto: GetAvatarOutputDto): AvatarViewModel { - return { - driverId: dto.driverId, - avatarUrl: dto.avatarUrl, - hasAvatar: dto.hasAvatar, - }; - } - - presentRequestGeneration(dto: RequestAvatarGenerationOutputDto): RequestAvatarGenerationViewModel { - return { - success: dto.success, - avatarUrl: dto.avatarUrl, - error: dto.error, - }; - } - - presentUpdate(dto: UpdateAvatarOutputDto): UpdateAvatarViewModel { - return { - success: dto.success, - error: dto.error, - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/CompleteOnboardingPresenter.ts b/apps/website/lib/presenters/CompleteOnboardingPresenter.ts deleted file mode 100644 index d630db6e5..000000000 --- a/apps/website/lib/presenters/CompleteOnboardingPresenter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CompleteOnboardingOutputDto } from '../dtos'; -import type { CompleteOnboardingViewModel } from '../view-models/CompleteOnboardingViewModel'; - -/** - * Complete Onboarding Presenter - * Transforms CompleteOnboardingOutputDto to CompleteOnboardingViewModel - */ -export class CompleteOnboardingPresenter { - present(dto: CompleteOnboardingOutputDto): CompleteOnboardingViewModel { - return { - driverId: dto.driverId, - success: dto.success, - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/DashboardOverviewPresenter.ts b/apps/website/lib/presenters/DashboardOverviewPresenter.ts deleted file mode 100644 index c92974478..000000000 --- a/apps/website/lib/presenters/DashboardOverviewPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { - IDashboardOverviewPresenter, - DashboardOverviewResultDTO, - DashboardOverviewViewModel, -} from '@core/racing/application/presenters/IDashboardOverviewPresenter'; - -export class DashboardOverviewPresenter implements IDashboardOverviewPresenter { - private viewModel: DashboardOverviewViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(dto: DashboardOverviewResultDTO): void { - this.viewModel = dto; - } - - getViewModel(): DashboardOverviewViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/DriverPresenter.ts b/apps/website/lib/presenters/DriverPresenter.ts deleted file mode 100644 index 59bfdb8ae..000000000 --- a/apps/website/lib/presenters/DriverPresenter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DriverDto } from '../dtos'; -import type { DriverViewModel } from '../view-models/DriverViewModel'; - -/** - * Driver Presenter - * Transforms DriverDto to DriverViewModel - */ -export class DriverPresenter { - present(dto: DriverDto): DriverViewModel { - return { - id: dto.id, - name: dto.name, - avatarUrl: dto.avatarUrl, - iracingId: dto.iracingId, - rating: dto.rating, - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts b/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts deleted file mode 100644 index 068350943..000000000 --- a/apps/website/lib/presenters/DriverRegistrationStatusPresenter.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DriverRegistrationStatusDto } from '../dtos'; -import type { DriverRegistrationStatusViewModel } from '../view-models'; -import { DriverRegistrationStatusViewModel as ViewModel } from '../view-models'; - -/** - * Driver Registration Status Presenter - * Transforms DriverRegistrationStatusDto to DriverRegistrationStatusViewModel - */ -export class DriverRegistrationStatusPresenter { - present(dto: DriverRegistrationStatusDto): DriverRegistrationStatusViewModel { - return new ViewModel(dto); - } -} - -// Legacy functional export for backward compatibility -export const presentDriverRegistrationStatus = (dto: DriverRegistrationStatusDto): DriverRegistrationStatusViewModel => { - const presenter = new DriverRegistrationStatusPresenter(); - return presenter.present(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/DriverTeamPresenter.ts b/apps/website/lib/presenters/DriverTeamPresenter.ts deleted file mode 100644 index 1aeb6ff4b..000000000 --- a/apps/website/lib/presenters/DriverTeamPresenter.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * DriverTeamPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ - -import { apiClient, type DriverTeamViewModel as ApiDriverTeamViewModel } from '@/lib/apiClient'; - -export interface DriverTeamMembershipViewModel { - role: string; - joinedAt: string; - isActive: boolean; -} - -export interface DriverTeamInfoViewModel { - id: string; - name: string; - tag?: string | undefined; - description?: string | undefined; - ownerId: string; - leagues?: string[] | undefined; -} - -export interface DriverTeamViewModel { - team: DriverTeamInfoViewModel; - membership: DriverTeamMembershipViewModel; - isOwner: boolean; - canManage: boolean; -} - -export interface IDriverTeamPresenter { - reset(): void; - getViewModel(): DriverTeamViewModel | null; -} - -/** - * Transform API response to view model - */ -function transformApiResponse(apiResponse: ApiDriverTeamViewModel): DriverTeamViewModel { - const isOwner = false; // Would need team owner info from API - const canManage = apiResponse.role === 'owner' || apiResponse.role === 'manager'; - - return { - team: { - id: apiResponse.teamId, - name: apiResponse.teamName, - ownerId: '', // Would need from API - }, - membership: { - role: apiResponse.role === 'driver' ? 'member' : apiResponse.role, - joinedAt: new Date(apiResponse.joinedAt).toISOString(), - isActive: true, - }, - isOwner, - canManage, - }; -} - -export class DriverTeamPresenter implements IDriverTeamPresenter { - private viewModel: DriverTeamViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - async fetchAndPresent(driverId: string): Promise { - const apiResponse = await apiClient.teams.getDriverTeam(driverId); - if (apiResponse) { - this.viewModel = transformApiResponse(apiResponse); - } else { - this.viewModel = null; - } - } - - getViewModel(): DriverTeamViewModel | null { - return this.viewModel; - } -} - -/** - * Convenience function to fetch and transform driver's team - */ -export async function fetchDriverTeam(driverId: string): Promise { - const apiResponse = await apiClient.teams.getDriverTeam(driverId); - if (!apiResponse) { - return null; - } - return transformApiResponse(apiResponse); -} \ No newline at end of file diff --git a/apps/website/lib/presenters/DriversLeaderboardPresenter.ts b/apps/website/lib/presenters/DriversLeaderboardPresenter.ts deleted file mode 100644 index 8a69c14ef..000000000 --- a/apps/website/lib/presenters/DriversLeaderboardPresenter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DriversLeaderboardDto } from '../dtos'; -import type { DriverLeaderboardViewModel } from '../view-models'; - -/** - * Drivers Leaderboard Presenter - * Transforms DriversLeaderboardDto to DriverLeaderboardViewModel - */ -export class DriversLeaderboardPresenter { - present(dto: DriversLeaderboardDto, previousDrivers?: any): DriverLeaderboardViewModel { - return new DriverLeaderboardViewModel(dto as any, previousDrivers); - } -} - -// Legacy functional export for backward compatibility -export const presentDriversLeaderboard = (dto: DriversLeaderboardDto, previousDrivers?: any): DriverLeaderboardViewModel => { - const presenter = new DriversLeaderboardPresenter(); - return presenter.present(dto, previousDrivers); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/EntitySponsorshipPricingPresenter.ts b/apps/website/lib/presenters/EntitySponsorshipPricingPresenter.ts deleted file mode 100644 index 98e521293..000000000 --- a/apps/website/lib/presenters/EntitySponsorshipPricingPresenter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IEntitySponsorshipPricingPresenter } from '@core/racing/application/presenters/IEntitySponsorshipPricingPresenter'; -import type { GetEntitySponsorshipPricingResultDTO } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase'; - -export class EntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter { - private data: GetEntitySponsorshipPricingResultDTO | null = null; - - present(data: GetEntitySponsorshipPricingResultDTO | null): void { - this.data = data; - } - - getData(): GetEntitySponsorshipPricingResultDTO | null { - return this.data; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/ImportRaceResultsPresenter.ts b/apps/website/lib/presenters/ImportRaceResultsPresenter.ts deleted file mode 100644 index fdb885530..000000000 --- a/apps/website/lib/presenters/ImportRaceResultsPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ImportRaceResultsSummaryDto } from '../dtos/ImportRaceResultsSummaryDto'; - -export interface ImportRaceResultsSummaryViewModel { - success: boolean; - raceId: string; - driversProcessed: number; - resultsRecorded: number; - errors?: string[]; -} - -export class ImportRaceResultsPresenter { - present(dto: ImportRaceResultsSummaryDto): ImportRaceResultsSummaryViewModel { - return { - success: dto.success, - raceId: dto.raceId, - driversProcessed: dto.driversProcessed, - resultsRecorded: dto.resultsRecorded, - errors: dto.errors, - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueAdminPresenter.ts b/apps/website/lib/presenters/LeagueAdminPresenter.ts deleted file mode 100644 index 497441e11..000000000 --- a/apps/website/lib/presenters/LeagueAdminPresenter.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * LeagueAdminPresenter - Pure data transformer - * Transforms API responses to view models without DI dependencies. - * All data fetching is done via apiClient. - */ - -import { apiClient } from '@/lib/apiClient'; -import type { - LeagueJoinRequestViewModel as ApiLeagueJoinRequestViewModel, - LeagueConfigFormModelDto, - LeagueSeasonSummaryViewModel as ApiLeagueSeasonSummaryViewModel, - DriverDTO, -} from '@/lib/apiClient'; - -// ============================================================================ -// View Model Types -// ============================================================================ - -export interface LeagueJoinRequestViewModel { - id: string; - leagueId: string; - driverId: string; - requestedAt: Date; - message?: string | undefined; - driver?: DriverDTO | undefined; -} - -export interface ProtestDriverSummary { - [driverId: string]: DriverDTO; -} - -export interface ProtestRaceSummary { - [raceId: string]: { - id: string; - name: string; - scheduledTime: string; - }; -} - -export interface LeagueOwnerSummaryViewModel { - driver: DriverDTO; - rating: number | null; - rank: number | null; -} - -export interface LeagueSummaryViewModel { - id: string; - ownerId: string; - settings: { - pointsSystem: string; - }; -} - -export interface LeagueAdminProtestsViewModel { - protests: Array<{ - id: string; - raceId: string; - complainantId: string; - defendantId: string; - description: string; - status: string; - createdAt: string; - }>; - racesById: ProtestRaceSummary; - driversById: ProtestDriverSummary; -} - -export interface LeagueAdminConfigViewModel { - form: LeagueConfigFormModelDto | null; -} - -export interface LeagueAdminPermissionsViewModel { - canRemoveMember: boolean; - canUpdateRoles: boolean; -} - -export interface LeagueSeasonSummaryViewModel { - seasonId: string; - name: string; - status: string; - startDate?: Date | undefined; - endDate?: Date | undefined; - isPrimary: boolean; - isParallelActive: boolean; -} - -export interface LeagueAdminViewModel { - joinRequests: LeagueJoinRequestViewModel[]; - ownerSummary: LeagueOwnerSummaryViewModel | null; - config: LeagueAdminConfigViewModel; - protests: LeagueAdminProtestsViewModel; -} - -export type MembershipRole = 'owner' | 'admin' | 'member'; - -// ============================================================================ -// Data Fetching Functions (using apiClient) -// ============================================================================ - -/** - * Load join requests for a league via API. - */ -export async function loadLeagueJoinRequests(leagueId: string): Promise { - const requests = await apiClient.leagues.getJoinRequests(leagueId); - - return requests.map((request: ApiLeagueJoinRequestViewModel) => { - const viewModel: LeagueJoinRequestViewModel = { - id: request.id, - leagueId: request.leagueId, - driverId: request.driverId, - requestedAt: new Date(request.requestedAt), - }; - - if (request.message) { - viewModel.message = request.message; - } - - return viewModel; - }); -} - -/** - * Approve a league join request and return updated join requests. - */ -export async function approveLeagueJoinRequest( - leagueId: string, - requestId: string -): Promise { - await apiClient.leagues.approveJoinRequest(leagueId, requestId); - return loadLeagueJoinRequests(leagueId); -} - -/** - * Reject a league join request. - */ -export async function rejectLeagueJoinRequest( - leagueId: string, - requestId: string -): Promise { - await apiClient.leagues.rejectJoinRequest(leagueId, requestId); - return loadLeagueJoinRequests(leagueId); -} - -/** - * Get permissions for a performer on league membership actions. - */ -export async function getLeagueAdminPermissions( - leagueId: string, - performerDriverId: string -): Promise { - const permissions = await apiClient.leagues.getAdminPermissions(leagueId, performerDriverId); - - return { - canRemoveMember: permissions.canManageMembers || permissions.isOwner || permissions.isAdmin, - canUpdateRoles: permissions.isOwner, - }; -} - -/** - * Remove a member from the league. - */ -export async function removeLeagueMember( - leagueId: string, - performerDriverId: string, - targetDriverId: string -): Promise { - await apiClient.leagues.removeMember(leagueId, performerDriverId, targetDriverId); -} - -/** - * Update a member's role. - */ -export async function updateLeagueMemberRole( - leagueId: string, - performerDriverId: string, - targetDriverId: string, - newRole: MembershipRole -): Promise { - await apiClient.leagues.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole); -} - -/** - * Load owner summary for a league. - */ -export async function loadLeagueOwnerSummary(params: { - leagueId: string; - ownerId: string; -}): Promise { - const ownerSummary = await apiClient.leagues.getOwnerSummary(params.leagueId, params.ownerId); - - if (!ownerSummary) { - return null; - } - - // For now, return a simplified version - the API should provide driver details - return { - driver: { - id: params.ownerId, - name: ownerSummary.leagueName, // This would need to be populated from API - }, - rating: null, - rank: null, - }; -} - -/** - * Load league full config form. - */ -export async function loadLeagueConfig( - leagueId: string -): Promise { - const config = await apiClient.leagues.getConfig(leagueId); - - return { - form: config, - }; -} - -/** - * Load protests for a league. - */ -export async function loadLeagueProtests(leagueId: string): Promise { - const protestsData = await apiClient.leagues.getProtests(leagueId); - - // Transform the API response - const racesById: ProtestRaceSummary = {}; - const driversById: ProtestDriverSummary = {}; - - return { - protests: protestsData.protests.map((p) => ({ - id: p.id, - raceId: p.raceId, - complainantId: p.complainantId, - defendantId: p.defendantId, - description: p.description, - status: p.status, - createdAt: p.createdAt, - })), - racesById, - driversById, - }; -} - -/** - * Load seasons for a league. - */ -export async function loadLeagueSeasons(leagueId: string): Promise { - const seasons = await apiClient.leagues.getSeasons(leagueId); - const activeCount = seasons.filter((s: ApiLeagueSeasonSummaryViewModel) => s.status === 'active').length; - - return seasons.map((s: ApiLeagueSeasonSummaryViewModel) => { - const viewModel: LeagueSeasonSummaryViewModel = { - seasonId: s.id, - name: s.name, - status: s.status, - isPrimary: false, // Would need to be provided by API - isParallelActive: activeCount > 1 && s.status === 'active', - }; - - if (s.startDate) { - viewModel.startDate = new Date(s.startDate); - } - if (s.endDate) { - viewModel.endDate = new Date(s.endDate); - } - - return viewModel; - }); -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts b/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts deleted file mode 100644 index ab323570f..000000000 --- a/apps/website/lib/presenters/LeagueDriverSeasonStatsPresenter.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { - ILeagueDriverSeasonStatsPresenter, - LeagueDriverSeasonStatsItemViewModel, - LeagueDriverSeasonStatsViewModel, - LeagueDriverSeasonStatsResultDTO, -} from '@core/racing/application/presenters/ILeagueDriverSeasonStatsPresenter'; - -export class LeagueDriverSeasonStatsPresenter implements ILeagueDriverSeasonStatsPresenter { - private viewModel: LeagueDriverSeasonStatsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(dto: LeagueDriverSeasonStatsResultDTO): void { - const { leagueId, standings, penalties, driverResults, driverRatings } = dto; - - const stats: LeagueDriverSeasonStatsItemViewModel[] = standings.map((standing) => { - const penalty = penalties.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 }; - const totalPenaltyPoints = penalty.baseDelta; - const bonusPoints = penalty.bonusDelta; - - const racesCompleted = standing.racesCompleted; - const pointsPerRace = racesCompleted > 0 ? standing.points / racesCompleted : 0; - - const ratingInfo = driverRatings.get(standing.driverId) ?? { rating: null, ratingChange: null }; - - const results = driverResults.get(standing.driverId) ?? []; - let avgFinish: number | null = null; - if (results.length > 0) { - const totalPositions = results.reduce((sum, r) => sum + r.position, 0); - const avg = totalPositions / results.length; - avgFinish = Number.isFinite(avg) ? Number(avg.toFixed(2)) : null; - } - - return { - leagueId, - driverId: standing.driverId, - position: standing.position, - driverName: '', - teamId: '', - teamName: '', - totalPoints: standing.points + totalPenaltyPoints + bonusPoints, - basePoints: standing.points, - penaltyPoints: Math.abs(totalPenaltyPoints), - bonusPoints, - pointsPerRace, - racesStarted: results.length, - racesFinished: results.length, - dnfs: 0, - noShows: 0, - avgFinish, - rating: ratingInfo.rating, - ratingChange: ratingInfo.ratingChange, - }; - }); - - stats.sort((a, b) => a.position - b.position); - - this.viewModel = { - leagueId, - stats, - }; - } - - getViewModel(): LeagueDriverSeasonStatsViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueFullConfigPresenter.ts b/apps/website/lib/presenters/LeagueFullConfigPresenter.ts deleted file mode 100644 index 67ff480e4..000000000 --- a/apps/website/lib/presenters/LeagueFullConfigPresenter.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { DropScorePolicy } from '@core/racing/domain/types/DropScorePolicy'; -import type { - ILeagueFullConfigPresenter, - LeagueFullConfigData, - LeagueConfigFormViewModel, -} from '@core/racing/application/presenters/ILeagueFullConfigPresenter'; - -export class LeagueFullConfigPresenter implements ILeagueFullConfigPresenter { - private viewModel: LeagueConfigFormViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(data: LeagueFullConfigData): void { - const { league, activeSeason, scoringConfig, game } = data; - - const patternId = scoringConfig?.scoringPresetId; - - const primaryChampionship = - scoringConfig && scoringConfig.championships && scoringConfig.championships.length > 0 - ? scoringConfig.championships[0] - : undefined; - - const dropPolicy = primaryChampionship?.dropScorePolicy ?? undefined; - const dropPolicyForm = this.mapDropPolicy(dropPolicy); - - const defaultQualifyingMinutes = 30; - const defaultMainRaceMinutes = 40; - const mainRaceMinutes = - typeof league.settings.sessionDuration === 'number' - ? league.settings.sessionDuration - : defaultMainRaceMinutes; - const qualifyingMinutes = defaultQualifyingMinutes; - - const roundsPlanned = 8; - - let sessionCount = 2; - if (primaryChampionship && Array.isArray(primaryChampionship.sessionTypes)) { - sessionCount = primaryChampionship.sessionTypes.length; - } - - const practiceMinutes = 20; - const sprintRaceMinutes = patternId === 'sprint-main-driver' ? 20 : undefined; - - this.viewModel = { - leagueId: league.id, - basics: { - name: league.name, - description: league.description, - visibility: 'public', - gameId: game?.id ?? 'iracing', - }, - structure: { - mode: 'solo', - maxDrivers: league.settings.maxDrivers ?? 32, - multiClassEnabled: false, - }, - championships: { - enableDriverChampionship: true, - enableTeamChampionship: false, - enableNationsChampionship: false, - enableTrophyChampionship: false, - }, - scoring: { - customScoringEnabled: !patternId, - ...(patternId ? { patternId } : {}), - }, - dropPolicy: dropPolicyForm, - timings: { - practiceMinutes, - qualifyingMinutes, - mainRaceMinutes, - sessionCount, - roundsPlanned, - ...(typeof sprintRaceMinutes === 'number' - ? { sprintRaceMinutes } - : {}), - }, - stewarding: { - decisionMode: 'admin_only', - requireDefense: true, - defenseTimeLimit: 48, - voteTimeLimit: 72, - protestDeadlineHours: 72, - stewardingClosesHours: 168, - notifyAccusedOnProtest: true, - notifyOnVoteRequired: true, - }, - }; - } - - getViewModel(): LeagueConfigFormViewModel | null { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; - } - - private mapDropPolicy(policy: DropScorePolicy | undefined): { strategy: string; n?: number } { - if (!policy || policy.strategy === 'none') { - return { strategy: 'none' }; - } - - if (policy.strategy === 'bestNResults') { - const n = typeof policy.count === 'number' ? policy.count : undefined; - return n !== undefined ? { strategy: 'bestNResults', n } : { strategy: 'none' }; - } - - if (policy.strategy === 'dropWorstN') { - const n = typeof policy.dropCount === 'number' ? policy.dropCount : undefined; - return n !== undefined ? { strategy: 'dropWorstN', n } : { strategy: 'none' }; - } - - return { strategy: 'none' }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueMemberPresenter.ts b/apps/website/lib/presenters/LeagueMemberPresenter.ts deleted file mode 100644 index e7beb0500..000000000 --- a/apps/website/lib/presenters/LeagueMemberPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LeagueMemberDto } from '../dtos'; -import { LeagueMemberViewModel } from '../view-models'; - -export const presentLeagueMember = (dto: LeagueMemberDto, currentUserId: string): LeagueMemberViewModel => { - return new LeagueMemberViewModel(dto, currentUserId); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueMembersPresenter.ts b/apps/website/lib/presenters/LeagueMembersPresenter.ts deleted file mode 100644 index b0551be8d..000000000 --- a/apps/website/lib/presenters/LeagueMembersPresenter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { LeagueMembershipsDto } from '../dtos'; -import { LeagueMemberViewModel } from '../view-models'; - -/** - * League Members Presenter - * - * Transforms league memberships DTO to view models for the UI. - */ -export class LeagueMembersPresenter { - /** - * Present league memberships with current user context - */ - present(dto: LeagueMembershipsDto, currentUserId: string): LeagueMemberViewModel[] { - return dto.members.map(member => new LeagueMemberViewModel(member, currentUserId)); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueSchedulePresenter.ts b/apps/website/lib/presenters/LeagueSchedulePresenter.ts deleted file mode 100644 index 01135b375..000000000 --- a/apps/website/lib/presenters/LeagueSchedulePresenter.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Race } from '@core/racing/domain/entities/Race'; -import type { IRaceRepository } from '@core/racing/application/ports/IRaceRepository'; -import type { IIsDriverRegisteredForRaceQuery } from '@core/racing/application/queries/IIsDriverRegisteredForRaceQuery'; -import type { IRegisterForRaceUseCase } from '@core/racing/application/use-cases/IRegisterForRaceUseCase'; -import type { IWithdrawFromRaceUseCase } from '@core/racing/application/use-cases/IWithdrawFromRaceUseCase'; - -export interface LeagueScheduleRaceItemViewModel { - id: string; - leagueId: string; - track: string; - car: string; - sessionType: string; - scheduledAt: Date; - status: Race['status']; - isUpcoming: boolean; - isPast: boolean; - isRegistered: boolean; -} - -export interface LeagueScheduleViewModel { - races: LeagueScheduleRaceItemViewModel[]; -} - -export interface ILeagueSchedulePresenter { - loadLeagueSchedule(leagueId: string, driverId: string): Promise; - registerForRace(raceId: string, leagueId: string, driverId: string): Promise; - withdrawFromRace(raceId: string, driverId: string): Promise; -} - -export class LeagueSchedulePresenter implements ILeagueSchedulePresenter { - constructor( - private raceRepository: IRaceRepository, - private isDriverRegisteredForRaceQuery: IIsDriverRegisteredForRaceQuery, - private registerForRaceUseCase: IRegisterForRaceUseCase, - private withdrawFromRaceUseCase: IWithdrawFromRaceUseCase, - ) {} - - /** - * Load league schedule with registration status for a given driver. - */ - async loadLeagueSchedule( - leagueId: string, - driverId: string, - ): Promise { - const allRaces = await this.raceRepository.findAll(); - const leagueRaces = allRaces - .filter((race) => race.leagueId === leagueId) - .sort( - (a, b) => - new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(), - ); - - const now = new Date(); - - const registrationStates: Record = {}; - await Promise.all( - leagueRaces.map(async (race) => { - const registered = await this.isDriverRegisteredForRaceQuery.execute({ - raceId: race.id, - driverId, - }); - registrationStates[race.id] = registered; - }), - ); - - const races: LeagueScheduleRaceItemViewModel[] = leagueRaces.map((race) => { - const raceDate = new Date(race.scheduledAt); - const isPast = race.status === 'completed' || raceDate <= now; - const isUpcoming = race.status === 'scheduled' && raceDate > now; - - return { - id: race.id, - leagueId: race.leagueId, - track: race.track, - car: race.car, - sessionType: race.sessionType, - scheduledAt: raceDate, - status: race.status, - isUpcoming, - isPast, - isRegistered: registrationStates[race.id] ?? false, - }; - }); - - return { races }; - } - - /** - * Register the driver for a race. - */ - async registerForRace( - raceId: string, - leagueId: string, - driverId: string, - ): Promise { - await this.registerForRaceUseCase.execute({ - raceId, - leagueId, - driverId, - }); - } - - /** - * Withdraw the driver from a race. - */ - async withdrawFromRace( - raceId: string, - driverId: string, - ): Promise { - await this.withdrawFromRaceUseCase.execute({ - raceId, - driverId, - }); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueSchedulePreviewPresenter.ts b/apps/website/lib/presenters/LeagueSchedulePreviewPresenter.ts deleted file mode 100644 index 3433f9be5..000000000 --- a/apps/website/lib/presenters/LeagueSchedulePreviewPresenter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ILeagueSchedulePreviewPresenter } from '@core/racing/application/presenters/ILeagueSchedulePreviewPresenter'; -import type { LeagueSchedulePreviewDTO } from '@core/racing/application/dto/LeagueScheduleDTO'; - -export class LeagueSchedulePreviewPresenter implements ILeagueSchedulePreviewPresenter { - private data: LeagueSchedulePreviewDTO | null = null; - - present(data: LeagueSchedulePreviewDTO): void { - this.data = data; - } - - getData(): LeagueSchedulePreviewDTO | null { - return this.data; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts b/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts deleted file mode 100644 index 44b6ba690..000000000 --- a/apps/website/lib/presenters/LeagueScoringConfigPresenter.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig'; -import type { BonusRule } from '@core/racing/domain/types/BonusRule'; -import type { - ILeagueScoringConfigPresenter, - LeagueScoringConfigData, - LeagueScoringConfigViewModel, - LeagueScoringChampionshipViewModel, -} from '@core/racing/application/presenters/ILeagueScoringConfigPresenter'; - -export class LeagueScoringConfigPresenter implements ILeagueScoringConfigPresenter { - private viewModel: LeagueScoringConfigViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(data: LeagueScoringConfigData): LeagueScoringConfigViewModel { - const championships: LeagueScoringChampionshipViewModel[] = - data.championships.map((champ) => this.mapChampionship(champ)); - - const dropPolicySummary = - data.preset?.dropPolicySummary ?? - this.deriveDropPolicyDescriptionFromChampionships(data.championships); - - this.viewModel = { - leagueId: data.leagueId, - seasonId: data.seasonId, - gameId: data.gameId, - gameName: data.gameName, - scoringPresetId: data.scoringPresetId ?? 'custom', - scoringPresetName: data.preset?.name ?? 'Custom', - dropPolicySummary, - championships, - }; - - return this.viewModel; - } - - getViewModel(): LeagueScoringConfigViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; - } - - private mapChampionship(championship: ChampionshipConfig): LeagueScoringChampionshipViewModel { - const sessionTypes = championship.sessionTypes.map((s) => s.toString()); - const pointsPreview = this.buildPointsPreview(championship.pointsTableBySessionType); - const bonusSummary = this.buildBonusSummary( - championship.bonusRulesBySessionType ?? {}, - ); - const dropPolicyDescription = this.deriveDropPolicyDescription( - championship.dropScorePolicy, - ); - - return { - id: championship.id, - name: championship.name, - type: championship.type, - sessionTypes, - pointsPreview, - bonusSummary, - dropPolicyDescription, - }; - } - - private buildPointsPreview( - tables: Record number }>, - ): Array<{ sessionType: string; position: number; points: number }> { - const preview: Array<{ - sessionType: string; - position: number; - points: number; - }> = []; - - const maxPositions = 10; - - for (const [sessionType, table] of Object.entries(tables)) { - for (let pos = 1; pos <= maxPositions; pos++) { - const points = table.getPointsForPosition(pos); - if (points && points !== 0) { - preview.push({ - sessionType, - position: pos, - points, - }); - } - } - } - - return preview; - } - - private buildBonusSummary( - bonusRulesBySessionType: Record, - ): string[] { - const summaries: string[] = []; - - for (const [sessionType, rules] of Object.entries(bonusRulesBySessionType)) { - for (const rule of rules) { - if (rule.type === 'fastestLap') { - const base = `Fastest lap in ${sessionType}`; - if (rule.requiresFinishInTopN) { - summaries.push( - `${base} +${rule.points} points if finishing P${rule.requiresFinishInTopN} or better`, - ); - } else { - summaries.push(`${base} +${rule.points} points`); - } - } else { - summaries.push( - `${rule.type} bonus in ${sessionType} worth ${rule.points} points`, - ); - } - } - } - - return summaries; - } - - private deriveDropPolicyDescriptionFromChampionships( - championships: ChampionshipConfig[], - ): string { - const first = championships[0]; - if (!first) { - return 'All results count'; - } - return this.deriveDropPolicyDescription(first.dropScorePolicy); - } - - private deriveDropPolicyDescription(policy: { - strategy: string; - count?: number; - dropCount?: number; - }): string { - if (!policy || policy.strategy === 'none') { - return 'All results count'; - } - - if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') { - return `Best ${policy.count} results count towards the championship`; - } - - if ( - policy.strategy === 'dropWorstN' && - typeof policy.dropCount === 'number' - ) { - return `Worst ${policy.dropCount} results are dropped from the championship total`; - } - - return 'Custom drop score rules apply'; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts b/apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts deleted file mode 100644 index 336415289..000000000 --- a/apps/website/lib/presenters/LeagueScoringPresetsPresenter.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { - ILeagueScoringPresetsPresenter, - LeagueScoringPresetsViewModel, - LeagueScoringPresetsResultDTO, -} from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter'; - -export class LeagueScoringPresetsPresenter implements ILeagueScoringPresetsPresenter { - private viewModel: LeagueScoringPresetsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(dto: LeagueScoringPresetsResultDTO): void { - const { presets } = dto; - - this.viewModel = { - presets, - totalCount: presets.length, - }; - } - - getViewModel(): LeagueScoringPresetsViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueStandingsPresenter.ts b/apps/website/lib/presenters/LeagueStandingsPresenter.ts deleted file mode 100644 index 687475f32..000000000 --- a/apps/website/lib/presenters/LeagueStandingsPresenter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { LeagueStandingsDto, StandingEntryDto } from '../dtos'; -import { LeagueStandingsViewModel } from '../view-models'; - -/** - * League Standings Presenter - * Transforms LeagueStandingsDto to LeagueStandingsViewModel - */ -export class LeagueStandingsPresenter { - present(dto: LeagueStandingsDto, currentUserId: string, previousStandings?: StandingEntryDto[]): LeagueStandingsViewModel { - return new LeagueStandingsViewModel(dto, currentUserId, previousStandings); - } -} - -// Legacy functional export for backward compatibility -export const presentLeagueStandings = (dto: LeagueStandingsDto, currentUserId: string, previousStandings?: StandingEntryDto[]): LeagueStandingsViewModel => { - const presenter = new LeagueStandingsPresenter(); - return presenter.present(dto, currentUserId, previousStandings); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueStatsPresenter.ts b/apps/website/lib/presenters/LeagueStatsPresenter.ts deleted file mode 100644 index a00b76a18..000000000 --- a/apps/website/lib/presenters/LeagueStatsPresenter.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { - ILeagueStatsPresenter, - LeagueStatsViewModel, -} from '@core/racing/application/presenters/ILeagueStatsPresenter'; - -export class LeagueStatsPresenter implements ILeagueStatsPresenter { - private viewModel: LeagueStatsViewModel | null = null; - - present( - leagueId: string, - totalRaces: number, - completedRaces: number, - scheduledRaces: number, - sofValues: number[] - ): LeagueStatsViewModel { - const averageSOF = sofValues.length > 0 - ? Math.round(sofValues.reduce((a, b) => a + b, 0) / sofValues.length) - : null; - - const highestSOF = sofValues.length > 0 ? Math.max(...sofValues) : null; - const lowestSOF = sofValues.length > 0 ? Math.min(...sofValues) : null; - - this.viewModel = { - leagueId, - totalRaces, - completedRaces, - scheduledRaces, - averageSOF, - highestSOF, - lowestSOF, - }; - - return this.viewModel; - } - - getViewModel(): LeagueStatsViewModel { - if (!this.viewModel) { - throw new Error('Presenter has not been called yet'); - } - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/LeagueSummaryPresenter.ts b/apps/website/lib/presenters/LeagueSummaryPresenter.ts deleted file mode 100644 index 4b83aa8ef..000000000 --- a/apps/website/lib/presenters/LeagueSummaryPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { AllLeaguesWithCapacityDto } from '../dtos'; -import { LeagueSummaryViewModel } from '../view-models'; - -/** - * League Summary Presenter - * Transforms AllLeaguesWithCapacityDto to array of LeagueSummaryViewModel - */ -export class LeagueSummaryPresenter { - present(dto: AllLeaguesWithCapacityDto): LeagueSummaryViewModel[] { - return dto.leagues.map(league => new LeagueSummaryViewModel(league)); - } -} - -// Legacy functional exports for backward compatibility -export const presentLeagueSummary = (dto: any): LeagueSummaryViewModel => { - return new LeagueSummaryViewModel(dto); -}; - -export const presentLeagueSummaries = (dtos: any[]): LeagueSummaryViewModel[] => { - return dtos.map(presentLeagueSummary); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/MediaPresenter.ts b/apps/website/lib/presenters/MediaPresenter.ts deleted file mode 100644 index 691c96f31..000000000 --- a/apps/website/lib/presenters/MediaPresenter.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { GetMediaOutputDto, UploadMediaOutputDto, DeleteMediaOutputDto } from '../dtos'; -import type { MediaViewModel, UploadMediaViewModel, DeleteMediaViewModel } from '../view-models'; - -/** - * Media Presenter - * Transforms media DTOs to ViewModels - */ -export class MediaPresenter { - presentMedia(dto: GetMediaOutputDto): MediaViewModel { - return { - id: dto.id, - url: dto.url, - type: dto.type, - category: dto.category, - uploadedAt: new Date(dto.uploadedAt), - size: dto.size, - }; - } - - presentUpload(dto: UploadMediaOutputDto): UploadMediaViewModel { - return { - success: dto.success, - mediaId: dto.mediaId, - url: dto.url, - error: dto.error, - }; - } - - presentDelete(dto: DeleteMediaOutputDto): DeleteMediaViewModel { - return { - success: dto.success, - error: dto.error, - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/MembershipFeePresenter.ts b/apps/website/lib/presenters/MembershipFeePresenter.ts deleted file mode 100644 index 297983b7e..000000000 --- a/apps/website/lib/presenters/MembershipFeePresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MembershipFeeDto } from '../dtos'; -import { MembershipFeeViewModel } from '../view-models'; - -export const presentMembershipFee = (dto: MembershipFeeDto): MembershipFeeViewModel => { - return new MembershipFeeViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/PaymentListPresenter.ts b/apps/website/lib/presenters/PaymentListPresenter.ts deleted file mode 100644 index 5b665d7c4..000000000 --- a/apps/website/lib/presenters/PaymentListPresenter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { GetPaymentsOutputDto } from '../dtos'; -import { PaymentViewModel } from '../view-models'; -import { presentPayment } from './PaymentPresenter'; - -/** - * Payment List Presenter - * - * Transforms payment list DTOs into ViewModels for UI consumption. - */ -export class PaymentListPresenter { - /** - * Transform payment list DTO to ViewModels - */ - present(dto: GetPaymentsOutputDto): PaymentViewModel[] { - return dto.payments.map(payment => presentPayment(payment)); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/PaymentPresenter.ts b/apps/website/lib/presenters/PaymentPresenter.ts deleted file mode 100644 index 164d3fe46..000000000 --- a/apps/website/lib/presenters/PaymentPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PaymentDto } from '../dtos'; -import { PaymentViewModel } from '../view-models'; - -export const presentPayment = (dto: PaymentDto): PaymentViewModel => { - return new PaymentViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/PendingSponsorshipRequestsPresenter.ts b/apps/website/lib/presenters/PendingSponsorshipRequestsPresenter.ts deleted file mode 100644 index ad00cd58a..000000000 --- a/apps/website/lib/presenters/PendingSponsorshipRequestsPresenter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { - IPendingSponsorshipRequestsPresenter, - PendingSponsorshipRequestsViewModel, -} from '@core/racing/application/presenters/IPendingSponsorshipRequestsPresenter'; -import type { GetPendingSponsorshipRequestsResultDTO } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; - -export class PendingSponsorshipRequestsPresenter implements IPendingSponsorshipRequestsPresenter { - private viewModel: PendingSponsorshipRequestsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(data: GetPendingSponsorshipRequestsResultDTO): void { - this.viewModel = data; - } - - getViewModel(): PendingSponsorshipRequestsViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/PrizePresenter.ts b/apps/website/lib/presenters/PrizePresenter.ts deleted file mode 100644 index f39923b7d..000000000 --- a/apps/website/lib/presenters/PrizePresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PrizeDto } from '../dtos'; -import { PrizeViewModel } from '../view-models'; - -export const presentPrize = (dto: PrizeDto): PrizeViewModel => { - return new PrizeViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/ProfileOverviewPresenter.ts b/apps/website/lib/presenters/ProfileOverviewPresenter.ts deleted file mode 100644 index 0a9c68865..000000000 --- a/apps/website/lib/presenters/ProfileOverviewPresenter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { - IProfileOverviewPresenter, - ProfileOverviewViewModel, -} from '@core/racing/application/presenters/IProfileOverviewPresenter'; - -export class ProfileOverviewPresenter implements IProfileOverviewPresenter { - private viewModel: ProfileOverviewViewModel | null = null; - - present(viewModel: ProfileOverviewViewModel): void { - this.viewModel = viewModel; - } - - getViewModel(): ProfileOverviewViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceDetailPresenter.ts b/apps/website/lib/presenters/RaceDetailPresenter.ts deleted file mode 100644 index c47ff3bdc..000000000 --- a/apps/website/lib/presenters/RaceDetailPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { RaceDetailDto } from '../dtos'; -import { RaceDetailViewModel } from '../view-models'; - -export class RaceDetailPresenter { - present(dto: RaceDetailDto): RaceDetailViewModel { - return new RaceDetailViewModel(dto); - } -} - -export const presentRaceDetail = (dto: RaceDetailDto): RaceDetailViewModel => { - const presenter = new RaceDetailPresenter(); - return presenter.present(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceListItemPresenter.ts b/apps/website/lib/presenters/RaceListItemPresenter.ts deleted file mode 100644 index acfa86a98..000000000 --- a/apps/website/lib/presenters/RaceListItemPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RaceListItemDto } from '../dtos'; -import { RaceListItemViewModel } from '../view-models'; - -export const presentRaceListItem = (dto: RaceListItemDto): RaceListItemViewModel => { - return new RaceListItemViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RacePenaltiesPresenter.ts b/apps/website/lib/presenters/RacePenaltiesPresenter.ts deleted file mode 100644 index e7a7f8651..000000000 --- a/apps/website/lib/presenters/RacePenaltiesPresenter.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { - IRacePenaltiesPresenter, - RacePenaltyViewModel, - RacePenaltiesResultDTO, - RacePenaltiesViewModel, -} from '@core/racing/application/presenters/IRacePenaltiesPresenter'; - -export class RacePenaltiesPresenter implements IRacePenaltiesPresenter { - private viewModel: RacePenaltiesViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(dto: RacePenaltiesResultDTO): void { - const { penalties, driverMap } = dto; - - const penaltyViewModels: RacePenaltyViewModel[] = penalties.map((penalty) => { - const value = typeof penalty.value === 'number' ? penalty.value : 0; - const protestId = penalty.protestId; - const appliedAt = penalty.appliedAt ? penalty.appliedAt.toISOString() : undefined; - const notes = penalty.notes; - - const base: RacePenaltyViewModel = { - id: penalty.id, - raceId: penalty.raceId, - driverId: penalty.driverId, - driverName: driverMap.get(penalty.driverId) || 'Unknown', - type: penalty.type, - value, - reason: penalty.reason, - issuedBy: penalty.issuedBy, - issuedByName: driverMap.get(penalty.issuedBy) || 'Unknown', - status: penalty.status, - description: penalty.getDescription(), - issuedAt: penalty.issuedAt.toISOString(), - }; - - return { - ...base, - ...(protestId ? { protestId } : {}), - ...(appliedAt ? { appliedAt } : {}), - ...(typeof notes === 'string' && notes.length > 0 ? { notes } : {}), - }; - }); - - this.viewModel = { - penalties: penaltyViewModels, - }; - } - - getViewModel(): RacePenaltiesViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/RacePresenter.ts b/apps/website/lib/presenters/RacePresenter.ts deleted file mode 100644 index ec06e5393..000000000 --- a/apps/website/lib/presenters/RacePresenter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { RaceDetailViewModel } from '../view-models/RaceDetailViewModel'; -import type { RaceDetailDto } from '../dtos/RaceDetailDto'; -import type { RacesPageDataDto } from '../dtos/RacesPageDataDto'; -import type { RacesPageViewModel } from '../view-models/RacesPageViewModel'; - -/** - * Race Presenter - * - * Stateless presenter that transforms race DTOs into view models. - * All methods are pure functions with no side effects. - */ -export class RacePresenter { - presentRaceDetail(dto: RaceDetailDto): RaceDetailViewModel { - return new RaceDetailViewModel(dto); - } - - presentRacesPage(dto: RacesPageDataDto): RacesPageViewModel { - return { - upcomingRaces: dto.races.filter(r => r.isUpcoming).map(r => this.presentRaceCard(r)), - completedRaces: dto.races.filter(r => r.status === 'completed').map(r => this.presentRaceCard(r)), - totalCount: dto.races.length, - }; - } - - private presentRaceCard(race: any): any { - return { - id: race.id, - title: race.title || race.track, - scheduledTime: race.scheduledTime || race.scheduledAt, - status: this.formatStatus(race.status), - }; - } - - private formatStatus(status: string): string { - const statusMap: Record = { - scheduled: 'Scheduled', - running: 'Live', - completed: 'Finished', - cancelled: 'Cancelled', - }; - return statusMap[status] || status; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceProtestsPresenter.ts b/apps/website/lib/presenters/RaceProtestsPresenter.ts deleted file mode 100644 index ff2142c2a..000000000 --- a/apps/website/lib/presenters/RaceProtestsPresenter.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { - IRaceProtestsPresenter, - RaceProtestViewModel, - RaceProtestsResultDTO, - RaceProtestsViewModel, -} from '@core/racing/application/presenters/IRaceProtestsPresenter'; - -export class RaceProtestsPresenter implements IRaceProtestsPresenter { - private viewModel: RaceProtestsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(dto: RaceProtestsResultDTO): void { - const { protests, driverMap } = dto; - - const protestViewModels: RaceProtestViewModel[] = protests.map((protest) => { - const base: RaceProtestViewModel = { - id: protest.id, - raceId: protest.raceId, - protestingDriverId: protest.protestingDriverId, - protestingDriverName: driverMap.get(protest.protestingDriverId) || 'Unknown', - accusedDriverId: protest.accusedDriverId, - accusedDriverName: driverMap.get(protest.accusedDriverId) || 'Unknown', - incident: protest.incident, - filedAt: protest.filedAt.toISOString(), - status: protest.status, - }; - - const comment = protest.comment; - const proofVideoUrl = protest.proofVideoUrl; - const reviewedBy = protest.reviewedBy; - const reviewedByName = - protest.reviewedBy !== undefined - ? driverMap.get(protest.reviewedBy) ?? 'Unknown' - : undefined; - const decisionNotes = protest.decisionNotes; - const reviewedAt = protest.reviewedAt?.toISOString(); - - return { - ...base, - ...(comment !== undefined ? { comment } : {}), - ...(proofVideoUrl !== undefined ? { proofVideoUrl } : {}), - ...(reviewedBy !== undefined ? { reviewedBy } : {}), - ...(reviewedByName !== undefined ? { reviewedByName } : {}), - ...(decisionNotes !== undefined ? { decisionNotes } : {}), - ...(reviewedAt !== undefined ? { reviewedAt } : {}), - }; - }); - - this.viewModel = { - protests: protestViewModels, - }; - } - - getViewModel(): RaceProtestsViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceRegistrationsPresenter.ts b/apps/website/lib/presenters/RaceRegistrationsPresenter.ts deleted file mode 100644 index 472b3f636..000000000 --- a/apps/website/lib/presenters/RaceRegistrationsPresenter.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { - IRaceRegistrationsPresenter, - RaceRegistrationsViewModel, - RaceRegistrationsResultDTO, -} from '@core/racing/application/presenters/IRaceRegistrationsPresenter'; - -export class RaceRegistrationsPresenter implements IRaceRegistrationsPresenter { - private viewModel: RaceRegistrationsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(input: RaceRegistrationsResultDTO): void { - const { registeredDriverIds } = input; - this.viewModel = { - registeredDriverIds, - count: registeredDriverIds.length, - }; - } - - getViewModel(): RaceRegistrationsViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsDetailPresenter.ts b/apps/website/lib/presenters/RaceResultsDetailPresenter.ts deleted file mode 100644 index f632398c3..000000000 --- a/apps/website/lib/presenters/RaceResultsDetailPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { RaceResultsDetailDto } from '../dtos'; -import { RaceResultsDetailViewModel } from '../view-models'; - -export class RaceResultsDetailPresenter { - present(dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel { - return new RaceResultsDetailViewModel(dto, currentUserId); - } -} - -export const presentRaceResultsDetail = (dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel => { - const presenter = new RaceResultsDetailPresenter(); - return presenter.present(dto, currentUserId); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsPresenter.test.ts b/apps/website/lib/presenters/RaceResultsPresenter.test.ts deleted file mode 100644 index 567eb1da1..000000000 --- a/apps/website/lib/presenters/RaceResultsPresenter.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { presentRaceResult } from './RaceResultsPresenter'; -import { RaceResultViewModel } from '../view-models'; -import type { RaceResultRowDto } from '../dtos'; - -describe('RaceResultsPresenter', () => { - describe('presentRaceResult', () => { - it('should transform RaceResultRowDto into RaceResultViewModel', () => { - const dto: RaceResultRowDto = { - id: '1', - raceId: 'race-1', - driverId: 'driver-1', - position: 1, - fastestLap: 85.5, - incidents: 0, - startPosition: 2, - }; - - const result = presentRaceResult(dto); - - expect(result).toBeInstanceOf(RaceResultViewModel); - expect(result.id).toBe('1'); - expect(result.raceId).toBe('race-1'); - expect(result.driverId).toBe('driver-1'); - expect(result.position).toBe(1); - expect(result.fastestLap).toBe(85.5); - expect(result.incidents).toBe(0); - expect(result.startPosition).toBe(2); - }); - - it('should handle zero values correctly', () => { - const dto: RaceResultRowDto = { - id: '2', - raceId: 'race-2', - driverId: 'driver-2', - position: 5, - fastestLap: 0, - incidents: 3, - startPosition: 5, - }; - - const result = presentRaceResult(dto); - - expect(result).toBeInstanceOf(RaceResultViewModel); - expect(result.id).toBe('2'); - expect(result.position).toBe(5); - expect(result.fastestLap).toBe(0); - expect(result.incidents).toBe(3); - expect(result.startPosition).toBe(5); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceResultsPresenter.ts b/apps/website/lib/presenters/RaceResultsPresenter.ts deleted file mode 100644 index a60ec0490..000000000 --- a/apps/website/lib/presenters/RaceResultsPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RaceResultRowDto } from '../dtos'; -import { RaceResultViewModel } from '../view-models'; - -export const presentRaceResult = (dto: RaceResultRowDto): RaceResultViewModel => { - return new RaceResultViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/RaceWithSOFPresenter.ts b/apps/website/lib/presenters/RaceWithSOFPresenter.ts deleted file mode 100644 index ad85d2ce7..000000000 --- a/apps/website/lib/presenters/RaceWithSOFPresenter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { RaceWithSOFDto } from '../dtos/RaceWithSOFDto'; - -export interface RaceWithSOFViewModel { - id: string; - track: string; - strengthOfField: number | null; -} - -export class RaceWithSOFPresenter { - present(dto: RaceWithSOFDto): RaceWithSOFViewModel { - return { - id: dto.id, - track: dto.track, - strengthOfField: dto.strengthOfField, - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/RacesPagePresenter.ts b/apps/website/lib/presenters/RacesPagePresenter.ts deleted file mode 100644 index 2fa2a8cd9..000000000 --- a/apps/website/lib/presenters/RacesPagePresenter.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { - IRacesPagePresenter, - RacesPageViewModel, - RaceListItemViewModel, - RacesPageResultDTO, -} from '@core/racing/application/presenters/IRacesPagePresenter'; - -export class RacesPagePresenter implements IRacesPagePresenter { - private viewModel: RacesPageViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(input: RacesPageResultDTO): void { - const { races } = input; - const now = new Date(); - const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); - - const raceViewModels: RaceListItemViewModel[] = races.map((race) => { - const scheduledAt = - typeof race.scheduledAt === 'string' - ? race.scheduledAt - : race.scheduledAt.toISOString(); - - const allowedStatuses: RaceListItemViewModel['status'][] = [ - 'scheduled', - 'running', - 'completed', - 'cancelled', - ]; - - const status: RaceListItemViewModel['status'] = - allowedStatuses.includes(race.status as RaceListItemViewModel['status']) - ? (race.status as RaceListItemViewModel['status']) - : 'scheduled'; - - return { - id: race.id, - track: race.track, - car: race.car, - scheduledAt, - status, - leagueId: race.leagueId, - leagueName: race.leagueName, - strengthOfField: race.strengthOfField, - isUpcoming: race.isUpcoming, - isLive: race.isLive, - isPast: race.isPast, - }; - }); - - const stats = { - total: raceViewModels.length, - scheduled: raceViewModels.filter(r => r.status === 'scheduled').length, - running: raceViewModels.filter(r => r.status === 'running').length, - completed: raceViewModels.filter(r => r.status === 'completed').length, - }; - - const liveRaces = raceViewModels.filter(r => r.isLive); - - const upcomingThisWeek = raceViewModels - .filter(r => { - const scheduledDate = new Date(r.scheduledAt); - return r.isUpcoming && scheduledDate >= now && scheduledDate <= nextWeek; - }) - .slice(0, 5); - - const recentResults = raceViewModels - .filter(r => r.status === 'completed') - .sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime()) - .slice(0, 3); - - this.viewModel = { - races: raceViewModels, - stats, - liveRaces, - upcomingThisWeek, - recentResults, - }; - } - - getViewModel(): RacesPageViewModel { - if (!this.viewModel) { - throw new Error('ViewModel not yet generated. Call present() first.'); - } - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/ScheduleRaceFormPresenter.ts b/apps/website/lib/presenters/ScheduleRaceFormPresenter.ts deleted file mode 100644 index e011e636e..000000000 --- a/apps/website/lib/presenters/ScheduleRaceFormPresenter.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * ScheduleRaceFormPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ - -import { apiClient } from '@/lib/apiClient'; - -export type SessionType = 'practice' | 'qualifying' | 'race'; - -export interface ScheduleRaceFormData { - leagueId: string; - track: string; - car: string; - sessionType: SessionType; - scheduledDate: string; - scheduledTime: string; -} - -export interface ScheduledRaceViewModel { - id: string; - leagueId: string; - track: string; - car: string; - sessionType: SessionType; - scheduledAt: Date; - status: string; -} - -export interface LeagueOptionViewModel { - id: string; - name: string; -} - -/** - * Load available leagues for the schedule form. - */ -export async function loadScheduleRaceFormLeagues(): Promise { - const response = await apiClient.leagues.getAllWithCapacity(); - return response.leagues.map((league) => ({ - id: league.id, - name: league.name, - })); -} - -/** - * Schedule a race via API. - * Note: This would need a dedicated API endpoint for race scheduling. - * For now, this is a placeholder that shows the expected interface. - */ -export async function scheduleRaceFromForm( - formData: ScheduleRaceFormData -): Promise { - const scheduledAt = new Date(`${formData.scheduledDate}T${formData.scheduledTime}`); - - // In the new architecture, race scheduling should be done via API - // This is a placeholder that returns expected data structure - // The API endpoint would need to be implemented: POST /races - - // For now, return a mock response - // TODO: Replace with actual API call when race creation endpoint is available - return { - id: `race-${Date.now()}`, - leagueId: formData.leagueId, - track: formData.track.trim(), - car: formData.car.trim(), - sessionType: formData.sessionType, - scheduledAt, - status: 'scheduled', - }; -} \ No newline at end of file diff --git a/apps/website/lib/presenters/SessionPresenter.ts b/apps/website/lib/presenters/SessionPresenter.ts deleted file mode 100644 index 7341fa99e..000000000 --- a/apps/website/lib/presenters/SessionPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { SessionDataDto } from '../dtos'; -import { SessionViewModel } from '../view-models'; - -/** - * Session Presenter - * Transforms session DTOs to ViewModels - */ -export class SessionPresenter { - presentSession(dto: SessionDataDto | null): SessionViewModel | null { - if (!dto) return null; - return new SessionViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorDashboardPresenter.ts b/apps/website/lib/presenters/SponsorDashboardPresenter.ts deleted file mode 100644 index 49694ed10..000000000 --- a/apps/website/lib/presenters/SponsorDashboardPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { SponsorDashboardDto } from '../dtos'; -import { SponsorDashboardViewModel } from '../view-models/SponsorDashboardViewModel'; - -/** - * Sponsor Dashboard Presenter - * - * Transforms sponsor dashboard DTOs into view models. - */ -export class SponsorDashboardPresenter { - present(dto: SponsorDashboardDto): SponsorDashboardViewModel { - return new SponsorDashboardViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorListPresenter.ts b/apps/website/lib/presenters/SponsorListPresenter.ts deleted file mode 100644 index 8c9f9f719..000000000 --- a/apps/website/lib/presenters/SponsorListPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { GetSponsorsOutputDto } from '../dtos'; -import { SponsorViewModel } from '../view-models'; - -/** - * Sponsor List Presenter - * - * Transforms sponsor list DTOs into view models. - */ -export class SponsorListPresenter { - present(dto: GetSponsorsOutputDto): SponsorViewModel[] { - return dto.sponsors.map(sponsor => new SponsorViewModel(sponsor)); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorPresenter.ts b/apps/website/lib/presenters/SponsorPresenter.ts deleted file mode 100644 index 4b9073c4e..000000000 --- a/apps/website/lib/presenters/SponsorPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { SponsorDto } from '../dtos'; -import { SponsorViewModel } from '../view-models'; - -/** - * Sponsor Presenter - * - * Transforms sponsor DTOs into view models. - */ -export class SponsorPresenter { - present(dto: SponsorDto): SponsorViewModel { - return new SponsorViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts b/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts deleted file mode 100644 index 4a6b2bec1..000000000 --- a/apps/website/lib/presenters/SponsorSponsorshipsPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { SponsorSponsorshipsDto } from '../dtos'; -import { SponsorSponsorshipsViewModel } from '../view-models/SponsorSponsorshipsViewModel'; - -/** - * Sponsor Sponsorships Presenter - * - * Transforms sponsor sponsorships DTOs into view models. - */ -export class SponsorSponsorshipsPresenter { - present(dto: SponsorSponsorshipsDto): SponsorSponsorshipsViewModel { - return new SponsorSponsorshipsViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorshipDetailPresenter.ts b/apps/website/lib/presenters/SponsorshipDetailPresenter.ts deleted file mode 100644 index bb548db11..000000000 --- a/apps/website/lib/presenters/SponsorshipDetailPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SponsorshipDetailDto } from '../dtos'; -import { SponsorshipDetailViewModel } from '../view-models'; - -export const presentSponsorshipDetail = (dto: SponsorshipDetailDto): SponsorshipDetailViewModel => { - return new SponsorshipDetailViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/SponsorshipPricingPresenter.ts b/apps/website/lib/presenters/SponsorshipPricingPresenter.ts deleted file mode 100644 index 2edd97109..000000000 --- a/apps/website/lib/presenters/SponsorshipPricingPresenter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { GetEntitySponsorshipPricingResultDto } from '../dtos'; -import { SponsorshipPricingViewModel } from '../view-models/SponsorshipPricingViewModel'; - -/** - * Sponsorship Pricing Presenter - * - * Transforms sponsorship pricing DTOs into view models. - */ -export class SponsorshipPricingPresenter { - present(dto: GetEntitySponsorshipPricingResultDto): SponsorshipPricingViewModel { - return new SponsorshipPricingViewModel(dto); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamAdminPresenter.ts b/apps/website/lib/presenters/TeamAdminPresenter.ts deleted file mode 100644 index c6c31c38c..000000000 --- a/apps/website/lib/presenters/TeamAdminPresenter.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * TeamAdminPresenter - Pure data transformer - * Transforms API responses to view models without DI dependencies. - * All data fetching is done via apiClient. - */ - -import { apiClient } from '@/lib/apiClient'; -import type { DriverDTO } from '@/lib/apiClient'; - -// ============================================================================ -// View Model Types -// ============================================================================ - -export interface TeamAdminJoinRequestViewModel { - id: string; - teamId: string; - driverId: string; - requestedAt: Date; - message?: string | undefined; - driver?: DriverDTO | undefined; -} - -export interface TeamAdminTeamSummaryViewModel { - id: string; - name: string; - tag: string; - description: string; - ownerId: string; -} - -export interface TeamAdminViewModel { - team: TeamAdminTeamSummaryViewModel; - requests: TeamAdminJoinRequestViewModel[]; -} - -// ============================================================================ -// Data Fetching Functions (using apiClient) -// ============================================================================ - -/** - * Load team admin view model via API. - */ -export async function loadTeamAdminViewModel( - team: TeamAdminTeamSummaryViewModel -): Promise { - const requests = await loadTeamJoinRequests(team.id); - return { - team: { - id: team.id, - name: team.name, - tag: team.tag, - description: team.description, - ownerId: team.ownerId, - }, - requests, - }; -} - -/** - * Load join requests for a team via API. - */ -export async function loadTeamJoinRequests( - teamId: string -): Promise { - const response = await apiClient.teams.getJoinRequests(teamId); - - return response.requests.map((req) => { - const viewModel: TeamAdminJoinRequestViewModel = { - id: req.id, - teamId: req.teamId, - driverId: req.driverId, - requestedAt: new Date(req.requestedAt), - }; - - if (req.message) { - viewModel.message = req.message; - } - - return viewModel; - }); -} - -/** - * Approve a team join request and return updated request view models. - */ -export async function approveTeamJoinRequestAndReload( - requestId: string, - teamId: string -): Promise { - await apiClient.teams.approveJoinRequest(teamId, requestId); - return loadTeamJoinRequests(teamId); -} - -/** - * Reject a team join request and return updated request view models. - */ -export async function rejectTeamJoinRequestAndReload( - requestId: string, - teamId: string -): Promise { - await apiClient.teams.rejectJoinRequest(teamId, requestId); - return loadTeamJoinRequests(teamId); -} - -/** - * Update team basic details via API. - */ -export async function updateTeamDetails(params: { - teamId: string; - name: string; - tag: string; - description: string; - updatedByDriverId: string; -}): Promise { - await apiClient.teams.update(params.teamId, { - name: params.name, - description: params.description, - }); -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamDetailsPresenter.ts b/apps/website/lib/presenters/TeamDetailsPresenter.ts deleted file mode 100644 index 8635da63c..000000000 --- a/apps/website/lib/presenters/TeamDetailsPresenter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TeamDetailsDto } from '../dtos'; -import { TeamDetailsViewModel } from '../view-models'; - -/** - * Team Details Presenter - * Transforms TeamDetailsDto to TeamDetailsViewModel - */ -export class TeamDetailsPresenter { - present(dto: TeamDetailsDto, currentUserId: string): TeamDetailsViewModel { - return new TeamDetailsViewModel(dto, currentUserId); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamJoinRequestPresenter.ts b/apps/website/lib/presenters/TeamJoinRequestPresenter.ts deleted file mode 100644 index cb523e814..000000000 --- a/apps/website/lib/presenters/TeamJoinRequestPresenter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { TeamJoinRequestItemDto } from '../dtos'; -import { TeamJoinRequestViewModel } from '../view-models'; - -/** - * Team Join Request Presenter - * Transforms TeamJoinRequestItemDto to TeamJoinRequestViewModel - */ -export class TeamJoinRequestPresenter { - present(dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean): TeamJoinRequestViewModel { - return new TeamJoinRequestViewModel(dto, currentUserId, isOwner); - } -} - -// Backward compatibility export (deprecated) -export const presentTeamJoinRequest = (dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean): TeamJoinRequestViewModel => { - const presenter = new TeamJoinRequestPresenter(); - return presenter.present(dto, currentUserId, isOwner); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamJoinRequestsPresenter.ts b/apps/website/lib/presenters/TeamJoinRequestsPresenter.ts deleted file mode 100644 index bfc5aa926..000000000 --- a/apps/website/lib/presenters/TeamJoinRequestsPresenter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { - ITeamJoinRequestsPresenter, - TeamJoinRequestViewModel, - TeamJoinRequestsViewModel, - TeamJoinRequestsResultDTO, -} from '@core/racing/application/presenters/ITeamJoinRequestsPresenter'; - -export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter { - private viewModel: TeamJoinRequestsViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(input: TeamJoinRequestsResultDTO): void { - const requestItems: TeamJoinRequestViewModel[] = input.requests.map((request) => ({ - requestId: request.id, - driverId: request.driverId, - driverName: input.driverNames[request.driverId] ?? 'Unknown Driver', - teamId: request.teamId, - status: 'pending', - requestedAt: request.requestedAt.toISOString(), - avatarUrl: input.avatarUrls[request.driverId] ?? '', - })); - - const pendingCount = requestItems.filter((r) => r.status === 'pending').length; - - this.viewModel = { - requests: requestItems, - pendingCount, - totalCount: requestItems.length, - }; - } - - getViewModel(): TeamJoinRequestsViewModel | null { - return this.viewModel; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamListPresenter.ts b/apps/website/lib/presenters/TeamListPresenter.ts deleted file mode 100644 index bb850dfda..000000000 --- a/apps/website/lib/presenters/TeamListPresenter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AllTeamsDto } from '../dtos'; -import { TeamSummaryViewModel } from '../view-models'; - -/** - * Team List Presenter - * Transforms AllTeamsDto to array of TeamSummaryViewModel - */ -export class TeamListPresenter { - present(dto: AllTeamsDto): TeamSummaryViewModel[] { - return dto.teams.map(team => new TeamSummaryViewModel(team)); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamMemberPresenter.ts b/apps/website/lib/presenters/TeamMemberPresenter.ts deleted file mode 100644 index fac61a0a5..000000000 --- a/apps/website/lib/presenters/TeamMemberPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TeamMemberDto } from '../dtos'; -import { TeamMemberViewModel } from '../view-models'; - -export const presentTeamMember = (dto: TeamMemberDto, currentUserId: string, teamOwnerId: string): TeamMemberViewModel => { - return new TeamMemberViewModel(dto, currentUserId, teamOwnerId); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamMembersPresenter.ts b/apps/website/lib/presenters/TeamMembersPresenter.ts deleted file mode 100644 index 39a499937..000000000 --- a/apps/website/lib/presenters/TeamMembersPresenter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TeamMembersDto } from '../dtos'; -import { TeamMemberViewModel } from '../view-models'; - -/** - * Team Members Presenter - * Transforms TeamMembersDto to array of TeamMemberViewModel - */ -export class TeamMembersPresenter { - present(dto: TeamMembersDto, currentUserId: string, teamOwnerId: string): TeamMemberViewModel[] { - return dto.members.map(member => new TeamMemberViewModel(member, currentUserId, teamOwnerId)); - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamRosterPresenter.ts b/apps/website/lib/presenters/TeamRosterPresenter.ts deleted file mode 100644 index b2b07043f..000000000 --- a/apps/website/lib/presenters/TeamRosterPresenter.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * TeamRosterPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ - -import { apiClient } from '@/lib/apiClient'; - -export type TeamRole = 'owner' | 'manager' | 'driver' | 'member'; - -export interface DriverDTO { - id: string; - name: string; - avatarUrl?: string | undefined; - country?: string | undefined; -} - -export interface TeamRosterMemberViewModel { - driver: DriverDTO; - role: TeamRole; - joinedAt: string; - rating: number | null; - overallRank: number | null; -} - -export interface TeamRosterViewModel { - members: TeamRosterMemberViewModel[]; - averageRating: number; -} - -/** - * Fetch team roster via API and transform to view model. - */ -export async function getTeamRosterViewModel( - teamId: string -): Promise { - const response = await apiClient.teams.getMembers(teamId); - - const members: TeamRosterMemberViewModel[] = response.members.map((member) => ({ - driver: { - id: member.driverId, - name: member.driver?.name ?? 'Unknown', - avatarUrl: member.driver?.avatarUrl, - }, - role: (member.role as TeamRole) ?? 'member', - joinedAt: member.joinedAt, - rating: null, // Would need from API - overallRank: null, // Would need from API - })); - - const averageRating = - members.length > 0 - ? Math.round( - members.reduce((sum, m) => sum + (m.rating ?? 0), 0) / members.length - ) - : 0; - - return { - members, - averageRating, - }; -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamStandingsPresenter.ts b/apps/website/lib/presenters/TeamStandingsPresenter.ts deleted file mode 100644 index 2cd687aea..000000000 --- a/apps/website/lib/presenters/TeamStandingsPresenter.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * TeamStandingsPresenter - Pure data transformer - * Transforms API response to view model without DI dependencies. - */ - -import { apiClient } from '@/lib/apiClient'; - -export interface TeamLeagueStandingViewModel { - leagueId: string; - leagueName: string; - position: number; - points: number; - wins: number; - racesCompleted: number; -} - -export interface TeamStandingsViewModel { - standings: TeamLeagueStandingViewModel[]; -} - -/** - * Compute team standings across the given leagues for a team. - * This would need a dedicated API endpoint for team standings. - * For now, returns empty standings - the API should provide this data. - * @param teamId - The team ID (will be used when API supports team standings) - * @param leagueIds - List of league IDs to fetch standings for - */ -export async function loadTeamStandings( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - teamId: string, - leagueIds: string[], -): Promise { - // In the new architecture, team standings should come from API - // For now, fetch each league's standings and aggregate - const teamStandings: TeamLeagueStandingViewModel[] = []; - - for (const leagueId of leagueIds) { - try { - const standings = await apiClient.leagues.getStandings(leagueId); - - // Since we don't have team-specific standings from API yet, - // this is a placeholder that returns basic data - teamStandings.push({ - leagueId, - leagueName: `League ${leagueId}`, // Would need from API - position: 0, - points: 0, - wins: 0, - racesCompleted: standings.standings.length > 0 ? 1 : 0, - }); - } catch { - // Skip leagues that fail to load - } - } - - return { standings: teamStandings }; -} \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamSummaryPresenter.ts b/apps/website/lib/presenters/TeamSummaryPresenter.ts deleted file mode 100644 index ee655601f..000000000 --- a/apps/website/lib/presenters/TeamSummaryPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TeamSummaryDto } from '../dtos'; -import { TeamSummaryViewModel } from '../view-models'; - -export const presentTeamSummary = (dto: TeamSummaryDto): TeamSummaryViewModel => { - return new TeamSummaryViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/TeamsLeaderboardPresenter.ts b/apps/website/lib/presenters/TeamsLeaderboardPresenter.ts deleted file mode 100644 index b6a53ede4..000000000 --- a/apps/website/lib/presenters/TeamsLeaderboardPresenter.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { - ITeamsLeaderboardPresenter, - TeamsLeaderboardViewModel, - TeamLeaderboardItemViewModel, - SkillLevel, - TeamsLeaderboardResultDTO, -} from '@core/racing/application/presenters/ITeamsLeaderboardPresenter'; - -interface TeamLeaderboardInput { - id: string; - name: string; - memberCount: number; - rating: number | null; - totalWins: number; - totalRaces: number; - performanceLevel: SkillLevel; - isRecruiting: boolean; - createdAt: Date; - description?: string | null; - specialization?: string | null; - region?: string | null; - languages?: string[]; -} - -export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter { - private viewModel: TeamsLeaderboardViewModel | null = null; - - reset(): void { - this.viewModel = null; - } - - present(input: TeamsLeaderboardResultDTO): void { - const teams = (input.teams ?? []) as TeamLeaderboardInput[]; - const recruitingCount = input.recruitingCount ?? 0; - - const transformedTeams = teams.map((team) => this.transformTeam(team)); - - const groupsBySkillLevel = transformedTeams.reduce>( - (acc, team) => { - if (!acc[team.performanceLevel]) { - acc[team.performanceLevel] = []; - } - acc[team.performanceLevel]!.push(team); - return acc; - }, - { - beginner: [], - intermediate: [], - advanced: [], - pro: [], - }, - ); - - const topTeams = transformedTeams - .filter((t) => t.rating !== null) - .slice() - .sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)) - .slice(0, 5); - - this.viewModel = { - teams: transformedTeams, - recruitingCount, - groupsBySkillLevel, - topTeams, - }; - } - - getViewModel(): TeamsLeaderboardViewModel | null { - return this.viewModel; - } - - private transformTeam(team: TeamLeaderboardInput): TeamLeaderboardItemViewModel { - let specialization: TeamLeaderboardItemViewModel['specialization']; - if ( - team.specialization === 'endurance' || - team.specialization === 'sprint' || - team.specialization === 'mixed' - ) { - specialization = team.specialization; - } else { - specialization = undefined; - } - - return { - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt, - description: team.description ?? '', - specialization: specialization ?? 'mixed', - region: team.region ?? '', - languages: team.languages ?? [], - }; - } -} \ No newline at end of file diff --git a/apps/website/lib/presenters/WalletPresenter.ts b/apps/website/lib/presenters/WalletPresenter.ts deleted file mode 100644 index 40d3cb6dc..000000000 --- a/apps/website/lib/presenters/WalletPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WalletDto, WalletTransactionDto } from '../dtos'; -import { WalletViewModel } from '../view-models'; - -export const presentWallet = (dto: WalletDto): WalletViewModel => { - return new WalletViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/presenters/WalletTransactionPresenter.ts b/apps/website/lib/presenters/WalletTransactionPresenter.ts deleted file mode 100644 index 5508978d6..000000000 --- a/apps/website/lib/presenters/WalletTransactionPresenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WalletTransactionDto } from '../dtos'; -import { WalletTransactionViewModel } from '../view-models'; - -export const presentWalletTransaction = (dto: WalletTransactionDto): WalletTransactionViewModel => { - return new WalletTransactionViewModel(dto); -}; \ No newline at end of file diff --git a/apps/website/lib/services/ServiceFactory.test.ts b/apps/website/lib/services/ServiceFactory.test.ts deleted file mode 100644 index 2f289e2a2..000000000 --- a/apps/website/lib/services/ServiceFactory.test.ts +++ /dev/null @@ -1,517 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { ServiceFactory } from './ServiceFactory'; - -// Mock API clients -vi.mock('../api/races/RacesApiClient', () => ({ - RacesApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/drivers/DriversApiClient', () => ({ - DriversApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/teams/TeamsApiClient', () => ({ - TeamsApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/leagues/LeaguesApiClient', () => ({ - LeaguesApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/sponsors/SponsorsApiClient', () => ({ - SponsorsApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/payments/PaymentsApiClient', () => ({ - PaymentsApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/auth/AuthApiClient', () => ({ - AuthApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/analytics/AnalyticsApiClient', () => ({ - AnalyticsApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -vi.mock('../api/media/MediaApiClient', () => ({ - MediaApiClient: class { - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - }, -})); - -// Mock presenters -vi.mock('../presenters/RaceDetailPresenter', () => ({ - RaceDetailPresenter: class {}, -})); - -vi.mock('../presenters/RaceResultsDetailPresenter', () => ({ - RaceResultsDetailPresenter: class {}, -})); - -vi.mock('../presenters/RaceWithSOFPresenter', () => ({ - RaceWithSOFPresenter: class {}, -})); - -vi.mock('../presenters/ImportRaceResultsPresenter', () => ({ - ImportRaceResultsPresenter: class {}, -})); - -vi.mock('../presenters/DriversLeaderboardPresenter', () => ({ - DriversLeaderboardPresenter: class {}, -})); - -vi.mock('../presenters/DriverPresenter', () => ({ - DriverPresenter: class {}, -})); - -vi.mock('../presenters/CompleteOnboardingPresenter', () => ({ - CompleteOnboardingPresenter: class {}, -})); - -vi.mock('../presenters/DriverRegistrationStatusPresenter', () => ({ - DriverRegistrationStatusPresenter: class {}, -})); - -vi.mock('../presenters/TeamDetailsPresenter', () => ({ - TeamDetailsPresenter: class {}, -})); - -vi.mock('../presenters/TeamListPresenter', () => ({ - TeamListPresenter: class {}, -})); - -vi.mock('../presenters/TeamMembersPresenter', () => ({ - TeamMembersPresenter: class {}, -})); - -vi.mock('../presenters/TeamJoinRequestPresenter', () => ({ - TeamJoinRequestPresenter: class {}, -})); - -vi.mock('../presenters/LeagueSummaryPresenter', () => ({ - LeagueSummaryPresenter: class {}, -})); - -vi.mock('../presenters/LeagueStandingsPresenter', () => ({ - LeagueStandingsPresenter: class {}, -})); - -vi.mock('../presenters/LeagueMembersPresenter', () => ({ - LeagueMembersPresenter: class {}, -})); - -vi.mock('../presenters/SponsorListPresenter', () => ({ - SponsorListPresenter: class {}, -})); - -vi.mock('../presenters/SponsorDashboardPresenter', () => ({ - SponsorDashboardPresenter: class {}, -})); - -vi.mock('../presenters/SponsorSponsorshipsPresenter', () => ({ - SponsorSponsorshipsPresenter: class {}, -})); - -vi.mock('../presenters/SponsorshipPricingPresenter', () => ({ - SponsorshipPricingPresenter: class {}, -})); - -vi.mock('../presenters/PaymentListPresenter', () => ({ - PaymentListPresenter: class {}, -})); - -vi.mock('../presenters/AnalyticsDashboardPresenter', () => ({ - AnalyticsDashboardPresenter: class {}, -})); - -vi.mock('../presenters/AnalyticsMetricsPresenter', () => ({ - AnalyticsMetricsPresenter: class {}, -})); - -vi.mock('../presenters/MediaPresenter', () => ({ - MediaPresenter: class {}, -})); - -vi.mock('../presenters/AvatarPresenter', () => ({ - AvatarPresenter: class {}, -})); - -vi.mock('../presenters/SessionPresenter', () => ({ - SessionPresenter: class {}, -})); - -vi.mock('../presenters/AnalyticsDashboardPresenter', () => ({ - AnalyticsDashboardPresenter: class {}, -})); - -vi.mock('../presenters/AnalyticsMetricsPresenter', () => ({ - AnalyticsMetricsPresenter: class {}, -})); - -vi.mock('../presenters/MediaPresenter', () => ({ - MediaPresenter: class {}, -})); - -vi.mock('../presenters/AvatarPresenter', () => ({ - AvatarPresenter: class {}, -})); - -vi.mock('../presenters/SessionPresenter', () => ({ - SessionPresenter: class {}, -})); - -vi.mock('../presenters', () => ({ - presentPayment: vi.fn(), - presentMembershipFee: vi.fn(), - presentPrize: vi.fn(), - presentWallet: vi.fn(), -})); - -// Mock services -vi.mock('./races/RaceService', () => ({ - RaceService: class { - constructor(...args: any[]) { - return { type: 'RaceService', args }; - } - }, -})); - -vi.mock('./races/RaceResultsService', () => ({ - RaceResultsService: class { - constructor(...args: any[]) { - return { type: 'RaceResultsService', args }; - } - }, -})); - -vi.mock('./drivers/DriverService', () => ({ - DriverService: class { - constructor(...args: any[]) { - return { type: 'DriverService', args }; - } - }, -})); - -vi.mock('./drivers/DriverRegistrationService', () => ({ - DriverRegistrationService: class { - constructor(...args: any[]) { - return { type: 'DriverRegistrationService', args }; - } - }, -})); - -vi.mock('./teams/TeamService', () => ({ - TeamService: class { - constructor(...args: any[]) { - return { type: 'TeamService', args }; - } - }, -})); - -vi.mock('./teams/TeamJoinService', () => ({ - TeamJoinService: class { - constructor(...args: any[]) { - return { type: 'TeamJoinService', args }; - } - }, -})); - -vi.mock('./leagues/LeagueService', () => ({ - LeagueService: class { - constructor(...args: any[]) { - return { type: 'LeagueService', args }; - } - }, -})); - -vi.mock('./leagues/LeagueMembershipService', () => ({ - LeagueMembershipService: class { - constructor(...args: any[]) { - return { type: 'LeagueMembershipService', args }; - } - }, -})); - -vi.mock('./sponsors/SponsorService', () => ({ - SponsorService: class { - constructor(...args: any[]) { - return { type: 'SponsorService', args }; - } - }, -})); - -vi.mock('./sponsors/SponsorshipService', () => ({ - SponsorshipService: class { - constructor(...args: any[]) { - return { type: 'SponsorshipService', args }; - } - }, -})); - -vi.mock('./payments/PaymentService', () => ({ - PaymentService: class { - constructor(...args: any[]) { - return { type: 'PaymentService', args }; - } - }, -})); - -vi.mock('./analytics/AnalyticsService', () => ({ - AnalyticsService: class { - constructor(...args: any[]) { - return { type: 'AnalyticsService', args }; - } - }, -})); - -vi.mock('./analytics/DashboardService', () => ({ - DashboardService: class { - constructor(...args: any[]) { - return { type: 'DashboardService', args }; - } - }, -})); - -vi.mock('./media/MediaService', () => ({ - MediaService: class { - constructor(...args: any[]) { - return { type: 'MediaService', args }; - } - }, -})); - -vi.mock('./media/AvatarService', () => ({ - AvatarService: class { - constructor(...args: any[]) { - return { type: 'AvatarService', args }; - } - }, -})); - -vi.mock('./payments/WalletService', () => ({ - WalletService: class { - constructor(...args: any[]) { - return { type: 'WalletService', args }; - } - }, -})); - -vi.mock('./payments/MembershipFeeService', () => ({ - MembershipFeeService: class { - constructor(...args: any[]) { - return { type: 'MembershipFeeService', args }; - } - }, -})); - -vi.mock('./auth/AuthService', () => ({ - AuthService: class { - constructor(...args: any[]) { - return { type: 'AuthService', args }; - } - }, -})); - -vi.mock('./auth/SessionService', () => ({ - SessionService: class { - constructor(...args: any[]) { - return { type: 'SessionService', args }; - } - }, -})); - -describe('ServiceFactory', () => { - let factory: ServiceFactory; - - beforeEach(() => { - vi.clearAllMocks(); - factory = new ServiceFactory('http://test-api.com'); - }); - - it('should create RaceService with correct dependencies', () => { - const service = factory.createRaceService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('RaceService'); - expect(service.args).toHaveLength(2); - }); - - it('should create RaceResultsService with correct dependencies', () => { - const service = factory.createRaceResultsService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('RaceResultsService'); - expect(service.args).toHaveLength(4); - }); - - it('should create DriverService with correct dependencies', () => { - const service = factory.createDriverService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('DriverService'); - expect(service.args).toHaveLength(4); - }); - - it('should create DriverRegistrationService with correct dependencies', () => { - const service = factory.createDriverRegistrationService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('DriverRegistrationService'); - expect(service.args).toHaveLength(2); - }); - - it('should create TeamService with correct dependencies', () => { - const service = factory.createTeamService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('TeamService'); - expect(service.args).toHaveLength(4); - }); - - it('should create TeamJoinService with correct dependencies', () => { - const service = factory.createTeamJoinService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('TeamJoinService'); - expect(service.args).toHaveLength(2); - }); - - it('should create LeagueService with correct dependencies', () => { - const service = factory.createLeagueService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('LeagueService'); - expect(service.args).toHaveLength(3); - }); - - it('should create LeagueMembershipService with correct dependencies', () => { - const service = factory.createLeagueMembershipService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('LeagueMembershipService'); - expect(service.args).toHaveLength(2); - }); - - it('should create SponsorService with correct dependencies', () => { - const service = factory.createSponsorService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('SponsorService'); - expect(service.args).toHaveLength(4); - }); - - it('should create SponsorshipService with correct dependencies', () => { - const service = factory.createSponsorshipService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('SponsorshipService'); - expect(service.args).toHaveLength(3); - }); - - it('should create PaymentService with correct dependencies', () => { - const service = factory.createPaymentService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('PaymentService'); - expect(service.args).toHaveLength(6); - }); - - it('should create AnalyticsService with correct dependencies', () => { - const service = factory.createAnalyticsService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('AnalyticsService'); - expect(service.args).toHaveLength(1); - }); - - it('should create DashboardService with correct dependencies', () => { - const service = factory.createDashboardService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('DashboardService'); - expect(service.args).toHaveLength(3); - }); - - it('should create MediaService with correct dependencies', () => { - const service = factory.createMediaService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('MediaService'); - expect(service.args).toHaveLength(2); - }); - - it('should create AvatarService with correct dependencies', () => { - const service = factory.createAvatarService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('AvatarService'); - expect(service.args).toHaveLength(2); - }); - - it('should create WalletService with correct dependencies', () => { - const service = factory.createWalletService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('WalletService'); - expect(service.args).toHaveLength(1); - }); - - it('should create MembershipFeeService with correct dependencies', () => { - const service = factory.createMembershipFeeService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('MembershipFeeService'); - expect(service.args).toHaveLength(1); - }); - - it('should create AuthService with correct dependencies', () => { - const service = factory.createAuthService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('AuthService'); - expect(service.args).toHaveLength(1); - }); - - it('should create SessionService with correct dependencies', () => { - const service = factory.createSessionService(); - - expect(service).toBeDefined(); - expect(service.type).toBe('SessionService'); - expect(service.args).toHaveLength(2); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/ServiceFactory.ts b/apps/website/lib/services/ServiceFactory.ts index 3f87db761..3222c91ea 100644 --- a/apps/website/lib/services/ServiceFactory.ts +++ b/apps/website/lib/services/ServiceFactory.ts @@ -29,42 +29,12 @@ import { MembershipFeeService } from './payments/MembershipFeeService'; import { AuthService } from './auth/AuthService'; import { SessionService } from './auth/SessionService'; -// Presenters -import { RaceDetailPresenter } from '../presenters/RaceDetailPresenter'; -import { RaceResultsDetailPresenter } from '../presenters/RaceResultsDetailPresenter'; -import { RaceWithSOFPresenter } from '../presenters/RaceWithSOFPresenter'; -import { ImportRaceResultsPresenter } from '../presenters/ImportRaceResultsPresenter'; -import { DriversLeaderboardPresenter } from '../presenters/DriversLeaderboardPresenter'; -import { DriverPresenter } from '../presenters/DriverPresenter'; -import { CompleteOnboardingPresenter } from '../presenters/CompleteOnboardingPresenter'; -import { DriverRegistrationStatusPresenter } from '../presenters/DriverRegistrationStatusPresenter'; -import { TeamDetailsPresenter } from '../presenters/TeamDetailsPresenter'; -import { TeamListPresenter } from '../presenters/TeamListPresenter'; -import { TeamMembersPresenter } from '../presenters/TeamMembersPresenter'; -import { TeamJoinRequestPresenter } from '../presenters/TeamJoinRequestPresenter'; -import { LeagueSummaryPresenter } from '../presenters/LeagueSummaryPresenter'; -import { LeagueStandingsPresenter } from '../presenters/LeagueStandingsPresenter'; -import { LeagueMembersPresenter } from '../presenters/LeagueMembersPresenter'; -import { SponsorListPresenter } from '../presenters/SponsorListPresenter'; -import { SponsorDashboardPresenter } from '../presenters/SponsorDashboardPresenter'; -import { SponsorSponsorshipsPresenter } from '../presenters/SponsorSponsorshipsPresenter'; -import { SponsorshipPricingPresenter } from '../presenters/SponsorshipPricingPresenter'; -import { PaymentListPresenter } from '../presenters/PaymentListPresenter'; -import { presentPayment } from '../presenters/PaymentPresenter'; -import { presentMembershipFee } from '../presenters/MembershipFeePresenter'; -import { presentPrize } from '../presenters/PrizePresenter'; -import { presentWallet } from '../presenters/WalletPresenter'; -import { AnalyticsDashboardPresenter } from '../presenters/AnalyticsDashboardPresenter'; -import { AnalyticsMetricsPresenter } from '../presenters/AnalyticsMetricsPresenter'; -import { MediaPresenter } from '../presenters/MediaPresenter'; -import { AvatarPresenter } from '../presenters/AvatarPresenter'; -import { SessionPresenter } from '../presenters/SessionPresenter'; - /** * ServiceFactory - Composition root for all services * * Centralizes service creation and dependency injection wiring. * Each factory method creates fresh instances with proper dependencies. + * Services now directly instantiate View Models instead of using Presenters. */ export class ServiceFactory { private readonly apiClients: { @@ -79,34 +49,6 @@ export class ServiceFactory { media: MediaApiClient; }; - private readonly presenters: { - raceDetail: RaceDetailPresenter; - raceResultsDetail: RaceResultsDetailPresenter; - raceWithSOF: RaceWithSOFPresenter; - importRaceResults: ImportRaceResultsPresenter; - driversLeaderboard: DriversLeaderboardPresenter; - driver: DriverPresenter; - completeOnboarding: CompleteOnboardingPresenter; - driverRegistrationStatus: DriverRegistrationStatusPresenter; - teamDetails: TeamDetailsPresenter; - teamList: TeamListPresenter; - teamMembers: TeamMembersPresenter; - teamJoinRequest: TeamJoinRequestPresenter; - leagueSummary: LeagueSummaryPresenter; - leagueStandings: LeagueStandingsPresenter; - leagueMembers: LeagueMembersPresenter; - sponsorList: SponsorListPresenter; - sponsorDashboard: SponsorDashboardPresenter; - sponsorSponsorships: SponsorSponsorshipsPresenter; - sponsorshipPricing: SponsorshipPricingPresenter; - paymentList: PaymentListPresenter; - analyticsDashboard: AnalyticsDashboardPresenter; - analyticsMetrics: AnalyticsMetricsPresenter; - media: MediaPresenter; - avatar: AvatarPresenter; - session: SessionPresenter; - }; - constructor(baseUrl: string) { // Initialize API clients this.apiClients = { @@ -120,159 +62,83 @@ export class ServiceFactory { analytics: new AnalyticsApiClient(baseUrl), media: new MediaApiClient(baseUrl), }; - - // Initialize presenters - this.presenters = { - raceDetail: new RaceDetailPresenter(), - raceResultsDetail: new RaceResultsDetailPresenter(), - raceWithSOF: new RaceWithSOFPresenter(), - importRaceResults: new ImportRaceResultsPresenter(), - driversLeaderboard: new DriversLeaderboardPresenter(), - driver: new DriverPresenter(), - completeOnboarding: new CompleteOnboardingPresenter(), - driverRegistrationStatus: new DriverRegistrationStatusPresenter(), - teamDetails: new TeamDetailsPresenter(), - teamList: new TeamListPresenter(), - teamMembers: new TeamMembersPresenter(), - teamJoinRequest: new TeamJoinRequestPresenter(), - leagueSummary: new LeagueSummaryPresenter(), - leagueStandings: new LeagueStandingsPresenter(), - leagueMembers: new LeagueMembersPresenter(), - sponsorList: new SponsorListPresenter(), - sponsorDashboard: new SponsorDashboardPresenter(), - sponsorSponsorships: new SponsorSponsorshipsPresenter(), - sponsorshipPricing: new SponsorshipPricingPresenter(), - paymentList: new PaymentListPresenter(), - analyticsDashboard: new AnalyticsDashboardPresenter(), - analyticsMetrics: new AnalyticsMetricsPresenter(), - media: new MediaPresenter(), - avatar: new AvatarPresenter(), - session: new SessionPresenter(), - }; } /** * Create RaceService instance */ createRaceService(): RaceService { - return new RaceService( - this.apiClients.races, - this.presenters.raceDetail - ); + return new RaceService(this.apiClients.races); } /** * Create RaceResultsService instance */ createRaceResultsService(): RaceResultsService { - return new RaceResultsService( - this.apiClients.races, - this.presenters.raceResultsDetail, - this.presenters.raceWithSOF, - this.presenters.importRaceResults - ); + return new RaceResultsService(this.apiClients.races); } /** * Create DriverService instance */ createDriverService(): DriverService { - return new DriverService( - this.apiClients.drivers, - this.presenters.driversLeaderboard, - this.presenters.driver, - this.presenters.completeOnboarding - ); + return new DriverService(this.apiClients.drivers); } /** * Create DriverRegistrationService instance */ createDriverRegistrationService(): DriverRegistrationService { - return new DriverRegistrationService( - this.apiClients.drivers, - this.presenters.driverRegistrationStatus - ); + return new DriverRegistrationService(this.apiClients.drivers); } /** * Create TeamService instance */ createTeamService(): TeamService { - return new TeamService( - this.apiClients.teams, - this.presenters.teamList, - this.presenters.teamDetails, - this.presenters.teamMembers - ); + return new TeamService(this.apiClients.teams); } /** * Create TeamJoinService instance */ createTeamJoinService(): TeamJoinService { - return new TeamJoinService( - this.apiClients.teams, - this.presenters.teamJoinRequest - ); + return new TeamJoinService(this.apiClients.teams); } /** * Create LeagueService instance */ createLeagueService(): LeagueService { - return new LeagueService( - this.apiClients.leagues, - this.presenters.leagueSummary, - this.presenters.leagueStandings - ); + return new LeagueService(this.apiClients.leagues); } /** * Create LeagueMembershipService instance */ createLeagueMembershipService(): LeagueMembershipService { - return new LeagueMembershipService( - this.apiClients.leagues, - this.presenters.leagueMembers - ); + return new LeagueMembershipService(this.apiClients.leagues); } /** * Create SponsorService instance */ createSponsorService(): SponsorService { - return new SponsorService( - this.apiClients.sponsors, - this.presenters.sponsorList, - this.presenters.sponsorDashboard, - this.presenters.sponsorSponsorships - ); + return new SponsorService(this.apiClients.sponsors); } /** * Create SponsorshipService instance */ createSponsorshipService(): SponsorshipService { - return new SponsorshipService( - this.apiClients.sponsors, - this.presenters.sponsorshipPricing, - this.presenters.sponsorSponsorships - ); + return new SponsorshipService(this.apiClients.sponsors); } /** * Create PaymentService instance */ createPaymentService(): PaymentService { - return new PaymentService( - this.apiClients.payments, - this.presenters.paymentList, - presentPayment, - presentMembershipFee, - presentPrize, - presentWallet - ); + return new PaymentService(this.apiClients.payments); } /** @@ -286,31 +152,21 @@ export class ServiceFactory { * Create DashboardService instance */ createDashboardService(): DashboardService { - return new DashboardService( - this.apiClients.analytics, - this.presenters.analyticsDashboard, - this.presenters.analyticsMetrics - ); + return new DashboardService(this.apiClients.analytics); } /** * Create MediaService instance */ createMediaService(): MediaService { - return new MediaService( - this.apiClients.media, - this.presenters.media - ); + return new MediaService(this.apiClients.media); } /** * Create AvatarService instance */ createAvatarService(): AvatarService { - return new AvatarService( - this.apiClients.media, - this.presenters.avatar - ); + return new AvatarService(this.apiClients.media); } /** @@ -338,9 +194,6 @@ export class ServiceFactory { * Create SessionService instance */ createSessionService(): SessionService { - return new SessionService( - this.apiClients.auth, - this.presenters.session - ); + return new SessionService(this.apiClients.auth); } } \ No newline at end of file diff --git a/apps/website/lib/services/analytics/AnalyticsService.test.ts b/apps/website/lib/services/analytics/AnalyticsService.test.ts index 0f0f3762f..4f16c17cd 100644 --- a/apps/website/lib/services/analytics/AnalyticsService.test.ts +++ b/apps/website/lib/services/analytics/AnalyticsService.test.ts @@ -1,193 +1,93 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, Mocked } from 'vitest'; import { AnalyticsService } from './AnalyticsService'; import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; -import type { RecordPageViewInputDto, RecordPageViewOutputDto, RecordEngagementInputDto, RecordEngagementOutputDto } from '../../dtos'; +import { RecordPageViewInputViewModel } from '../../view-models/RecordPageViewInputViewModel'; +import { RecordEngagementInputViewModel } from '../../view-models/RecordEngagementInputViewModel'; describe('AnalyticsService', () => { - let mockApiClient: AnalyticsApiClient; + let mockApiClient: Mocked; let service: AnalyticsService; beforeEach(() => { mockApiClient = { recordPageView: vi.fn(), recordEngagement: vi.fn(), - getDashboardData: vi.fn(), - getAnalyticsMetrics: vi.fn(), - } as unknown as AnalyticsApiClient; + } as Mocked; service = new AnalyticsService(mockApiClient); }); describe('recordPageView', () => { - it('should record page view via API client', async () => { - // Arrange - const input: RecordPageViewInputDto = { - page: '/dashboard', - timestamp: '2025-12-17T20:00:00Z', - userId: 'user-1', - }; + it('should call apiClient.recordPageView with correct input', async () => { + const input = new RecordPageViewInputViewModel({ + path: '/dashboard', + userId: 'user-123', + }); - const expectedOutput: RecordPageViewOutputDto = { - success: true, - }; + const expectedOutput = { pageViewId: 'pv-123' }; + mockApiClient.recordPageView.mockResolvedValue(expectedOutput); - vi.mocked(mockApiClient.recordPageView).mockResolvedValue(expectedOutput); - - // Act const result = await service.recordPageView(input); - // Assert - expect(mockApiClient.recordPageView).toHaveBeenCalledWith(input); - expect(mockApiClient.recordPageView).toHaveBeenCalledTimes(1); - expect(result).toBe(expectedOutput); + expect(mockApiClient.recordPageView).toHaveBeenCalledWith({ + path: '/dashboard', + userId: 'user-123', + }); + expect(result).toEqual(expectedOutput); }); - it('should propagate API client errors', async () => { - // Arrange - const input: RecordPageViewInputDto = { - page: '/dashboard', - timestamp: '2025-12-17T20:00:00Z', - userId: 'user-1', - }; + it('should call apiClient.recordPageView without userId when not provided', async () => { + const input = new RecordPageViewInputViewModel({ + path: '/home', + }); - const error = new Error('API Error: Failed to record page view'); - vi.mocked(mockApiClient.recordPageView).mockRejectedValue(error); + const expectedOutput = { pageViewId: 'pv-456' }; + mockApiClient.recordPageView.mockResolvedValue(expectedOutput); - // Act & Assert - await expect(service.recordPageView(input)).rejects.toThrow( - 'API Error: Failed to record page view' - ); - - expect(mockApiClient.recordPageView).toHaveBeenCalledWith(input); - expect(mockApiClient.recordPageView).toHaveBeenCalledTimes(1); - }); - - it('should handle different input parameters', async () => { - // Arrange - const input: RecordPageViewInputDto = { - page: '/races', - timestamp: '2025-12-18T10:30:00Z', - userId: 'user-2', - }; - - const expectedOutput: RecordPageViewOutputDto = { - success: true, - }; - - vi.mocked(mockApiClient.recordPageView).mockResolvedValue(expectedOutput); - - // Act const result = await service.recordPageView(input); - // Assert - expect(mockApiClient.recordPageView).toHaveBeenCalledWith(input); - expect(result).toBe(expectedOutput); + expect(mockApiClient.recordPageView).toHaveBeenCalledWith({ + path: '/home', + }); + expect(result).toEqual(expectedOutput); }); }); describe('recordEngagement', () => { - it('should record engagement event via API client', async () => { - // Arrange - const input: RecordEngagementInputDto = { - event: 'button_click', - element: 'register-race-btn', - page: '/races', - timestamp: '2025-12-17T20:00:00Z', - userId: 'user-1', - }; + it('should call apiClient.recordEngagement with correct input', async () => { + const input = new RecordEngagementInputViewModel({ + eventType: 'button_click', + userId: 'user-123', + metadata: { buttonId: 'submit', page: '/form' }, + }); - const expectedOutput: RecordEngagementOutputDto = { - success: true, - }; + const expectedOutput = { eventId: 'event-123', engagementWeight: 1.5 }; + mockApiClient.recordEngagement.mockResolvedValue(expectedOutput); - vi.mocked(mockApiClient.recordEngagement).mockResolvedValue(expectedOutput); - - // Act const result = await service.recordEngagement(input); - // Assert - expect(mockApiClient.recordEngagement).toHaveBeenCalledWith(input); - expect(mockApiClient.recordEngagement).toHaveBeenCalledTimes(1); - expect(result).toBe(expectedOutput); + expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({ + eventType: 'button_click', + userId: 'user-123', + metadata: { buttonId: 'submit', page: '/form' }, + }); + expect(result).toEqual(expectedOutput); }); - it('should propagate API client errors', async () => { - // Arrange - const input: RecordEngagementInputDto = { - event: 'form_submit', - element: 'contact-form', - page: '/contact', - timestamp: '2025-12-17T20:00:00Z', - userId: 'user-1', - }; + it('should call apiClient.recordEngagement without optional fields', async () => { + const input = new RecordEngagementInputViewModel({ + eventType: 'page_load', + }); - const error = new Error('API Error: Failed to record engagement'); - vi.mocked(mockApiClient.recordEngagement).mockRejectedValue(error); + const expectedOutput = { eventId: 'event-456', engagementWeight: 0.5 }; + mockApiClient.recordEngagement.mockResolvedValue(expectedOutput); - // Act & Assert - await expect(service.recordEngagement(input)).rejects.toThrow( - 'API Error: Failed to record engagement' - ); - - expect(mockApiClient.recordEngagement).toHaveBeenCalledWith(input); - expect(mockApiClient.recordEngagement).toHaveBeenCalledTimes(1); - }); - - it('should handle different engagement types', async () => { - // Arrange - const input: RecordEngagementInputDto = { - event: 'scroll', - element: 'race-list', - page: '/races', - timestamp: '2025-12-18T10:30:00Z', - userId: 'user-2', - }; - - const expectedOutput: RecordEngagementOutputDto = { - success: true, - }; - - vi.mocked(mockApiClient.recordEngagement).mockResolvedValue(expectedOutput); - - // Act const result = await service.recordEngagement(input); - // Assert - expect(mockApiClient.recordEngagement).toHaveBeenCalledWith(input); - expect(result).toBe(expectedOutput); - }); - }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient', () => { - // This test verifies the constructor signature - expect(() => { - new AnalyticsService(mockApiClient); - }).not.toThrow(); - }); - - it('should use injected apiClient', async () => { - // Arrange - const customApiClient = { - recordPageView: vi.fn().mockResolvedValue({ success: true }), - recordEngagement: vi.fn().mockResolvedValue({ success: true }), - getDashboardData: vi.fn(), - getAnalyticsMetrics: vi.fn(), - } as unknown as AnalyticsApiClient; - - const customService = new AnalyticsService(customApiClient); - - const input: RecordPageViewInputDto = { - page: '/test', - timestamp: '2025-12-17T20:00:00Z', - userId: 'user-1', - }; - - // Act - await customService.recordPageView(input); - - // Assert - expect(customApiClient.recordPageView).toHaveBeenCalledWith(input); + expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({ + eventType: 'page_load', + }); + expect(result).toEqual(expectedOutput); }); }); }); \ No newline at end of file diff --git a/apps/website/lib/services/analytics/AnalyticsService.ts b/apps/website/lib/services/analytics/AnalyticsService.ts index 207612c57..a628ea57e 100644 --- a/apps/website/lib/services/analytics/AnalyticsService.ts +++ b/apps/website/lib/services/analytics/AnalyticsService.ts @@ -1,5 +1,7 @@ import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; -import type { RecordPageViewInputDto, RecordPageViewOutputDto, RecordEngagementInputDto, RecordEngagementOutputDto } from '../../dtos'; +import { RecordPageViewOutputDTO, RecordEngagementOutputDTO } from '../../types/generated'; +import { RecordPageViewInputViewModel } from '../../view-models/RecordPageViewInputViewModel'; +import { RecordEngagementInputViewModel } from '../../view-models/RecordEngagementInputViewModel'; /** * Analytics Service @@ -15,22 +17,29 @@ export class AnalyticsService { /** * Record a page view */ - async recordPageView(input: RecordPageViewInputDto): Promise { - try { - return await this.apiClient.recordPageView(input); - } catch (error) { - throw error; + async recordPageView(input: RecordPageViewInputViewModel): Promise { + const apiInput: { path: string; userId?: string } = { + path: input.path, + }; + if (input.userId) { + apiInput.userId = input.userId; } + return await this.apiClient.recordPageView(apiInput); } /** * Record an engagement event */ - async recordEngagement(input: RecordEngagementInputDto): Promise { - try { - return await this.apiClient.recordEngagement(input); - } catch (error) { - throw error; + async recordEngagement(input: RecordEngagementInputViewModel): Promise { + const apiInput: { eventType: string; userId?: string; metadata?: Record } = { + eventType: input.eventType, + }; + if (input.userId) { + apiInput.userId = input.userId; } + if (input.metadata) { + apiInput.metadata = input.metadata; + } + return await this.apiClient.recordEngagement(apiInput); } } \ No newline at end of file diff --git a/apps/website/lib/services/analytics/DashboardService.test.ts b/apps/website/lib/services/analytics/DashboardService.test.ts index ccbd83b85..2bf654250 100644 --- a/apps/website/lib/services/analytics/DashboardService.test.ts +++ b/apps/website/lib/services/analytics/DashboardService.test.ts @@ -1,227 +1,80 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, Mocked } from 'vitest'; import { DashboardService } from './DashboardService'; import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; -import { AnalyticsDashboardPresenter } from '../../presenters/AnalyticsDashboardPresenter'; -import { AnalyticsMetricsPresenter } from '../../presenters/AnalyticsMetricsPresenter'; -import type { AnalyticsDashboardDto, AnalyticsMetricsDto } from '../../dtos'; import { AnalyticsDashboardViewModel, AnalyticsMetricsViewModel } from '../../view-models'; describe('DashboardService', () => { - let mockApiClient: AnalyticsApiClient; - let mockDashboardPresenter: AnalyticsDashboardPresenter; - let mockMetricsPresenter: AnalyticsMetricsPresenter; + let mockApiClient: Mocked; let service: DashboardService; beforeEach(() => { mockApiClient = { - recordPageView: vi.fn(), - recordEngagement: vi.fn(), getDashboardData: vi.fn(), getAnalyticsMetrics: vi.fn(), - } as unknown as AnalyticsApiClient; + recordPageView: vi.fn(), + recordEngagement: vi.fn(), + } as Mocked; - mockDashboardPresenter = { - present: vi.fn(), - } as unknown as AnalyticsDashboardPresenter; - - mockMetricsPresenter = { - present: vi.fn(), - } as unknown as AnalyticsMetricsPresenter; - - service = new DashboardService(mockApiClient, mockDashboardPresenter, mockMetricsPresenter); + service = new DashboardService(mockApiClient); }); describe('getDashboardData', () => { - it('should fetch dashboard data from API and transform via presenter', async () => { - // Arrange - const mockDto: AnalyticsDashboardDto = { - totalUsers: 1000, - activeUsers: 750, - totalRaces: 50, - totalLeagues: 25, + it('should call apiClient.getDashboardData and return AnalyticsDashboardViewModel', async () => { + const dto = { + totalUsers: 100, + activeUsers: 50, + totalRaces: 20, + totalLeagues: 5, }; + mockApiClient.getDashboardData.mockResolvedValue(dto); - const mockViewModel: AnalyticsDashboardViewModel = new AnalyticsDashboardViewModel(mockDto); - - vi.mocked(mockApiClient.getDashboardData).mockResolvedValue(mockDto); - vi.mocked(mockDashboardPresenter.present).mockReturnValue(mockViewModel); - - // Act const result = await service.getDashboardData(); - // Assert - expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); - expect(mockDashboardPresenter.present).toHaveBeenCalledWith(mockDto); - expect(mockDashboardPresenter.present).toHaveBeenCalledTimes(1); - expect(result).toBe(mockViewModel); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Failed to fetch dashboard data'); - vi.mocked(mockApiClient.getDashboardData).mockRejectedValue(error); - - // Act & Assert - await expect(service.getDashboardData()).rejects.toThrow( - 'API Error: Failed to fetch dashboard data' - ); - - expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); - expect(mockDashboardPresenter.present).not.toHaveBeenCalled(); - }); - - it('should propagate presenter errors', async () => { - // Arrange - const mockDto: AnalyticsDashboardDto = { - totalUsers: 500, - activeUsers: 300, - totalRaces: 20, - totalLeagues: 10, - }; - - const error = new Error('Presenter Error: Invalid DTO structure'); - vi.mocked(mockApiClient.getDashboardData).mockResolvedValue(mockDto); - vi.mocked(mockDashboardPresenter.present).mockImplementation(() => { - throw error; - }); - - // Act & Assert - await expect(service.getDashboardData()).rejects.toThrow( - 'Presenter Error: Invalid DTO structure' - ); - - expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); - expect(mockDashboardPresenter.present).toHaveBeenCalledWith(mockDto); + expect(mockApiClient.getDashboardData).toHaveBeenCalled(); + expect(result).toBeInstanceOf(AnalyticsDashboardViewModel); + expect(result.totalUsers).toBe(100); + expect(result.activeUsers).toBe(50); + expect(result.totalRaces).toBe(20); + expect(result.totalLeagues).toBe(5); }); }); describe('getAnalyticsMetrics', () => { - it('should fetch analytics metrics from API and transform via presenter', async () => { - // Arrange - const mockDto: AnalyticsMetricsDto = { - pageViews: 5000, - uniqueVisitors: 1200, - averageSessionDuration: 180, - bounceRate: 35.5, + it('should call apiClient.getAnalyticsMetrics and return AnalyticsMetricsViewModel', async () => { + const dto = { + pageViews: 1000, + uniqueVisitors: 500, + averageSessionDuration: 300, + bounceRate: 0.25, }; + mockApiClient.getAnalyticsMetrics.mockResolvedValue(dto); - const mockViewModel: AnalyticsMetricsViewModel = new AnalyticsMetricsViewModel(mockDto); - - vi.mocked(mockApiClient.getAnalyticsMetrics).mockResolvedValue(mockDto); - vi.mocked(mockMetricsPresenter.present).mockReturnValue(mockViewModel); - - // Act const result = await service.getAnalyticsMetrics(); - // Assert - expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1); - expect(mockMetricsPresenter.present).toHaveBeenCalledWith(mockDto); - expect(mockMetricsPresenter.present).toHaveBeenCalledTimes(1); - expect(result).toBe(mockViewModel); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Failed to fetch analytics metrics'); - vi.mocked(mockApiClient.getAnalyticsMetrics).mockRejectedValue(error); - - // Act & Assert - await expect(service.getAnalyticsMetrics()).rejects.toThrow( - 'API Error: Failed to fetch analytics metrics' - ); - - expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1); - expect(mockMetricsPresenter.present).not.toHaveBeenCalled(); - }); - - it('should propagate presenter errors', async () => { - // Arrange - const mockDto: AnalyticsMetricsDto = { - pageViews: 2500, - uniqueVisitors: 600, - averageSessionDuration: 120, - bounceRate: 45.2, - }; - - const error = new Error('Presenter Error: Invalid metrics data'); - vi.mocked(mockApiClient.getAnalyticsMetrics).mockResolvedValue(mockDto); - vi.mocked(mockMetricsPresenter.present).mockImplementation(() => { - throw error; - }); - - // Act & Assert - await expect(service.getAnalyticsMetrics()).rejects.toThrow( - 'Presenter Error: Invalid metrics data' - ); - - expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalledTimes(1); - expect(mockMetricsPresenter.present).toHaveBeenCalledWith(mockDto); + expect(mockApiClient.getAnalyticsMetrics).toHaveBeenCalled(); + expect(result).toBeInstanceOf(AnalyticsMetricsViewModel); + expect(result.pageViews).toBe(1000); + expect(result.uniqueVisitors).toBe(500); + expect(result.averageSessionDuration).toBe(300); + expect(result.bounceRate).toBe(0.25); }); }); describe('getDashboardOverview', () => { - it('should delegate to getDashboardData for backward compatibility', async () => { - // Arrange - const mockDto: AnalyticsDashboardDto = { - totalUsers: 800, - activeUsers: 600, + it('should call getDashboardData and return the result', async () => { + const dto = { + totalUsers: 200, + activeUsers: 100, totalRaces: 40, - totalLeagues: 20, + totalLeagues: 10, }; + mockApiClient.getDashboardData.mockResolvedValue(dto); - const mockViewModel: AnalyticsDashboardViewModel = new AnalyticsDashboardViewModel(mockDto); - - vi.mocked(mockApiClient.getDashboardData).mockResolvedValue(mockDto); - vi.mocked(mockDashboardPresenter.present).mockReturnValue(mockViewModel); - - // Act const result = await service.getDashboardOverview(); - // Assert - expect(mockApiClient.getDashboardData).toHaveBeenCalledTimes(1); - expect(mockDashboardPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toBe(mockViewModel); + expect(mockApiClient.getDashboardData).toHaveBeenCalled(); + expect(result).toBeInstanceOf(AnalyticsDashboardViewModel); + expect(result.totalUsers).toBe(200); }); }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient, dashboardPresenter, and metricsPresenter', () => { - // This test verifies the constructor signature - expect(() => { - new DashboardService(mockApiClient, mockDashboardPresenter, mockMetricsPresenter); - }).not.toThrow(); - }); - - it('should use injected dependencies', async () => { - // Arrange - const customApiClient = { - recordPageView: vi.fn(), - recordEngagement: vi.fn(), - getDashboardData: vi.fn().mockResolvedValue({ - totalUsers: 100, - activeUsers: 80, - totalRaces: 5, - totalLeagues: 3, - }), - getAnalyticsMetrics: vi.fn(), - } as unknown as AnalyticsApiClient; - - const customDashboardPresenter = { - present: vi.fn().mockReturnValue({} as AnalyticsDashboardViewModel), - } as unknown as AnalyticsDashboardPresenter; - - const customMetricsPresenter = { - present: vi.fn(), - } as unknown as AnalyticsMetricsPresenter; - - const customService = new DashboardService(customApiClient, customDashboardPresenter, customMetricsPresenter); - - // Act - await customService.getDashboardData(); - - // Assert - expect(customApiClient.getDashboardData).toHaveBeenCalledTimes(1); - expect(customDashboardPresenter.present).toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file +}); diff --git a/apps/website/lib/services/analytics/DashboardService.ts b/apps/website/lib/services/analytics/DashboardService.ts index 1045cbb28..1af13e867 100644 --- a/apps/website/lib/services/analytics/DashboardService.ts +++ b/apps/website/lib/services/analytics/DashboardService.ts @@ -1,50 +1,30 @@ import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; -import { AnalyticsDashboardPresenter } from '../../presenters/AnalyticsDashboardPresenter'; -import { AnalyticsMetricsPresenter } from '../../presenters/AnalyticsMetricsPresenter'; -import type { AnalyticsDashboardViewModel, AnalyticsMetricsViewModel } from '../../view-models'; +import { AnalyticsDashboardViewModel, AnalyticsMetricsViewModel } from '../../view-models'; /** * Dashboard Service * - * Orchestrates dashboard operations by coordinating API calls and presentation logic. + * Orchestrates dashboard operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class DashboardService { constructor( - private readonly apiClient: AnalyticsApiClient, - private readonly analyticsDashboardPresenter: AnalyticsDashboardPresenter, - private readonly analyticsMetricsPresenter: AnalyticsMetricsPresenter + private readonly apiClient: AnalyticsApiClient ) {} /** - * Get dashboard data with presentation transformation + * Get dashboard data with view model transformation */ async getDashboardData(): Promise { - try { - const dto = await this.apiClient.getDashboardData(); - return this.analyticsDashboardPresenter.present(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getDashboardData(); + return new AnalyticsDashboardViewModel(dto); } /** - * Get analytics metrics with presentation transformation + * Get analytics metrics with view model transformation */ async getAnalyticsMetrics(): Promise { - try { - const dto = await this.apiClient.getAnalyticsMetrics(); - return this.analyticsMetricsPresenter.present(dto); - } catch (error) { - throw error; - } - } - - /** - * Get dashboard overview (legacy method for backward compatibility) - * TODO: Remove when no longer needed - */ - async getDashboardOverview(): Promise { - return this.getDashboardData(); + const dto = await this.apiClient.getAnalyticsMetrics(); + return new AnalyticsMetricsViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/auth/AuthService.test.ts b/apps/website/lib/services/auth/AuthService.test.ts deleted file mode 100644 index 654e8171b..000000000 --- a/apps/website/lib/services/auth/AuthService.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AuthService } from './AuthService'; -import { AuthApiClient } from '../../api/auth/AuthApiClient'; -import type { LoginParamsDto, SignupParamsDto, SessionDataDto } from '../../dtos'; - -describe('AuthService', () => { - let mockApiClient: AuthApiClient; - let service: AuthService; - - beforeEach(() => { - mockApiClient = { - signup: vi.fn(), - login: vi.fn(), - logout: vi.fn(), - getSession: vi.fn(), - getIracingAuthUrl: vi.fn(), - } as unknown as AuthApiClient; - - service = new AuthService(mockApiClient); - }); - - describe('signup', () => { - it('should sign up user via API client', async () => { - // Arrange - const params: SignupParamsDto = { - email: 'test@example.com', - password: 'password123', - displayName: 'Test User', - }; - - const expectedSession: SessionDataDto = { - userId: 'user-1', - email: 'test@example.com', - displayName: 'Test User', - isAuthenticated: true, - }; - - vi.mocked(mockApiClient.signup).mockResolvedValue(expectedSession); - - // Act - const result = await service.signup(params); - - // Assert - expect(mockApiClient.signup).toHaveBeenCalledWith(params); - expect(mockApiClient.signup).toHaveBeenCalledTimes(1); - expect(result).toBe(expectedSession); - }); - - it('should propagate API client errors', async () => { - // Arrange - const params: SignupParamsDto = { - email: 'test@example.com', - password: 'password123', - }; - - const error = new Error('API Error: Failed to sign up'); - vi.mocked(mockApiClient.signup).mockRejectedValue(error); - - // Act & Assert - await expect(service.signup(params)).rejects.toThrow( - 'API Error: Failed to sign up' - ); - - expect(mockApiClient.signup).toHaveBeenCalledWith(params); - expect(mockApiClient.signup).toHaveBeenCalledTimes(1); - }); - }); - - describe('login', () => { - it('should log in user via API client', async () => { - // Arrange - const params: LoginParamsDto = { - email: 'test@example.com', - password: 'password123', - }; - - const expectedSession: SessionDataDto = { - userId: 'user-1', - email: 'test@example.com', - isAuthenticated: true, - }; - - vi.mocked(mockApiClient.login).mockResolvedValue(expectedSession); - - // Act - const result = await service.login(params); - - // Assert - expect(mockApiClient.login).toHaveBeenCalledWith(params); - expect(mockApiClient.login).toHaveBeenCalledTimes(1); - expect(result).toBe(expectedSession); - }); - - it('should propagate API client errors', async () => { - // Arrange - const params: LoginParamsDto = { - email: 'test@example.com', - password: 'password123', - }; - - const error = new Error('API Error: Invalid credentials'); - vi.mocked(mockApiClient.login).mockRejectedValue(error); - - // Act & Assert - await expect(service.login(params)).rejects.toThrow( - 'API Error: Invalid credentials' - ); - - expect(mockApiClient.login).toHaveBeenCalledWith(params); - expect(mockApiClient.login).toHaveBeenCalledTimes(1); - }); - }); - - describe('logout', () => { - it('should log out user via API client', async () => { - // Arrange - vi.mocked(mockApiClient.logout).mockResolvedValue(undefined); - - // Act - await service.logout(); - - // Assert - expect(mockApiClient.logout).toHaveBeenCalledTimes(1); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Failed to logout'); - vi.mocked(mockApiClient.logout).mockRejectedValue(error); - - // Act & Assert - await expect(service.logout()).rejects.toThrow( - 'API Error: Failed to logout' - ); - - expect(mockApiClient.logout).toHaveBeenCalledTimes(1); - }); - }); - - describe('getIracingAuthUrl', () => { - it('should get iRacing auth URL via API client', () => { - // Arrange - const returnTo = '/dashboard'; - const expectedUrl = 'http://localhost:3001/auth/iracing/start?returnTo=%2Fdashboard'; - - vi.mocked(mockApiClient.getIracingAuthUrl).mockReturnValue(expectedUrl); - - // Act - const result = service.getIracingAuthUrl(returnTo); - - // Assert - expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(returnTo); - expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledTimes(1); - expect(result).toBe(expectedUrl); - }); - - it('should handle undefined returnTo', () => { - // Arrange - const expectedUrl = 'http://localhost:3001/auth/iracing/start'; - - vi.mocked(mockApiClient.getIracingAuthUrl).mockReturnValue(expectedUrl); - - // Act - const result = service.getIracingAuthUrl(); - - // Assert - expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(undefined); - expect(result).toBe(expectedUrl); - }); - }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient', () => { - // This test verifies the constructor signature - expect(() => { - new AuthService(mockApiClient); - }).not.toThrow(); - }); - - it('should use injected apiClient', async () => { - // Arrange - const customApiClient = { - signup: vi.fn().mockResolvedValue({ userId: 'user-1', email: 'test@example.com', isAuthenticated: true }), - login: vi.fn(), - logout: vi.fn(), - getSession: vi.fn(), - getIracingAuthUrl: vi.fn(), - } as unknown as AuthApiClient; - - const customService = new AuthService(customApiClient); - - const params: SignupParamsDto = { - email: 'test@example.com', - password: 'password123', - }; - - // Act - await customService.signup(params); - - // Assert - expect(customApiClient.signup).toHaveBeenCalledWith(params); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/auth/AuthService.ts b/apps/website/lib/services/auth/AuthService.ts index da0a8005c..b9420a04e 100644 --- a/apps/website/lib/services/auth/AuthService.ts +++ b/apps/website/lib/services/auth/AuthService.ts @@ -1,5 +1,9 @@ import { AuthApiClient } from '../../api/auth/AuthApiClient'; -import type { LoginParamsDto, SignupParamsDto, SessionDataDto } from '../../dtos'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type LoginParamsDto = { email: string; password: string }; +type SignupParamsDto = { email: string; password: string; displayName: string }; +type SessionDataDto = { userId: string; email: string; displayName: string; token: string }; /** * Auth Service diff --git a/apps/website/lib/services/auth/SessionService.test.ts b/apps/website/lib/services/auth/SessionService.test.ts deleted file mode 100644 index f579b1bfc..000000000 --- a/apps/website/lib/services/auth/SessionService.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SessionService } from './SessionService'; -import { AuthApiClient } from '../../api/auth/AuthApiClient'; -import { SessionPresenter } from '../../presenters/SessionPresenter'; -import { SessionViewModel } from '../../view-models'; -import type { SessionDataDto } from '../../dtos'; - -describe('SessionService', () => { - let mockApiClient: AuthApiClient; - let mockPresenter: SessionPresenter; - let service: SessionService; - - beforeEach(() => { - mockApiClient = { - getSession: vi.fn(), - signup: vi.fn(), - login: vi.fn(), - logout: vi.fn(), - getIracingAuthUrl: vi.fn(), - } as unknown as AuthApiClient; - - mockPresenter = { - presentSession: vi.fn(), - } as unknown as SessionPresenter; - - service = new SessionService(mockApiClient, mockPresenter); - }); - - describe('getSession', () => { - it('should get session via API client and present it', async () => { - // Arrange - const dto: SessionDataDto = { - userId: 'user-1', - email: 'test@example.com', - displayName: 'Test User', - driverId: 'driver-1', - isAuthenticated: true, - }; - - const expectedViewModel = new SessionViewModel(dto); - - vi.mocked(mockApiClient.getSession).mockResolvedValue(dto); - vi.mocked(mockPresenter.presentSession).mockReturnValue(expectedViewModel); - - // Act - const result = await service.getSession(); - - // Assert - expect(mockApiClient.getSession).toHaveBeenCalledTimes(1); - expect(mockPresenter.presentSession).toHaveBeenCalledWith(dto); - expect(mockPresenter.presentSession).toHaveBeenCalledTimes(1); - expect(result).toBe(expectedViewModel); - }); - - it('should return null when session is null', async () => { - // Arrange - vi.mocked(mockApiClient.getSession).mockResolvedValue(null); - vi.mocked(mockPresenter.presentSession).mockReturnValue(null); - - // Act - const result = await service.getSession(); - - // Assert - expect(mockApiClient.getSession).toHaveBeenCalledTimes(1); - expect(mockPresenter.presentSession).toHaveBeenCalledWith(null); - expect(result).toBeNull(); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Failed to get session'); - vi.mocked(mockApiClient.getSession).mockRejectedValue(error); - - // Act & Assert - await expect(service.getSession()).rejects.toThrow( - 'API Error: Failed to get session' - ); - - expect(mockApiClient.getSession).toHaveBeenCalledTimes(1); - expect(mockPresenter.presentSession).not.toHaveBeenCalled(); - }); - - it('should handle different session data', async () => { - // Arrange - const dto: SessionDataDto = { - userId: 'user-2', - email: 'another@example.com', - isAuthenticated: false, - }; - - const expectedViewModel = new SessionViewModel(dto); - - vi.mocked(mockApiClient.getSession).mockResolvedValue(dto); - vi.mocked(mockPresenter.presentSession).mockReturnValue(expectedViewModel); - - // Act - const result = await service.getSession(); - - // Assert - expect(mockApiClient.getSession).toHaveBeenCalledTimes(1); - expect(mockPresenter.presentSession).toHaveBeenCalledWith(dto); - expect(result).toBe(expectedViewModel); - }); - }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient and presenter', () => { - // This test verifies the constructor signature - expect(() => { - new SessionService(mockApiClient, mockPresenter); - }).not.toThrow(); - }); - - it('should use injected dependencies', async () => { - // Arrange - const customApiClient = { - getSession: vi.fn().mockResolvedValue({ userId: 'user-1', email: 'test@example.com', isAuthenticated: true }), - signup: vi.fn(), - login: vi.fn(), - logout: vi.fn(), - getIracingAuthUrl: vi.fn(), - } as unknown as AuthApiClient; - - const customPresenter = { - presentSession: vi.fn().mockReturnValue(new SessionViewModel({ userId: 'user-1', email: 'test@example.com', isAuthenticated: true })), - } as unknown as SessionPresenter; - - const customService = new SessionService(customApiClient, customPresenter); - - // Act - await customService.getSession(); - - // Assert - expect(customApiClient.getSession).toHaveBeenCalledTimes(1); - expect(customPresenter.presentSession).toHaveBeenCalledTimes(1); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/auth/SessionService.ts b/apps/website/lib/services/auth/SessionService.ts index 4a0476169..9fa934669 100644 --- a/apps/website/lib/services/auth/SessionService.ts +++ b/apps/website/lib/services/auth/SessionService.ts @@ -1,28 +1,22 @@ import { AuthApiClient } from '../../api/auth/AuthApiClient'; -import { SessionPresenter } from '../../presenters/SessionPresenter'; -import type { SessionViewModel } from '../../view-models'; +import { SessionViewModel } from '../../view-models'; /** * Session Service * - * Orchestrates session operations by coordinating API calls and presentation logic. + * Orchestrates session operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class SessionService { constructor( - private readonly apiClient: AuthApiClient, - private readonly presenter: SessionPresenter + private readonly apiClient: AuthApiClient ) {} /** - * Get current user session with presentation transformation + * Get current user session with view model transformation */ async getSession(): Promise { - try { - const dto = await this.apiClient.getSession(); - return this.presenter.presentSession(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getSession(); + return dto ? new SessionViewModel(dto) : null; } } \ No newline at end of file diff --git a/apps/website/lib/services/drivers/DriverRegistrationService.test.ts b/apps/website/lib/services/drivers/DriverRegistrationService.test.ts deleted file mode 100644 index 32350d993..000000000 --- a/apps/website/lib/services/drivers/DriverRegistrationService.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DriverRegistrationService } from './DriverRegistrationService'; -import type { DriversApiClient } from '../../api/drivers/DriversApiClient'; -import type { DriverRegistrationStatusPresenter } from '../../presenters/DriverRegistrationStatusPresenter'; -import type { DriverRegistrationStatusDto } from '../../dtos'; -import type { DriverRegistrationStatusViewModel } from '../../view-models'; - -describe('DriverRegistrationService', () => { - let service: DriverRegistrationService; - let mockApiClient: DriversApiClient; - let mockStatusPresenter: DriverRegistrationStatusPresenter; - - beforeEach(() => { - mockApiClient = { - getRegistrationStatus: vi.fn(), - } as unknown as DriversApiClient; - - mockStatusPresenter = { - present: vi.fn(), - } as unknown as DriverRegistrationStatusPresenter; - - service = new DriverRegistrationService( - mockApiClient, - mockStatusPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(DriverRegistrationService); - }); - }); - - describe('getDriverRegistrationStatus', () => { - it('should fetch registration status from API and transform via presenter', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId = 'race-456'; - - const mockDto: DriverRegistrationStatusDto = { - isRegistered: true, - raceId: 'race-456', - driverId: 'driver-123', - }; - - const mockViewModel = { - isRegistered: true, - raceId: 'race-456', - driverId: 'driver-123', - statusMessage: 'Registered for this race', - statusColor: 'green', - statusBadgeVariant: 'success', - registrationButtonText: 'Withdraw', - canRegister: false, - } as DriverRegistrationStatusViewModel; - - vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto); - vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getDriverRegistrationStatus(driverId, raceId); - - // Assert - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId); - expect(mockStatusPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle unregistered driver status', async () => { - // Arrange - const driverId = 'driver-789'; - const raceId = 'race-101'; - - const mockDto: DriverRegistrationStatusDto = { - isRegistered: false, - raceId: 'race-101', - driverId: 'driver-789', - }; - - const mockViewModel = { - isRegistered: false, - raceId: 'race-101', - driverId: 'driver-789', - statusMessage: 'Not registered', - statusColor: 'red', - statusBadgeVariant: 'warning', - registrationButtonText: 'Register', - canRegister: true, - } as DriverRegistrationStatusViewModel; - - vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto); - vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getDriverRegistrationStatus(driverId, raceId); - - // Assert - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId); - expect(mockStatusPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.canRegister).toBe(true); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId = 'race-456'; - const error = new Error('Failed to fetch registration status'); - - vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(error); - - // Act & Assert - await expect(service.getDriverRegistrationStatus(driverId, raceId)) - .rejects.toThrow('Failed to fetch registration status'); - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId); - expect(mockStatusPresenter.present).not.toHaveBeenCalled(); - }); - - it('should handle network errors gracefully', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId = 'race-456'; - const networkError = new Error('Network request failed'); - - vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(networkError); - - // Act & Assert - await expect(service.getDriverRegistrationStatus(driverId, raceId)) - .rejects.toThrow('Network request failed'); - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId); - }); - - it('should handle API errors with proper error propagation', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId = 'race-456'; - const apiError = new Error('Race not found'); - - vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(apiError); - - // Act & Assert - await expect(service.getDriverRegistrationStatus(driverId, raceId)) - .rejects.toThrow('Race not found'); - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId); - expect(mockStatusPresenter.present).not.toHaveBeenCalled(); - }); - - it('should handle multiple consecutive calls correctly', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId1 = 'race-456'; - const raceId2 = 'race-789'; - - const mockDto1: DriverRegistrationStatusDto = { - isRegistered: true, - raceId: raceId1, - driverId, - }; - - const mockDto2: DriverRegistrationStatusDto = { - isRegistered: false, - raceId: raceId2, - driverId, - }; - - const mockViewModel1 = { - isRegistered: true, - raceId: raceId1, - driverId, - } as DriverRegistrationStatusViewModel; - - const mockViewModel2 = { - isRegistered: false, - raceId: raceId2, - driverId, - } as DriverRegistrationStatusViewModel; - - vi.mocked(mockApiClient.getRegistrationStatus) - .mockResolvedValueOnce(mockDto1) - .mockResolvedValueOnce(mockDto2); - - vi.mocked(mockStatusPresenter.present) - .mockReturnValueOnce(mockViewModel1) - .mockReturnValueOnce(mockViewModel2); - - // Act - const result1 = await service.getDriverRegistrationStatus(driverId, raceId1); - const result2 = await service.getDriverRegistrationStatus(driverId, raceId2); - - // Assert - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledTimes(2); - expect(mockApiClient.getRegistrationStatus).toHaveBeenNthCalledWith(1, driverId, raceId1); - expect(mockApiClient.getRegistrationStatus).toHaveBeenNthCalledWith(2, driverId, raceId2); - expect(result1.isRegistered).toBe(true); - expect(result2.isRegistered).toBe(false); - }); - - it('should handle different driver IDs for same race', async () => { - // Arrange - const driverId1 = 'driver-123'; - const driverId2 = 'driver-456'; - const raceId = 'race-789'; - - const mockDto1: DriverRegistrationStatusDto = { - isRegistered: true, - raceId, - driverId: driverId1, - }; - - const mockDto2: DriverRegistrationStatusDto = { - isRegistered: false, - raceId, - driverId: driverId2, - }; - - const mockViewModel1 = { - isRegistered: true, - raceId, - driverId: driverId1, - } as DriverRegistrationStatusViewModel; - - const mockViewModel2 = { - isRegistered: false, - raceId, - driverId: driverId2, - } as DriverRegistrationStatusViewModel; - - vi.mocked(mockApiClient.getRegistrationStatus) - .mockResolvedValueOnce(mockDto1) - .mockResolvedValueOnce(mockDto2); - - vi.mocked(mockStatusPresenter.present) - .mockReturnValueOnce(mockViewModel1) - .mockReturnValueOnce(mockViewModel2); - - // Act - const result1 = await service.getDriverRegistrationStatus(driverId1, raceId); - const result2 = await service.getDriverRegistrationStatus(driverId2, raceId); - - // Assert - expect(result1.driverId).toBe(driverId1); - expect(result1.isRegistered).toBe(true); - expect(result2.driverId).toBe(driverId2); - expect(result2.isRegistered).toBe(false); - }); - - it('should handle unauthorized access errors', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId = 'race-456'; - const authError = new Error('Unauthorized: Driver not found'); - - vi.mocked(mockApiClient.getRegistrationStatus).mockRejectedValue(authError); - - // Act & Assert - await expect(service.getDriverRegistrationStatus(driverId, raceId)) - .rejects.toThrow('Unauthorized: Driver not found'); - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId); - expect(mockStatusPresenter.present).not.toHaveBeenCalled(); - }); - - it('should call presenter only after successful API response', async () => { - // Arrange - const driverId = 'driver-123'; - const raceId = 'race-456'; - - const mockDto: DriverRegistrationStatusDto = { - isRegistered: true, - raceId: 'race-456', - driverId: 'driver-123', - }; - - const mockViewModel = { - isRegistered: true, - raceId: 'race-456', - driverId: 'driver-123', - } as DriverRegistrationStatusViewModel; - - vi.mocked(mockApiClient.getRegistrationStatus).mockResolvedValue(mockDto); - vi.mocked(mockStatusPresenter.present).mockReturnValue(mockViewModel); - - // Act - await service.getDriverRegistrationStatus(driverId, raceId); - - // Assert - verify call order - expect(mockApiClient.getRegistrationStatus).toHaveBeenCalled(); - expect(mockStatusPresenter.present).toHaveBeenCalledAfter( - mockApiClient.getRegistrationStatus as any - ); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/drivers/DriverRegistrationService.ts b/apps/website/lib/services/drivers/DriverRegistrationService.ts index df2806b41..5c96fe509 100644 --- a/apps/website/lib/services/drivers/DriverRegistrationService.ts +++ b/apps/website/lib/services/drivers/DriverRegistrationService.ts @@ -1,17 +1,15 @@ import type { DriversApiClient } from '../../api/drivers/DriversApiClient'; -import type { DriverRegistrationStatusPresenter } from '../../presenters/DriverRegistrationStatusPresenter'; -import type { DriverRegistrationStatusViewModel } from '../../view-models'; +import { DriverRegistrationStatusViewModel } from '../../view-models'; /** * Driver Registration Service * - * Orchestrates driver registration status operations by coordinating API calls and presentation logic. + * Orchestrates driver registration status operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class DriverRegistrationService { constructor( - private readonly apiClient: DriversApiClient, - private readonly statusPresenter: DriverRegistrationStatusPresenter + private readonly apiClient: DriversApiClient ) {} /** @@ -22,6 +20,6 @@ export class DriverRegistrationService { raceId: string ): Promise { const dto = await this.apiClient.getRegistrationStatus(driverId, raceId); - return this.statusPresenter.present(dto); + return new DriverRegistrationStatusViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/drivers/DriverService.test.ts b/apps/website/lib/services/drivers/DriverService.test.ts deleted file mode 100644 index 7bf8f8b16..000000000 --- a/apps/website/lib/services/drivers/DriverService.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DriverService } from './DriverService'; -import type { DriversApiClient } from '../../api/drivers/DriversApiClient'; -import type { DriversLeaderboardPresenter } from '../../presenters/DriversLeaderboardPresenter'; -import type { DriverPresenter } from '../../presenters/DriverPresenter'; -import type { CompleteOnboardingPresenter } from '../../presenters/CompleteOnboardingPresenter'; -import type { DriversLeaderboardDto, CompleteOnboardingOutputDto, DriverDto, CompleteOnboardingInputDto } from '../../dtos'; -import type { DriverLeaderboardViewModel, DriverViewModel, CompleteOnboardingViewModel } from '../../view-models'; - -describe('DriverService', () => { - let service: DriverService; - let mockApiClient: DriversApiClient; - let mockLeaderboardPresenter: DriversLeaderboardPresenter; - let mockDriverPresenter: DriverPresenter; - let mockOnboardingPresenter: CompleteOnboardingPresenter; - - beforeEach(() => { - mockApiClient = { - getLeaderboard: vi.fn(), - completeOnboarding: vi.fn(), - getCurrent: vi.fn(), - } as unknown as DriversApiClient; - - mockLeaderboardPresenter = { - present: vi.fn(), - } as unknown as DriversLeaderboardPresenter; - - mockDriverPresenter = { - present: vi.fn(), - } as unknown as DriverPresenter; - - mockOnboardingPresenter = { - present: vi.fn(), - } as unknown as CompleteOnboardingPresenter; - - service = new DriverService( - mockApiClient, - mockLeaderboardPresenter, - mockDriverPresenter, - mockOnboardingPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(DriverService); - }); - }); - - describe('getDriverLeaderboard', () => { - it('should fetch leaderboard from API and transform via presenter', async () => { - // Arrange - const mockDto: DriversLeaderboardDto = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 2500, - races: 50, - wins: 10, - isActive: true, - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 2300, - races: 40, - wins: 8, - isActive: true, - }, - ], - }; - - const mockViewModel = { - drivers: [ - { - id: 'driver-1', - name: 'John Doe', - rating: 2500, - races: 50, - wins: 10, - isActive: true, - }, - { - id: 'driver-2', - name: 'Jane Smith', - rating: 2300, - races: 40, - wins: 8, - isActive: true, - }, - ], - } as DriverLeaderboardViewModel; - - vi.mocked(mockApiClient.getLeaderboard).mockResolvedValue(mockDto); - vi.mocked(mockLeaderboardPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getDriverLeaderboard(); - - // Assert - expect(mockApiClient.getLeaderboard).toHaveBeenCalled(); - expect(mockLeaderboardPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle empty leaderboard', async () => { - // Arrange - const mockDto: DriversLeaderboardDto = { - drivers: [], - }; - - const mockViewModel = { - drivers: [], - } as DriverLeaderboardViewModel; - - vi.mocked(mockApiClient.getLeaderboard).mockResolvedValue(mockDto); - vi.mocked(mockLeaderboardPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getDriverLeaderboard(); - - // Assert - expect(mockApiClient.getLeaderboard).toHaveBeenCalled(); - expect(mockLeaderboardPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Leaderboard fetch failed'); - vi.mocked(mockApiClient.getLeaderboard).mockRejectedValue(error); - - // Act & Assert - await expect(service.getDriverLeaderboard()).rejects.toThrow('Leaderboard fetch failed'); - expect(mockApiClient.getLeaderboard).toHaveBeenCalled(); - expect(mockLeaderboardPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('completeDriverOnboarding', () => { - it('should complete onboarding and transform via presenter', async () => { - // Arrange - const input: CompleteOnboardingInputDto = { - iracingId: '123456', - displayName: 'John Doe', - }; - - const mockDto: CompleteOnboardingOutputDto = { - driverId: 'driver-123', - success: true, - }; - - const mockViewModel: CompleteOnboardingViewModel = { - driverId: 'driver-123', - success: true, - }; - - vi.mocked(mockApiClient.completeOnboarding).mockResolvedValue(mockDto); - vi.mocked(mockOnboardingPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.completeDriverOnboarding(input); - - // Assert - expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input); - expect(mockOnboardingPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle onboarding failure', async () => { - // Arrange - const input: CompleteOnboardingInputDto = { - iracingId: '123456', - displayName: 'John Doe', - }; - - const mockDto: CompleteOnboardingOutputDto = { - driverId: '', - success: false, - }; - - const mockViewModel: CompleteOnboardingViewModel = { - driverId: '', - success: false, - }; - - vi.mocked(mockApiClient.completeOnboarding).mockResolvedValue(mockDto); - vi.mocked(mockOnboardingPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.completeDriverOnboarding(input); - - // Assert - expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input); - expect(mockOnboardingPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: CompleteOnboardingInputDto = { - iracingId: '123456', - displayName: 'John Doe', - }; - const error = new Error('Onboarding failed'); - vi.mocked(mockApiClient.completeOnboarding).mockRejectedValue(error); - - // Act & Assert - await expect(service.completeDriverOnboarding(input)).rejects.toThrow('Onboarding failed'); - expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input); - expect(mockOnboardingPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getCurrentDriver', () => { - it('should fetch current driver and transform via presenter', async () => { - // Arrange - const mockDto: DriverDto = { - id: 'driver-123', - name: 'John Doe', - avatarUrl: 'https://example.com/avatar.jpg', - iracingId: '123456', - rating: 2500, - }; - - const mockViewModel: DriverViewModel = { - id: 'driver-123', - name: 'John Doe', - avatarUrl: 'https://example.com/avatar.jpg', - iracingId: '123456', - rating: 2500, - }; - - vi.mocked(mockApiClient.getCurrent).mockResolvedValue(mockDto); - vi.mocked(mockDriverPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getCurrentDriver(); - - // Assert - expect(mockApiClient.getCurrent).toHaveBeenCalled(); - expect(mockDriverPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should return null when no current driver', async () => { - // Arrange - vi.mocked(mockApiClient.getCurrent).mockResolvedValue(null); - - // Act - const result = await service.getCurrentDriver(); - - // Assert - expect(mockApiClient.getCurrent).toHaveBeenCalled(); - expect(mockDriverPresenter.present).not.toHaveBeenCalled(); - expect(result).toBeNull(); - }); - - it('should handle driver without optional fields', async () => { - // Arrange - const mockDto: DriverDto = { - id: 'driver-123', - name: 'John Doe', - }; - - const mockViewModel: DriverViewModel = { - id: 'driver-123', - name: 'John Doe', - }; - - vi.mocked(mockApiClient.getCurrent).mockResolvedValue(mockDto); - vi.mocked(mockDriverPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getCurrentDriver(); - - // Assert - expect(mockApiClient.getCurrent).toHaveBeenCalled(); - expect(mockDriverPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch current driver'); - vi.mocked(mockApiClient.getCurrent).mockRejectedValue(error); - - // Act & Assert - await expect(service.getCurrentDriver()).rejects.toThrow('Failed to fetch current driver'); - expect(mockApiClient.getCurrent).toHaveBeenCalled(); - expect(mockDriverPresenter.present).not.toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/drivers/DriverService.ts b/apps/website/lib/services/drivers/DriverService.ts index 3e92cae76..d169e88de 100644 --- a/apps/website/lib/services/drivers/DriverService.ts +++ b/apps/website/lib/services/drivers/DriverService.ts @@ -1,51 +1,44 @@ import type { DriversApiClient } from '../../api/drivers/DriversApiClient'; -import type { DriversLeaderboardPresenter } from '../../presenters/DriversLeaderboardPresenter'; -import type { DriverPresenter } from '../../presenters/DriverPresenter'; -import type { CompleteOnboardingPresenter } from '../../presenters/CompleteOnboardingPresenter'; -import type { DriverLeaderboardViewModel } from '../../view-models'; -import type { DriverViewModel } from '../../view-models/DriverViewModel'; -import type { CompleteOnboardingViewModel } from '../../view-models/CompleteOnboardingViewModel'; -// Import generated types instead of manual DTOs -import type { CompleteOnboardingInputDTO } from '../../types/api-helpers'; +import { DriverLeaderboardViewModel } from '../../view-models'; +import { DriverViewModel } from '../../view-models/DriverViewModel'; +import { CompleteOnboardingViewModel } from '../../view-models/CompleteOnboardingViewModel'; +import type { CompleteOnboardingInputDTO } from '../../types/generated'; /** * Driver Service * - * Orchestrates driver operations by coordinating API calls and presentation logic. + * Orchestrates driver operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class DriverService { constructor( - private readonly apiClient: DriversApiClient, - private readonly leaderboardPresenter: DriversLeaderboardPresenter, - private readonly driverPresenter: DriverPresenter, - private readonly onboardingPresenter: CompleteOnboardingPresenter + private readonly apiClient: DriversApiClient ) {} /** - * Get driver leaderboard with presentation transformation + * Get driver leaderboard with view model transformation */ async getDriverLeaderboard(): Promise { const dto = await this.apiClient.getLeaderboard(); - return this.leaderboardPresenter.present(dto); + return new DriverLeaderboardViewModel(dto); } /** - * Complete driver onboarding with presentation transformation + * Complete driver onboarding with view model transformation */ async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise { const dto = await this.apiClient.completeOnboarding(input); - return this.onboardingPresenter.present(dto); + return new CompleteOnboardingViewModel(dto); } /** - * Get current driver with presentation transformation + * Get current driver with view model transformation */ async getCurrentDriver(): Promise { const dto = await this.apiClient.getCurrent(); if (!dto) { return null; } - return this.driverPresenter.present(dto); + return new DriverViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueMembershipService.test.ts b/apps/website/lib/services/leagues/LeagueMembershipService.test.ts deleted file mode 100644 index 300ec0fc0..000000000 --- a/apps/website/lib/services/leagues/LeagueMembershipService.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { LeagueMembershipService } from './LeagueMembershipService'; -import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; -import type { LeagueMembersPresenter } from '../../presenters/LeagueMembersPresenter'; -import type { LeagueMembershipsDto } from '../../dtos'; -import type { LeagueMemberViewModel } from '../../view-models'; - -describe('LeagueMembershipService', () => { - let service: LeagueMembershipService; - let mockApiClient: LeaguesApiClient; - let mockLeagueMembersPresenter: LeagueMembersPresenter; - - beforeEach(() => { - mockApiClient = { - getMemberships: vi.fn(), - removeMember: vi.fn(), - } as unknown as LeaguesApiClient; - - mockLeagueMembersPresenter = { - present: vi.fn(), - } as unknown as LeagueMembersPresenter; - - service = new LeagueMembershipService( - mockApiClient, - mockLeagueMembersPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(LeagueMembershipService); - }); - }); - - describe('getLeagueMemberships', () => { - it('should fetch league memberships from API and transform via presenter', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'user-456'; - - const mockDto: LeagueMembershipsDto = { - members: [ - { - driverId: 'driver-1', - role: 'owner', - joinedAt: '2024-01-01', - }, - { - driverId: 'driver-2', - role: 'member', - joinedAt: '2024-01-02', - }, - ], - }; - - const mockViewModels: LeagueMemberViewModel[] = [ - { - driverId: 'driver-1', - role: 'owner', - joinedAt: '2024-01-01', - } as LeagueMemberViewModel, - { - driverId: 'driver-2', - role: 'member', - joinedAt: '2024-01-02', - } as LeagueMemberViewModel, - ]; - - vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockDto); - vi.mocked(mockLeagueMembersPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getLeagueMemberships(leagueId, currentUserId); - - // Assert - expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId); - expect(mockLeagueMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId); - expect(result).toEqual(mockViewModels); - }); - - it('should handle empty memberships list', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'user-456'; - - const mockDto: LeagueMembershipsDto = { - members: [], - }; - - const mockViewModels: LeagueMemberViewModel[] = []; - - vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockDto); - vi.mocked(mockLeagueMembersPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getLeagueMemberships(leagueId, currentUserId); - - // Assert - expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId); - expect(mockLeagueMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId); - expect(result).toEqual([]); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'user-456'; - const error = new Error('Failed to fetch memberships'); - vi.mocked(mockApiClient.getMemberships).mockRejectedValue(error); - - // Act & Assert - await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('Failed to fetch memberships'); - expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId); - expect(mockLeagueMembersPresenter.present).not.toHaveBeenCalled(); - }); - - it('should pass correct currentUserId to presenter', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'current-user-789'; - - const mockDto: LeagueMembershipsDto = { - members: [ - { - driverId: 'driver-1', - role: 'member', - joinedAt: '2024-01-01', - }, - ], - }; - - const mockViewModels: LeagueMemberViewModel[] = [ - { - driverId: 'driver-1', - role: 'member', - joinedAt: '2024-01-01', - } as LeagueMemberViewModel, - ]; - - vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockDto); - vi.mocked(mockLeagueMembersPresenter.present).mockReturnValue(mockViewModels); - - // Act - await service.getLeagueMemberships(leagueId, currentUserId); - - // Assert - expect(mockLeagueMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId); - }); - }); - - describe('removeMember', () => { - it('should remove a member from league', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'driver-remove'; - - const mockOutput = { success: true }; - - vi.mocked(mockApiClient.removeMember).mockResolvedValue(mockOutput); - - // Act - const result = await service.removeMember(leagueId, performerDriverId, targetDriverId); - - // Assert - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - expect(result).toEqual(mockOutput); - }); - - it('should handle removal failure', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'driver-remove'; - - const mockOutput = { success: false }; - - vi.mocked(mockApiClient.removeMember).mockResolvedValue(mockOutput); - - // Act - const result = await service.removeMember(leagueId, performerDriverId, targetDriverId); - - // Assert - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - expect(result).toEqual(mockOutput); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'driver-remove'; - const error = new Error('Failed to remove member'); - vi.mocked(mockApiClient.removeMember).mockRejectedValue(error); - - // Act & Assert - await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('Failed to remove member'); - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - }); - - it('should handle unauthorized removal attempt', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'non-admin-driver'; - const targetDriverId = 'driver-remove'; - const error = new Error('Unauthorized'); - vi.mocked(mockApiClient.removeMember).mockRejectedValue(error); - - // Act & Assert - await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('Unauthorized'); - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - }); - - it('should handle removing non-existent member', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'non-existent-driver'; - const error = new Error('Member not found'); - vi.mocked(mockApiClient.removeMember).mockRejectedValue(error); - - // Act & Assert - await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('Member not found'); - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueMembershipService.ts b/apps/website/lib/services/leagues/LeagueMembershipService.ts index bcb9eeaf5..5b215a519 100644 --- a/apps/website/lib/services/leagues/LeagueMembershipService.ts +++ b/apps/website/lib/services/leagues/LeagueMembershipService.ts @@ -1,25 +1,24 @@ import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; -import type { LeagueMemberViewModel } from '../../view-models'; -import type { LeagueMembersPresenter } from '../../presenters/LeagueMembersPresenter'; +import { LeagueMemberViewModel } from '../../view-models'; +import type { LeagueMemberDTO } from '../../types/generated'; /** * League Membership Service * - * Orchestrates league membership operations by coordinating API calls and presentation logic. + * Orchestrates league membership operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class LeagueMembershipService { constructor( - private readonly apiClient: LeaguesApiClient, - private readonly leagueMembersPresenter: LeagueMembersPresenter + private readonly apiClient: LeaguesApiClient ) {} /** - * Get league memberships with presentation transformation + * Get league memberships with view model transformation */ async getLeagueMemberships(leagueId: string, currentUserId: string): Promise { const dto = await this.apiClient.getMemberships(leagueId); - return this.leagueMembersPresenter.present(dto, currentUserId); + return dto.members.map((member: LeagueMemberDTO) => new LeagueMemberViewModel(member, currentUserId)); } /** diff --git a/apps/website/lib/services/leagues/LeagueService.test.ts b/apps/website/lib/services/leagues/LeagueService.test.ts deleted file mode 100644 index 462ab97b1..000000000 --- a/apps/website/lib/services/leagues/LeagueService.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { LeagueService } from './LeagueService'; -import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; -import type { LeagueSummaryPresenter } from '../../presenters/LeagueSummaryPresenter'; -import type { LeagueStandingsPresenter } from '../../presenters/LeagueStandingsPresenter'; -import type { - AllLeaguesWithCapacityDto, - LeagueStandingsDto, - LeagueStatsDto, - LeagueScheduleDto, - LeagueMembershipsDto, - CreateLeagueInputDto, - CreateLeagueOutputDto, -} from '../../dtos'; -import type { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models'; - -describe('LeagueService', () => { - let service: LeagueService; - let mockApiClient: LeaguesApiClient; - let mockLeagueSummaryPresenter: LeagueSummaryPresenter; - let mockLeagueStandingsPresenter: LeagueStandingsPresenter; - - beforeEach(() => { - mockApiClient = { - getAllWithCapacity: vi.fn(), - getTotal: vi.fn(), - getStandings: vi.fn(), - getSchedule: vi.fn(), - getMemberships: vi.fn(), - create: vi.fn(), - removeMember: vi.fn(), - } as unknown as LeaguesApiClient; - - mockLeagueSummaryPresenter = { - present: vi.fn(), - } as unknown as LeagueSummaryPresenter; - - mockLeagueStandingsPresenter = { - present: vi.fn(), - } as unknown as LeagueStandingsPresenter; - - service = new LeagueService( - mockApiClient, - mockLeagueSummaryPresenter, - mockLeagueStandingsPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(LeagueService); - }); - }); - - describe('getAllLeagues', () => { - it('should fetch all leagues from API and transform via presenter', async () => { - // Arrange - const mockDto: AllLeaguesWithCapacityDto = { - leagues: [ - { - id: 'league-1', - name: 'Championship League', - description: 'Top tier racing', - memberCount: 10, - maxMembers: 20, - isPublic: true, - ownerId: 'owner-1', - }, - { - id: 'league-2', - name: 'Rookie League', - description: 'Entry level racing', - memberCount: 5, - maxMembers: 15, - isPublic: true, - ownerId: 'owner-2', - }, - ], - }; - - const mockViewModels: LeagueSummaryViewModel[] = [ - { - id: 'league-1', - name: 'Championship League', - description: 'Top tier racing', - memberCount: 10, - maxMembers: 20, - isPublic: true, - ownerId: 'owner-1', - } as LeagueSummaryViewModel, - { - id: 'league-2', - name: 'Rookie League', - description: 'Entry level racing', - memberCount: 5, - maxMembers: 15, - isPublic: true, - ownerId: 'owner-2', - } as LeagueSummaryViewModel, - ]; - - vi.mocked(mockApiClient.getAllWithCapacity).mockResolvedValue(mockDto); - vi.mocked(mockLeagueSummaryPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getAllLeagues(); - - // Assert - expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled(); - expect(mockLeagueSummaryPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModels); - }); - - it('should handle empty leagues list', async () => { - // Arrange - const mockDto: AllLeaguesWithCapacityDto = { - leagues: [], - }; - - const mockViewModels: LeagueSummaryViewModel[] = []; - - vi.mocked(mockApiClient.getAllWithCapacity).mockResolvedValue(mockDto); - vi.mocked(mockLeagueSummaryPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getAllLeagues(); - - // Assert - expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled(); - expect(mockLeagueSummaryPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual([]); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch leagues'); - vi.mocked(mockApiClient.getAllWithCapacity).mockRejectedValue(error); - - // Act & Assert - await expect(service.getAllLeagues()).rejects.toThrow('Failed to fetch leagues'); - expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled(); - expect(mockLeagueSummaryPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getLeagueStandings', () => { - it('should fetch league standings and transform via presenter', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'user-456'; - - const mockDto: LeagueStandingsDto = { - standings: [ - { - position: 1, - driverId: 'driver-1', - points: 100, - }, - { - position: 2, - driverId: 'driver-2', - points: 85, - }, - ], - drivers: [], - memberships: [], - }; - - const mockViewModel: LeagueStandingsViewModel = { - standings: [ - { - position: 1, - driverId: 'driver-1', - points: 100, - }, - { - position: 2, - driverId: 'driver-2', - points: 85, - }, - ], - drivers: [], - memberships: [], - } as LeagueStandingsViewModel; - - vi.mocked(mockApiClient.getStandings).mockResolvedValue(mockDto); - vi.mocked(mockLeagueStandingsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getLeagueStandings(leagueId, currentUserId); - - // Assert - expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId); - expect(mockLeagueStandingsPresenter.present).toHaveBeenCalledWith( - expect.objectContaining(mockDto), - currentUserId - ); - expect(result).toEqual(mockViewModel); - }); - - it('should handle empty standings', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'user-456'; - - const mockDto: LeagueStandingsDto = { - standings: [], - drivers: [], - memberships: [], - }; - - const mockViewModel: LeagueStandingsViewModel = { - standings: [], - drivers: [], - memberships: [], - } as LeagueStandingsViewModel; - - vi.mocked(mockApiClient.getStandings).mockResolvedValue(mockDto); - vi.mocked(mockLeagueStandingsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getLeagueStandings(leagueId, currentUserId); - - // Assert - expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId); - expect(mockLeagueStandingsPresenter.present).toHaveBeenCalled(); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const leagueId = 'league-123'; - const currentUserId = 'user-456'; - const error = new Error('Failed to fetch standings'); - vi.mocked(mockApiClient.getStandings).mockRejectedValue(error); - - // Act & Assert - await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('Failed to fetch standings'); - expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId); - expect(mockLeagueStandingsPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getLeagueStats', () => { - it('should fetch league statistics', async () => { - // Arrange - const mockStats: LeagueStatsDto = { - totalLeagues: 42, - }; - - vi.mocked(mockApiClient.getTotal).mockResolvedValue(mockStats); - - // Act - const result = await service.getLeagueStats(); - - // Assert - expect(mockApiClient.getTotal).toHaveBeenCalled(); - expect(result).toEqual(mockStats); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch stats'); - vi.mocked(mockApiClient.getTotal).mockRejectedValue(error); - - // Act & Assert - await expect(service.getLeagueStats()).rejects.toThrow('Failed to fetch stats'); - expect(mockApiClient.getTotal).toHaveBeenCalled(); - }); - }); - - describe('getLeagueSchedule', () => { - it('should fetch league schedule', async () => { - // Arrange - const leagueId = 'league-123'; - const mockSchedule: LeagueScheduleDto = { - races: [ - { - id: 'race-1', - name: 'Race 1', - date: '2024-01-01', - }, - { - id: 'race-2', - name: 'Race 2', - date: '2024-01-08', - }, - ], - }; - - vi.mocked(mockApiClient.getSchedule).mockResolvedValue(mockSchedule); - - // Act - const result = await service.getLeagueSchedule(leagueId); - - // Assert - expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId); - expect(result).toEqual(mockSchedule); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const leagueId = 'league-123'; - const error = new Error('Failed to fetch schedule'); - vi.mocked(mockApiClient.getSchedule).mockRejectedValue(error); - - // Act & Assert - await expect(service.getLeagueSchedule(leagueId)).rejects.toThrow('Failed to fetch schedule'); - expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId); - }); - }); - - describe('getLeagueMemberships', () => { - it('should fetch league memberships', async () => { - // Arrange - const leagueId = 'league-123'; - const mockMemberships: LeagueMembershipsDto = { - memberships: [ - { - driverId: 'driver-1', - role: 'admin', - joinedAt: '2024-01-01', - }, - { - driverId: 'driver-2', - role: 'member', - joinedAt: '2024-01-02', - }, - ], - }; - - vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockMemberships); - - // Act - const result = await service.getLeagueMemberships(leagueId); - - // Assert - expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId); - expect(result).toEqual(mockMemberships); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const leagueId = 'league-123'; - const error = new Error('Failed to fetch memberships'); - vi.mocked(mockApiClient.getMemberships).mockRejectedValue(error); - - // Act & Assert - await expect(service.getLeagueMemberships(leagueId)).rejects.toThrow('Failed to fetch memberships'); - expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId); - }); - }); - - describe('createLeague', () => { - it('should create a new league', async () => { - // Arrange - const input: CreateLeagueInputDto = { - name: 'New League', - description: 'A brand new league', - maxMembers: 25, - isPublic: true, - }; - - const mockOutput: CreateLeagueOutputDto = { - id: 'league-new', - name: 'New League', - success: true, - }; - - vi.mocked(mockApiClient.create).mockResolvedValue(mockOutput); - - // Act - const result = await service.createLeague(input); - - // Assert - expect(mockApiClient.create).toHaveBeenCalledWith(input); - expect(result).toEqual(mockOutput); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: CreateLeagueInputDto = { - name: 'New League', - description: 'A brand new league', - maxMembers: 25, - isPublic: true, - }; - const error = new Error('Failed to create league'); - vi.mocked(mockApiClient.create).mockRejectedValue(error); - - // Act & Assert - await expect(service.createLeague(input)).rejects.toThrow('Failed to create league'); - expect(mockApiClient.create).toHaveBeenCalledWith(input); - }); - }); - - describe('removeMember', () => { - it('should remove a member from league', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'driver-remove'; - - const mockOutput = { success: true }; - - vi.mocked(mockApiClient.removeMember).mockResolvedValue(mockOutput); - - // Act - const result = await service.removeMember(leagueId, performerDriverId, targetDriverId); - - // Assert - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - expect(result).toEqual(mockOutput); - }); - - it('should handle removal failure', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'driver-remove'; - - const mockOutput = { success: false }; - - vi.mocked(mockApiClient.removeMember).mockResolvedValue(mockOutput); - - // Act - const result = await service.removeMember(leagueId, performerDriverId, targetDriverId); - - // Assert - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - expect(result).toEqual(mockOutput); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const leagueId = 'league-123'; - const performerDriverId = 'driver-admin'; - const targetDriverId = 'driver-remove'; - const error = new Error('Failed to remove member'); - vi.mocked(mockApiClient.removeMember).mockRejectedValue(error); - - // Act & Assert - await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('Failed to remove member'); - expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/leagues/LeagueService.ts b/apps/website/lib/services/leagues/LeagueService.ts index fcab7f348..de93b2851 100644 --- a/apps/website/lib/services/leagues/LeagueService.ts +++ b/apps/website/lib/services/leagues/LeagueService.ts @@ -1,32 +1,33 @@ import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; -import type { LeagueSummaryPresenter } from '../../presenters/LeagueSummaryPresenter'; -import type { LeagueStandingsPresenter } from '../../presenters/LeagueStandingsPresenter'; -import type { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models'; -import type { CreateLeagueInputDto, CreateLeagueOutputDto, LeagueStatsDto, LeagueScheduleDto, LeagueMembershipsDto } from '../../dtos'; +import { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models'; +import type { CreateLeagueInputDTO, CreateLeagueOutputDTO, LeagueWithCapacityDTO } from '../../types/generated'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type LeagueStatsDto = { totalLeagues: number }; +type LeagueScheduleDto = { races: Array }; +type LeagueMembershipsDto = { memberships: Array }; /** * League Service * - * Orchestrates league operations by coordinating API calls and presentation logic. + * Orchestrates league operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class LeagueService { constructor( - private readonly apiClient: LeaguesApiClient, - private readonly leagueSummaryPresenter: LeagueSummaryPresenter, - private readonly leagueStandingsPresenter: LeagueStandingsPresenter + private readonly apiClient: LeaguesApiClient ) {} /** - * Get all leagues with presentation transformation + * Get all leagues with view model transformation */ async getAllLeagues(): Promise { const dto = await this.apiClient.getAllWithCapacity(); - return this.leagueSummaryPresenter.present(dto); + return dto.leagues.map((league: LeagueWithCapacityDTO) => new LeagueSummaryViewModel(league)); } /** - * Get league standings with presentation transformation + * Get league standings with view model transformation */ async getLeagueStandings(leagueId: string, currentUserId: string): Promise { const dto = await this.apiClient.getStandings(leagueId); @@ -36,7 +37,7 @@ export class LeagueService { drivers: [], // TODO: fetch drivers memberships: [], // TODO: fetch memberships }; - return this.leagueStandingsPresenter.present(dtoWithExtras, currentUserId); + return new LeagueStandingsViewModel(dtoWithExtras, currentUserId); } /** @@ -63,7 +64,7 @@ export class LeagueService { /** * Create a new league */ - async createLeague(input: CreateLeagueInputDto): Promise { + async createLeague(input: CreateLeagueInputDTO): Promise { return await this.apiClient.create(input); } diff --git a/apps/website/lib/services/media/AvatarService.test.ts b/apps/website/lib/services/media/AvatarService.test.ts deleted file mode 100644 index d7da1c217..000000000 --- a/apps/website/lib/services/media/AvatarService.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AvatarService } from './AvatarService'; -import type { MediaApiClient } from '../../api/media/MediaApiClient'; -import type { AvatarPresenter } from '../../presenters/AvatarPresenter'; -import type { - RequestAvatarGenerationInputDto, - RequestAvatarGenerationOutputDto, - GetAvatarOutputDto, - UpdateAvatarInputDto, - UpdateAvatarOutputDto -} from '../../dtos'; -import type { - RequestAvatarGenerationViewModel, - AvatarViewModel, - UpdateAvatarViewModel -} from '../../view-models'; - -describe('AvatarService', () => { - let service: AvatarService; - let mockApiClient: MediaApiClient; - let mockPresenter: AvatarPresenter; - - beforeEach(() => { - mockApiClient = { - requestAvatarGeneration: vi.fn(), - getAvatar: vi.fn(), - updateAvatar: vi.fn(), - } as unknown as MediaApiClient; - - mockPresenter = { - presentRequestGeneration: vi.fn(), - presentAvatar: vi.fn(), - presentUpdate: vi.fn(), - } as unknown as AvatarPresenter; - - service = new AvatarService(mockApiClient, mockPresenter); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(AvatarService); - }); - }); - - describe('requestAvatarGeneration', () => { - it('should request avatar generation and transform via presenter', async () => { - // Arrange - const input: RequestAvatarGenerationInputDto = { - driverId: 'driver-123', - style: 'realistic', - }; - - const mockDto: RequestAvatarGenerationOutputDto = { - success: true, - avatarUrl: 'https://example.com/avatar/generated.jpg', - }; - - const mockViewModel: RequestAvatarGenerationViewModel = { - success: true, - avatarUrl: 'https://example.com/avatar/generated.jpg', - }; - - vi.mocked(mockApiClient.requestAvatarGeneration).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentRequestGeneration).mockReturnValue(mockViewModel); - - // Act - const result = await service.requestAvatarGeneration(input); - - // Assert - expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input); - expect(mockPresenter.presentRequestGeneration).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle generation failure', async () => { - // Arrange - const input: RequestAvatarGenerationInputDto = { - driverId: 'driver-123', - }; - - const mockDto: RequestAvatarGenerationOutputDto = { - success: false, - error: 'Generation failed', - }; - - const mockViewModel: RequestAvatarGenerationViewModel = { - success: false, - error: 'Generation failed', - }; - - vi.mocked(mockApiClient.requestAvatarGeneration).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentRequestGeneration).mockReturnValue(mockViewModel); - - // Act - const result = await service.requestAvatarGeneration(input); - - // Assert - expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input); - expect(mockPresenter.presentRequestGeneration).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: RequestAvatarGenerationInputDto = { - driverId: 'driver-123', - }; - const error = new Error('Network error'); - vi.mocked(mockApiClient.requestAvatarGeneration).mockRejectedValue(error); - - // Act & Assert - await expect(service.requestAvatarGeneration(input)).rejects.toThrow('Network error'); - expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input); - expect(mockPresenter.presentRequestGeneration).not.toHaveBeenCalled(); - }); - }); - - describe('getAvatar', () => { - it('should fetch avatar and transform via presenter', async () => { - // Arrange - const driverId = 'driver-123'; - const mockDto: GetAvatarOutputDto = { - driverId: 'driver-123', - avatarUrl: 'https://example.com/avatar.jpg', - hasAvatar: true, - }; - - const mockViewModel: AvatarViewModel = { - driverId: 'driver-123', - avatarUrl: 'https://example.com/avatar.jpg', - hasAvatar: true, - }; - - vi.mocked(mockApiClient.getAvatar).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentAvatar).mockReturnValue(mockViewModel); - - // Act - const result = await service.getAvatar(driverId); - - // Assert - expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId); - expect(mockPresenter.presentAvatar).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle driver without avatar', async () => { - // Arrange - const driverId = 'driver-123'; - const mockDto: GetAvatarOutputDto = { - driverId: 'driver-123', - hasAvatar: false, - }; - - const mockViewModel: AvatarViewModel = { - driverId: 'driver-123', - hasAvatar: false, - }; - - vi.mocked(mockApiClient.getAvatar).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentAvatar).mockReturnValue(mockViewModel); - - // Act - const result = await service.getAvatar(driverId); - - // Assert - expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId); - expect(mockPresenter.presentAvatar).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.hasAvatar).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const driverId = 'driver-123'; - const error = new Error('Avatar not found'); - vi.mocked(mockApiClient.getAvatar).mockRejectedValue(error); - - // Act & Assert - await expect(service.getAvatar(driverId)).rejects.toThrow('Avatar not found'); - expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId); - expect(mockPresenter.presentAvatar).not.toHaveBeenCalled(); - }); - }); - - describe('updateAvatar', () => { - it('should update avatar and transform via presenter', async () => { - // Arrange - const input: UpdateAvatarInputDto = { - driverId: 'driver-123', - avatarUrl: 'https://example.com/new-avatar.jpg', - }; - - const mockDto: UpdateAvatarOutputDto = { - success: true, - }; - - const mockViewModel: UpdateAvatarViewModel = { - success: true, - }; - - vi.mocked(mockApiClient.updateAvatar).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentUpdate).mockReturnValue(mockViewModel); - - // Act - const result = await service.updateAvatar(input); - - // Assert - expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input); - expect(mockPresenter.presentUpdate).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle update failure', async () => { - // Arrange - const input: UpdateAvatarInputDto = { - driverId: 'driver-123', - avatarUrl: 'https://example.com/new-avatar.jpg', - }; - - const mockDto: UpdateAvatarOutputDto = { - success: false, - error: 'Update failed', - }; - - const mockViewModel: UpdateAvatarViewModel = { - success: false, - error: 'Update failed', - }; - - vi.mocked(mockApiClient.updateAvatar).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentUpdate).mockReturnValue(mockViewModel); - - // Act - const result = await service.updateAvatar(input); - - // Assert - expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input); - expect(mockPresenter.presentUpdate).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: UpdateAvatarInputDto = { - driverId: 'driver-123', - avatarUrl: 'https://example.com/new-avatar.jpg', - }; - const error = new Error('Update failed'); - vi.mocked(mockApiClient.updateAvatar).mockRejectedValue(error); - - // Act & Assert - await expect(service.updateAvatar(input)).rejects.toThrow('Update failed'); - expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input); - expect(mockPresenter.presentUpdate).not.toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/media/AvatarService.ts b/apps/website/lib/services/media/AvatarService.ts index d4226c801..65612dad5 100644 --- a/apps/website/lib/services/media/AvatarService.ts +++ b/apps/website/lib/services/media/AvatarService.ts @@ -1,10 +1,9 @@ import type { MediaApiClient } from '../../api/media/MediaApiClient'; -import type { AvatarPresenter } from '../../presenters/AvatarPresenter'; -import type { - RequestAvatarGenerationInputDto, - UpdateAvatarInputDto -} from '../../dtos'; -import type { +import type { RequestAvatarGenerationInputDTO } from '../../types/generated'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type UpdateAvatarInputDto = { driverId: string; avatarUrl: string }; +import { RequestAvatarGenerationViewModel, AvatarViewModel, UpdateAvatarViewModel @@ -13,48 +12,35 @@ import type { /** * Avatar Service * - * Orchestrates avatar operations by coordinating API calls and presentation logic. + * Orchestrates avatar operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class AvatarService { constructor( - private readonly apiClient: MediaApiClient, - private readonly presenter: AvatarPresenter + private readonly apiClient: MediaApiClient ) {} /** - * Request avatar generation with presentation transformation + * Request avatar generation with view model transformation */ - async requestAvatarGeneration(input: RequestAvatarGenerationInputDto): Promise { - try { - const dto = await this.apiClient.requestAvatarGeneration(input); - return this.presenter.presentRequestGeneration(dto); - } catch (error) { - throw error; - } + async requestAvatarGeneration(input: RequestAvatarGenerationInputDTO): Promise { + const dto = await this.apiClient.requestAvatarGeneration(input); + return new RequestAvatarGenerationViewModel(dto); } /** - * Get avatar for driver with presentation transformation + * Get avatar for driver with view model transformation */ async getAvatar(driverId: string): Promise { - try { - const dto = await this.apiClient.getAvatar(driverId); - return this.presenter.presentAvatar(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getAvatar(driverId); + return new AvatarViewModel(dto); } /** - * Update avatar for driver with presentation transformation + * Update avatar for driver with view model transformation */ async updateAvatar(input: UpdateAvatarInputDto): Promise { - try { - const dto = await this.apiClient.updateAvatar(input); - return this.presenter.presentUpdate(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.updateAvatar(input); + return new UpdateAvatarViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/media/MediaService.test.ts b/apps/website/lib/services/media/MediaService.test.ts deleted file mode 100644 index dd719be51..000000000 --- a/apps/website/lib/services/media/MediaService.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { MediaService } from './MediaService'; -import type { MediaApiClient } from '../../api/media/MediaApiClient'; -import type { MediaPresenter } from '../../presenters/MediaPresenter'; -import type { - UploadMediaInputDto, - UploadMediaOutputDto, - GetMediaOutputDto, - DeleteMediaOutputDto -} from '../../dtos'; -import type { - UploadMediaViewModel, - MediaViewModel, - DeleteMediaViewModel -} from '../../view-models'; - -describe('MediaService', () => { - let service: MediaService; - let mockApiClient: MediaApiClient; - let mockPresenter: MediaPresenter; - - beforeEach(() => { - mockApiClient = { - uploadMedia: vi.fn(), - getMedia: vi.fn(), - deleteMedia: vi.fn(), - } as unknown as MediaApiClient; - - mockPresenter = { - presentUpload: vi.fn(), - presentMedia: vi.fn(), - presentDelete: vi.fn(), - } as unknown as MediaPresenter; - - service = new MediaService(mockApiClient, mockPresenter); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(MediaService); - }); - }); - - describe('uploadMedia', () => { - it('should upload media and transform via presenter', async () => { - // Arrange - const input: UploadMediaInputDto = { - file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }), - type: 'image', - category: 'avatar', - }; - - const mockDto: UploadMediaOutputDto = { - success: true, - mediaId: 'media-123', - url: 'https://example.com/media/test.jpg', - }; - - const mockViewModel: UploadMediaViewModel = { - success: true, - mediaId: 'media-123', - url: 'https://example.com/media/test.jpg', - }; - - vi.mocked(mockApiClient.uploadMedia).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentUpload).mockReturnValue(mockViewModel); - - // Act - const result = await service.uploadMedia(input); - - // Assert - expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input); - expect(mockPresenter.presentUpload).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle upload failure', async () => { - // Arrange - const input: UploadMediaInputDto = { - file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }), - type: 'image', - }; - - const mockDto: UploadMediaOutputDto = { - success: false, - error: 'Upload failed', - }; - - const mockViewModel: UploadMediaViewModel = { - success: false, - error: 'Upload failed', - }; - - vi.mocked(mockApiClient.uploadMedia).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentUpload).mockReturnValue(mockViewModel); - - // Act - const result = await service.uploadMedia(input); - - // Assert - expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input); - expect(mockPresenter.presentUpload).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: UploadMediaInputDto = { - file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }), - type: 'image', - }; - const error = new Error('Network error'); - vi.mocked(mockApiClient.uploadMedia).mockRejectedValue(error); - - // Act & Assert - await expect(service.uploadMedia(input)).rejects.toThrow('Network error'); - expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input); - expect(mockPresenter.presentUpload).not.toHaveBeenCalled(); - }); - }); - - describe('getMedia', () => { - it('should fetch media and transform via presenter', async () => { - // Arrange - const mediaId = 'media-123'; - const mockDto: GetMediaOutputDto = { - id: 'media-123', - url: 'https://example.com/media/test.jpg', - type: 'image', - category: 'avatar', - uploadedAt: '2023-01-01T00:00:00Z', - size: 1024, - }; - - const mockViewModel: MediaViewModel = { - id: 'media-123', - url: 'https://example.com/media/test.jpg', - type: 'image', - category: 'avatar', - uploadedAt: new Date('2023-01-01T00:00:00Z'), - size: 1024, - }; - - vi.mocked(mockApiClient.getMedia).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentMedia).mockReturnValue(mockViewModel); - - // Act - const result = await service.getMedia(mediaId); - - // Assert - expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId); - expect(mockPresenter.presentMedia).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle media without optional fields', async () => { - // Arrange - const mediaId = 'media-123'; - const mockDto: GetMediaOutputDto = { - id: 'media-123', - url: 'https://example.com/media/test.jpg', - type: 'image', - uploadedAt: '2023-01-01T00:00:00Z', - }; - - const mockViewModel: MediaViewModel = { - id: 'media-123', - url: 'https://example.com/media/test.jpg', - type: 'image', - uploadedAt: new Date('2023-01-01T00:00:00Z'), - }; - - vi.mocked(mockApiClient.getMedia).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentMedia).mockReturnValue(mockViewModel); - - // Act - const result = await service.getMedia(mediaId); - - // Assert - expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId); - expect(mockPresenter.presentMedia).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const mediaId = 'media-123'; - const error = new Error('Media not found'); - vi.mocked(mockApiClient.getMedia).mockRejectedValue(error); - - // Act & Assert - await expect(service.getMedia(mediaId)).rejects.toThrow('Media not found'); - expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId); - expect(mockPresenter.presentMedia).not.toHaveBeenCalled(); - }); - }); - - describe('deleteMedia', () => { - it('should delete media and transform via presenter', async () => { - // Arrange - const mediaId = 'media-123'; - const mockDto: DeleteMediaOutputDto = { - success: true, - }; - - const mockViewModel: DeleteMediaViewModel = { - success: true, - }; - - vi.mocked(mockApiClient.deleteMedia).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentDelete).mockReturnValue(mockViewModel); - - // Act - const result = await service.deleteMedia(mediaId); - - // Assert - expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId); - expect(mockPresenter.presentDelete).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle delete failure', async () => { - // Arrange - const mediaId = 'media-123'; - const mockDto: DeleteMediaOutputDto = { - success: false, - error: 'Delete failed', - }; - - const mockViewModel: DeleteMediaViewModel = { - success: false, - error: 'Delete failed', - }; - - vi.mocked(mockApiClient.deleteMedia).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.presentDelete).mockReturnValue(mockViewModel); - - // Act - const result = await service.deleteMedia(mediaId); - - // Assert - expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId); - expect(mockPresenter.presentDelete).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.success).toBe(false); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const mediaId = 'media-123'; - const error = new Error('Delete failed'); - vi.mocked(mockApiClient.deleteMedia).mockRejectedValue(error); - - // Act & Assert - await expect(service.deleteMedia(mediaId)).rejects.toThrow('Delete failed'); - expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId); - expect(mockPresenter.presentDelete).not.toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/media/MediaService.ts b/apps/website/lib/services/media/MediaService.ts index 7bf4f3fdd..09e5e1103 100644 --- a/apps/website/lib/services/media/MediaService.ts +++ b/apps/website/lib/services/media/MediaService.ts @@ -1,53 +1,41 @@ import type { MediaApiClient } from '../../api/media/MediaApiClient'; -import type { MediaPresenter } from '../../presenters/MediaPresenter'; -import type { UploadMediaInputDto, GetMediaOutputDto, DeleteMediaOutputDto } from '../../dtos'; -import type { MediaViewModel, UploadMediaViewModel, DeleteMediaViewModel } from '../../view-models'; +import { MediaViewModel, UploadMediaViewModel, DeleteMediaViewModel } from '../../view-models'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type UploadMediaInputDto = { url: string; mediaType: string; entityType: string; entityId: string }; /** * Media Service * - * Orchestrates media operations by coordinating API calls and presentation logic. + * Orchestrates media operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class MediaService { constructor( - private readonly apiClient: MediaApiClient, - private readonly presenter: MediaPresenter + private readonly apiClient: MediaApiClient ) {} /** - * Upload media file with presentation transformation + * Upload media file with view model transformation */ async uploadMedia(input: UploadMediaInputDto): Promise { - try { - const dto = await this.apiClient.uploadMedia(input); - return this.presenter.presentUpload(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.uploadMedia(input); + return new UploadMediaViewModel(dto); } /** - * Get media by ID with presentation transformation + * Get media by ID with view model transformation */ async getMedia(mediaId: string): Promise { - try { - const dto = await this.apiClient.getMedia(mediaId); - return this.presenter.presentMedia(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getMedia(mediaId); + return new MediaViewModel(dto); } /** - * Delete media by ID with presentation transformation + * Delete media by ID with view model transformation */ async deleteMedia(mediaId: string): Promise { - try { - const dto = await this.apiClient.deleteMedia(mediaId); - return this.presenter.presentDelete(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.deleteMedia(mediaId); + return new DeleteMediaViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/payments/MembershipFeeService.ts b/apps/website/lib/services/payments/MembershipFeeService.ts index b1665c5b9..cd749fa5e 100644 --- a/apps/website/lib/services/payments/MembershipFeeService.ts +++ b/apps/website/lib/services/payments/MembershipFeeService.ts @@ -1,11 +1,11 @@ import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; -import { presentMembershipFee } from '../../presenters/MembershipFeePresenter'; -import type { MembershipFeeViewModel } from '../../view-models'; +import { MembershipFeeViewModel } from '../../view-models'; +import type { MembershipFeeDto } from '../../types/generated'; /** * Membership Fee Service * - * Orchestrates membership fee operations by coordinating API calls and presentation logic. + * Orchestrates membership fee operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class MembershipFeeService { @@ -14,14 +14,10 @@ export class MembershipFeeService { ) {} /** - * Get membership fees by league ID with presentation transformation + * Get membership fees by league ID with view model transformation */ async getMembershipFees(leagueId: string): Promise { - try { - const dto = await this.apiClient.getMembershipFees(leagueId); - return dto.fees.map(presentMembershipFee); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getMembershipFees(leagueId); + return dto.fees.map((fee: MembershipFeeDto) => new MembershipFeeViewModel(fee)); } } \ No newline at end of file diff --git a/apps/website/lib/services/payments/PaymentService.test.ts b/apps/website/lib/services/payments/PaymentService.test.ts deleted file mode 100644 index e38c30d4f..000000000 --- a/apps/website/lib/services/payments/PaymentService.test.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { PaymentService } from './PaymentService'; -import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; -import type { PaymentListPresenter } from '../../presenters/PaymentListPresenter'; -import type { - GetPaymentsOutputDto, - CreatePaymentInputDto, - CreatePaymentOutputDto, - GetMembershipFeesOutputDto, - GetPrizesOutputDto, - GetWalletOutputDto, - PaymentDto, - MembershipFeeDto, - PrizeDto, - WalletDto, -} from '../../dtos'; -import type { - PaymentViewModel, - MembershipFeeViewModel, - PrizeViewModel, - WalletViewModel, -} from '../../view-models'; - -describe('PaymentService', () => { - let service: PaymentService; - let mockApiClient: PaymentsApiClient; - let mockPaymentListPresenter: PaymentListPresenter; - let mockPresentPayment: (dto: any) => PaymentViewModel; - let mockPresentMembershipFee: (dto: any) => MembershipFeeViewModel; - let mockPresentPrize: (dto: any) => PrizeViewModel; - let mockPresentWallet: (dto: any) => WalletViewModel; - - beforeEach(() => { - mockApiClient = { - getPayments: vi.fn(), - createPayment: vi.fn(), - getMembershipFees: vi.fn(), - getPrizes: vi.fn(), - getWallet: vi.fn(), - } as unknown as PaymentsApiClient; - - mockPaymentListPresenter = { - present: vi.fn(), - } as unknown as PaymentListPresenter; - - mockPresentPayment = vi.fn(); - mockPresentMembershipFee = vi.fn(); - mockPresentPrize = vi.fn(); - mockPresentWallet = vi.fn(); - - service = new PaymentService( - mockApiClient, - mockPaymentListPresenter, - mockPresentPayment, - mockPresentMembershipFee, - mockPresentPrize, - mockPresentWallet - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(PaymentService); - }); - }); - - describe('getPayments', () => { - it('should fetch all payments and transform via presenter', async () => { - // Arrange - const mockDto: GetPaymentsOutputDto = { - payments: [ - { - id: 'payment-1', - amount: 100, - currency: 'USD', - status: 'completed', - createdAt: '2024-01-01', - }, - { - id: 'payment-2', - amount: 200, - currency: 'EUR', - status: 'pending', - createdAt: '2024-01-02', - }, - ], - }; - - const mockViewModels: PaymentViewModel[] = [ - { - id: 'payment-1', - amount: 100, - currency: 'USD', - status: 'completed', - createdAt: '2024-01-01', - } as PaymentViewModel, - { - id: 'payment-2', - amount: 200, - currency: 'EUR', - status: 'pending', - createdAt: '2024-01-02', - } as PaymentViewModel, - ]; - - vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); - vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getPayments(); - - // Assert - expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, undefined); - expect(mockPaymentListPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModels); - }); - - it('should filter payments by leagueId', async () => { - // Arrange - const leagueId = 'league-123'; - const mockDto: GetPaymentsOutputDto = { payments: [] }; - const mockViewModels: PaymentViewModel[] = []; - - vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); - vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); - - // Act - await service.getPayments(leagueId); - - // Assert - expect(mockApiClient.getPayments).toHaveBeenCalledWith(leagueId, undefined); - }); - - it('should filter payments by driverId', async () => { - // Arrange - const driverId = 'driver-456'; - const mockDto: GetPaymentsOutputDto = { payments: [] }; - const mockViewModels: PaymentViewModel[] = []; - - vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); - vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); - - // Act - await service.getPayments(undefined, driverId); - - // Assert - expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, driverId); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch payments'); - vi.mocked(mockApiClient.getPayments).mockRejectedValue(error); - - // Act & Assert - await expect(service.getPayments()).rejects.toThrow('Failed to fetch payments'); - expect(mockApiClient.getPayments).toHaveBeenCalled(); - expect(mockPaymentListPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getPayment', () => { - it('should fetch single payment by ID', async () => { - // Arrange - const paymentId = 'payment-123'; - const mockDto: GetPaymentsOutputDto = { - payments: [ - { - id: paymentId, - amount: 100, - currency: 'USD', - status: 'completed', - createdAt: '2024-01-01', - }, - ], - }; - - const mockViewModel: PaymentViewModel = { - id: paymentId, - amount: 100, - currency: 'USD', - status: 'completed', - createdAt: '2024-01-01', - } as PaymentViewModel; - - vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); - vi.mocked(mockPresentPayment).mockReturnValue(mockViewModel); - - // Act - const result = await service.getPayment(paymentId); - - // Assert - expect(mockApiClient.getPayments).toHaveBeenCalled(); - expect(mockPresentPayment).toHaveBeenCalledWith(mockDto.payments[0]); - expect(result).toEqual(mockViewModel); - }); - - it('should throw error when payment not found', async () => { - // Arrange - const paymentId = 'non-existent'; - const mockDto: GetPaymentsOutputDto = { payments: [] }; - - vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); - - // Act & Assert - await expect(service.getPayment(paymentId)).rejects.toThrow( - `Payment with ID ${paymentId} not found` - ); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('API error'); - vi.mocked(mockApiClient.getPayments).mockRejectedValue(error); - - // Act & Assert - await expect(service.getPayment('payment-123')).rejects.toThrow('API error'); - }); - }); - - describe('createPayment', () => { - it('should create a new payment', async () => { - // Arrange - const input: CreatePaymentInputDto = { - amount: 150, - currency: 'USD', - leagueId: 'league-123', - driverId: 'driver-456', - }; - - const mockOutput: CreatePaymentOutputDto = { - paymentId: 'payment-new', - success: true, - }; - - vi.mocked(mockApiClient.createPayment).mockResolvedValue(mockOutput); - - // Act - const result = await service.createPayment(input); - - // Assert - expect(mockApiClient.createPayment).toHaveBeenCalledWith(input); - expect(result).toEqual(mockOutput); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: CreatePaymentInputDto = { - amount: 150, - currency: 'USD', - }; - const error = new Error('Payment creation failed'); - vi.mocked(mockApiClient.createPayment).mockRejectedValue(error); - - // Act & Assert - await expect(service.createPayment(input)).rejects.toThrow('Payment creation failed'); - }); - }); - - describe('getMembershipFees', () => { - it('should fetch membership fees for a league', async () => { - // Arrange - const leagueId = 'league-123'; - const mockDto: GetMembershipFeesOutputDto = { - fees: [ - { - leagueId, - amount: 50, - currency: 'USD', - period: 'monthly', - }, - ], - memberPayments: [], - }; - - const mockViewModel: MembershipFeeViewModel = { - leagueId, - amount: 50, - currency: 'USD', - period: 'monthly', - } as MembershipFeeViewModel; - - vi.mocked(mockApiClient.getMembershipFees).mockResolvedValue(mockDto); - vi.mocked(mockPresentMembershipFee).mockReturnValue(mockViewModel); - - // Act - const result = await service.getMembershipFees(leagueId); - - // Assert - expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId); - expect(mockPresentMembershipFee).toHaveBeenCalledWith(mockDto.fees[0]); - expect(result).toEqual([mockViewModel]); - }); - - it('should handle empty fees list', async () => { - // Arrange - const leagueId = 'league-123'; - const mockDto: GetMembershipFeesOutputDto = { - fees: [], - memberPayments: [], - }; - - vi.mocked(mockApiClient.getMembershipFees).mockResolvedValue(mockDto); - - // Act - const result = await service.getMembershipFees(leagueId); - - // Assert - expect(result).toEqual([]); - expect(mockPresentMembershipFee).not.toHaveBeenCalled(); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch fees'); - vi.mocked(mockApiClient.getMembershipFees).mockRejectedValue(error); - - // Act & Assert - await expect(service.getMembershipFees('league-123')).rejects.toThrow('Failed to fetch fees'); - }); - }); - - describe('getPrizes', () => { - it('should fetch all prizes', async () => { - // Arrange - const mockDto: GetPrizesOutputDto = { - prizes: [ - { - id: 'prize-1', - name: 'First Place', - amount: 1000, - currency: 'USD', - position: 1, - }, - ], - }; - - const mockViewModel: PrizeViewModel = { - id: 'prize-1', - name: 'First Place', - amount: 1000, - currency: 'USD', - position: 1, - } as PrizeViewModel; - - vi.mocked(mockApiClient.getPrizes).mockResolvedValue(mockDto); - vi.mocked(mockPresentPrize).mockReturnValue(mockViewModel); - - // Act - const result = await service.getPrizes(); - - // Assert - expect(mockApiClient.getPrizes).toHaveBeenCalledWith(undefined, undefined); - expect(mockPresentPrize).toHaveBeenCalledWith(mockDto.prizes[0]); - expect(result).toEqual([mockViewModel]); - }); - - it('should filter prizes by leagueId and seasonId', async () => { - // Arrange - const leagueId = 'league-123'; - const seasonId = 'season-456'; - const mockDto: GetPrizesOutputDto = { prizes: [] }; - - vi.mocked(mockApiClient.getPrizes).mockResolvedValue(mockDto); - - // Act - await service.getPrizes(leagueId, seasonId); - - // Assert - expect(mockApiClient.getPrizes).toHaveBeenCalledWith(leagueId, seasonId); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch prizes'); - vi.mocked(mockApiClient.getPrizes).mockRejectedValue(error); - - // Act & Assert - await expect(service.getPrizes()).rejects.toThrow('Failed to fetch prizes'); - }); - }); - - describe('getWallet', () => { - it('should fetch wallet for a driver', async () => { - // Arrange - const driverId = 'driver-123'; - const mockDto: GetWalletOutputDto = { - driverId, - balance: 500, - currency: 'USD', - transactions: [], - }; - - const mockViewModel: WalletViewModel = { - driverId, - balance: 500, - currency: 'USD', - transactions: [], - } as WalletViewModel; - - vi.mocked(mockApiClient.getWallet).mockResolvedValue(mockDto); - vi.mocked(mockPresentWallet).mockReturnValue(mockViewModel); - - // Act - const result = await service.getWallet(driverId); - - // Assert - expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId); - expect(mockPresentWallet).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch wallet'); - vi.mocked(mockApiClient.getWallet).mockRejectedValue(error); - - // Act & Assert - await expect(service.getWallet('driver-123')).rejects.toThrow('Failed to fetch wallet'); - }); - }); - - describe('processPayment', () => { - it('should process payment using createPayment', async () => { - // Arrange - const input: CreatePaymentInputDto = { - amount: 100, - currency: 'USD', - }; - - const mockOutput: CreatePaymentOutputDto = { - paymentId: 'payment-123', - success: true, - }; - - vi.mocked(mockApiClient.createPayment).mockResolvedValue(mockOutput); - - // Act - const result = await service.processPayment(input); - - // Assert - expect(mockApiClient.createPayment).toHaveBeenCalledWith(input); - expect(result).toEqual(mockOutput); - }); - - it('should propagate errors', async () => { - // Arrange - const input: CreatePaymentInputDto = { - amount: 100, - currency: 'USD', - }; - const error = new Error('Processing failed'); - vi.mocked(mockApiClient.createPayment).mockRejectedValue(error); - - // Act & Assert - await expect(service.processPayment(input)).rejects.toThrow('Processing failed'); - }); - }); - - describe('getPaymentHistory', () => { - it('should fetch payment history for a driver', async () => { - // Arrange - const driverId = 'driver-123'; - const mockDto: GetPaymentsOutputDto = { - payments: [ - { - id: 'payment-1', - amount: 100, - currency: 'USD', - status: 'completed', - createdAt: '2024-01-01', - }, - ], - }; - - const mockViewModels: PaymentViewModel[] = [ - { - id: 'payment-1', - amount: 100, - currency: 'USD', - status: 'completed', - createdAt: '2024-01-01', - } as PaymentViewModel, - ]; - - vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); - vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getPaymentHistory(driverId); - - // Assert - expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, driverId); - expect(result).toEqual(mockViewModels); - }); - - it('should propagate errors', async () => { - // Arrange - const error = new Error('History fetch failed'); - vi.mocked(mockApiClient.getPayments).mockRejectedValue(error); - - // Act & Assert - await expect(service.getPaymentHistory('driver-123')).rejects.toThrow('History fetch failed'); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/payments/PaymentService.ts b/apps/website/lib/services/payments/PaymentService.ts index c07db6919..ef34c7fe1 100644 --- a/apps/website/lib/services/payments/PaymentService.ts +++ b/apps/website/lib/services/payments/PaymentService.ts @@ -1,10 +1,10 @@ import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; -import type { PaymentListPresenter } from '../../presenters/PaymentListPresenter'; -import type { - CreatePaymentInputDto, - CreatePaymentOutputDto, -} from '../../dtos'; -import type { +import type { PaymentDto, MembershipFeeDto, PrizeDto } from '../../types/generated'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type CreatePaymentInputDto = { amount: number; leagueId: string; driverId: string; description: string }; +type CreatePaymentOutputDto = { id: string; success: boolean }; +import { PaymentViewModel, MembershipFeeViewModel, PrizeViewModel, @@ -14,114 +14,77 @@ import type { /** * Payment Service * - * Orchestrates payment operations by coordinating API calls and presentation logic. + * Orchestrates payment operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class PaymentService { constructor( - private readonly apiClient: PaymentsApiClient, - private readonly paymentListPresenter: PaymentListPresenter, - private readonly presentPayment: (dto: any) => PaymentViewModel, - private readonly presentMembershipFee: (dto: any) => MembershipFeeViewModel, - private readonly presentPrize: (dto: any) => PrizeViewModel, - private readonly presentWallet: (dto: any) => WalletViewModel + private readonly apiClient: PaymentsApiClient ) {} /** * Get all payments with optional filters */ async getPayments(leagueId?: string, driverId?: string): Promise { - try { - const dto = await this.apiClient.getPayments(leagueId, driverId); - return this.paymentListPresenter.present(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getPayments(leagueId, driverId); + return dto.payments.map((payment: PaymentDto) => new PaymentViewModel(payment)); } /** * Get single payment by ID */ async getPayment(paymentId: string): Promise { - try { - // Note: Assuming the API returns a single payment from the list - const dto = await this.apiClient.getPayments(); - const payment = dto.payments.find(p => p.id === paymentId); - if (!payment) { - throw new Error(`Payment with ID ${paymentId} not found`); - } - return this.presentPayment(payment); - } catch (error) { - throw error; + // Note: Assuming the API returns a single payment from the list + const dto = await this.apiClient.getPayments(); + const payment = dto.payments.find((p: PaymentDto) => p.id === paymentId); + if (!payment) { + throw new Error(`Payment with ID ${paymentId} not found`); } + return new PaymentViewModel(payment); } /** * Create a new payment */ async createPayment(input: CreatePaymentInputDto): Promise { - try { - return await this.apiClient.createPayment(input); - } catch (error) { - throw error; - } + return await this.apiClient.createPayment(input); } /** * Get membership fees for a league */ async getMembershipFees(leagueId: string): Promise { - try { - const dto = await this.apiClient.getMembershipFees(leagueId); - return dto.fees.map(fee => this.presentMembershipFee(fee)); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getMembershipFees(leagueId); + return dto.fees.map((fee: MembershipFeeDto) => new MembershipFeeViewModel(fee)); } /** * Get prizes with optional filters */ async getPrizes(leagueId?: string, seasonId?: string): Promise { - try { - const dto = await this.apiClient.getPrizes(leagueId, seasonId); - return dto.prizes.map(prize => this.presentPrize(prize)); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getPrizes(leagueId, seasonId); + return dto.prizes.map((prize: PrizeDto) => new PrizeViewModel(prize)); } /** * Get wallet for a driver */ async getWallet(driverId: string): Promise { - try { - const dto = await this.apiClient.getWallet(driverId); - return this.presentWallet(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getWallet(driverId); + return new WalletViewModel(dto); } /** * Process a payment (alias for createPayment) */ async processPayment(input: CreatePaymentInputDto): Promise { - try { - return await this.createPayment(input); - } catch (error) { - throw error; - } + return await this.createPayment(input); } /** * Get payment history for a user (driver) */ async getPaymentHistory(driverId: string): Promise { - try { - return await this.getPayments(undefined, driverId); - } catch (error) { - throw error; - } + return await this.getPayments(undefined, driverId); } } \ No newline at end of file diff --git a/apps/website/lib/services/payments/WalletService.test.ts b/apps/website/lib/services/payments/WalletService.test.ts deleted file mode 100644 index c96911d5e..000000000 --- a/apps/website/lib/services/payments/WalletService.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { WalletService } from './WalletService'; -import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; -import type { GetWalletOutputDto } from '../../dtos'; - -import { presentWallet } from '../../presenters/WalletPresenter'; - -// Mock the presenter -vi.mock('../../presenters/WalletPresenter', () => ({ - presentWallet: vi.fn(), -})); - -describe('WalletService', () => { - let mockApiClient: PaymentsApiClient; - let service: WalletService; - - beforeEach(() => { - mockApiClient = { - getWallet: vi.fn(), - getMembershipFees: vi.fn(), - getPayments: vi.fn(), - createPayment: vi.fn(), - getPrizes: vi.fn(), - } as unknown as PaymentsApiClient; - - service = new WalletService(mockApiClient); - }); - - describe('getWallet', () => { - it('should get wallet via API client and present it', async () => { - // Arrange - const driverId = 'driver-1'; - const dto: GetWalletOutputDto = { - balance: 1000, - currency: 'USD', - transactions: [], - }; - - const expectedViewModel = { - balance: 1000, - currency: 'USD', - transactions: [], - }; - - vi.mocked(mockApiClient.getWallet).mockResolvedValue(dto); - - vi.mocked(presentWallet).mockReturnValue(expectedViewModel); - - // Act - const result = await service.getWallet(driverId); - - // Assert - expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId); - expect(mockApiClient.getWallet).toHaveBeenCalledTimes(1); - expect(presentWallet).toHaveBeenCalledWith(dto); - expect(result).toBe(expectedViewModel); - }); - - it('should propagate API client errors', async () => { - // Arrange - const driverId = 'driver-1'; - const error = new Error('API Error: Failed to get wallet'); - vi.mocked(mockApiClient.getWallet).mockRejectedValue(error); - - // Act & Assert - await expect(service.getWallet(driverId)).rejects.toThrow( - 'API Error: Failed to get wallet' - ); - - expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId); - expect(mockApiClient.getWallet).toHaveBeenCalledTimes(1); - }); - - it('should handle different driver IDs', async () => { - // Arrange - const driverId = 'driver-2'; - const dto: GetWalletOutputDto = { - balance: 500, - currency: 'EUR', - transactions: [], - }; - - const expectedViewModel = { - balance: 500, - currency: 'EUR', - transactions: [], - }; - - vi.mocked(mockApiClient.getWallet).mockResolvedValue(dto); - - vi.mocked(presentWallet).mockReturnValue(expectedViewModel); - - // Act - const result = await service.getWallet(driverId); - - // Assert - expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId); - expect(result).toBe(expectedViewModel); - }); - }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient', () => { - // This test verifies the constructor signature - expect(() => { - new WalletService(mockApiClient); - }).not.toThrow(); - }); - - it('should use injected apiClient', async () => { - // Arrange - const customApiClient = { - getWallet: vi.fn().mockResolvedValue({ balance: 200, currency: 'USD', transactions: [] }), - getMembershipFees: vi.fn(), - getPayments: vi.fn(), - createPayment: vi.fn(), - getPrizes: vi.fn(), - } as unknown as PaymentsApiClient; - - const customService = new WalletService(customApiClient); - - vi.mocked(presentWallet).mockReturnValue({ balance: 200, currency: 'USD', transactions: [] }); - - // Act - await customService.getWallet('driver-1'); - - // Assert - expect(customApiClient.getWallet).toHaveBeenCalledWith('driver-1'); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/payments/WalletService.ts b/apps/website/lib/services/payments/WalletService.ts index 423196324..29b50dbe1 100644 --- a/apps/website/lib/services/payments/WalletService.ts +++ b/apps/website/lib/services/payments/WalletService.ts @@ -1,11 +1,10 @@ import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; -import { presentWallet } from '../../presenters/WalletPresenter'; -import type { WalletViewModel } from '../../view-models'; +import { WalletViewModel } from '../../view-models'; /** * Wallet Service * - * Orchestrates wallet operations by coordinating API calls and presentation logic. + * Orchestrates wallet operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class WalletService { @@ -14,14 +13,10 @@ export class WalletService { ) {} /** - * Get wallet by driver ID with presentation transformation + * Get wallet by driver ID with view model transformation */ async getWallet(driverId: string): Promise { - try { - const dto = await this.apiClient.getWallet(driverId); - return presentWallet(dto); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getWallet(driverId); + return new WalletViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceResultsService.test.ts b/apps/website/lib/services/races/RaceResultsService.test.ts deleted file mode 100644 index d3c7bce87..000000000 --- a/apps/website/lib/services/races/RaceResultsService.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { RaceResultsService } from './RaceResultsService'; -import { RacesApiClient } from '../../api/races/RacesApiClient'; -import { RaceResultsDetailPresenter } from '../../presenters/RaceResultsDetailPresenter'; -import { RaceWithSOFPresenter } from '../../presenters/RaceWithSOFPresenter'; -import { ImportRaceResultsPresenter } from '../../presenters/ImportRaceResultsPresenter'; -import type { RaceResultsDetailDto, RaceWithSOFDto, ImportRaceResultsSummaryDto } from '../../dtos'; -import type { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel'; -import type { RaceWithSOFViewModel } from '../../presenters/RaceWithSOFPresenter'; -import type { ImportRaceResultsSummaryViewModel } from '../../presenters/ImportRaceResultsPresenter'; - -describe('RaceResultsService', () => { - let service: RaceResultsService; - let mockApiClient: RacesApiClient; - let mockResultsDetailPresenter: RaceResultsDetailPresenter; - let mockSOFPresenter: RaceWithSOFPresenter; - let mockImportPresenter: ImportRaceResultsPresenter; - - beforeEach(() => { - mockApiClient = { - getResultsDetail: vi.fn(), - getWithSOF: vi.fn(), - importResults: vi.fn(), - } as unknown as RacesApiClient; - - mockResultsDetailPresenter = { - present: vi.fn(), - } as unknown as RaceResultsDetailPresenter; - - mockSOFPresenter = { - present: vi.fn(), - } as unknown as RaceWithSOFPresenter; - - mockImportPresenter = { - present: vi.fn(), - } as unknown as ImportRaceResultsPresenter; - - service = new RaceResultsService( - mockApiClient, - mockResultsDetailPresenter, - mockSOFPresenter, - mockImportPresenter - ); - }); - - describe('getResultsDetail', () => { - it('should fetch race results detail and transform via presenter', async () => { - // Arrange - const raceId = 'race-123'; - const currentUserId = 'user-456'; - const mockDto: Partial = { - raceId, - results: [], - }; - const mockViewModel: Partial = { - raceId, - results: [], - }; - - vi.mocked(mockApiClient.getResultsDetail).mockResolvedValue(mockDto as RaceResultsDetailDto); - vi.mocked(mockResultsDetailPresenter.present).mockReturnValue(mockViewModel as RaceResultsDetailViewModel); - - // Act - const result = await service.getResultsDetail(raceId, currentUserId); - - // Assert - expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId); - expect(mockResultsDetailPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId); - expect(result).toEqual(mockViewModel); - }); - - it('should fetch race results detail without currentUserId', async () => { - // Arrange - const raceId = 'race-123'; - const mockDto: Partial = { - raceId, - results: [], - }; - const mockViewModel: Partial = { - raceId, - results: [], - }; - - vi.mocked(mockApiClient.getResultsDetail).mockResolvedValue(mockDto as RaceResultsDetailDto); - vi.mocked(mockResultsDetailPresenter.present).mockReturnValue(mockViewModel as RaceResultsDetailViewModel); - - // Act - const result = await service.getResultsDetail(raceId); - - // Assert - expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId); - expect(mockResultsDetailPresenter.present).toHaveBeenCalledWith(mockDto, undefined); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const raceId = 'race-123'; - const error = new Error('API Error'); - vi.mocked(mockApiClient.getResultsDetail).mockRejectedValue(error); - - // Act & Assert - await expect(service.getResultsDetail(raceId)).rejects.toThrow('API Error'); - expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId); - expect(mockResultsDetailPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getWithSOF', () => { - it('should fetch race with SOF and transform via presenter', async () => { - // Arrange - const raceId = 'race-123'; - const mockDto: RaceWithSOFDto = { - id: raceId, - track: 'Spa-Francorchamps', - strengthOfField: 2500, - }; - const mockViewModel: RaceWithSOFViewModel = { - id: raceId, - track: 'Spa-Francorchamps', - strengthOfField: 2500, - }; - - vi.mocked(mockApiClient.getWithSOF).mockResolvedValue(mockDto); - vi.mocked(mockSOFPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getWithSOF(raceId); - - // Assert - expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId); - expect(mockSOFPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle null strengthOfField', async () => { - // Arrange - const raceId = 'race-123'; - const mockDto: RaceWithSOFDto = { - id: raceId, - track: 'Spa-Francorchamps', - strengthOfField: null, - }; - const mockViewModel: RaceWithSOFViewModel = { - id: raceId, - track: 'Spa-Francorchamps', - strengthOfField: null, - }; - - vi.mocked(mockApiClient.getWithSOF).mockResolvedValue(mockDto); - vi.mocked(mockSOFPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getWithSOF(raceId); - - // Assert - expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId); - expect(mockSOFPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const raceId = 'race-123'; - const error = new Error('SOF calculation failed'); - vi.mocked(mockApiClient.getWithSOF).mockRejectedValue(error); - - // Act & Assert - await expect(service.getWithSOF(raceId)).rejects.toThrow('SOF calculation failed'); - expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId); - expect(mockSOFPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('importResults', () => { - it('should import race results and transform via presenter', async () => { - // Arrange - const raceId = 'race-123'; - const input = { - sessionId: 'session-456', - results: [ - { position: 1, driverId: 'driver-1', finishTime: 120000 }, - { position: 2, driverId: 'driver-2', finishTime: 121000 }, - ], - }; - const mockDto: ImportRaceResultsSummaryDto = { - success: true, - raceId, - driversProcessed: 2, - resultsRecorded: 2, - }; - const mockViewModel: ImportRaceResultsSummaryViewModel = { - success: true, - raceId, - driversProcessed: 2, - resultsRecorded: 2, - }; - - vi.mocked(mockApiClient.importResults).mockResolvedValue(mockDto); - vi.mocked(mockImportPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.importResults(raceId, input); - - // Assert - expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input); - expect(mockImportPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle import with errors', async () => { - // Arrange - const raceId = 'race-123'; - const input = { sessionId: 'session-456', results: [] }; - const mockDto: ImportRaceResultsSummaryDto = { - success: false, - raceId, - driversProcessed: 5, - resultsRecorded: 3, - errors: ['Driver not found: driver-99', 'Invalid time for driver-88'], - }; - const mockViewModel: ImportRaceResultsSummaryViewModel = { - success: false, - raceId, - driversProcessed: 5, - resultsRecorded: 3, - errors: ['Driver not found: driver-99', 'Invalid time for driver-88'], - }; - - vi.mocked(mockApiClient.importResults).mockResolvedValue(mockDto); - vi.mocked(mockImportPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.importResults(raceId, input); - - // Assert - expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input); - expect(mockImportPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.errors).toHaveLength(2); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const raceId = 'race-123'; - const input = { sessionId: 'session-456', results: [] }; - const error = new Error('Import failed'); - vi.mocked(mockApiClient.importResults).mockRejectedValue(error); - - // Act & Assert - await expect(service.importResults(raceId, input)).rejects.toThrow('Import failed'); - expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input); - expect(mockImportPresenter.present).not.toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceResultsService.ts b/apps/website/lib/services/races/RaceResultsService.ts index c299d5fd7..b34bf93b9 100644 --- a/apps/website/lib/services/races/RaceResultsService.ts +++ b/apps/website/lib/services/races/RaceResultsService.ts @@ -1,11 +1,13 @@ import { RacesApiClient } from '../../api/races/RacesApiClient'; -import { RaceResultsDetailPresenter } from '../../presenters/RaceResultsDetailPresenter'; -import { RaceWithSOFPresenter } from '../../presenters/RaceWithSOFPresenter'; -import type { RaceWithSOFViewModel } from '../../presenters/RaceWithSOFPresenter'; -import { ImportRaceResultsPresenter } from '../../presenters/ImportRaceResultsPresenter'; -import type { ImportRaceResultsSummaryViewModel } from '../../presenters/ImportRaceResultsPresenter'; -import type { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel'; -import type { ImportRaceResultsInputDto } from '../../dtos'; +import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type ImportRaceResultsInputDto = { raceId: string; results: Array }; + +// Note: RaceWithSOFViewModel and ImportRaceResultsSummaryViewModel are defined in presenters +// These will need to be converted to proper view models +type RaceWithSOFViewModel = any; // TODO: Create proper view model +type ImportRaceResultsSummaryViewModel = any; // TODO: Create proper view model /** * Race Results Service @@ -15,33 +17,32 @@ import type { ImportRaceResultsInputDto } from '../../dtos'; */ export class RaceResultsService { constructor( - private readonly apiClient: RacesApiClient, - private readonly resultsDetailPresenter: RaceResultsDetailPresenter, - private readonly sofPresenter: RaceWithSOFPresenter, - private readonly importPresenter: ImportRaceResultsPresenter + private readonly apiClient: RacesApiClient ) {} /** - * Get race results detail with presentation transformation + * Get race results detail with view model transformation */ async getResultsDetail(raceId: string, currentUserId?: string): Promise { const dto = await this.apiClient.getResultsDetail(raceId); - return this.resultsDetailPresenter.present(dto, currentUserId); + return new RaceResultsDetailViewModel(dto, currentUserId || ''); } /** * Get race with strength of field calculation + * TODO: Create RaceWithSOFViewModel and use it here */ async getWithSOF(raceId: string): Promise { const dto = await this.apiClient.getWithSOF(raceId); - return this.sofPresenter.present(dto); + return dto; // TODO: return new RaceWithSOFViewModel(dto); } /** * Import race results and get summary + * TODO: Create ImportRaceResultsSummaryViewModel and use it here */ async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise { const dto = await this.apiClient.importResults(raceId, input); - return this.importPresenter.present(dto); + return dto; // TODO: return new ImportRaceResultsSummaryViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceService.test.ts b/apps/website/lib/services/races/RaceService.test.ts deleted file mode 100644 index f59e56182..000000000 --- a/apps/website/lib/services/races/RaceService.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { RaceService } from './RaceService'; -import { RacesApiClient } from '../../api/races/RacesApiClient'; -import { RaceDetailPresenter } from '../../presenters/RaceDetailPresenter'; -import type { RaceDetailDto, RacesPageDataDto, RaceStatsDto } from '../../dtos'; -import type { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel'; - -describe('RaceService', () => { - let mockApiClient: RacesApiClient; - let mockPresenter: RaceDetailPresenter; - let service: RaceService; - - beforeEach(() => { - mockApiClient = { - getDetail: vi.fn(), - getPageData: vi.fn(), - getTotal: vi.fn(), - } as unknown as RacesApiClient; - - mockPresenter = { - present: vi.fn(), - } as unknown as RaceDetailPresenter; - - service = new RaceService(mockApiClient, mockPresenter); - }); - - describe('getRaceDetail', () => { - it('should fetch race detail from API and transform via presenter', async () => { - // Arrange - const mockDto: RaceDetailDto = { - race: { - id: 'race-1', - name: 'Test Race', - scheduledTime: '2025-12-17T20:00:00Z', - status: 'upcoming', - trackName: 'Spa-Francorchamps', - carClasses: ['GT3'], - }, - league: null, - entryList: [], - registration: { - isRegistered: false, - canRegister: true, - }, - userResult: null, - }; - - const mockViewModel: RaceDetailViewModel = { - race: mockDto.race, - league: mockDto.league, - entryList: mockDto.entryList, - registration: mockDto.registration, - userResult: mockDto.userResult, - isRegistered: false, - canRegister: true, - raceStatusDisplay: 'Upcoming', - formattedScheduledTime: expect.any(String), - entryCount: 0, - hasResults: false, - registrationStatusMessage: 'You can register for this race', - } as unknown as RaceDetailViewModel; - - vi.mocked(mockApiClient.getDetail).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getRaceDetail('race-1', 'driver-1'); - - // Assert - expect(mockApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1'); - expect(mockApiClient.getDetail).toHaveBeenCalledTimes(1); - expect(mockPresenter.present).toHaveBeenCalledWith(mockDto); - expect(mockPresenter.present).toHaveBeenCalledTimes(1); - expect(result).toBe(mockViewModel); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Race not found'); - vi.mocked(mockApiClient.getDetail).mockRejectedValue(error); - - // Act & Assert - await expect( - service.getRaceDetail('invalid-race', 'driver-1') - ).rejects.toThrow('API Error: Race not found'); - - expect(mockApiClient.getDetail).toHaveBeenCalledWith('invalid-race', 'driver-1'); - expect(mockPresenter.present).not.toHaveBeenCalled(); - }); - - it('should propagate presenter errors', async () => { - // Arrange - const mockDto: RaceDetailDto = { - race: null, - league: null, - entryList: [], - registration: { - isRegistered: false, - canRegister: false, - }, - userResult: null, - }; - - const error = new Error('Presenter Error: Invalid DTO structure'); - vi.mocked(mockApiClient.getDetail).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.present).mockImplementation(() => { - throw error; - }); - - // Act & Assert - await expect( - service.getRaceDetail('race-1', 'driver-1') - ).rejects.toThrow('Presenter Error: Invalid DTO structure'); - - expect(mockApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1'); - expect(mockPresenter.present).toHaveBeenCalledWith(mockDto); - }); - }); - - describe('getRacesPageData', () => { - it('should fetch races page data from API', async () => { - // Arrange - const mockPageData: RacesPageDataDto = { - races: [ - { - id: 'race-1', - name: 'Test Race 1', - scheduledTime: '2025-12-17T20:00:00Z', - trackName: 'Spa-Francorchamps', - }, - { - id: 'race-2', - name: 'Test Race 2', - scheduledTime: '2025-12-18T20:00:00Z', - trackName: 'Monza', - }, - ], - totalCount: 2, - }; - - vi.mocked(mockApiClient.getPageData).mockResolvedValue(mockPageData); - - // Act - const result = await service.getRacesPageData(); - - // Assert - expect(mockApiClient.getPageData).toHaveBeenCalledTimes(1); - expect(result).toBe(mockPageData); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Failed to fetch page data'); - vi.mocked(mockApiClient.getPageData).mockRejectedValue(error); - - // Act & Assert - await expect(service.getRacesPageData()).rejects.toThrow( - 'API Error: Failed to fetch page data' - ); - - expect(mockApiClient.getPageData).toHaveBeenCalledTimes(1); - }); - }); - - describe('getRacesTotal', () => { - it('should fetch race statistics from API', async () => { - // Arrange - const mockStats: RaceStatsDto = { - total: 42, - upcoming: 10, - live: 2, - finished: 30, - }; - - vi.mocked(mockApiClient.getTotal).mockResolvedValue(mockStats); - - // Act - const result = await service.getRacesTotal(); - - // Assert - expect(mockApiClient.getTotal).toHaveBeenCalledTimes(1); - expect(result).toBe(mockStats); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Failed to fetch statistics'); - vi.mocked(mockApiClient.getTotal).mockRejectedValue(error); - - // Act & Assert - await expect(service.getRacesTotal()).rejects.toThrow( - 'API Error: Failed to fetch statistics' - ); - - expect(mockApiClient.getTotal).toHaveBeenCalledTimes(1); - }); - }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient and raceDetailPresenter', () => { - // This test verifies the constructor signature - expect(() => { - new RaceService(mockApiClient, mockPresenter); - }).not.toThrow(); - }); - - it('should use injected dependencies', async () => { - // Arrange - const customApiClient = { - getDetail: vi.fn().mockResolvedValue({ - race: null, - league: null, - entryList: [], - registration: { isRegistered: false, canRegister: false }, - userResult: null, - }), - getPageData: vi.fn(), - getTotal: vi.fn(), - } as unknown as RacesApiClient; - - const customPresenter = { - present: vi.fn().mockReturnValue({} as RaceDetailViewModel), - } as unknown as RaceDetailPresenter; - - const customService = new RaceService(customApiClient, customPresenter); - - // Act - await customService.getRaceDetail('race-1', 'driver-1'); - - // Assert - expect(customApiClient.getDetail).toHaveBeenCalledWith('race-1', 'driver-1'); - expect(customPresenter.present).toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/races/RaceService.ts b/apps/website/lib/services/races/RaceService.ts index 32d163456..4f4323360 100644 --- a/apps/website/lib/services/races/RaceService.ts +++ b/apps/website/lib/services/races/RaceService.ts @@ -1,34 +1,35 @@ import { RacesApiClient } from '../../api/races/RacesApiClient'; -import { RaceDetailPresenter } from '../../presenters/RaceDetailPresenter'; -import type { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel'; -import type { RacesPageDataDto, RaceStatsDto } from '../../dtos'; +import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type RacesPageDataDto = { races: Array }; +type RaceStatsDto = { totalRaces: number }; /** * Race Service * - * Orchestrates race operations by coordinating API calls and presentation logic. + * Orchestrates race operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class RaceService { constructor( - private readonly apiClient: RacesApiClient, - private readonly raceDetailPresenter: RaceDetailPresenter + private readonly apiClient: RacesApiClient ) {} /** - * Get race detail with presentation transformation + * Get race detail with view model transformation */ async getRaceDetail( raceId: string, driverId: string ): Promise { const dto = await this.apiClient.getDetail(raceId, driverId); - return this.raceDetailPresenter.present(dto); + return new RaceDetailViewModel(dto); } /** * Get races page data - * TODO: Add presenter transformation when presenter is available + * TODO: Add view model transformation when view model is available */ async getRacesPageData(): Promise { return this.apiClient.getPageData(); @@ -36,7 +37,7 @@ export class RaceService { /** * Get total races statistics - * TODO: Add presenter transformation when presenter is available + * TODO: Add view model transformation when view model is available */ async getRacesTotal(): Promise { return this.apiClient.getTotal(); diff --git a/apps/website/lib/services/sponsors/SponsorService.test.ts b/apps/website/lib/services/sponsors/SponsorService.test.ts deleted file mode 100644 index 93922eefb..000000000 --- a/apps/website/lib/services/sponsors/SponsorService.test.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SponsorService } from './SponsorService'; -import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient'; -import type { SponsorListPresenter } from '../../presenters/SponsorListPresenter'; -import type { SponsorDashboardPresenter } from '../../presenters/SponsorDashboardPresenter'; -import type { SponsorSponsorshipsPresenter } from '../../presenters/SponsorSponsorshipsPresenter'; -import type { - GetSponsorsOutputDto, - SponsorDashboardDto, - SponsorSponsorshipsDto, - CreateSponsorInputDto, - CreateSponsorOutputDto, - GetEntitySponsorshipPricingResultDto, -} from '../../dtos'; -import type { SponsorViewModel, SponsorDashboardViewModel, SponsorSponsorshipsViewModel } from '../../view-models'; - -describe('SponsorService', () => { - let service: SponsorService; - let mockApiClient: SponsorsApiClient; - let mockSponsorListPresenter: SponsorListPresenter; - let mockSponsorDashboardPresenter: SponsorDashboardPresenter; - let mockSponsorSponsorshipsPresenter: SponsorSponsorshipsPresenter; - - beforeEach(() => { - mockApiClient = { - getAll: vi.fn(), - getDashboard: vi.fn(), - getSponsorships: vi.fn(), - create: vi.fn(), - getPricing: vi.fn(), - } as unknown as SponsorsApiClient; - - mockSponsorListPresenter = { - present: vi.fn(), - } as unknown as SponsorListPresenter; - - mockSponsorDashboardPresenter = { - present: vi.fn(), - } as unknown as SponsorDashboardPresenter; - - mockSponsorSponsorshipsPresenter = { - present: vi.fn(), - } as unknown as SponsorSponsorshipsPresenter; - - service = new SponsorService( - mockApiClient, - mockSponsorListPresenter, - mockSponsorDashboardPresenter, - mockSponsorSponsorshipsPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(SponsorService); - }); - }); - - describe('getAllSponsors', () => { - it('should fetch all sponsors from API and transform via presenter', async () => { - // Arrange - const mockDto: GetSponsorsOutputDto = { - sponsors: [ - { - id: 'sponsor-1', - name: 'Sponsor Alpha', - logoUrl: 'https://example.com/logo1.png', - websiteUrl: 'https://alpha.com', - }, - { - id: 'sponsor-2', - name: 'Sponsor Beta', - logoUrl: 'https://example.com/logo2.png', - websiteUrl: 'https://beta.com', - }, - ], - }; - - const mockViewModels: SponsorViewModel[] = [ - { - id: 'sponsor-1', - name: 'Sponsor Alpha', - logoUrl: 'https://example.com/logo1.png', - websiteUrl: 'https://alpha.com', - } as SponsorViewModel, - { - id: 'sponsor-2', - name: 'Sponsor Beta', - logoUrl: 'https://example.com/logo2.png', - websiteUrl: 'https://beta.com', - } as SponsorViewModel, - ]; - - vi.mocked(mockApiClient.getAll).mockResolvedValue(mockDto); - vi.mocked(mockSponsorListPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getAllSponsors(); - - // Assert - expect(mockApiClient.getAll).toHaveBeenCalled(); - expect(mockSponsorListPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModels); - }); - - it('should handle empty sponsors list', async () => { - // Arrange - const mockDto: GetSponsorsOutputDto = { - sponsors: [], - }; - - const mockViewModels: SponsorViewModel[] = []; - - vi.mocked(mockApiClient.getAll).mockResolvedValue(mockDto); - vi.mocked(mockSponsorListPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getAllSponsors(); - - // Assert - expect(mockApiClient.getAll).toHaveBeenCalled(); - expect(mockSponsorListPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual([]); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch sponsors'); - vi.mocked(mockApiClient.getAll).mockRejectedValue(error); - - // Act & Assert - await expect(service.getAllSponsors()).rejects.toThrow('Failed to fetch sponsors'); - expect(mockApiClient.getAll).toHaveBeenCalled(); - expect(mockSponsorListPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getSponsorDashboard', () => { - it('should fetch sponsor dashboard and transform via presenter', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - - const mockDto: SponsorDashboardDto = { - sponsorId, - sponsorName: 'Sponsor Alpha', - totalSponsorships: 10, - activeSponsorships: 7, - totalInvestment: 50000, - }; - - const mockViewModel: SponsorDashboardViewModel = { - sponsorId, - sponsorName: 'Sponsor Alpha', - totalSponsorships: 10, - activeSponsorships: 7, - totalInvestment: 50000, - } as SponsorDashboardViewModel; - - vi.mocked(mockApiClient.getDashboard).mockResolvedValue(mockDto); - vi.mocked(mockSponsorDashboardPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorDashboard(sponsorId); - - // Assert - expect(mockApiClient.getDashboard).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorDashboardPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should return null when dashboard is not found', async () => { - // Arrange - const sponsorId = 'non-existent'; - - vi.mocked(mockApiClient.getDashboard).mockResolvedValue(null); - - // Act - const result = await service.getSponsorDashboard(sponsorId); - - // Assert - expect(mockApiClient.getDashboard).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorDashboardPresenter.present).not.toHaveBeenCalled(); - expect(result).toBeNull(); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - const error = new Error('Failed to fetch dashboard'); - vi.mocked(mockApiClient.getDashboard).mockRejectedValue(error); - - // Act & Assert - await expect(service.getSponsorDashboard(sponsorId)).rejects.toThrow('Failed to fetch dashboard'); - expect(mockApiClient.getDashboard).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorDashboardPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getSponsorSponsorships', () => { - it('should fetch sponsor sponsorships and transform via presenter', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - - const mockDto: SponsorSponsorshipsDto = { - sponsorId, - sponsorName: 'Sponsor Alpha', - sponsorships: [ - { - id: 'sponsorship-1', - leagueId: 'league-1', - leagueName: 'League One', - seasonId: 'season-1', - tier: 'main', - status: 'active', - amount: 10000, - currency: 'USD', - }, - ], - }; - - const mockViewModel: SponsorSponsorshipsViewModel = { - sponsorId, - sponsorName: 'Sponsor Alpha', - sponsorships: [], - } as SponsorSponsorshipsViewModel; - - vi.mocked(mockApiClient.getSponsorships).mockResolvedValue(mockDto); - vi.mocked(mockSponsorSponsorshipsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorSponsorships(sponsorId); - - // Assert - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should return null when sponsorships are not found', async () => { - // Arrange - const sponsorId = 'non-existent'; - - vi.mocked(mockApiClient.getSponsorships).mockResolvedValue(null); - - // Act - const result = await service.getSponsorSponsorships(sponsorId); - - // Assert - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).not.toHaveBeenCalled(); - expect(result).toBeNull(); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - const error = new Error('Failed to fetch sponsorships'); - vi.mocked(mockApiClient.getSponsorships).mockRejectedValue(error); - - // Act & Assert - await expect(service.getSponsorSponsorships(sponsorId)).rejects.toThrow('Failed to fetch sponsorships'); - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('createSponsor', () => { - it('should create a new sponsor', async () => { - // Arrange - const input: CreateSponsorInputDto = { - name: 'New Sponsor', - logoUrl: 'https://example.com/logo.png', - websiteUrl: 'https://newsponsor.com', - userId: 'user-123', - }; - - const mockOutput: CreateSponsorOutputDto = { - sponsorId: 'sponsor-new', - success: true, - }; - - vi.mocked(mockApiClient.create).mockResolvedValue(mockOutput); - - // Act - const result = await service.createSponsor(input); - - // Assert - expect(mockApiClient.create).toHaveBeenCalledWith(input); - expect(result).toEqual(mockOutput); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: CreateSponsorInputDto = { - name: 'New Sponsor', - userId: 'user-123', - }; - const error = new Error('Failed to create sponsor'); - vi.mocked(mockApiClient.create).mockRejectedValue(error); - - // Act & Assert - await expect(service.createSponsor(input)).rejects.toThrow('Failed to create sponsor'); - expect(mockApiClient.create).toHaveBeenCalledWith(input); - }); - }); - - describe('getSponsorshipPricing', () => { - it('should fetch sponsorship pricing', async () => { - // Arrange - const mockPricing: GetEntitySponsorshipPricingResultDto = { - pricingItems: [ - { - tier: 'main', - price: 10000, - currency: 'USD', - benefits: ['Logo placement', 'Race announcements'], - }, - { - tier: 'secondary', - price: 5000, - currency: 'USD', - benefits: ['Logo placement'], - }, - ], - }; - - vi.mocked(mockApiClient.getPricing).mockResolvedValue(mockPricing); - - // Act - const result = await service.getSponsorshipPricing(); - - // Assert - expect(mockApiClient.getPricing).toHaveBeenCalled(); - expect(result).toEqual(mockPricing); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch pricing'); - vi.mocked(mockApiClient.getPricing).mockRejectedValue(error); - - // Act & Assert - await expect(service.getSponsorshipPricing()).rejects.toThrow('Failed to fetch pricing'); - expect(mockApiClient.getPricing).toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/sponsors/SponsorService.ts b/apps/website/lib/services/sponsors/SponsorService.ts index 6f0fddef2..083c2f40c 100644 --- a/apps/website/lib/services/sponsors/SponsorService.ts +++ b/apps/website/lib/services/sponsors/SponsorService.ts @@ -1,58 +1,57 @@ import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient'; -import type { SponsorListPresenter } from '../../presenters/SponsorListPresenter'; -import type { SponsorDashboardPresenter } from '../../presenters/SponsorDashboardPresenter'; -import type { SponsorSponsorshipsPresenter } from '../../presenters/SponsorSponsorshipsPresenter'; -import type { SponsorViewModel, SponsorDashboardViewModel, SponsorSponsorshipsViewModel } from '../../view-models'; -import type { CreateSponsorInputDto, CreateSponsorOutputDto, GetEntitySponsorshipPricingResultDto } from '../../dtos'; +import { SponsorViewModel, SponsorDashboardViewModel, SponsorSponsorshipsViewModel } from '../../view-models'; +import type { CreateSponsorInputDTO } from '../../types/generated'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type CreateSponsorOutputDto = { id: string; name: string }; +type GetEntitySponsorshipPricingResultDto = { pricing: Array<{ entityType: string; price: number }> }; +type SponsorDTO = { id: string; name: string; logoUrl?: string; websiteUrl?: string }; /** * Sponsor Service * - * Orchestrates sponsor operations by coordinating API calls and presentation logic. + * Orchestrates sponsor operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class SponsorService { constructor( - private readonly apiClient: SponsorsApiClient, - private readonly sponsorListPresenter: SponsorListPresenter, - private readonly sponsorDashboardPresenter: SponsorDashboardPresenter, - private readonly sponsorSponsorshipsPresenter: SponsorSponsorshipsPresenter + private readonly apiClient: SponsorsApiClient ) {} /** - * Get all sponsors with presentation transformation + * Get all sponsors with view model transformation */ async getAllSponsors(): Promise { const dto = await this.apiClient.getAll(); - return this.sponsorListPresenter.present(dto); + return dto.sponsors.map((sponsor: SponsorDTO) => new SponsorViewModel(sponsor)); } /** - * Get sponsor dashboard with presentation transformation + * Get sponsor dashboard with view model transformation */ async getSponsorDashboard(sponsorId: string): Promise { const dto = await this.apiClient.getDashboard(sponsorId); if (!dto) { return null; } - return this.sponsorDashboardPresenter.present(dto); + return new SponsorDashboardViewModel(dto); } /** - * Get sponsor sponsorships with presentation transformation + * Get sponsor sponsorships with view model transformation */ async getSponsorSponsorships(sponsorId: string): Promise { const dto = await this.apiClient.getSponsorships(sponsorId); if (!dto) { return null; } - return this.sponsorSponsorshipsPresenter.present(dto); + return new SponsorSponsorshipsViewModel(dto); } /** * Create a new sponsor */ - async createSponsor(input: CreateSponsorInputDto): Promise { + async createSponsor(input: CreateSponsorInputDTO): Promise { return await this.apiClient.create(input); } diff --git a/apps/website/lib/services/sponsors/SponsorshipService.test.ts b/apps/website/lib/services/sponsors/SponsorshipService.test.ts deleted file mode 100644 index 99e7496eb..000000000 --- a/apps/website/lib/services/sponsors/SponsorshipService.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SponsorshipService } from './SponsorshipService'; -import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient'; -import type { SponsorshipPricingPresenter } from '../../presenters/SponsorshipPricingPresenter'; -import type { SponsorSponsorshipsPresenter } from '../../presenters/SponsorSponsorshipsPresenter'; -import type { - GetEntitySponsorshipPricingResultDto, - SponsorSponsorshipsDto, -} from '../../dtos'; -import type { SponsorshipPricingViewModel, SponsorSponsorshipsViewModel } from '../../view-models'; - -describe('SponsorshipService', () => { - let service: SponsorshipService; - let mockApiClient: SponsorsApiClient; - let mockSponsorshipPricingPresenter: SponsorshipPricingPresenter; - let mockSponsorSponsorshipsPresenter: SponsorSponsorshipsPresenter; - - beforeEach(() => { - mockApiClient = { - getPricing: vi.fn(), - getSponsorships: vi.fn(), - } as unknown as SponsorsApiClient; - - mockSponsorshipPricingPresenter = { - present: vi.fn(), - } as unknown as SponsorshipPricingPresenter; - - mockSponsorSponsorshipsPresenter = { - present: vi.fn(), - } as unknown as SponsorSponsorshipsPresenter; - - service = new SponsorshipService( - mockApiClient, - mockSponsorshipPricingPresenter, - mockSponsorSponsorshipsPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(SponsorshipService); - }); - }); - - describe('getSponsorshipPricing', () => { - it('should fetch sponsorship pricing from API and transform via presenter', async () => { - // Arrange - const mockDto: GetEntitySponsorshipPricingResultDto = { - mainSlotPrice: 10000, - secondarySlotPrice: 5000, - currency: 'USD', - }; - - const mockViewModel: SponsorshipPricingViewModel = { - mainSlotPrice: 10000, - secondarySlotPrice: 5000, - currency: 'USD', - formattedMainSlotPrice: 'USD 10,000', - formattedSecondarySlotPrice: 'USD 5,000', - priceDifference: 5000, - formattedPriceDifference: 'USD 5,000', - secondaryDiscountPercentage: 50, - } as SponsorshipPricingViewModel; - - vi.mocked(mockApiClient.getPricing).mockResolvedValue(mockDto); - vi.mocked(mockSponsorshipPricingPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorshipPricing(); - - // Assert - expect(mockApiClient.getPricing).toHaveBeenCalled(); - expect(mockSponsorshipPricingPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should handle different currencies', async () => { - // Arrange - const mockDto: GetEntitySponsorshipPricingResultDto = { - mainSlotPrice: 8000, - secondarySlotPrice: 4000, - currency: 'EUR', - }; - - const mockViewModel: SponsorshipPricingViewModel = { - mainSlotPrice: 8000, - secondarySlotPrice: 4000, - currency: 'EUR', - formattedMainSlotPrice: 'EUR 8,000', - formattedSecondarySlotPrice: 'EUR 4,000', - priceDifference: 4000, - formattedPriceDifference: 'EUR 4,000', - secondaryDiscountPercentage: 50, - } as SponsorshipPricingViewModel; - - vi.mocked(mockApiClient.getPricing).mockResolvedValue(mockDto); - vi.mocked(mockSponsorshipPricingPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorshipPricing(); - - // Assert - expect(mockApiClient.getPricing).toHaveBeenCalled(); - expect(mockSponsorshipPricingPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result.currency).toBe('EUR'); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch pricing'); - vi.mocked(mockApiClient.getPricing).mockRejectedValue(error); - - // Act & Assert - await expect(service.getSponsorshipPricing()).rejects.toThrow('Failed to fetch pricing'); - expect(mockApiClient.getPricing).toHaveBeenCalled(); - expect(mockSponsorshipPricingPresenter.present).not.toHaveBeenCalled(); - }); - - it('should handle zero prices', async () => { - // Arrange - const mockDto: GetEntitySponsorshipPricingResultDto = { - mainSlotPrice: 0, - secondarySlotPrice: 0, - currency: 'USD', - }; - - const mockViewModel: SponsorshipPricingViewModel = { - mainSlotPrice: 0, - secondarySlotPrice: 0, - currency: 'USD', - formattedMainSlotPrice: 'USD 0', - formattedSecondarySlotPrice: 'USD 0', - priceDifference: 0, - formattedPriceDifference: 'USD 0', - secondaryDiscountPercentage: 0, - } as SponsorshipPricingViewModel; - - vi.mocked(mockApiClient.getPricing).mockResolvedValue(mockDto); - vi.mocked(mockSponsorshipPricingPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorshipPricing(); - - // Assert - expect(mockApiClient.getPricing).toHaveBeenCalled(); - expect(mockSponsorshipPricingPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - }); - - describe('getSponsorSponsorships', () => { - it('should fetch sponsor sponsorships and transform via presenter', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - - const mockDto: SponsorSponsorshipsDto = { - sponsorId, - sponsorName: 'Sponsor Alpha', - sponsorships: [ - { - id: 'sponsorship-1', - leagueId: 'league-1', - leagueName: 'League One', - seasonId: 'season-1', - tier: 'main', - status: 'active', - amount: 10000, - currency: 'USD', - }, - { - id: 'sponsorship-2', - leagueId: 'league-2', - leagueName: 'League Two', - seasonId: 'season-2', - tier: 'secondary', - status: 'active', - amount: 5000, - currency: 'USD', - }, - ], - }; - - const mockViewModel: SponsorSponsorshipsViewModel = { - sponsorId, - sponsorName: 'Sponsor Alpha', - sponsorships: [], - totalCount: 2, - activeCount: 2, - hasSponsorships: true, - totalInvestment: 15000, - formattedTotalInvestment: 'USD 15,000', - } as SponsorSponsorshipsViewModel; - - vi.mocked(mockApiClient.getSponsorships).mockResolvedValue(mockDto); - vi.mocked(mockSponsorSponsorshipsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorSponsorships(sponsorId); - - // Assert - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - }); - - it('should return null when sponsorships are not found', async () => { - // Arrange - const sponsorId = 'non-existent'; - - vi.mocked(mockApiClient.getSponsorships).mockResolvedValue(null); - - // Act - const result = await service.getSponsorSponsorships(sponsorId); - - // Assert - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).not.toHaveBeenCalled(); - expect(result).toBeNull(); - }); - - it('should handle empty sponsorships list', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - - const mockDto: SponsorSponsorshipsDto = { - sponsorId, - sponsorName: 'Sponsor Alpha', - sponsorships: [], - }; - - const mockViewModel: SponsorSponsorshipsViewModel = { - sponsorId, - sponsorName: 'Sponsor Alpha', - sponsorships: [], - totalCount: 0, - activeCount: 0, - hasSponsorships: false, - totalInvestment: 0, - formattedTotalInvestment: 'USD 0', - } as SponsorSponsorshipsViewModel; - - vi.mocked(mockApiClient.getSponsorships).mockResolvedValue(mockDto); - vi.mocked(mockSponsorSponsorshipsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorSponsorships(sponsorId); - - // Assert - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result?.totalCount).toBe(0); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const sponsorId = 'sponsor-123'; - const error = new Error('Failed to fetch sponsorships'); - vi.mocked(mockApiClient.getSponsorships).mockRejectedValue(error); - - // Act & Assert - await expect(service.getSponsorSponsorships(sponsorId)).rejects.toThrow('Failed to fetch sponsorships'); - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).not.toHaveBeenCalled(); - }); - - it('should handle multiple sponsorship tiers', async () => { - // Arrange - const sponsorId = 'sponsor-456'; - - const mockDto: SponsorSponsorshipsDto = { - sponsorId, - sponsorName: 'Sponsor Beta', - sponsorships: [ - { - id: 'sponsorship-1', - leagueId: 'league-1', - leagueName: 'League One', - seasonId: 'season-1', - tier: 'main', - status: 'active', - amount: 10000, - currency: 'USD', - }, - { - id: 'sponsorship-2', - leagueId: 'league-2', - leagueName: 'League Two', - seasonId: 'season-2', - tier: 'secondary', - status: 'pending', - amount: 5000, - currency: 'USD', - }, - { - id: 'sponsorship-3', - leagueId: 'league-3', - leagueName: 'League Three', - seasonId: 'season-3', - tier: 'main', - status: 'expired', - amount: 10000, - currency: 'USD', - }, - ], - }; - - const mockViewModel: SponsorSponsorshipsViewModel = { - sponsorId, - sponsorName: 'Sponsor Beta', - sponsorships: [], - totalCount: 3, - activeCount: 1, - hasSponsorships: true, - totalInvestment: 25000, - formattedTotalInvestment: 'USD 25,000', - } as SponsorSponsorshipsViewModel; - - vi.mocked(mockApiClient.getSponsorships).mockResolvedValue(mockDto); - vi.mocked(mockSponsorSponsorshipsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getSponsorSponsorships(sponsorId); - - // Assert - expect(mockApiClient.getSponsorships).toHaveBeenCalledWith(sponsorId); - expect(mockSponsorSponsorshipsPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModel); - expect(result?.totalCount).toBe(3); - expect(result?.activeCount).toBe(1); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/sponsors/SponsorshipService.ts b/apps/website/lib/services/sponsors/SponsorshipService.ts index 033b17cb7..6cc109797 100644 --- a/apps/website/lib/services/sponsors/SponsorshipService.ts +++ b/apps/website/lib/services/sponsors/SponsorshipService.ts @@ -1,44 +1,40 @@ import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient'; -import type { SponsorshipPricingPresenter } from '../../presenters/SponsorshipPricingPresenter'; -import type { SponsorSponsorshipsPresenter } from '../../presenters/SponsorSponsorshipsPresenter'; -import type { +import { SponsorshipPricingViewModel, SponsorSponsorshipsViewModel } from '../../view-models'; -import type { - GetEntitySponsorshipPricingResultDto, - SponsorSponsorshipsDto -} from '../../dtos'; +import type { SponsorSponsorshipsDTO } from '../../types/generated'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type GetEntitySponsorshipPricingResultDto = { pricing: Array<{ entityType: string; price: number }> }; /** * Sponsorship Service * - * Orchestrates sponsorship operations by coordinating API calls and presentation logic. + * Orchestrates sponsorship operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class SponsorshipService { constructor( - private readonly apiClient: SponsorsApiClient, - private readonly sponsorshipPricingPresenter: SponsorshipPricingPresenter, - private readonly sponsorSponsorshipsPresenter: SponsorSponsorshipsPresenter + private readonly apiClient: SponsorsApiClient ) {} /** - * Get sponsorship pricing with presentation transformation + * Get sponsorship pricing with view model transformation */ async getSponsorshipPricing(): Promise { const dto = await this.apiClient.getPricing(); - return this.sponsorshipPricingPresenter.present(dto); + return new SponsorshipPricingViewModel(dto); } /** - * Get sponsor sponsorships with presentation transformation + * Get sponsor sponsorships with view model transformation */ async getSponsorSponsorships(sponsorId: string): Promise { const dto = await this.apiClient.getSponsorships(sponsorId); if (!dto) { return null; } - return this.sponsorSponsorshipsPresenter.present(dto); + return new SponsorSponsorshipsViewModel(dto); } } \ No newline at end of file diff --git a/apps/website/lib/services/teams/TeamJoinService.test.ts b/apps/website/lib/services/teams/TeamJoinService.test.ts deleted file mode 100644 index eaa151420..000000000 --- a/apps/website/lib/services/teams/TeamJoinService.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { TeamJoinService } from './TeamJoinService'; -import { TeamsApiClient } from '../../api/teams/TeamsApiClient'; -import { TeamJoinRequestPresenter } from '../../presenters/TeamJoinRequestPresenter'; -import type { TeamJoinRequestsDto, TeamJoinRequestItemDto } from '../../dtos'; -import type { TeamJoinRequestViewModel } from '../../view-models'; - -describe('TeamJoinService', () => { - let mockApiClient: TeamsApiClient; - let mockPresenter: TeamJoinRequestPresenter; - let service: TeamJoinService; - - beforeEach(() => { - mockApiClient = { - getJoinRequests: vi.fn(), - } as unknown as TeamsApiClient; - - mockPresenter = { - present: vi.fn(), - } as unknown as TeamJoinRequestPresenter; - - service = new TeamJoinService(mockApiClient, mockPresenter); - }); - - describe('getJoinRequests', () => { - it('should fetch join requests from API and transform via presenter', async () => { - // Arrange - const mockRequestDto: TeamJoinRequestItemDto = { - id: 'request-1', - teamId: 'team-1', - driverId: 'driver-1', - requestedAt: '2025-12-17T20:00:00Z', - message: 'Please let me join', - }; - - const mockDto: TeamJoinRequestsDto = { - requests: [mockRequestDto], - }; - - const mockViewModel: TeamJoinRequestViewModel = { - id: 'request-1', - teamId: 'team-1', - driverId: 'driver-1', - requestedAt: '2025-12-17T20:00:00Z', - message: 'Please let me join', - canApprove: true, - formattedRequestedAt: '12/17/2025, 9:00:00 PM', - status: 'Pending', - statusColor: 'yellow', - approveButtonText: 'Approve', - rejectButtonText: 'Reject', - } as unknown as TeamJoinRequestViewModel; - - vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getJoinRequests('team-1', 'driver-owner', true); - - // Assert - expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1'); - expect(mockApiClient.getJoinRequests).toHaveBeenCalledTimes(1); - expect(mockPresenter.present).toHaveBeenCalledWith(mockRequestDto, 'driver-owner', true); - expect(mockPresenter.present).toHaveBeenCalledTimes(1); - expect(result).toEqual([mockViewModel]); - }); - - it('should handle multiple join requests', async () => { - // Arrange - const mockRequestDto1: TeamJoinRequestItemDto = { - id: 'request-1', - teamId: 'team-1', - driverId: 'driver-1', - requestedAt: '2025-12-17T20:00:00Z', - }; - - const mockRequestDto2: TeamJoinRequestItemDto = { - id: 'request-2', - teamId: 'team-1', - driverId: 'driver-2', - requestedAt: '2025-12-17T21:00:00Z', - message: 'I want to join', - }; - - const mockDto: TeamJoinRequestsDto = { - requests: [mockRequestDto1, mockRequestDto2], - }; - - const mockViewModel1 = { id: 'request-1' } as TeamJoinRequestViewModel; - const mockViewModel2 = { id: 'request-2' } as TeamJoinRequestViewModel; - - vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.present) - .mockReturnValueOnce(mockViewModel1) - .mockReturnValueOnce(mockViewModel2); - - // Act - const result = await service.getJoinRequests('team-1', 'driver-owner', true); - - // Assert - expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1'); - expect(mockPresenter.present).toHaveBeenCalledTimes(2); - expect(mockPresenter.present).toHaveBeenNthCalledWith(1, mockRequestDto1, 'driver-owner', true); - expect(mockPresenter.present).toHaveBeenNthCalledWith(2, mockRequestDto2, 'driver-owner', true); - expect(result).toEqual([mockViewModel1, mockViewModel2]); - }); - - it('should handle empty join requests list', async () => { - // Arrange - const mockDto: TeamJoinRequestsDto = { - requests: [], - }; - - vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto); - - // Act - const result = await service.getJoinRequests('team-1', 'driver-owner', true); - - // Assert - expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1'); - expect(mockPresenter.present).not.toHaveBeenCalled(); - expect(result).toEqual([]); - }); - - it('should propagate API client errors', async () => { - // Arrange - const error = new Error('API Error: Team not found'); - vi.mocked(mockApiClient.getJoinRequests).mockRejectedValue(error); - - // Act & Assert - await expect( - service.getJoinRequests('invalid-team', 'driver-1', false) - ).rejects.toThrow('API Error: Team not found'); - - expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('invalid-team'); - expect(mockPresenter.present).not.toHaveBeenCalled(); - }); - - it('should propagate presenter errors', async () => { - // Arrange - const mockRequestDto: TeamJoinRequestItemDto = { - id: 'request-1', - teamId: 'team-1', - driverId: 'driver-1', - requestedAt: '2025-12-17T20:00:00Z', - }; - - const mockDto: TeamJoinRequestsDto = { - requests: [mockRequestDto], - }; - - const error = new Error('Presenter Error: Invalid DTO structure'); - vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.present).mockImplementation(() => { - throw error; - }); - - // Act & Assert - await expect( - service.getJoinRequests('team-1', 'driver-1', false) - ).rejects.toThrow('Presenter Error: Invalid DTO structure'); - - expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1'); - expect(mockPresenter.present).toHaveBeenCalledWith(mockRequestDto, 'driver-1', false); - }); - - it('should pass correct isOwner flag to presenter', async () => { - // Arrange - const mockRequestDto: TeamJoinRequestItemDto = { - id: 'request-1', - teamId: 'team-1', - driverId: 'driver-1', - requestedAt: '2025-12-17T20:00:00Z', - }; - - const mockDto: TeamJoinRequestsDto = { - requests: [mockRequestDto], - }; - - const mockViewModel = { id: 'request-1' } as TeamJoinRequestViewModel; - - vi.mocked(mockApiClient.getJoinRequests).mockResolvedValue(mockDto); - vi.mocked(mockPresenter.present).mockReturnValue(mockViewModel); - - // Act - non-owner - await service.getJoinRequests('team-1', 'driver-member', false); - - // Assert - expect(mockPresenter.present).toHaveBeenCalledWith(mockRequestDto, 'driver-member', false); - }); - }); - - describe('approveJoinRequest', () => { - it('should throw not implemented error', async () => { - // Act & Assert - await expect( - service.approveJoinRequest('team-1', 'request-1') - ).rejects.toThrow('Not implemented: API endpoint for approving join requests'); - }); - - it('should propagate errors when API is implemented', async () => { - // This test ensures error handling is in place for future implementation - // Act & Assert - await expect( - service.approveJoinRequest('team-1', 'request-1') - ).rejects.toThrow(); - }); - }); - - describe('rejectJoinRequest', () => { - it('should throw not implemented error', async () => { - // Act & Assert - await expect( - service.rejectJoinRequest('team-1', 'request-1') - ).rejects.toThrow('Not implemented: API endpoint for rejecting join requests'); - }); - - it('should propagate errors when API is implemented', async () => { - // This test ensures error handling is in place for future implementation - // Act & Assert - await expect( - service.rejectJoinRequest('team-1', 'request-1') - ).rejects.toThrow(); - }); - }); - - describe('Constructor Dependency Injection', () => { - it('should require apiClient and teamJoinRequestPresenter', () => { - // This test verifies the constructor signature - expect(() => { - new TeamJoinService(mockApiClient, mockPresenter); - }).not.toThrow(); - }); - - it('should use injected dependencies', async () => { - // Arrange - const customApiClient = { - getJoinRequests: vi.fn().mockResolvedValue({ requests: [] }), - } as unknown as TeamsApiClient; - - const customPresenter = { - present: vi.fn().mockReturnValue({} as TeamJoinRequestViewModel), - } as unknown as TeamJoinRequestPresenter; - - const customService = new TeamJoinService(customApiClient, customPresenter); - - // Act - await customService.getJoinRequests('team-1', 'driver-1', true); - - // Assert - expect(customApiClient.getJoinRequests).toHaveBeenCalledWith('team-1'); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/teams/TeamJoinService.ts b/apps/website/lib/services/teams/TeamJoinService.ts index 1a9d17151..0135bb14b 100644 --- a/apps/website/lib/services/teams/TeamJoinService.ts +++ b/apps/website/lib/services/teams/TeamJoinService.ts @@ -1,52 +1,46 @@ import type { TeamsApiClient } from '../../api/teams/TeamsApiClient'; -import type { TeamJoinRequestPresenter } from '../../presenters/TeamJoinRequestPresenter'; -import type { TeamJoinRequestViewModel } from '../../view-models'; +import { TeamJoinRequestViewModel } from '../../view-models'; + +type TeamJoinRequestDTO = { + id: string; + teamId: string; + driverId: string; + requestedAt: string; + message?: string; +}; /** * Team Join Service * - * Orchestrates team join/leave operations by coordinating API calls and presentation logic. + * Orchestrates team join/leave operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class TeamJoinService { constructor( - private readonly apiClient: TeamsApiClient, - private readonly teamJoinRequestPresenter: TeamJoinRequestPresenter + private readonly apiClient: TeamsApiClient ) {} /** - * Get team join requests with presentation transformation + * Get team join requests with view model transformation */ async getJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise { - try { - const dto = await this.apiClient.getJoinRequests(teamId); - return dto.requests.map(r => this.teamJoinRequestPresenter.present(r, currentUserId, isOwner)); - } catch (error) { - throw error; - } + const dto = await this.apiClient.getJoinRequests(teamId); + return dto.requests.map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner)); } /** * Approve a team join request */ async approveJoinRequest(teamId: string, requestId: string): Promise { - try { - // TODO: implement API call when endpoint is available - throw new Error('Not implemented: API endpoint for approving join requests'); - } catch (error) { - throw error; - } + // TODO: implement API call when endpoint is available + throw new Error('Not implemented: API endpoint for approving join requests'); } /** * Reject a team join request */ async rejectJoinRequest(teamId: string, requestId: string): Promise { - try { - // TODO: implement API call when endpoint is available - throw new Error('Not implemented: API endpoint for rejecting join requests'); - } catch (error) { - throw error; - } + // TODO: implement API call when endpoint is available + throw new Error('Not implemented: API endpoint for rejecting join requests'); } } \ No newline at end of file diff --git a/apps/website/lib/services/teams/TeamService.test.ts b/apps/website/lib/services/teams/TeamService.test.ts deleted file mode 100644 index f12698c5d..000000000 --- a/apps/website/lib/services/teams/TeamService.test.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { TeamService } from './TeamService'; -import type { TeamsApiClient } from '../../api/teams/TeamsApiClient'; -import type { TeamDetailsPresenter } from '../../presenters/TeamDetailsPresenter'; -import type { TeamListPresenter } from '../../presenters/TeamListPresenter'; -import type { TeamMembersPresenter } from '../../presenters/TeamMembersPresenter'; -import type { - AllTeamsDto, - TeamDetailsDto, - TeamMembersDto, - CreateTeamInputDto, - CreateTeamOutputDto, - UpdateTeamInputDto, - UpdateTeamOutputDto, - DriverTeamDto, -} from '../../dtos'; -import type { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models'; - -describe('TeamService', () => { - let service: TeamService; - let mockApiClient: TeamsApiClient; - let mockTeamListPresenter: TeamListPresenter; - let mockTeamDetailsPresenter: TeamDetailsPresenter; - let mockTeamMembersPresenter: TeamMembersPresenter; - - beforeEach(() => { - mockApiClient = { - getAll: vi.fn(), - getDetails: vi.fn(), - getMembers: vi.fn(), - create: vi.fn(), - update: vi.fn(), - getDriverTeam: vi.fn(), - } as unknown as TeamsApiClient; - - mockTeamListPresenter = { - present: vi.fn(), - } as unknown as TeamListPresenter; - - mockTeamDetailsPresenter = { - present: vi.fn(), - } as unknown as TeamDetailsPresenter; - - mockTeamMembersPresenter = { - present: vi.fn(), - } as unknown as TeamMembersPresenter; - - service = new TeamService( - mockApiClient, - mockTeamListPresenter, - mockTeamDetailsPresenter, - mockTeamMembersPresenter - ); - }); - - describe('constructor', () => { - it('should create instance with injected dependencies', () => { - expect(service).toBeInstanceOf(TeamService); - }); - }); - - describe('getAllTeams', () => { - it('should fetch all teams from API and transform via presenter', async () => { - // Arrange - const mockDto: AllTeamsDto = { - teams: [ - { - id: 'team-1', - name: 'Team Alpha', - logoUrl: 'https://example.com/logo1.png', - memberCount: 5, - rating: 2500, - }, - { - id: 'team-2', - name: 'Team Beta', - logoUrl: 'https://example.com/logo2.png', - memberCount: 3, - rating: 2300, - }, - ], - }; - - const mockViewModels: TeamSummaryViewModel[] = [ - { - id: 'team-1', - name: 'Team Alpha', - logoUrl: 'https://example.com/logo1.png', - memberCount: 5, - rating: 2500, - } as TeamSummaryViewModel, - { - id: 'team-2', - name: 'Team Beta', - logoUrl: 'https://example.com/logo2.png', - memberCount: 3, - rating: 2300, - } as TeamSummaryViewModel, - ]; - - vi.mocked(mockApiClient.getAll).mockResolvedValue(mockDto); - vi.mocked(mockTeamListPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getAllTeams(); - - // Assert - expect(mockApiClient.getAll).toHaveBeenCalled(); - expect(mockTeamListPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual(mockViewModels); - }); - - it('should handle empty teams list', async () => { - // Arrange - const mockDto: AllTeamsDto = { - teams: [], - }; - - const mockViewModels: TeamSummaryViewModel[] = []; - - vi.mocked(mockApiClient.getAll).mockResolvedValue(mockDto); - vi.mocked(mockTeamListPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getAllTeams(); - - // Assert - expect(mockApiClient.getAll).toHaveBeenCalled(); - expect(mockTeamListPresenter.present).toHaveBeenCalledWith(mockDto); - expect(result).toEqual([]); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const error = new Error('Failed to fetch teams'); - vi.mocked(mockApiClient.getAll).mockRejectedValue(error); - - // Act & Assert - await expect(service.getAllTeams()).rejects.toThrow('Failed to fetch teams'); - expect(mockApiClient.getAll).toHaveBeenCalled(); - expect(mockTeamListPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getTeamDetails', () => { - it('should fetch team details and transform via presenter', async () => { - // Arrange - const teamId = 'team-123'; - const currentUserId = 'user-456'; - - const mockDto: TeamDetailsDto = { - id: teamId, - name: 'Team Alpha', - description: 'A competitive racing team', - logoUrl: 'https://example.com/logo.png', - memberCount: 5, - ownerId: 'user-789', - }; - - const mockViewModel: TeamDetailsViewModel = { - id: teamId, - name: 'Team Alpha', - description: 'A competitive racing team', - logoUrl: 'https://example.com/logo.png', - memberCount: 5, - ownerId: 'user-789', - members: [], - } as TeamDetailsViewModel; - - vi.mocked(mockApiClient.getDetails).mockResolvedValue(mockDto); - vi.mocked(mockTeamDetailsPresenter.present).mockReturnValue(mockViewModel); - - // Act - const result = await service.getTeamDetails(teamId, currentUserId); - - // Assert - expect(mockApiClient.getDetails).toHaveBeenCalledWith(teamId); - expect(mockTeamDetailsPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId); - expect(result).toEqual(mockViewModel); - }); - - it('should return null when team is not found', async () => { - // Arrange - const teamId = 'non-existent'; - const currentUserId = 'user-456'; - - vi.mocked(mockApiClient.getDetails).mockResolvedValue(null); - - // Act - const result = await service.getTeamDetails(teamId, currentUserId); - - // Assert - expect(mockApiClient.getDetails).toHaveBeenCalledWith(teamId); - expect(mockTeamDetailsPresenter.present).not.toHaveBeenCalled(); - expect(result).toBeNull(); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const teamId = 'team-123'; - const currentUserId = 'user-456'; - const error = new Error('Failed to fetch team details'); - vi.mocked(mockApiClient.getDetails).mockRejectedValue(error); - - // Act & Assert - await expect(service.getTeamDetails(teamId, currentUserId)).rejects.toThrow('Failed to fetch team details'); - expect(mockApiClient.getDetails).toHaveBeenCalledWith(teamId); - expect(mockTeamDetailsPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('getTeamMembers', () => { - it('should fetch team members and transform via presenter', async () => { - // Arrange - const teamId = 'team-123'; - const currentUserId = 'user-456'; - const teamOwnerId = 'user-789'; - - const mockDto: TeamMembersDto = { - members: [ - { - driverId: 'driver-1', - role: 'owner', - joinedAt: '2024-01-01', - }, - { - driverId: 'driver-2', - role: 'member', - joinedAt: '2024-01-02', - }, - ], - }; - - const mockViewModels: TeamMemberViewModel[] = [ - { - driverId: 'driver-1', - role: 'owner', - joinedAt: '2024-01-01', - } as TeamMemberViewModel, - { - driverId: 'driver-2', - role: 'member', - joinedAt: '2024-01-02', - } as TeamMemberViewModel, - ]; - - vi.mocked(mockApiClient.getMembers).mockResolvedValue(mockDto); - vi.mocked(mockTeamMembersPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getTeamMembers(teamId, currentUserId, teamOwnerId); - - // Assert - expect(mockApiClient.getMembers).toHaveBeenCalledWith(teamId); - expect(mockTeamMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId, teamOwnerId); - expect(result).toEqual(mockViewModels); - }); - - it('should handle empty members list', async () => { - // Arrange - const teamId = 'team-123'; - const currentUserId = 'user-456'; - const teamOwnerId = 'user-789'; - - const mockDto: TeamMembersDto = { - members: [], - }; - - const mockViewModels: TeamMemberViewModel[] = []; - - vi.mocked(mockApiClient.getMembers).mockResolvedValue(mockDto); - vi.mocked(mockTeamMembersPresenter.present).mockReturnValue(mockViewModels); - - // Act - const result = await service.getTeamMembers(teamId, currentUserId, teamOwnerId); - - // Assert - expect(mockApiClient.getMembers).toHaveBeenCalledWith(teamId); - expect(mockTeamMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId, teamOwnerId); - expect(result).toEqual([]); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const teamId = 'team-123'; - const currentUserId = 'user-456'; - const teamOwnerId = 'user-789'; - const error = new Error('Failed to fetch team members'); - vi.mocked(mockApiClient.getMembers).mockRejectedValue(error); - - // Act & Assert - await expect(service.getTeamMembers(teamId, currentUserId, teamOwnerId)).rejects.toThrow('Failed to fetch team members'); - expect(mockApiClient.getMembers).toHaveBeenCalledWith(teamId); - expect(mockTeamMembersPresenter.present).not.toHaveBeenCalled(); - }); - }); - - describe('createTeam', () => { - it('should create a new team', async () => { - // Arrange - const input: CreateTeamInputDto = { - name: 'New Team', - description: 'A new racing team', - }; - - const mockOutput: CreateTeamOutputDto = { - id: 'team-new', - name: 'New Team', - success: true, - }; - - vi.mocked(mockApiClient.create).mockResolvedValue(mockOutput); - - // Act - const result = await service.createTeam(input); - - // Assert - expect(mockApiClient.create).toHaveBeenCalledWith(input); - expect(result).toEqual(mockOutput); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const input: CreateTeamInputDto = { - name: 'New Team', - description: 'A new racing team', - }; - const error = new Error('Failed to create team'); - vi.mocked(mockApiClient.create).mockRejectedValue(error); - - // Act & Assert - await expect(service.createTeam(input)).rejects.toThrow('Failed to create team'); - expect(mockApiClient.create).toHaveBeenCalledWith(input); - }); - }); - - describe('updateTeam', () => { - it('should update team details', async () => { - // Arrange - const teamId = 'team-123'; - const input: UpdateTeamInputDto = { - name: 'Updated Team Name', - description: 'Updated description', - }; - - const mockOutput: UpdateTeamOutputDto = { - id: teamId, - success: true, - }; - - vi.mocked(mockApiClient.update).mockResolvedValue(mockOutput); - - // Act - const result = await service.updateTeam(teamId, input); - - // Assert - expect(mockApiClient.update).toHaveBeenCalledWith(teamId, input); - expect(result).toEqual(mockOutput); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const teamId = 'team-123'; - const input: UpdateTeamInputDto = { - name: 'Updated Team Name', - }; - const error = new Error('Failed to update team'); - vi.mocked(mockApiClient.update).mockRejectedValue(error); - - // Act & Assert - await expect(service.updateTeam(teamId, input)).rejects.toThrow('Failed to update team'); - expect(mockApiClient.update).toHaveBeenCalledWith(teamId, input); - }); - }); - - describe('getDriverTeam', () => { - it('should fetch driver team', async () => { - // Arrange - const driverId = 'driver-123'; - - const mockDto: DriverTeamDto = { - teamId: 'team-456', - teamName: 'Team Alpha', - role: 'member', - }; - - vi.mocked(mockApiClient.getDriverTeam).mockResolvedValue(mockDto); - - // Act - const result = await service.getDriverTeam(driverId); - - // Assert - expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith(driverId); - expect(result).toEqual(mockDto); - }); - - it('should return null when driver has no team', async () => { - // Arrange - const driverId = 'driver-123'; - - vi.mocked(mockApiClient.getDriverTeam).mockResolvedValue(null); - - // Act - const result = await service.getDriverTeam(driverId); - - // Assert - expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith(driverId); - expect(result).toBeNull(); - }); - - it('should propagate errors from API client', async () => { - // Arrange - const driverId = 'driver-123'; - const error = new Error('Failed to fetch driver team'); - vi.mocked(mockApiClient.getDriverTeam).mockRejectedValue(error); - - // Act & Assert - await expect(service.getDriverTeam(driverId)).rejects.toThrow('Failed to fetch driver team'); - expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith(driverId); - }); - }); -}); \ No newline at end of file diff --git a/apps/website/lib/services/teams/TeamService.ts b/apps/website/lib/services/teams/TeamService.ts index c3cba61ce..911bcdb9a 100644 --- a/apps/website/lib/services/teams/TeamService.ts +++ b/apps/website/lib/services/teams/TeamService.ts @@ -1,49 +1,51 @@ import type { TeamsApiClient } from '../../api/teams/TeamsApiClient'; -import type { TeamDetailsPresenter } from '../../presenters/TeamDetailsPresenter'; -import type { TeamListPresenter } from '../../presenters/TeamListPresenter'; -import type { TeamMembersPresenter } from '../../presenters/TeamMembersPresenter'; -import type { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models'; -import type { CreateTeamInputDto, CreateTeamOutputDto, UpdateTeamInputDto, UpdateTeamOutputDto, DriverTeamDto } from '../../dtos'; +import { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models'; + +// TODO: Move these types to apps/website/lib/types/generated when available +type CreateTeamInputDto = { name: string; tag: string; description?: string }; +type CreateTeamOutputDto = { id: string; success: boolean }; +type UpdateTeamInputDto = { name?: string; tag?: string; description?: string }; +type UpdateTeamOutputDto = { success: boolean }; +type DriverTeamDto = { teamId: string; teamName: string; role: string }; +type TeamSummaryDTO = { id: string; name: string; logoUrl?: string; memberCount: number; rating: number }; +type TeamMemberDTO = { driverId: string; driver?: any; role: string; joinedAt: string }; /** * Team Service * - * Orchestrates team operations by coordinating API calls and presentation logic. + * Orchestrates team operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ export class TeamService { constructor( - private readonly apiClient: TeamsApiClient, - private readonly teamListPresenter: TeamListPresenter, - private readonly teamDetailsPresenter: TeamDetailsPresenter, - private readonly teamMembersPresenter: TeamMembersPresenter + private readonly apiClient: TeamsApiClient ) {} /** - * Get all teams with presentation transformation + * Get all teams with view model transformation */ async getAllTeams(): Promise { const dto = await this.apiClient.getAll(); - return this.teamListPresenter.present(dto); + return dto.teams.map((team: TeamSummaryDTO) => new TeamSummaryViewModel(team)); } /** - * Get team details with presentation transformation + * Get team details with view model transformation */ async getTeamDetails(teamId: string, currentUserId: string): Promise { const dto = await this.apiClient.getDetails(teamId); if (!dto) { return null; } - return this.teamDetailsPresenter.present(dto, currentUserId); + return new TeamDetailsViewModel(dto, currentUserId); } /** - * Get team members with presentation transformation + * Get team members with view model transformation */ async getTeamMembers(teamId: string, currentUserId: string, teamOwnerId: string): Promise { const dto = await this.apiClient.getMembers(teamId); - return this.teamMembersPresenter.present(dto, currentUserId, teamOwnerId); + return dto.members.map((member: TeamMemberDTO) => new TeamMemberViewModel(member, currentUserId, teamOwnerId)); } /** diff --git a/apps/website/lib/types/generated/api.ts b/apps/website/lib/types/generated/api.ts deleted file mode 100644 index af5845dfa..000000000 --- a/apps/website/lib/types/generated/api.ts +++ /dev/null @@ -1,469 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export type paths = Record; -export type webhooks = Record; -export interface components { - schemas: { - SponsorshipPricingItemDTO: { - id: string; - level: string; - price: number; - currency: string; - }; - SponsorshipDetailDTO: { - id: string; - leagueId: string; - leagueName: string; - seasonId: string; - seasonName: string; - }; - SponsoredLeagueDTO: { - id: string; - name: string; - }; - SponsorSponsorshipsDTO: { - sponsorId: string; - sponsorName: string; - }; - SponsorDashboardMetricsDTO: { - impressions: number; - impressionsChange: number; - uniqueViewers: number; - viewersChange: number; - races: number; - drivers: number; - exposure: number; - exposureChange: number; - }; - SponsorDashboardInvestmentDTO: { - activeSponsorships: number; - totalInvestment: number; - costPerThousandViews: number; - }; - SponsorDashboardDTO: { - sponsorId: string; - sponsorName: string; - }; - GetSponsorSponsorshipsQueryParamsDTO: { - sponsorId: string; - }; - GetSponsorDashboardQueryParamsDTO: { - sponsorId: string; - }; - CreateSponsorInputDTO: { - name: string; - contactEmail: string; - }; - UpdatePaymentStatusInputDTO: { - paymentId: string; - }; - PaymentDto: { - id: string; - }; - MembershipFeeDto: { - id: string; - leagueId: string; - }; - MemberPaymentDto: { - id: string; - feeId: string; - driverId: string; - amount: number; - platformFee: number; - netAmount: number; - }; - PrizeDto: { - id: string; - leagueId: string; - seasonId: string; - position: number; - name: string; - amount: number; - }; - WalletDto: { - id: string; - leagueId: string; - balance: number; - totalRevenue: number; - totalPlatformFees: number; - totalWithdrawn: number; - /** Format: date-time */ - createdAt: string; - currency: string; - }; - TransactionDto: { - id: string; - walletId: string; - }; - PaymentDTO: { - id: string; - }; - WithdrawFromRaceParamsDTO: { - raceId: string; - driverId: string; - }; - RequestProtestDefenseCommandDTO: { - protestId: string; - stewardId: string; - }; - RegisterForRaceParamsDTO: { - raceId: string; - leagueId: string; - driverId: string; - }; - RacesPageDataRaceDTO: { - id: string; - track: string; - car: string; - scheduledAt: string; - status: string; - leagueId: string; - leagueName: string; - }; - RaceWithSOFDTO: { - id: string; - track: string; - }; - RaceStatsDTO: { - totalRaces: number; - }; - RaceResultsDetailDTO: { - raceId: string; - track: string; - }; - RaceResultDTO: { - driverId: string; - driverName: string; - avatarUrl: string; - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; - }; - RaceProtestDTO: { - id: string; - protestingDriverId: string; - accusedDriverId: string; - incident: Record; - lap: number; - description: string; - }; - RacePenaltyDTO: { - id: string; - driverId: string; - type: string; - value: number; - reason: string; - issuedBy: string; - issuedAt: string; - }; - RaceDetailUserResultDTO: { - position: number; - startPosition: number; - incidents: number; - fastestLap: number; - positionChange: number; - isPodium: boolean; - isClean: boolean; - }; - RaceDetailRegistrationDTO: { - isUserRegistered: boolean; - canRegister: boolean; - }; - RaceDetailRaceDTO: { - id: string; - leagueId: string; - track: string; - car: string; - scheduledAt: string; - sessionType: string; - status: string; - }; - RaceDetailLeagueDTO: { - id: string; - name: string; - description: string; - settings: Record; - maxDrivers?: number; - qualifyingFormat?: string; - }; - RaceDetailEntryDTO: { - id: string; - name: string; - country: string; - avatarUrl: string; - }; - RaceDTO: { - id: string; - name: string; - date: string; - }; - RaceActionParamsDTO: { - raceId: string; - }; - QuickPenaltyCommandDTO: { - raceId: string; - driverId: string; - adminId: string; - }; - ImportRaceResultsDTO: { - raceId: string; - resultsFileContent: string; - }; - GetRaceDetailParamsDTODTO: { - raceId: string; - driverId: string; - }; - FileProtestCommandDTO: { - raceId: string; - protestingDriverId: string; - accusedDriverId: string; - incident: Record; - lap: number; - description: string; - timeInRace?: number; - }; - DashboardRecentResultDTO: { - raceId: string; - raceName: string; - leagueId: string; - leagueName: string; - finishedAt: string; - position: number; - incidents: number; - }; - DashboardRaceSummaryDTO: { - id: string; - leagueId: string; - leagueName: string; - track: string; - car: string; - scheduledAt: string; - }; - DashboardLeagueStandingSummaryDTO: { - leagueId: string; - leagueName: string; - position: number; - totalDrivers: number; - points: number; - }; - DashboardFriendSummaryDTO: { - id: string; - name: string; - country: string; - avatarUrl: string; - }; - DashboardFeedSummaryDTO: { - notificationCount: number; - }; - DashboardFeedItemSummaryDTO: { - id: string; - enum: string; - }; - DashboardDriverSummaryDTO: { - id: string; - name: string; - country: string; - avatarUrl: string; - }; - ApplyPenaltyCommandDTO: { - raceId: string; - driverId: string; - stewardId: string; - enum: string; - }; - RequestAvatarGenerationInputDTO: { - userId: string; - facePhotoData: string; - suitColor: string; - }; - UpdateLeagueMemberRoleOutputDTO: { - success: boolean; - }; - UpdateLeagueMemberRoleInputDTO: { - leagueId: string; - performerDriverId: string; - targetDriverId: string; - }; - SeasonDTO: { - seasonId: string; - name: string; - leagueId: string; - }; - RemoveLeagueMemberOutputDTO: { - success: boolean; - }; - RemoveLeagueMemberInputDTO: { - leagueId: string; - performerDriverId: string; - targetDriverId: string; - }; - RejectJoinRequestOutputDTO: { - success: boolean; - }; - RejectJoinRequestInputDTO: { - requestId: string; - leagueId: string; - }; - ProtestDTO: { - id: string; - raceId: string; - protestingDriverId: string; - accusedDriverId: string; - /** Format: date-time */ - submittedAt: string; - description: string; - }; - LeagueWithCapacityDTO: { - id: string; - name: string; - }; - LeagueSummaryDTO: { - id: string; - name: string; - }; - LeagueStatsDTO: { - totalMembers: number; - totalRaces: number; - averageRating: number; - }; - LeagueStandingDTO: { - driverId: string; - }; - LeagueSeasonSummaryDTO: { - seasonId: string; - name: string; - status: string; - }; - LeagueMemberDTO: { - driverId: string; - }; - LeagueJoinRequestDTO: { - id: string; - leagueId: string; - driverId: string; - /** Format: date-time */ - requestedAt: string; - }; - LeagueConfigFormModelTimingsDTO: { - raceDayOfWeek: string; - raceTimeHour: number; - raceTimeMinute: number; - }; - LeagueConfigFormModelStructureDTO: { - mode: string; - }; - LeagueConfigFormModelScoringDTO: { - type: string; - points: number; - }; - LeagueConfigFormModelDTO: { - leagueId: string; - }; - LeagueConfigFormModelBasicsDTO: { - name: string; - description: string; - }; - LeagueAdminPermissionsDTO: { - canRemoveMember: boolean; - canUpdateRoles: boolean; - }; - GetLeagueSeasonsQueryDTO: { - leagueId: string; - }; - GetLeagueProtestsQueryDTO: { - leagueId: string; - }; - GetLeagueOwnerSummaryQueryDTO: { - ownerId: string; - leagueId: string; - }; - GetLeagueJoinRequestsQueryDTO: { - leagueId: string; - }; - GetLeagueAdminPermissionsInputDTO: { - leagueId: string; - performerDriverId: string; - }; - GetLeagueAdminConfigQueryDTO: { - leagueId: string; - }; - CreateLeagueOutputDTO: { - leagueId: string; - success: boolean; - }; - CreateLeagueInputDTO: { - name: string; - description: string; - }; - ApproveJoinRequestOutputDTO: { - success: boolean; - }; - ApproveJoinRequestInputDTO: { - requestId: string; - leagueId: string; - }; - GetDriverRegistrationStatusQueryDTO: { - raceId: string; - driverId: string; - }; - DriverStatsDTO: { - totalDrivers: number; - }; - DriverRegistrationStatusDTO: { - isRegistered: boolean; - raceId: string; - driverId: string; - }; - DriverLeaderboardItemDTO: { - id: string; - name: string; - rating: number; - skillLevel: string; - nationality: string; - racesCompleted: number; - wins: number; - podiums: number; - isActive: boolean; - rank: number; - }; - CompleteOnboardingOutputDTO: { - success: boolean; - }; - CompleteOnboardingInputDTO: { - firstName: string; - lastName: string; - displayName: string; - country: string; - }; - AuthenticatedUserDTO: { - userId: string; - email: string; - displayName: string; - }; - AuthSessionDTO: { - token: string; - user: components["schemas"]["AuthenticatedUserDTO"]; - }; - RecordPageViewOutputDTO: { - pageViewId: string; - }; - RecordEngagementOutputDTO: { - eventId: string; - engagementWeight: number; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export type operations = Record; diff --git a/apps/website/lib/types/generated/index.ts b/apps/website/lib/types/generated/index.ts deleted file mode 100644 index 740ae4319..000000000 --- a/apps/website/lib/types/generated/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Auto-generated DTO type exports - * This file is generated by scripts/generate-api-types.ts - * Do not edit manually - regenerate using: npm run api:sync-types - */ - -// Re-export all schema types from the generated OpenAPI types -export type { components, paths, operations } from './api'; - -// Re-export individual DTO types -export type { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO'; -export type { SponsorshipDetailDTO } from './SponsorshipDetailDTO'; -export type { SponsoredLeagueDTO } from './SponsoredLeagueDTO'; -export type { SponsorSponsorshipsDTO } from './SponsorSponsorshipsDTO'; -export type { SponsorDashboardMetricsDTO } from './SponsorDashboardMetricsDTO'; -export type { SponsorDashboardInvestmentDTO } from './SponsorDashboardInvestmentDTO'; -export type { SponsorDashboardDTO } from './SponsorDashboardDTO'; -export type { GetSponsorSponsorshipsQueryParamsDTO } from './GetSponsorSponsorshipsQueryParamsDTO'; -export type { GetSponsorDashboardQueryParamsDTO } from './GetSponsorDashboardQueryParamsDTO'; -export type { CreateSponsorInputDTO } from './CreateSponsorInputDTO'; -export type { UpdatePaymentStatusInputDTO } from './UpdatePaymentStatusInputDTO'; -export type { PaymentDto } from './PaymentDto'; -export type { MembershipFeeDto } from './MembershipFeeDto'; -export type { MemberPaymentDto } from './MemberPaymentDto'; -export type { PrizeDto } from './PrizeDto'; -export type { WalletDto } from './WalletDto'; -export type { TransactionDto } from './TransactionDto'; -export type { PaymentDTO } from './PaymentDTO'; -export type { WithdrawFromRaceParamsDTO } from './WithdrawFromRaceParamsDTO'; -export type { RequestProtestDefenseCommandDTO } from './RequestProtestDefenseCommandDTO'; -export type { RegisterForRaceParamsDTO } from './RegisterForRaceParamsDTO'; -export type { RacesPageDataRaceDTO } from './RacesPageDataRaceDTO'; -export type { RaceWithSOFDTO } from './RaceWithSOFDTO'; -export type { RaceStatsDTO } from './RaceStatsDTO'; -export type { RaceResultsDetailDTO } from './RaceResultsDetailDTO'; -export type { RaceResultDTO } from './RaceResultDTO'; -export type { RaceProtestDTO } from './RaceProtestDTO'; -export type { RacePenaltyDTO } from './RacePenaltyDTO'; -export type { RaceDetailUserResultDTO } from './RaceDetailUserResultDTO'; -export type { RaceDetailRegistrationDTO } from './RaceDetailRegistrationDTO'; -export type { RaceDetailRaceDTO } from './RaceDetailRaceDTO'; -export type { RaceDetailLeagueDTO } from './RaceDetailLeagueDTO'; -export type { RaceDetailEntryDTO } from './RaceDetailEntryDTO'; -export type { RaceDTO } from './RaceDTO'; -export type { RaceActionParamsDTO } from './RaceActionParamsDTO'; -export type { QuickPenaltyCommandDTO } from './QuickPenaltyCommandDTO'; -export type { ImportRaceResultsDTO } from './ImportRaceResultsDTO'; -export type { GetRaceDetailParamsDTODTO } from './GetRaceDetailParamsDTODTO'; -export type { FileProtestCommandDTO } from './FileProtestCommandDTO'; -export type { DashboardRecentResultDTO } from './DashboardRecentResultDTO'; -export type { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO'; -export type { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO'; -export type { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO'; -export type { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO'; -export type { DashboardFeedItemSummaryDTO } from './DashboardFeedItemSummaryDTO'; -export type { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO'; -export type { ApplyPenaltyCommandDTO } from './ApplyPenaltyCommandDTO'; -export type { RequestAvatarGenerationInputDTO } from './RequestAvatarGenerationInputDTO'; -export type { UpdateLeagueMemberRoleOutputDTO } from './UpdateLeagueMemberRoleOutputDTO'; -export type { UpdateLeagueMemberRoleInputDTO } from './UpdateLeagueMemberRoleInputDTO'; -export type { SeasonDTO } from './SeasonDTO'; -export type { RemoveLeagueMemberOutputDTO } from './RemoveLeagueMemberOutputDTO'; -export type { RemoveLeagueMemberInputDTO } from './RemoveLeagueMemberInputDTO'; -export type { RejectJoinRequestOutputDTO } from './RejectJoinRequestOutputDTO'; -export type { RejectJoinRequestInputDTO } from './RejectJoinRequestInputDTO'; -export type { ProtestDTO } from './ProtestDTO'; -export type { LeagueWithCapacityDTO } from './LeagueWithCapacityDTO'; -export type { LeagueSummaryDTO } from './LeagueSummaryDTO'; -export type { LeagueStatsDTO } from './LeagueStatsDTO'; -export type { LeagueStandingDTO } from './LeagueStandingDTO'; -export type { LeagueSeasonSummaryDTO } from './LeagueSeasonSummaryDTO'; -export type { LeagueMemberDTO } from './LeagueMemberDTO'; -export type { LeagueJoinRequestDTO } from './LeagueJoinRequestDTO'; -export type { LeagueConfigFormModelTimingsDTO } from './LeagueConfigFormModelTimingsDTO'; -export type { LeagueConfigFormModelStructureDTO } from './LeagueConfigFormModelStructureDTO'; -export type { LeagueConfigFormModelScoringDTO } from './LeagueConfigFormModelScoringDTO'; -export type { LeagueConfigFormModelDTO } from './LeagueConfigFormModelDTO'; -export type { LeagueConfigFormModelBasicsDTO } from './LeagueConfigFormModelBasicsDTO'; -export type { LeagueAdminPermissionsDTO } from './LeagueAdminPermissionsDTO'; -export type { GetLeagueSeasonsQueryDTO } from './GetLeagueSeasonsQueryDTO'; -export type { GetLeagueProtestsQueryDTO } from './GetLeagueProtestsQueryDTO'; -export type { GetLeagueOwnerSummaryQueryDTO } from './GetLeagueOwnerSummaryQueryDTO'; -export type { GetLeagueJoinRequestsQueryDTO } from './GetLeagueJoinRequestsQueryDTO'; -export type { GetLeagueAdminPermissionsInputDTO } from './GetLeagueAdminPermissionsInputDTO'; -export type { GetLeagueAdminConfigQueryDTO } from './GetLeagueAdminConfigQueryDTO'; -export type { CreateLeagueOutputDTO } from './CreateLeagueOutputDTO'; -export type { CreateLeagueInputDTO } from './CreateLeagueInputDTO'; -export type { ApproveJoinRequestOutputDTO } from './ApproveJoinRequestOutputDTO'; -export type { ApproveJoinRequestInputDTO } from './ApproveJoinRequestInputDTO'; -export type { GetDriverRegistrationStatusQueryDTO } from './GetDriverRegistrationStatusQueryDTO'; -export type { DriverStatsDTO } from './DriverStatsDTO'; -export type { DriverRegistrationStatusDTO } from './DriverRegistrationStatusDTO'; -export type { DriverLeaderboardItemDTO } from './DriverLeaderboardItemDTO'; -export type { CompleteOnboardingOutputDTO } from './CompleteOnboardingOutputDTO'; -export type { CompleteOnboardingInputDTO } from './CompleteOnboardingInputDTO'; -export type { AuthenticatedUserDTO } from './AuthenticatedUserDTO'; -export type { AuthSessionDTO } from './AuthSessionDTO'; -export type { RecordPageViewOutputDTO } from './RecordPageViewOutputDTO'; -export type { RecordEngagementOutputDTO } from './RecordEngagementOutputDTO'; diff --git a/apps/website/lib/view-models/CompleteOnboardingViewModel.ts b/apps/website/lib/view-models/CompleteOnboardingViewModel.ts index 0a86d9ee1..a227c77ba 100644 --- a/apps/website/lib/view-models/CompleteOnboardingViewModel.ts +++ b/apps/website/lib/view-models/CompleteOnboardingViewModel.ts @@ -6,11 +6,10 @@ import { CompleteOnboardingOutputDTO } from '../types/generated/CompleteOnboardi */ export class CompleteOnboardingViewModel implements CompleteOnboardingOutputDTO { success: boolean; - driverId: string; + driverId?: string; - constructor(dto: CompleteOnboardingOutputDTO & { driverId: string }) { + constructor(dto: CompleteOnboardingOutputDTO) { this.success = dto.success; - this.driverId = dto.driverId; } /** UI-specific: Whether onboarding was successful */ diff --git a/apps/website/lib/view-models/RecordEngagementInputViewModel.ts b/apps/website/lib/view-models/RecordEngagementInputViewModel.ts new file mode 100644 index 000000000..1c24a6b50 --- /dev/null +++ b/apps/website/lib/view-models/RecordEngagementInputViewModel.ts @@ -0,0 +1,32 @@ +/** + * Record engagement input view model + * Represents input data for recording an engagement event + * + * Note: No matching generated DTO available yet + */ +export class RecordEngagementInputViewModel { + eventType: string; + userId?: string; + metadata?: Record; + + constructor(data: { eventType: string; userId?: string; metadata?: Record }) { + this.eventType = data.eventType; + this.userId = data.userId; + this.metadata = data.metadata; + } + + /** UI-specific: Formatted event type for display */ + get displayEventType(): string { + return this.eventType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + } + + /** UI-specific: Has metadata */ + get hasMetadata(): boolean { + return this.metadata !== undefined && Object.keys(this.metadata).length > 0; + } + + /** UI-specific: Metadata keys count */ + get metadataKeysCount(): number { + return this.metadata ? Object.keys(this.metadata).length : 0; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/RecordPageViewInputViewModel.ts b/apps/website/lib/view-models/RecordPageViewInputViewModel.ts new file mode 100644 index 000000000..bd03ba079 --- /dev/null +++ b/apps/website/lib/view-models/RecordPageViewInputViewModel.ts @@ -0,0 +1,25 @@ +/** + * Record page view input view model + * Represents input data for recording a page view + * + * Note: No matching generated DTO available yet + */ +export class RecordPageViewInputViewModel { + path: string; + userId?: string; + + constructor(data: { path: string; userId?: string }) { + this.path = data.path; + this.userId = data.userId; + } + + /** UI-specific: Formatted path for display */ + get displayPath(): string { + return this.path.startsWith('/') ? this.path : `/${this.path}`; + } + + /** UI-specific: Has user context */ + get hasUserContext(): boolean { + return this.userId !== undefined && this.userId !== ''; + } +} \ No newline at end of file diff --git a/apps/website/lib/view-models/index.ts b/apps/website/lib/view-models/index.ts new file mode 100644 index 000000000..a84c88363 --- /dev/null +++ b/apps/website/lib/view-models/index.ts @@ -0,0 +1,48 @@ +/** + * View Models Index + * + * Central export file for all view models. + * View models represent fully prepared UI state and should only be consumed by UI components. + */ + +export { AnalyticsDashboardViewModel } from './AnalyticsDashboardViewModel'; +export { AnalyticsMetricsViewModel } from './AnalyticsMetricsViewModel'; +export { AvatarViewModel } from './AvatarViewModel'; +export { CompleteOnboardingViewModel } from './CompleteOnboardingViewModel'; +export { DeleteMediaViewModel } from './DeleteMediaViewModel'; +export { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel'; +export { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel'; +export { DriverRegistrationStatusViewModel } from './DriverRegistrationStatusViewModel'; +export { DriverViewModel } from './DriverViewModel'; +export { LeagueAdminViewModel } from './LeagueAdminViewModel'; +export { LeagueJoinRequestViewModel } from './LeagueJoinRequestViewModel'; +export { LeagueMemberViewModel } from './LeagueMemberViewModel'; +export { LeagueStandingsViewModel } from './LeagueStandingsViewModel'; +export { LeagueSummaryViewModel } from './LeagueSummaryViewModel'; +export { MediaViewModel } from './MediaViewModel'; +export { MembershipFeeViewModel } from './MembershipFeeViewModel'; +export { PaymentViewModel } from './PaymentViewModel'; +export { PrizeViewModel } from './PrizeViewModel'; +export { ProtestViewModel } from './ProtestViewModel'; +export { RaceDetailViewModel } from './RaceDetailViewModel'; +export { RaceListItemViewModel } from './RaceListItemViewModel'; +export { RaceResultsDetailViewModel } from './RaceResultsDetailViewModel'; +export { RaceResultViewModel } from './RaceResultViewModel'; +export { RacesPageViewModel } from './RacesPageViewModel'; +export { RequestAvatarGenerationViewModel } from './RequestAvatarGenerationViewModel'; +export { SessionViewModel } from './SessionViewModel'; +export { SponsorDashboardViewModel } from './SponsorDashboardViewModel'; +export { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel'; +export { SponsorshipPricingViewModel } from './SponsorshipPricingViewModel'; +export { SponsorSponsorshipsViewModel } from './SponsorSponsorshipsViewModel'; +export { SponsorViewModel } from './SponsorViewModel'; +export { StandingEntryViewModel } from './StandingEntryViewModel'; +export { TeamDetailsViewModel } from './TeamDetailsViewModel'; +export { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel'; +export { TeamMemberViewModel } from './TeamMemberViewModel'; +export { TeamSummaryViewModel } from './TeamSummaryViewModel'; +export { UpdateAvatarViewModel } from './UpdateAvatarViewModel'; +export { UploadMediaViewModel } from './UploadMediaViewModel'; +export { UserProfileViewModel } from './UserProfileViewModel'; +export { WalletTransactionViewModel } from './WalletTransactionViewModel'; +export { WalletViewModel } from './WalletViewModel'; \ No newline at end of file diff --git a/scripts/generate-api-types.ts b/scripts/generate-api-types.ts index e9ab1c888..4459b4a21 100644 --- a/scripts/generate-api-types.ts +++ b/scripts/generate-api-types.ts @@ -36,18 +36,19 @@ async function generateTypes() { await fs.mkdir(outputDir, { recursive: true }); try { + // Skip generating monolithic api.ts file // Use openapi-typescript to generate types - console.log('📝 Running openapi-typescript...'); - execSync(`npx openapi-typescript "${openapiPath}" -o "${outputFile}"`, { - stdio: 'inherit', - cwd: path.join(__dirname, '..') - }); + // console.log('📝 Running openapi-typescript...'); + // execSync(`npx openapi-typescript "${openapiPath}" -o "${outputFile}"`, { + // stdio: 'inherit', + // cwd: path.join(__dirname, '..') + // }); + + // console.log(`✅ TypeScript types generated at: ${outputFile}`); - console.log(`✅ TypeScript types generated at: ${outputFile}`); - // Generate individual DTO files await generateIndividualDtoFiles(openapiPath, outputDir); - + } catch (error) { console.error('❌ Failed to generate types:', error); process.exit(1); @@ -68,33 +69,13 @@ async function generateIndividualDtoFiles(openapiPath: string, outputDir: string const schema = schemas[schemaName]; const fileName = `${schemaName}.ts`; const filePath = path.join(outputDir, fileName); - + const fileContent = generateDtoFileContent(schemaName, schema, schemas); await fs.writeFile(filePath, fileContent); console.log(` ✅ Generated ${fileName}`); } - - // Generate index file that re-exports all DTOs - let indexContent = `/** - * Auto-generated DTO type exports - * This file is generated by scripts/generate-api-types.ts - * Do not edit manually - regenerate using: npm run api:sync-types - */ -// Re-export all schema types from the generated OpenAPI types -export type { components, paths, operations } from './api'; - -// Re-export individual DTO types -`; - - for (const schemaName of schemaNames) { - indexContent += `export type { ${schemaName} } from './${schemaName}';\n`; - } - - const indexPath = path.join(outputDir, 'index.ts'); - await fs.writeFile(indexPath, indexContent); - - console.log(`✅ Generated ${schemaNames.length} individual DTO files and index at: ${outputDir}`); + console.log(`✅ Generated ${schemaNames.length} individual DTO files at: ${outputDir}`); } function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record): string {