import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient"; import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient"; import { RacesApiClient } from "@/lib/api/races/RacesApiClient"; import { ApiError } from '@/lib/api/base/ApiError'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { isProductionEnvironment } from '@/lib/config/env'; import { Result } from '@/lib/contracts/Result'; import { DomainError, Service } from '@/lib/contracts/services/Service'; import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/AllLeaguesWithCapacityAndScoringDTO'; import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO"; import type { CreateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/CreateLeagueScheduleRaceInputDTO'; import type { CreateLeagueScheduleRaceOutputDTO } from '@/lib/types/generated/CreateLeagueScheduleRaceOutputDTO'; import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO'; import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO'; import type { LeagueRosterJoinRequestDTO } from "@/lib/types/generated/LeagueRosterJoinRequestDTO"; import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO'; import type { LeagueScheduleRaceMutationSuccessDTO } from '@/lib/types/generated/LeagueScheduleRaceMutationSuccessDTO'; import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO'; import type { LeagueSeasonSchedulePublishOutputDTO } from '@/lib/types/generated/LeagueSeasonSchedulePublishOutputDTO'; import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO'; import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO'; import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; import type { UpdateLeagueScheduleRaceInputDTO } from '@/lib/types/generated/UpdateLeagueScheduleRaceInputDTO'; import type { MembershipRole } from "@/lib/types/MembershipRole"; import { injectable, unmanaged } from 'inversify'; // TODO these data interfaces violate our architecture, see VIEW_DATA export interface LeagueScheduleAdminData { leagueId: string; seasonId: string; seasons: LeagueSeasonSummaryDTO[]; schedule: LeagueScheduleDTO; } export interface LeagueRosterAdminData { leagueId: string; members: LeagueRosterMemberDTO[]; joinRequests: LeagueRosterJoinRequestDTO[]; } export interface LeagueDetailData { league: LeagueWithCapacityAndScoringDTO; owner: GetDriverOutputDTO | null; scoringConfig: LeagueScoringConfigDTO | null; memberships: LeagueMembershipsDTO; races: RaceDTO[]; sponsors: any[]; } /** * League Service - DTO Only * * Returns Result. No ViewModels or UX logic. * All client-side presentation logic must be handled by hooks/components. * @server-safe */ @injectable() export class LeagueService implements Service { private readonly baseUrl: string; private apiClient: LeaguesApiClient; private driversApiClient: DriversApiClient; private racesApiClient: RacesApiClient; constructor(@unmanaged() apiClient?: LeaguesApiClient) { const baseUrl = getWebsiteApiBaseUrl(); this.baseUrl = baseUrl; const logger = new ConsoleLogger(); const errorReporter = new EnhancedErrorReporter(logger, { showUserNotifications: false, logToConsole: true, reportToExternal: isProductionEnvironment(), }); if (apiClient) { this.apiClient = apiClient; } else { this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger); } this.driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger); this.racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger); } async getLeagueStandings(leagueId: string): Promise { try { const data = await this.apiClient.getStandings(leagueId); return (data as any).value || data; } catch (error: unknown) { throw error; } } async getLeagueStats(): Promise { try { const data = await this.apiClient.getTotal(); return (data as any).value || data; } catch (error: unknown) { throw error; } } async getLeagueSchedule(leagueId: string): Promise { try { const data = await this.apiClient.getSchedule(leagueId); return (data as any).value || data; } catch (error: unknown) { throw error; } } async getLeagueMemberships(leagueId: string): Promise { try { const data = await this.apiClient.getMemberships(leagueId); return (data as any).value || data; } catch (error: unknown) { throw error; } } async createLeague(input: CreateLeagueInputDTO): Promise { try { const data = await this.apiClient.create(input); return (data as any).value || data; } catch (error: unknown) { throw error; } } async removeMember(leagueId: string, targetDriverId: string): Promise { try { const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId); return { success: dto.success }; } catch (error: unknown) { throw error; } } async getAllLeagues(): Promise> { try { const dto = await this.apiClient.getAllWithCapacityAndScoring(); return Result.ok(dto); } catch (error: unknown) { // Map API error types to domain error types if (error instanceof ApiError) { const errorType = error.type; switch (errorType) { case 'NOT_FOUND': return Result.err({ type: 'notFound', message: error.message }); case 'AUTH_ERROR': return Result.err({ type: 'unauthorized', message: error.message }); case 'SERVER_ERROR': return Result.err({ type: 'serverError', message: error.message }); default: return Result.err({ type: 'serverError', message: error.message || 'Failed to fetch leagues' }); } } return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch leagues' }); } } async getLeagueDetailData(leagueId: string): Promise> { try { const [apiDto, memberships, racesPageData] = await Promise.all([ this.apiClient.getAllWithCapacityAndScoring(), this.apiClient.getMemberships(leagueId), this.racesApiClient.getPageData(leagueId), ]); if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { const membershipCount = Array.isArray(memberships?.members) ? memberships.members.length : 0; const racesCount = Array.isArray(racesPageData?.races) ? racesPageData.races.length : 0; const race0 = racesCount > 0 ? racesPageData.races[0] : null; console.info( '[LeagueService.getLeagueDetailData] baseUrl=%s leagueId=%s memberships=%d races=%d race0=%o apiDto=%o', this.baseUrl, leagueId, membershipCount, racesCount, race0, apiDto ); } if (!apiDto || !apiDto.leagues) { return Result.err({ type: 'notFound', message: 'Leagues not found' }); } const leagues = Array.isArray(apiDto.leagues) ? apiDto.leagues : []; const league = leagues.find(l => l.id === leagueId); if (!league) { return Result.err({ type: 'notFound', message: 'League not found' }); } // Fetch owner if ownerId exists let owner: GetDriverOutputDTO | null = null; if (league.ownerId) { owner = await this.driversApiClient.getDriver(league.ownerId); } // Fetch scoring config if available let scoringConfig: LeagueScoringConfigDTO | null = null; try { const config = await this.apiClient.getLeagueConfig(leagueId); if (config.form?.scoring) { // Map form scoring to LeagueScoringConfigDTO if possible, or just use partial scoringConfig = { leagueId, seasonId: '', // Not available in this context gameId: '', gameName: '', scoringPresetId: (config.form.scoring as any).presetId, dropPolicySummary: '', championships: [], }; } } catch (e) { console.warn('Failed to fetch league scoring config', e); } const races: RaceDTO[] = (racesPageData?.races || []).map((r) => ({ id: r.id, name: `${r.track} - ${r.car}`, date: r.scheduledAt, leagueName: r.leagueName, status: r.status, strengthOfField: r.strengthOfField, })) as unknown as RaceDTO[]; return Result.ok({ league, owner, scoringConfig, memberships, races, sponsors: [], // Sponsors integration can be added here }); } catch (error: unknown) { console.error('LeagueService.getLeagueDetailData failed:', error); // Map API error types to domain error types if (error instanceof ApiError) { const errorType = error.type; switch (errorType) { case 'NOT_FOUND': return Result.err({ type: 'notFound', message: error.message }); case 'AUTH_ERROR': return Result.err({ type: 'unauthorized', message: error.message }); case 'SERVER_ERROR': return Result.err({ type: 'serverError', message: error.message }); default: return Result.err({ type: 'serverError', message: error.message || 'Failed to fetch league detail' }); } } return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league detail' }); } } async getScheduleAdminData(leagueId: string, seasonId?: string): Promise> { try { const seasons = await this.apiClient.getSeasons(leagueId); if (!seasons || seasons.length === 0) { return Result.err({ type: 'notFound', message: 'No seasons found for league' }); } const targetSeasonId = seasonId || (seasons.find(s => s.status === 'active')?.seasonId || seasons[0].seasonId); const schedule = await this.apiClient.getSchedule(leagueId, targetSeasonId); return Result.ok({ leagueId, seasonId: targetSeasonId, seasons, schedule, }); } catch (error: unknown) { console.error('LeagueService.getScheduleAdminData failed:', error); return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch schedule admin data' }); } } async getRosterAdminData(leagueId: string): Promise> { try { const [members, joinRequests] = await Promise.all([ this.apiClient.getAdminRosterMembers(leagueId), this.apiClient.getAdminRosterJoinRequests(leagueId), ]); return Result.ok({ leagueId, members, joinRequests, }); } catch (error: unknown) { console.error('LeagueService.getRosterAdminData failed:', error); return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' }); } } async getLeagueSeasons(leagueId: string): Promise> { try { const data = await this.apiClient.getSeasons(leagueId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league seasons' }); } } async getLeagueSeasonSummaries(leagueId: string): Promise> { return this.getLeagueSeasons(leagueId); } async getAdminSchedule(leagueId: string, seasonId: string): Promise> { try { const data = await this.apiClient.getSchedule(leagueId, seasonId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch admin schedule' }); } } async publishAdminSchedule(leagueId: string, seasonId: string): Promise> { try { const data = await this.apiClient.publishSeasonSchedule(leagueId, seasonId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to publish schedule' }); } } async unpublishAdminSchedule(leagueId: string, seasonId: string): Promise> { try { const data = await this.apiClient.unpublishSeasonSchedule(leagueId, seasonId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to unpublish schedule' }); } } async createAdminScheduleRace( leagueId: string, seasonId: string, input: { track: string; car: string; scheduledAtIso: string }, ): Promise> { try { const payload: CreateLeagueScheduleRaceInputDTO = { ...input, example: '' }; const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, payload); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' }); } } async updateAdminScheduleRace( leagueId: string, seasonId: string, raceId: string, input: Partial<{ track: string; car: string; scheduledAtIso: string }>, ): Promise> { try { const payload: UpdateLeagueScheduleRaceInputDTO = { ...input, example: '' }; const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, payload); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' }); } } async deleteAdminScheduleRace(leagueId: string, seasonId: string, raceId: string): Promise> { try { const data = await this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to delete race' }); } } // TODO wtf what a stupid method?? async getLeagueScheduleDto(leagueId: string, seasonId: string): Promise> { return this.getAdminSchedule(leagueId, seasonId); } async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise> { return this.publishAdminSchedule(leagueId, seasonId); } async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise> { return this.unpublishAdminSchedule(leagueId, seasonId); } async createLeagueSeasonScheduleRace( leagueId: string, seasonId: string, input: CreateLeagueScheduleRaceInputDTO, ): Promise> { try { const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, input); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' }); } } async updateLeagueSeasonScheduleRace( leagueId: string, seasonId: string, raceId: string, input: UpdateLeagueScheduleRaceInputDTO, ): Promise> { try { const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, input); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' }); } } async deleteLeagueSeasonScheduleRace( leagueId: string, seasonId: string, raceId: string, ): Promise> { return this.deleteAdminScheduleRace(leagueId, seasonId, raceId); } async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise> { try { const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole); return Result.ok({ success: dto.success }); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update member role' }); } } async getAdminRosterMembers(leagueId: string): Promise> { try { const data = await this.apiClient.getAdminRosterMembers(leagueId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster members' }); } } async getAdminRosterJoinRequests(leagueId: string): Promise> { try { const data = await this.apiClient.getAdminRosterJoinRequests(leagueId); return Result.ok(data); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch join requests' }); } } async approveJoinRequest(leagueId: string, joinRequestId: string): Promise> { try { const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId); return Result.ok({ success: dto.success }); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' }); } } async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise> { try { const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId); return Result.ok({ success: dto.success }); } catch (error: unknown) { return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' }); } } async getLeagueDetail(): Promise> { return Result.err({ type: 'notImplemented', message: 'League detail endpoint not implemented' }); } async getLeagueDetailPageData(): Promise> { return Result.err({ type: 'notImplemented', message: 'League detail page data endpoint not implemented' }); } async getScoringPresets(): Promise> { return Result.err({ type: 'notImplemented', message: 'Scoring presets endpoint not implemented' }); } }