website refactor

This commit is contained in:
2026-01-14 02:02:24 +01:00
parent 8d7c709e0c
commit 4522d41aef
291 changed files with 12763 additions and 9309 deletions

View File

@@ -1,8 +1,8 @@
import { ApiClient } from '@/lib/api';
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
@@ -25,10 +25,9 @@ export class LeagueMembershipService {
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
}
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> {
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsDTO> {
const dto = await this.getClient().getMemberships(leagueId);
const members: LeagueMemberDTO[] = dto.members ?? [];
return members.map((m) => new LeagueMemberViewModel(m, currentUserId));
return dto;
}
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
@@ -164,4 +163,4 @@ export class LeagueMembershipService {
clearLeagueMemberships(leagueId: string): void {
LeagueMembershipService.clearLeagueMemberships(leagueId);
}
}
}

View File

@@ -1,12 +1,6 @@
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
import { LeagueService } from './LeagueService';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { LeagueStandingsViewModel } from '@/lib/view-models/LeagueStandingsViewModel';
import { LeagueStatsViewModel } from '@/lib/view-models/LeagueStatsViewModel';
import { LeagueScheduleViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import { LeagueMembershipsViewModel } from '@/lib/view-models/LeagueMembershipsViewModel';
import { RemoveMemberViewModel } from '@/lib/view-models/RemoveMemberViewModel';
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
import type { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
import type { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO';
@@ -24,14 +18,15 @@ describe('LeagueService', () => {
getSchedule: vi.fn(),
getMemberships: vi.fn(),
create: vi.fn(),
removeMember: vi.fn(),
removeRosterMember: vi.fn(),
updateRosterMemberRole: vi.fn(),
} as unknown as Mocked<LeaguesApiClient>;
service = new LeagueService(mockApiClient);
});
describe('getAllLeagues', () => {
it('should call apiClient.getAllWithCapacityAndScoring and return array of LeagueSummaryViewModel', async () => {
it('should call apiClient.getAllWithCapacityAndScoring and return DTO', async () => {
const mockDto = {
totalCount: 2,
leagues: [
@@ -45,8 +40,7 @@ describe('LeagueService', () => {
const result = await service.getAllLeagues();
expect(mockApiClient.getAllWithCapacityAndScoring).toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result.map((l) => l.id)).toEqual(['league-1', 'league-2']);
expect(result).toEqual(mockDto);
});
it('should handle empty leagues array', async () => {
@@ -56,7 +50,7 @@ describe('LeagueService', () => {
const result = await service.getAllLeagues();
expect(result).toHaveLength(0);
expect(result).toEqual(mockDto);
});
it('should throw error when apiClient.getAllWithCapacityAndScoring fails', async () => {
@@ -68,32 +62,29 @@ describe('LeagueService', () => {
});
describe('getLeagueStandings', () => {
it('should call apiClient.getStandings and return LeagueStandingsViewModel', async () => {
it('should call apiClient.getStandings and return DTO', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = { standings: [] } as any;
mockApiClient.getStandings.mockResolvedValue({ standings: [] } as any);
mockApiClient.getMemberships.mockResolvedValue({ members: [] } as any);
mockApiClient.getStandings.mockResolvedValue(mockDto);
const result = await service.getLeagueStandings(leagueId, currentUserId);
const result = await service.getLeagueStandings(leagueId);
expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueStandingsViewModel);
expect(result).toEqual(mockDto);
});
it('should throw error when apiClient.getStandings fails', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getStandings.mockRejectedValue(error);
await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('API call failed');
await expect(service.getLeagueStandings(leagueId)).rejects.toThrow('API call failed');
});
});
describe('getLeagueStats', () => {
it('should call apiClient.getTotal and return LeagueStatsViewModel', async () => {
it('should call apiClient.getTotal and return DTO', async () => {
const mockDto = { totalLeagues: 42 };
mockApiClient.getTotal.mockResolvedValue(mockDto);
@@ -101,8 +92,7 @@ describe('LeagueService', () => {
const result = await service.getLeagueStats();
expect(mockApiClient.getTotal).toHaveBeenCalled();
expect(result).toBeInstanceOf(LeagueStatsViewModel);
expect(result.totalLeagues).toBe(42);
expect(result).toEqual(mockDto);
});
it('should throw error when apiClient.getTotal fails', async () => {
@@ -114,7 +104,7 @@ describe('LeagueService', () => {
});
describe('getLeagueSchedule', () => {
it('should call apiClient.getSchedule and return LeagueScheduleViewModel with Date parsing', async () => {
it('should call apiClient.getSchedule and return DTO', async () => {
const leagueId = 'league-123';
const mockDto = {
races: [
@@ -128,8 +118,7 @@ describe('LeagueService', () => {
const result = await service.getLeagueSchedule(leagueId);
expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueScheduleViewModel);
expect(result.raceCount).toBe(2);
expect(result).toEqual(mockDto);
});
it('should handle empty races array', async () => {
@@ -140,13 +129,11 @@ describe('LeagueService', () => {
const result = await service.getLeagueSchedule(leagueId);
expect(result.races).toEqual([]);
expect(result.hasRaces).toBe(false);
expect(result).toEqual(mockDto);
});
it('should throw error when apiClient.getSchedule fails', async () => {
const leagueId = 'league-123';
const error = new Error('API call failed');
mockApiClient.getSchedule.mockRejectedValue(error);
@@ -155,49 +142,37 @@ describe('LeagueService', () => {
});
describe('getLeagueMemberships', () => {
it('should call apiClient.getMemberships and return LeagueMembershipsViewModel', async () => {
it('should call apiClient.getMemberships and return DTO', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
members: [{ driverId: 'driver-1' }, { driverId: 'driver-2' }],
} as any;
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
const result = await service.getLeagueMemberships(leagueId);
expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueMembershipsViewModel);
expect(result.memberships).toHaveLength(2);
const first = result.memberships[0]!;
expect(first).toBeInstanceOf(LeagueMemberViewModel);
expect(first.driverId).toBe('driver-1');
expect(result).toEqual(mockDto);
});
it('should handle empty memberships array', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = { members: [] } as any;
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
const result = await service.getLeagueMemberships(leagueId);
expect(result.memberships).toHaveLength(0);
expect(result.hasMembers).toBe(false);
expect(result).toEqual(mockDto);
});
it('should throw error when apiClient.getMemberships fails', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getMemberships.mockRejectedValue(error);
await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('API call failed');
await expect(service.getLeagueMemberships(leagueId)).rejects.toThrow('API call failed');
});
});
@@ -238,46 +213,41 @@ describe('LeagueService', () => {
});
describe('removeMember', () => {
it('should call apiClient.removeMember and return RemoveMemberViewModel', async () => {
it('should call apiClient.removeRosterMember and return DTO', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockDto: RemoveLeagueMemberOutputDTO = { success: true };
mockApiClient.removeMember.mockResolvedValue(mockDto);
mockApiClient.removeRosterMember.mockResolvedValue(mockDto);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
const result = await service.removeMember(leagueId, targetDriverId);
expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
expect(result).toBeInstanceOf(RemoveMemberViewModel);
expect(result.success).toBe(true);
expect(mockApiClient.removeRosterMember).toHaveBeenCalledWith(leagueId, targetDriverId);
expect(result).toEqual(mockDto);
});
it('should handle unsuccessful removal', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockDto: RemoveLeagueMemberOutputDTO = { success: false };
mockApiClient.removeMember.mockResolvedValue(mockDto);
mockApiClient.removeRosterMember.mockResolvedValue(mockDto);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
const result = await service.removeMember(leagueId, targetDriverId);
expect(result.success).toBe(false);
expect(result.successMessage).toBe('Failed to remove member.');
});
it('should throw error when apiClient.removeMember fails', async () => {
it('should throw error when apiClient.removeRosterMember fails', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const error = new Error('API call failed');
mockApiClient.removeMember.mockRejectedValue(error);
mockApiClient.removeRosterMember.mockRejectedValue(error);
await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
await expect(service.removeMember(leagueId, targetDriverId)).rejects.toThrow('API call failed');
});
});
});
});

View File

@@ -0,0 +1,28 @@
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
/**
* League Settings Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class LeagueSettingsService {
constructor(
private readonly leagueApiClient: LeaguesApiClient,
private readonly driverApiClient: DriversApiClient
) {}
async getLeagueSettings(leagueId: string): Promise<any> {
// This would typically call multiple endpoints to gather all settings data
// For now, return a basic structure
return {
league: await this.leagueApiClient.getAllWithCapacityAndScoring(),
config: { /* config data */ }
};
}
async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<{ success: boolean }> {
return this.leagueApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId);
}
}

View File

@@ -0,0 +1,41 @@
import { RaceService } from '@/lib/services/races/RaceService';
import { ProtestService } from '@/lib/services/protests/ProtestService';
import { PenaltyService } from '@/lib/services/penalties/PenaltyService';
import { DriverService } from '@/lib/services/drivers/DriverService';
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
/**
* League Stewarding Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class LeagueStewardingService {
constructor(
private readonly raceService: RaceService,
private readonly protestService: ProtestService,
private readonly penaltyService: PenaltyService,
private readonly driverService: DriverService,
private readonly membershipService: LeagueMembershipService
) {}
async getLeagueProtests(leagueId: string): Promise<any> {
return this.protestService.getLeagueProtests(leagueId);
}
async getProtestById(leagueId: string, protestId: string): Promise<any> {
return this.protestService.getProtestById(leagueId, protestId);
}
async applyPenalty(input: any): Promise<void> {
return this.protestService.applyPenalty(input);
}
async requestDefense(input: any): Promise<void> {
return this.protestService.requestDefense(input);
}
async reviewProtest(input: any): Promise<void> {
return this.protestService.reviewProtest(input);
}
}

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
import { LeagueWalletService } from './LeagueWalletService';
import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient';
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
describe('LeagueWalletService', () => {
let mockApiClient: Mocked<WalletsApiClient>;
@@ -17,7 +16,7 @@ describe('LeagueWalletService', () => {
});
describe('getWalletForLeague', () => {
it('should call apiClient.getLeagueWallet and return LeagueWalletViewModel', async () => {
it('should call apiClient.getLeagueWallet and return DTO', async () => {
const leagueId = 'league-123';
const mockDto = {
balance: 1000,
@@ -47,11 +46,7 @@ describe('LeagueWalletService', () => {
const result = await service.getWalletForLeague(leagueId);
expect(mockApiClient.getLeagueWallet).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueWalletViewModel);
expect(result.balance).toBe(1000);
expect(result.currency).toBe('USD');
expect(result.transactions).toHaveLength(1);
expect(result.formattedBalance).toBe('$1000.00');
expect(result).toEqual(mockDto);
});
it('should throw error when apiClient.getLeagueWallet fails', async () => {
@@ -98,4 +93,4 @@ describe('LeagueWalletService', () => {
await expect(service.withdraw(leagueId, amount, currency, seasonId, destinationAccount)).rejects.toThrow('Withdrawal failed');
});
});
});
});

View File

@@ -0,0 +1,40 @@
import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient';
import type { LeagueWalletDTO, WithdrawRequestDTO, WithdrawResponseDTO } from '@/lib/api/wallets/WalletsApiClient';
/**
* LeagueWalletService - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class LeagueWalletService {
constructor(
private readonly apiClient: WalletsApiClient
) {}
/**
* Get wallet for a league
*/
async getWalletForLeague(leagueId: string): Promise<LeagueWalletDTO> {
return this.apiClient.getLeagueWallet(leagueId);
}
/**
* Withdraw from league wallet
*/
async withdraw(
leagueId: string,
amount: number,
currency: string,
seasonId: string,
destinationAccount: string
): Promise<WithdrawResponseDTO> {
const payload: WithdrawRequestDTO = {
amount,
currency,
seasonId,
destinationAccount,
};
return this.apiClient.withdrawFromLeagueWallet(leagueId, payload);
}
}