Files
gridpilot.gg/apps/website/lib/services/leagues/LeagueService.ts
2026-01-24 12:47:49 +01:00

484 lines
19 KiB
TypeScript

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 { ApiError } from '@/lib/gateways/api/base/ApiError';
import { DriversApiClient } from "@/lib/gateways/api/drivers/DriversApiClient";
import { LeaguesApiClient } from "@/lib/gateways/api/leagues/LeaguesApiClient";
import { RacesApiClient } from "@/lib/gateways/api/races/RacesApiClient";
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<ApiDto, DomainError>. 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<any> {
try {
const data = await this.apiClient.getStandings(leagueId);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async getLeagueStats(): Promise<any> {
try {
const data = await this.apiClient.getTotal();
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async getLeagueSchedule(leagueId: string): Promise<any> {
try {
const data = await this.apiClient.getSchedule(leagueId);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async getLeagueMemberships(leagueId: string): Promise<any> {
try {
const data = await this.apiClient.getMemberships(leagueId);
return (data as any).value || data;
} catch (error: unknown) {
throw error;
}
}
async createLeague(input: CreateLeagueInputDTO): Promise<any> {
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<any> {
try {
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
return { success: dto.success };
} catch (error: unknown) {
throw error;
}
}
async getAllLeagues(): Promise<Result<AllLeaguesWithCapacityAndScoringDTO, DomainError>> {
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<Result<LeagueDetailData, DomainError>> {
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') {
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',
this.baseUrl,
leagueId,
membershipCount,
racesCount,
race0,
);
}
if (!apiDto || !apiDto.leagues) {
return Result.err({ type: 'notFound', message: 'Leagues not found' });
}
const league = apiDto.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<Result<LeagueScheduleAdminData, DomainError>> {
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<Result<LeagueRosterAdminData, DomainError>> {
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<Result<LeagueSeasonSummaryDTO[], DomainError>> {
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<Result<LeagueSeasonSummaryDTO[], DomainError>> {
return this.getLeagueSeasons(leagueId);
}
async getAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
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<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
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<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
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<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
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<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
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<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
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<Result<LeagueScheduleDTO, DomainError>> {
return this.getAdminSchedule(leagueId, seasonId);
}
async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
return this.publishAdminSchedule(leagueId, seasonId);
}
async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
return this.unpublishAdminSchedule(leagueId, seasonId);
}
async createLeagueSeasonScheduleRace(
leagueId: string,
seasonId: string,
input: CreateLeagueScheduleRaceInputDTO,
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
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<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
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<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
return this.deleteAdminScheduleRace(leagueId, seasonId, raceId);
}
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<Result<{ success: boolean }, DomainError>> {
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<Result<LeagueRosterMemberDTO[], DomainError>> {
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<Result<LeagueRosterJoinRequestDTO[], DomainError>> {
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<Result<{ success: boolean }, DomainError>> {
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<Result<{ success: boolean }, DomainError>> {
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<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'League detail endpoint not implemented' });
}
async getLeagueDetailPageData(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'League detail page data endpoint not implemented' });
}
async getScoringPresets(): Promise<Result<never, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'Scoring presets endpoint not implemented' });
}
}