diff --git a/apps/website/hooks/auth/useCurrentSession.ts b/apps/website/hooks/auth/useCurrentSession.ts index bd6579b15..f4beee60b 100644 --- a/apps/website/hooks/auth/useCurrentSession.ts +++ b/apps/website/hooks/auth/useCurrentSession.ts @@ -19,7 +19,7 @@ export function useCurrentSession( throw new ApiError(error.message, 'SERVER_ERROR', { timestamp: new Date().toISOString() }); } const session = result.unwrap(); - return session ? new SessionViewModel(session.user) : null; + return session ? new SessionViewModel(session.user as any) : null; }, initialData: options?.initialData, ...options, diff --git a/apps/website/lib/di/container.ts b/apps/website/lib/di/container.ts index b2d727c68..f222d61fe 100644 --- a/apps/website/lib/di/container.ts +++ b/apps/website/lib/di/container.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata'; import { Container } from 'inversify'; // Module imports diff --git a/apps/website/lib/infrastructure/logging/ConsoleLogger.ts b/apps/website/lib/infrastructure/logging/ConsoleLogger.ts index 077772750..4f431ef1d 100644 --- a/apps/website/lib/infrastructure/logging/ConsoleLogger.ts +++ b/apps/website/lib/infrastructure/logging/ConsoleLogger.ts @@ -1,7 +1,9 @@ +import { injectable } from 'inversify'; import { Logger } from '../../interfaces/Logger'; type LogLevel = 'debug' | 'info' | 'warn' | 'error'; +@injectable() export class ConsoleLogger implements Logger { private readonly COLORS: Record = { debug: '#888888', diff --git a/apps/website/lib/mutations/auth/LoginMutation.ts b/apps/website/lib/mutations/auth/LoginMutation.ts index c68948906..b9a6ae104 100644 --- a/apps/website/lib/mutations/auth/LoginMutation.ts +++ b/apps/website/lib/mutations/auth/LoginMutation.ts @@ -20,7 +20,7 @@ export class LoginMutation { if (result.isErr()) { return Result.err(result.getError().message); } - return Result.ok(new SessionViewModel(result.unwrap().user)); + return Result.ok(result.unwrap()); } catch (error: any) { const errorMessage = error instanceof Error ? error.message : 'Login failed'; return Result.err(errorMessage); diff --git a/apps/website/lib/mutations/auth/SignupMutation.ts b/apps/website/lib/mutations/auth/SignupMutation.ts index f619fc7ae..3028bbd1a 100644 --- a/apps/website/lib/mutations/auth/SignupMutation.ts +++ b/apps/website/lib/mutations/auth/SignupMutation.ts @@ -20,7 +20,7 @@ export class SignupMutation { if (result.isErr()) { return Result.err(result.getError().message); } - return Result.ok(new SessionViewModel(result.unwrap().user)); + return Result.ok(result.unwrap()); } catch (error: any) { const errorMessage = error instanceof Error ? error.message : 'Signup failed'; return Result.err(errorMessage); diff --git a/apps/website/lib/page-queries/TeamLeaderboardPageQuery.ts b/apps/website/lib/page-queries/TeamLeaderboardPageQuery.ts index 2de010057..c6ff9a580 100644 --- a/apps/website/lib/page-queries/TeamLeaderboardPageQuery.ts +++ b/apps/website/lib/page-queries/TeamLeaderboardPageQuery.ts @@ -18,7 +18,7 @@ export class TeamLeaderboardPageQuery implements PageQuery ({ + const teams = result.unwrap().map((t: any) => ({ id: t.id, name: t.name, logoUrl: t.logoUrl, diff --git a/apps/website/lib/services/admin/AdminService.ts b/apps/website/lib/services/admin/AdminService.ts index 8ff7db3ff..8804f81e0 100644 --- a/apps/website/lib/services/admin/AdminService.ts +++ b/apps/website/lib/services/admin/AdminService.ts @@ -1,3 +1,4 @@ +import { injectable } from 'inversify'; import { AdminApiClient } from '@/lib/api/admin/AdminApiClient'; import type { UserDto, DashboardStats, UserListResponse } from '@/lib/types/admin'; import { Result } from '@/lib/contracts/Result'; @@ -14,6 +15,7 @@ import { getWebsiteServerEnv } from '@/lib/config/env'; * All client-side presentation logic must be handled by presenters/templates. * @server-safe */ +@injectable() export class AdminService implements Service { private apiClient: AdminApiClient; diff --git a/apps/website/lib/services/analytics/DashboardService.ts b/apps/website/lib/services/analytics/DashboardService.ts index 3f4d9e771..82c743136 100644 --- a/apps/website/lib/services/analytics/DashboardService.ts +++ b/apps/website/lib/services/analytics/DashboardService.ts @@ -1,3 +1,4 @@ +import { injectable } from 'inversify'; import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient'; import { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; import { Result } from '@/lib/contracts/Result'; @@ -13,6 +14,7 @@ import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; * Pure service that creates its own API client and returns Result types. * No business logic, only data fetching and error mapping. */ +@injectable() export class DashboardService implements Service { private apiClient: DashboardApiClient; diff --git a/apps/website/lib/services/auth/AuthService.ts b/apps/website/lib/services/auth/AuthService.ts index c86b31b7e..dd731b8df 100644 --- a/apps/website/lib/services/auth/AuthService.ts +++ b/apps/website/lib/services/auth/AuthService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { AuthApiClient } from '@/lib/api/auth/AuthApiClient'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; @@ -18,10 +19,11 @@ import { SessionViewModel } from '@/lib/view-models/SessionViewModel'; * Orchestrates authentication operations. * Returns raw API DTOs. No ViewModels or UX logic. */ +@injectable() export class AuthService implements Service { private apiClient: AuthApiClient; - constructor(apiClient?: AuthApiClient) { + constructor(@unmanaged() apiClient?: AuthApiClient) { if (apiClient) { this.apiClient = apiClient; } else { @@ -36,30 +38,30 @@ export class AuthService implements Service { } } - async login(params: LoginParamsDTO): Promise { + async login(params: LoginParamsDTO): Promise> { try { const dto = await this.apiClient.login(params); - return new SessionViewModel(dto.user); + return Result.ok(new SessionViewModel(dto.user)); } catch (error: unknown) { - throw error; + return Result.err({ type: 'unauthorized', message: (error as Error).message || 'Login failed' }); } } - async signup(params: SignupParamsDTO): Promise { + async signup(params: SignupParamsDTO): Promise> { try { const dto = await this.apiClient.signup(params); - return new SessionViewModel(dto.user); + return Result.ok(new SessionViewModel(dto.user)); } catch (error: unknown) { - throw error; + return Result.err({ type: 'validation', message: (error as Error).message || 'Signup failed' }); } } - async logout(): Promise { + async logout(): Promise> { try { await this.apiClient.logout(); return Result.ok(undefined); } catch (error: unknown) { - throw error; + return Result.err({ type: 'serverError', message: (error as Error).message || 'Logout failed' }); } } @@ -81,12 +83,12 @@ export class AuthService implements Service { } } - async getSession(): Promise { + async getSession(): Promise> { try { const dto = await this.apiClient.getSession(); - return dto; + return Result.ok(dto); } catch (error: unknown) { - throw error; + return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get session' }); } } -} +} \ 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 3ad401347..91a16d96d 100644 --- a/apps/website/lib/services/auth/SessionService.ts +++ b/apps/website/lib/services/auth/SessionService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; import { AuthService } from './AuthService'; @@ -10,25 +11,26 @@ import { SessionViewModel } from '@/lib/view-models/SessionViewModel'; * Orchestrates session-related operations. * Returns raw API DTOs. No ViewModels or UX logic. */ +@injectable() export class SessionService implements Service { private authService: AuthService; - constructor(apiClient?: any) { + constructor(@unmanaged() apiClient?: any) { this.authService = new AuthService(apiClient); } /** * Get current user session */ - async getSession(): Promise { + async getSession(): Promise> { try { const res = await this.authService.getSession(); - if (!res) return null; + if (!res) return Result.ok(null); const data = (res as any).value || res; - if (!data || !data.user) return null; - return new SessionViewModel(data.user); + if (!data || !data.user) return Result.ok(null); + return Result.ok(new SessionViewModel(data.user)); } catch (error: unknown) { - throw error; + return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get session' }); } } } diff --git a/apps/website/lib/services/drivers/DriverService.ts b/apps/website/lib/services/drivers/DriverService.ts index 2b8193c2d..73c360e45 100644 --- a/apps/website/lib/services/drivers/DriverService.ts +++ b/apps/website/lib/services/drivers/DriverService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient'; import type { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO'; import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; @@ -19,10 +20,11 @@ import { CompleteOnboardingViewModel } from '@/lib/view-models/CompleteOnboardin * Returns raw API DTOs. No ViewModels or UX logic. * All client-side presentation logic must be handled by hooks/components. */ +@injectable() export class DriverService implements Service { private readonly apiClient: DriversApiClient; - constructor(apiClient?: DriversApiClient) { + constructor(@unmanaged() apiClient?: DriversApiClient) { if (apiClient) { this.apiClient = apiClient; } else { diff --git a/apps/website/lib/services/landing/LandingService.ts b/apps/website/lib/services/landing/LandingService.ts index 5d03a8b16..ca1515a3b 100644 --- a/apps/website/lib/services/landing/LandingService.ts +++ b/apps/website/lib/services/landing/LandingService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; @@ -16,13 +17,18 @@ import { HomeDiscoveryViewModel } from '@/lib/view-models/HomeDiscoveryViewModel * Returns raw API DTOs. No ViewModels or UX logic. * All client-side presentation logic must be handled by hooks/components. */ +@injectable() export class LandingService implements Service { private racesApi: RacesApiClient; private leaguesApi: LeaguesApiClient; private teamsApi: TeamsApiClient; private authApi: AuthApiClient; - constructor(racesApi?: RacesApiClient, leaguesApi?: LeaguesApiClient, teamsApi?: TeamsApiClient) { + constructor( + @unmanaged() racesApi?: RacesApiClient, + @unmanaged() leaguesApi?: LeaguesApiClient, + @unmanaged() teamsApi?: TeamsApiClient + ) { const baseUrl = getWebsiteApiBaseUrl(); const logger = new ConsoleLogger(); const errorReporter = new EnhancedErrorReporter(logger, { diff --git a/apps/website/lib/services/leagues/LeagueMembershipService.ts b/apps/website/lib/services/leagues/LeagueMembershipService.ts index f44bcba6b..55ec77959 100644 --- a/apps/website/lib/services/leagues/LeagueMembershipService.ts +++ b/apps/website/lib/services/leagues/LeagueMembershipService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { Result } from '@/lib/contracts/Result'; @@ -15,12 +16,13 @@ export interface LeagueRosterAdminData { joinRequests: LeagueRosterJoinRequestDTO[]; } +@injectable() export class LeagueMembershipService implements Service { private apiClient: LeaguesApiClient; // eslint-disable-next-line @typescript-eslint/no-explicit-any private static cachedMemberships = new Map(); - constructor(apiClient?: LeaguesApiClient) { + constructor(@unmanaged() apiClient?: LeaguesApiClient) { if (apiClient) { this.apiClient = apiClient; } else { diff --git a/apps/website/lib/services/leagues/LeagueService.ts b/apps/website/lib/services/leagues/LeagueService.ts index 8e0d4eb0d..44cd9299f 100644 --- a/apps/website/lib/services/leagues/LeagueService.ts +++ b/apps/website/lib/services/leagues/LeagueService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient"; import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient"; import { SponsorsApiClient } from "@/lib/api/sponsors/SponsorsApiClient"; @@ -16,6 +17,10 @@ import type { LeagueScheduleRaceMutationSuccessDTO } from '@/lib/types/generated import type { LeagueSeasonSchedulePublishOutputDTO } from '@/lib/types/generated/LeagueSeasonSchedulePublishOutputDTO'; import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; +import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO'; +import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel'; +import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel'; +import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; @@ -50,13 +55,14 @@ export interface LeagueDetailData { * All client-side presentation logic must be handled by hooks/components. * @server-safe */ +@injectable() export class LeagueService implements Service { private apiClient: LeaguesApiClient; private driversApiClient?: DriversApiClient; private sponsorsApiClient?: SponsorsApiClient; private racesApiClient?: RacesApiClient; - constructor(apiClient?: LeaguesApiClient) { + constructor(@unmanaged() apiClient?: LeaguesApiClient) { if (apiClient) { this.apiClient = apiClient; } else { diff --git a/apps/website/lib/services/leagues/LeagueSettingsService.ts b/apps/website/lib/services/leagues/LeagueSettingsService.ts index 59351816f..c2a426326 100644 --- a/apps/website/lib/services/leagues/LeagueSettingsService.ts +++ b/apps/website/lib/services/leagues/LeagueSettingsService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { Result } from '@/lib/contracts/Result'; import { Service, type DomainError } from '@/lib/contracts/services/Service'; import { type LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto'; @@ -5,12 +6,13 @@ import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient'; import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel'; +@injectable() export class LeagueSettingsService implements Service { private static cachedMemberships = new Map(); constructor( - private readonly leaguesApiClient?: LeaguesApiClient, - private readonly driversApiClient?: DriversApiClient, + @unmanaged() private readonly leaguesApiClient?: LeaguesApiClient, + @unmanaged() private readonly driversApiClient?: DriversApiClient, ) {} async getLeagueSettings(leagueId: string): Promise { diff --git a/apps/website/lib/services/leagues/LeagueStewardingService.ts b/apps/website/lib/services/leagues/LeagueStewardingService.ts index 0e645f507..3f8f9ba63 100644 --- a/apps/website/lib/services/leagues/LeagueStewardingService.ts +++ b/apps/website/lib/services/leagues/LeagueStewardingService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { Result } from '@/lib/contracts/Result'; import { Service, type DomainError } from '@/lib/contracts/services/Service'; import { type StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto'; @@ -8,13 +9,14 @@ import { DriverService } from '../drivers/DriverService'; import { LeagueMembershipService } from './LeagueMembershipService'; import { LeagueStewardingViewModel } from '@/lib/view-models/LeagueStewardingViewModel'; +@injectable() export class LeagueStewardingService implements Service { constructor( - private readonly raceService?: RaceService, - private readonly protestService?: ProtestService, - private readonly penaltyService?: PenaltyService, - private readonly driverService?: DriverService, - private readonly leagueMembershipService?: LeagueMembershipService, + @unmanaged() private readonly raceService?: RaceService, + @unmanaged() private readonly protestService?: ProtestService, + @unmanaged() private readonly penaltyService?: PenaltyService, + @unmanaged() private readonly driverService?: DriverService, + @unmanaged() private readonly leagueMembershipService?: LeagueMembershipService, ) {} async getLeagueStewardingData(leagueId: string): Promise { diff --git a/apps/website/lib/services/leagues/LeagueWalletService.ts b/apps/website/lib/services/leagues/LeagueWalletService.ts index 8df320a0b..7c27ae5fb 100644 --- a/apps/website/lib/services/leagues/LeagueWalletService.ts +++ b/apps/website/lib/services/leagues/LeagueWalletService.ts @@ -1,10 +1,12 @@ +import { injectable, unmanaged } from 'inversify'; import { Result } from '@/lib/contracts/Result'; import { Service, DomainError } from '@/lib/contracts/services/Service'; import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient'; +@injectable() export class LeagueWalletService implements Service { - constructor(private readonly apiClient?: WalletsApiClient) {} + constructor(@unmanaged() private readonly apiClient?: WalletsApiClient) {} async getWalletForLeague(leagueId: string): Promise { if (this.apiClient) { diff --git a/apps/website/lib/services/penalties/PenaltyService.ts b/apps/website/lib/services/penalties/PenaltyService.ts index cc35552fa..f165a9e18 100644 --- a/apps/website/lib/services/penalties/PenaltyService.ts +++ b/apps/website/lib/services/penalties/PenaltyService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { PenaltiesApiClient } from '@/lib/api/penalties/PenaltiesApiClient'; import type { PenaltyTypesReferenceDTO } from '@/lib/types/PenaltyTypesReferenceDTO'; import { Result } from '@/lib/contracts/Result'; @@ -12,10 +13,11 @@ import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporte * Orchestrates penalty operations by coordinating API calls and view model creation. * All dependencies are injected via constructor. */ +@injectable() export class PenaltyService implements Service { private readonly apiClient: PenaltiesApiClient; - constructor(apiClient?: PenaltiesApiClient) { + constructor(@unmanaged() apiClient?: PenaltiesApiClient) { if (apiClient) { this.apiClient = apiClient; } else { diff --git a/apps/website/lib/services/policy/PolicyService.ts b/apps/website/lib/services/policy/PolicyService.ts index a16565dad..ab7c0d400 100644 --- a/apps/website/lib/services/policy/PolicyService.ts +++ b/apps/website/lib/services/policy/PolicyService.ts @@ -1,3 +1,4 @@ +import { injectable } from 'inversify'; import { PolicyApiClient, type FeatureState, type PolicySnapshotDto } from '@/lib/api/policy/PolicyApiClient'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; @@ -13,6 +14,7 @@ export interface CapabilityEvaluationResult { shouldShowComingSoon: boolean; } +@injectable() export class PolicyService implements Service { private readonly apiClient: PolicyApiClient; diff --git a/apps/website/lib/services/protests/ProtestService.ts b/apps/website/lib/services/protests/ProtestService.ts index 37dee1524..fa6651b61 100644 --- a/apps/website/lib/services/protests/ProtestService.ts +++ b/apps/website/lib/services/protests/ProtestService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient'; import type { ApplyPenaltyCommandDTO } from '@/lib/types/generated/ApplyPenaltyCommandDTO'; import type { RequestProtestDefenseCommandDTO } from '@/lib/types/generated/RequestProtestDefenseCommandDTO'; @@ -18,10 +19,11 @@ import { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel * All client-side presentation logic must be handled by hooks/components. * @server-safe */ +@injectable() export class ProtestService implements Service { private readonly apiClient: ProtestsApiClient; - constructor(apiClient?: ProtestsApiClient) { + constructor(@unmanaged() apiClient?: ProtestsApiClient) { if (apiClient) { this.apiClient = apiClient; } else { diff --git a/apps/website/lib/services/races/RaceResultsService.ts b/apps/website/lib/services/races/RaceResultsService.ts index b922fb0be..dda495c08 100644 --- a/apps/website/lib/services/races/RaceResultsService.ts +++ b/apps/website/lib/services/races/RaceResultsService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; @@ -15,10 +16,11 @@ import { ImportRaceResultsSummaryViewModel } from '@/lib/view-models/ImportRaceR * Orchestration service for race results operations. * Returns raw API DTOs. No ViewModels or UX logic. */ +@injectable() export class RaceResultsService implements Service { private apiClient: RacesApiClient; - constructor(apiClient?: RacesApiClient) { + constructor(@unmanaged() apiClient?: RacesApiClient) { if (apiClient) { this.apiClient = apiClient; } else { diff --git a/apps/website/lib/services/races/RaceService.ts b/apps/website/lib/services/races/RaceService.ts index c7eb112f5..eefb8a55a 100644 --- a/apps/website/lib/services/races/RaceService.ts +++ b/apps/website/lib/services/races/RaceService.ts @@ -1,3 +1,4 @@ +import { injectable } from 'inversify'; import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; @@ -12,6 +13,7 @@ import { ApiError } from '@/lib/api/base/ApiError'; * Returns raw API DTOs. No ViewModels or UX logic. * All client-side presentation logic must be handled by hooks/components. */ +@injectable() export class RaceService implements Service { private apiClient: RacesApiClient; diff --git a/apps/website/lib/services/races/RaceStewardingService.ts b/apps/website/lib/services/races/RaceStewardingService.ts index 6e53be6a4..9680aa688 100644 --- a/apps/website/lib/services/races/RaceStewardingService.ts +++ b/apps/website/lib/services/races/RaceStewardingService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient'; import { PenaltiesApiClient } from '@/lib/api/penalties/PenaltiesApiClient'; @@ -15,12 +16,17 @@ import { RaceStewardingViewModel } from '@/lib/view-models/RaceStewardingViewMod * Orchestration service for race stewarding operations. * Returns raw API DTOs. No ViewModels or UX logic. */ +@injectable() export class RaceStewardingService implements Service { private racesApiClient: RacesApiClient; private protestsApiClient: ProtestsApiClient; private penaltiesApiClient: PenaltiesApiClient; - constructor(racesApiClient?: RacesApiClient, protestsApiClient?: ProtestsApiClient, penaltiesApiClient?: PenaltiesApiClient) { + constructor( + @unmanaged() racesApiClient?: RacesApiClient, + @unmanaged() protestsApiClient?: ProtestsApiClient, + @unmanaged() penaltiesApiClient?: PenaltiesApiClient + ) { if (racesApiClient && protestsApiClient && penaltiesApiClient) { this.racesApiClient = racesApiClient; this.protestsApiClient = protestsApiClient; diff --git a/apps/website/lib/services/teams/TeamJoinService.ts b/apps/website/lib/services/teams/TeamJoinService.ts index 0888e3f4d..6a7964f3a 100644 --- a/apps/website/lib/services/teams/TeamJoinService.ts +++ b/apps/website/lib/services/teams/TeamJoinService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient'; import { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel'; import { Result } from '@/lib/contracts/Result'; @@ -13,10 +14,11 @@ import { isProductionEnvironment } from '@/lib/config/env'; * Returns ViewModels for team join requests. * Handles presentation logic for join request management. */ +@injectable() export class TeamJoinService implements Service { private apiClient: TeamsApiClient; - constructor(apiClient?: TeamsApiClient) { + constructor(@unmanaged() apiClient?: TeamsApiClient) { if (apiClient) { this.apiClient = apiClient; } else { @@ -31,23 +33,24 @@ export class TeamJoinService implements Service { } } - async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise { + async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise> { try { const result = await this.apiClient.getJoinRequests(teamId); const requests = (result as any).requests || result; - return requests.map((request: any) => + const viewModels = requests.map((request: any) => new TeamJoinRequestViewModel(request, currentDriverId, isOwner) ); + return Result.ok(viewModels); } catch (error: any) { - throw error; + return Result.err({ type: 'serverError', message: error.message || 'Failed to fetch join requests' }); } } - async approveJoinRequest(): Promise { - throw new Error('Not implemented: API endpoint for approving join requests'); + async approveJoinRequest(): Promise> { + return Result.err({ type: 'notImplemented', message: 'Not implemented: API endpoint for approving join requests' }); } - async rejectJoinRequest(): Promise { - throw new Error('Not implemented: API endpoint for rejecting join requests'); + async rejectJoinRequest(): Promise> { + return Result.err({ type: 'notImplemented', message: 'Not implemented: API endpoint for rejecting join requests' }); } } diff --git a/apps/website/lib/services/teams/TeamService.ts b/apps/website/lib/services/teams/TeamService.ts index d4b45888a..14fcc9b6e 100644 --- a/apps/website/lib/services/teams/TeamService.ts +++ b/apps/website/lib/services/teams/TeamService.ts @@ -1,3 +1,4 @@ +import { injectable, unmanaged } from 'inversify'; import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient'; import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; import type { CreateTeamInputDTO } from '@/lib/types/generated/CreateTeamInputDTO'; @@ -7,6 +8,7 @@ import type { UpdateTeamOutputDTO } from '@/lib/types/generated/UpdateTeamOutput import type { GetDriverTeamOutputDTO } from '@/lib/types/generated/GetDriverTeamOutputDTO'; import type { GetTeamMembershipOutputDTO } from '@/lib/types/generated/GetTeamMembershipOutputDTO'; import type { GetTeamJoinRequestsOutputDTO } from '@/lib/types/generated/GetTeamJoinRequestsOutputDTO'; +import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO'; import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel'; import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel'; import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel'; @@ -23,10 +25,11 @@ import { isProductionEnvironment } from '@/lib/config/env'; * Returns raw API DTOs. No ViewModels or UX logic. * All client-side presentation logic must be handled by hooks/components. */ +@injectable() export class TeamService implements Service { private apiClient: TeamsApiClient; - constructor(apiClient?: TeamsApiClient) { + constructor(@unmanaged() apiClient?: TeamsApiClient) { if (apiClient) { this.apiClient = apiClient; } else { @@ -51,86 +54,86 @@ export class TeamService implements Service { } async update(teamId: string, input: UpdateTeamInputDTO): Promise> { - return this.updateTeam(teamId, input); + try { + const result = await this.apiClient.update(teamId, input); + return Result.ok((result as any).value || result); + } catch (error: unknown) { + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to update team' }); + } } async create(input: CreateTeamInputDTO): Promise> { - return this.createTeam(input); + try { + const result = await this.apiClient.create(input); + return Result.ok((result as any).value || result); + } catch (error: unknown) { + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to create team' }); + } } - async getTeamDetails(teamId: string, _: string): Promise { + async getTeamDetails(teamId: string, _: string): Promise> { try { const result = await this.apiClient.getDetails(teamId); if (!result) { - return null; + return Result.err({ type: 'notFound', message: 'Team not found' }); } - const data = (result as any).value || result; - return new TeamDetailsViewModel(data, {} as any); + return Result.ok((result as any).value || result); } catch (error: unknown) { - throw error; + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team details' }); } } - async getTeamMembers(teamId: string, currentDriverId: string, ownerId: string): Promise { + async getTeamMembers(teamId: string, currentDriverId: string, ownerId: string): Promise> { try { const result = await this.apiClient.getMembers(teamId); const members = (result as any).members || result; - return members.map((member: any) => new TeamMemberViewModel(member, currentDriverId, ownerId)); + const viewModels = members.map((member: any) => new TeamMemberViewModel(member, currentDriverId, ownerId)); + return Result.ok(viewModels); } catch (error: unknown) { - throw error; + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team members' }); } } - async getTeamJoinRequests(teamId: string): Promise { + async getTeamJoinRequests(teamId: string): Promise> { try { const result = await this.apiClient.getJoinRequests(teamId); - return (result as any).value || result; + return Result.ok((result as any).value || result); } catch (error: unknown) { - throw error; + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch join requests' }); } } - async createTeam(input: CreateTeamInputDTO): Promise { - try { - const result = await this.apiClient.create(input); - return (result as any).value || result; - } catch (error: unknown) { - throw error; - } + async createTeam(input: CreateTeamInputDTO): Promise> { + return this.create(input); } - async updateTeam(teamId: string, input: UpdateTeamInputDTO): Promise { - try { - const result = await this.apiClient.update(teamId, input); - return (result as any).value || result; - } catch (error: unknown) { - throw error; - } + async updateTeam(teamId: string, input: UpdateTeamInputDTO): Promise> { + return this.update(teamId, input); } - async getDriverTeam(driverId: string): Promise { + async getDriverTeam(driverId: string): Promise> { try { const result = await this.apiClient.getDriverTeam(driverId); - if (!result) return null; + if (!result) return Result.ok(null); const data = (result as any).value || result; - if (!data.team) return null; - return { + if (!data.team) return Result.ok(null); + return Result.ok({ teamId: data.team.id, teamName: data.team.name, role: data.membership?.role, - }; + }); } catch (error: unknown) { - throw error; + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch driver team' }); } } - async getAllTeams(): Promise { + async getAllTeams(): Promise> { try { const result = await this.apiClient.getAll(); const teams = (result as any).teams || result; - return teams.map((t: any) => new TeamSummaryViewModel(t)); + return Result.ok(teams); } catch (error: unknown) { - throw error; + return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch all teams' }); } } @@ -142,4 +145,4 @@ export class TeamService implements Service { return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team membership' }); } } -} +} \ No newline at end of file diff --git a/apps/website/templates/FatalErrorTemplate.test.tsx b/apps/website/templates/FatalErrorTemplate.test.tsx deleted file mode 100644 index df2e06614..000000000 --- a/apps/website/templates/FatalErrorTemplate.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; -import { FatalErrorTemplate, type FatalErrorViewData } from './FatalErrorTemplate'; - -describe('FatalErrorTemplate', () => { - const mockError = new Error('Fatal system error'); - const mockViewData: FatalErrorViewData = { - error: mockError - }; - - const mockReset = vi.fn(); - const mockOnHome = vi.fn(); - - it('renders the error message via ErrorScreen', () => { - render(); - - expect(screen.getByText('Fatal system error')).toBeDefined(); - expect(screen.getByText('System Malfunction')).toBeDefined(); - }); - - it('calls reset when Retry Session is clicked', () => { - render(); - - const button = screen.getByText('Retry Session'); - fireEvent.click(button); - - expect(mockReset).toHaveBeenCalledTimes(1); - }); - - it('calls onHome when Return to Pits is clicked', () => { - render(); - - const button = screen.getByText('Return to Pits'); - fireEvent.click(button); - - expect(mockOnHome).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/website/templates/NotFoundTemplate.test.tsx b/apps/website/templates/NotFoundTemplate.test.tsx deleted file mode 100644 index d1de039fc..000000000 --- a/apps/website/templates/NotFoundTemplate.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; -import { NotFoundTemplate, type NotFoundViewData } from './NotFoundTemplate'; - -describe('NotFoundTemplate', () => { - const mockViewData: NotFoundViewData = { - errorCode: 'Error 404', - title: 'OFF TRACK', - message: 'The requested sector does not exist.', - actionLabel: 'Return to Pits' - }; - - const mockOnHomeClick = vi.fn(); - - it('renders the error code, title and message', () => { - render(); - - expect(screen.getByText('Error 404')).toBeDefined(); - expect(screen.getByText('OFF TRACK')).toBeDefined(); - expect(screen.getByText('The requested sector does not exist.')).toBeDefined(); - }); - - it('calls onHomeClick when the button is clicked', () => { - render(); - - const button = screen.getByText('Return to Pits'); - fireEvent.click(button); - - expect(mockOnHomeClick).toHaveBeenCalledTimes(1); - }); -});