Some checks failed
CI / lint-typecheck (pull_request) Failing after 4m50s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
485 lines
19 KiB
TypeScript
485 lines
19 KiB
TypeScript
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<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' && 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<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' });
|
|
}
|
|
}
|