website refactor
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
28
apps/website/lib/services/leagues/LeagueSettingsService.ts
Normal file
28
apps/website/lib/services/leagues/LeagueSettingsService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
41
apps/website/lib/services/leagues/LeagueStewardingService.ts
Normal file
41
apps/website/lib/services/leagues/LeagueStewardingService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
apps/website/lib/services/leagues/LeagueWalletService.ts
Normal file
40
apps/website/lib/services/leagues/LeagueWalletService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user