website refactor

This commit is contained in:
2026-01-14 10:51:05 +01:00
parent 4522d41aef
commit 0d89ad027e
291 changed files with 6887 additions and 3685 deletions

View File

@@ -3,7 +3,9 @@ import type { UserDto, DashboardStats, UserListResponse, ListUsersQuery } from '
import { Result } from '@/lib/contracts/Result';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { DomainError } from '@/lib/contracts/services/Service';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
/**
* Admin Service - DTO Only
@@ -12,16 +14,17 @@ import { DomainError } from '@/lib/contracts/services/Service';
* All client-side presentation logic must be handled by presenters/templates.
* @server-safe
*/
export class AdminService {
export class AdminService implements Service {
private apiClient: AdminApiClient;
constructor() {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const { NODE_ENV } = getWebsiteServerEnv();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
reportToExternal: NODE_ENV === 'production',
});
this.apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
}

View File

@@ -1,9 +1,11 @@
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
import { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ApiError } from '@/lib/api/base/ApiError';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
/**
* DashboardService
@@ -11,11 +13,11 @@ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
* Pure service that creates its own API client and returns Result types.
* No business logic, only data fetching and error mapping.
*/
export class DashboardService {
export class DashboardService implements Service {
private apiClient: DashboardApiClient;
constructor() {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
this.apiClient = new DashboardApiClient(baseUrl, errorReporter, logger);
@@ -26,18 +28,26 @@ export class DashboardService {
const dto = await this.apiClient.getDashboardOverview();
return Result.ok(dto);
} catch (error) {
console.error('DashboardService.getDashboardOverview failed:', error);
// Convert ApiError to DomainError
if (error instanceof ApiError) {
switch (error.type) {
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 });
case 'NETWORK_ERROR':
case 'TIMEOUT_ERROR':
return Result.err({ type: 'networkError', message: error.message });
default:
return Result.err({ type: 'unknown', message: error.message });
}
}
// Handle non-ApiError cases
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'unauthorized', message: error.message });
}
if (error.message.includes('404')) {
return Result.err({ type: 'notFound', message: error.message });
}
if (error.message.includes('5') || error.message.includes('server')) {
return Result.err({ type: 'serverError', message: error.message });
}
return Result.err({ type: 'unknown', message: error.message });
}
return Result.err({ type: 'unknown', message: 'Dashboard fetch failed' });

View File

@@ -0,0 +1,10 @@
/**
* Auth Page Parameters
*
* Input parameters for auth page processing.
*/
export interface AuthPageParams {
returnTo?: string | null;
token?: string | null;
}

View File

@@ -10,11 +10,7 @@ import { LoginPageDTO } from './types/LoginPageDTO';
import { ForgotPasswordPageDTO } from './types/ForgotPasswordPageDTO';
import { ResetPasswordPageDTO } from './types/ResetPasswordPageDTO';
import { SignupPageDTO } from './types/SignupPageDTO';
export interface AuthPageParams {
returnTo?: string | null;
token?: string | null;
}
import { AuthPageParams } from './AuthPageParams';
export class AuthPageService {
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, string>> {

View File

@@ -0,0 +1,58 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { isProductionEnvironment } from '@/lib/config/env';
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
type DriverProfilePageServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
/**
* DriverProfilePageService
*
* Service for the driver profile page.
* Returns raw API DTOs. No ViewModels or UX logic.
*/
export class DriverProfilePageService implements Service {
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfilePageServiceError>> {
const logger = new ConsoleLogger();
try {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const dto = await apiClient.getDriverProfile(driverId);
if (!dto.currentDriver) {
return Result.err('notFound');
}
return Result.ok(dto);
} catch (error) {
const errorAny = error as { statusCode?: number; message?: string };
if (errorAny.statusCode === 401 || errorAny.message?.toLowerCase().includes('unauthorized')) {
return Result.err('unauthorized');
}
if (errorAny.statusCode === 404 || errorAny.message?.toLowerCase().includes('not found')) {
return Result.err('notFound');
}
logger.error('DriverProfilePageService failed', error instanceof Error ? error : undefined, { error: errorAny });
if (errorAny.statusCode && errorAny.statusCode >= 500) {
return Result.err('serverError');
}
return Result.err('unknown');
}
}
}

View File

@@ -0,0 +1,58 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { isProductionEnvironment } from '@/lib/config/env';
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
type DriversPageServiceError = 'notFound' | 'serverError' | 'unknown';
/**
* DriversPageService
*
* Service for the drivers listing page.
* Returns raw API DTOs. No ViewModels or UX logic.
*/
export class DriversPageService implements Service {
async getLeaderboard(): Promise<Result<DriversLeaderboardDTO, DriversPageServiceError>> {
const logger = new ConsoleLogger();
try {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const result = await apiClient.getLeaderboard();
if (!result || !result.drivers) {
return Result.err('notFound');
}
// Transform to the expected DTO format
const dto: DriversLeaderboardDTO = {
drivers: result.drivers,
totalRaces: result.drivers.reduce((sum, driver) => sum + driver.racesCompleted, 0),
totalWins: result.drivers.reduce((sum, driver) => sum + driver.wins, 0),
activeCount: result.drivers.filter(driver => driver.isActive).length,
};
return Result.ok(dto);
} catch (error) {
const errorAny = error as { statusCode?: number; message?: string };
logger.error('DriversPageService failed', error instanceof Error ? error : undefined, { error: errorAny });
if (errorAny.statusCode && errorAny.statusCode >= 500) {
return Result.err('serverError');
}
return Result.err('unknown');
}
}
}

View File

@@ -0,0 +1,17 @@
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
/**
* Livery Service
*
* Currently not implemented - returns NotImplemented errors for all endpoints.
*/
export class LiveryService implements Service {
async getLiveries(): Promise<Result<void, 'NOT_IMPLEMENTED'>> {
return Result.err('NOT_IMPLEMENTED');
}
async uploadLivery(): Promise<Result<void, 'NOT_IMPLEMENTED'>> {
return Result.err('NOT_IMPLEMENTED');
}
}

View File

@@ -0,0 +1,17 @@
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
/**
* Settings Service
*
* Currently not implemented - returns NotImplemented errors for all endpoints.
*/
export class SettingsService implements Service {
async getSettings(): Promise<Result<void, 'NOT_IMPLEMENTED'>> {
return Result.err('NOT_IMPLEMENTED');
}
async updateSettings(): Promise<Result<void, 'NOT_IMPLEMENTED'>> {
return Result.err('NOT_IMPLEMENTED');
}
}

View File

@@ -1,4 +1,5 @@
import { redirect } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
// API Clients
@@ -44,7 +45,7 @@ export async function getHomeData(): Promise<HomeDataDTO> {
// Check session and redirect if logged in
const session = await sessionService.getSession();
if (session) {
redirect('/dashboard');
redirect(routes.protected.dashboard);
}
// Get feature flags

View File

@@ -1,3 +1,6 @@
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
/**
* Landing Service - DTO Only
*
@@ -10,4 +13,8 @@ export class LandingService {
async getLandingData(): Promise<any> {
return { featuredLeagues: [], stats: {} };
}
async signup(email: string): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'Email signup endpoint' });
}
}

View File

@@ -0,0 +1,59 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { Result } from '@/lib/contracts/Result';
import { Service, DomainError } from '@/lib/contracts/services/Service';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
export interface DriverRankingsData {
drivers: DriverLeaderboardItemDTO[];
}
export class DriverRankingsService implements Service {
async getDriverRankings(): Promise<Result<DriverRankingsData, DomainError>> {
try {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const result = await apiClient.getLeaderboard();
if (!result || !result.drivers) {
return Result.err({ type: 'notFound', message: 'No driver rankings available' });
}
const data: DriverRankingsData = {
drivers: result.drivers,
};
return Result.ok(data);
} catch (error) {
// Convert ApiError to DomainError
if (error instanceof ApiError) {
switch (error.type) {
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 });
case 'NETWORK_ERROR':
case 'TIMEOUT_ERROR':
return Result.err({ type: 'networkError', message: error.message });
default:
return Result.err({ type: 'unknown', message: error.message });
}
}
// Handle non-ApiError cases
if (error instanceof Error) {
return Result.err({ type: 'unknown', message: error.message });
}
return Result.err({ type: 'unknown', message: 'Driver rankings fetch failed' });
}
}
}

View File

@@ -0,0 +1,77 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { Result } from '@/lib/contracts/Result';
import { Service, DomainError } from '@/lib/contracts/services/Service';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
export interface LeaderboardsData {
drivers: { drivers: DriverLeaderboardItemDTO[] };
teams: { teams: TeamListItemDTO[] };
}
export class LeaderboardsService implements Service {
async getLeaderboards(): Promise<Result<LeaderboardsData, DomainError>> {
try {
const baseUrl = getWebsiteApiBaseUrl();
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
const [driverResult, teamResult] = await Promise.all([
driversApiClient.getLeaderboard(),
teamsApiClient.getAll(),
]);
if (!driverResult && !teamResult) {
return Result.err({ type: 'notFound', message: 'No leaderboard data available' });
}
// Check if team ranking is needed but not provided by API
// TeamListItemDTO does not have a rank field, so we cannot provide ranked team data
if (teamResult && teamResult.teams.length > 0) {
const hasRankField = teamResult.teams.some(team => 'rank' in team);
if (!hasRankField) {
return Result.err({ type: 'serverError', message: 'Team ranking not implemented' });
}
}
const data: LeaderboardsData = {
drivers: driverResult || { drivers: [] },
teams: teamResult || { teams: [] },
};
return Result.ok(data);
} catch (error) {
// Convert ApiError to DomainError
if (error instanceof ApiError) {
switch (error.type) {
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 });
case 'NETWORK_ERROR':
case 'TIMEOUT_ERROR':
return Result.err({ type: 'networkError', message: error.message });
default:
return Result.err({ type: 'unknown', message: error.message });
}
}
// Handle non-ApiError cases
if (error instanceof Error) {
return Result.err({ type: 'unknown', message: error.message });
}
return Result.err({ type: 'unknown', message: 'Leaderboards fetch failed' });
}
}
}

View File

@@ -1,5 +1,7 @@
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
/**
* League Settings Service - DTO Only
@@ -25,4 +27,20 @@ export class LeagueSettingsService {
async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<{ success: boolean }> {
return this.leagueApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId);
}
async selectScoringPreset(leagueId: string, preset: string): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'selectScoringPreset' });
}
async toggleCustomScoring(leagueId: string, enabled: boolean): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'toggleCustomScoring' });
}
getPresetEmoji(preset: string): string {
return '🏆';
}
getPresetDescription(preset: string): string {
return `Scoring preset: ${preset}`;
}
}

View File

@@ -0,0 +1,97 @@
import { ApiClient } from '@/lib/api';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
type ProfileLeaguesServiceError = 'notFound' | 'redirect' | 'unknown';
interface ProfileLeaguesPageDto {
ownedLeagues: Array<{
leagueId: string;
name: string;
description: string;
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
}>;
memberLeagues: Array<{
leagueId: string;
name: string;
description: string;
membershipRole: 'owner' | 'admin' | 'steward' | 'member';
}>;
}
interface MembershipDTO {
driverId: string;
role: string;
status?: 'active' | 'inactive';
}
export class ProfileLeaguesService implements Service {
async getProfileLeagues(driverId: string): Promise<Result<ProfileLeaguesPageDto, ProfileLeaguesServiceError>> {
try {
const baseUrl = getWebsiteApiBaseUrl();
const apiClient = new ApiClient(baseUrl);
const leaguesDto = await apiClient.leagues.getAllWithCapacity();
if (!leaguesDto?.leagues) {
return Result.err('notFound');
}
// Fetch all memberships in parallel
const leagueMemberships = await Promise.all(
leaguesDto.leagues.map(async (league) => {
try {
const membershipsDto = await apiClient.leagues.getMemberships(league.id);
let memberships: MembershipDTO[] = [];
if (membershipsDto && typeof membershipsDto === 'object') {
if ('members' in membershipsDto && Array.isArray((membershipsDto as { members?: unknown }).members)) {
memberships = (membershipsDto as { members: MembershipDTO[] }).members;
} else if ('memberships' in membershipsDto && Array.isArray((membershipsDto as { memberships?: unknown }).memberships)) {
memberships = (membershipsDto as { memberships: MembershipDTO[] }).memberships;
}
}
const currentMembership = memberships.find((m) => m.driverId === driverId);
if (currentMembership && currentMembership.status === 'active') {
return {
leagueId: league.id,
name: league.name,
description: league.description,
membershipRole: currentMembership.role as 'owner' | 'admin' | 'steward' | 'member',
};
}
return null;
} catch {
return null;
}
})
);
// Filter and categorize
const validLeagues = leagueMemberships.filter((l): l is NonNullable<typeof l> => l !== null);
const ownedLeagues = validLeagues.filter((l) => l.membershipRole === 'owner');
const memberLeagues = validLeagues.filter((l) => l.membershipRole !== 'owner');
return Result.ok({
ownedLeagues,
memberLeagues,
});
} catch (error) {
const errorAny = error as { statusCode?: number; message?: string };
if (errorAny.statusCode === 404 || errorAny.message?.toLowerCase().includes('not found')) {
return Result.err('notFound');
}
if (errorAny.statusCode === 302 || errorAny.message?.toLowerCase().includes('redirect')) {
return Result.err('redirect');
}
return Result.err('unknown');
}
}
}

View File

@@ -1,163 +1,21 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { MediaService } from './MediaService';
import { MediaApiClient } from '@/lib/api/media/MediaApiClient';
import { MediaViewModel } from '@/lib/view-models/MediaViewModel';
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
import { DeleteMediaViewModel } from '@/lib/view-models/DeleteMediaViewModel';
import { Result } from '@/lib/contracts/Result';
// Simple test that verifies the service structure
describe('MediaService', () => {
let mockApiClient: Mocked<MediaApiClient>;
let service: MediaService;
beforeEach(() => {
mockApiClient = {
uploadMedia: vi.fn(),
getMedia: vi.fn(),
deleteMedia: vi.fn(),
requestAvatarGeneration: vi.fn(),
getAvatar: vi.fn(),
updateAvatar: vi.fn(),
validateFacePhoto: vi.fn(),
} as unknown as Mocked<MediaApiClient>;
service = new MediaService(mockApiClient);
it('should be defined', () => {
expect(MediaService).toBeDefined();
});
describe('uploadMedia', () => {
it('should call apiClient.uploadMedia with correct input and return view model', async () => {
const input = {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
type: 'image',
category: 'avatar' as const,
};
const expectedOutput = { success: true, mediaId: 'media-123', url: 'https://example.com/media.jpg' };
mockApiClient.uploadMedia.mockResolvedValue(expectedOutput);
const result = await service.uploadMedia(input);
expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(UploadMediaViewModel);
expect(result.success).toBe(true);
expect(result.mediaId).toBe('media-123');
expect(result.url).toBe('https://example.com/media.jpg');
expect(result.isSuccessful).toBe(true);
expect(result.hasError).toBe(false);
});
it('should handle error response', async () => {
const input = {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
type: 'image',
};
const expectedOutput = { success: false, error: 'Upload failed' };
mockApiClient.uploadMedia.mockResolvedValue(expectedOutput);
const result = await service.uploadMedia(input);
expect(result).toBeInstanceOf(UploadMediaViewModel);
expect(result.success).toBe(false);
expect(result.error).toBe('Upload failed');
expect(result.isSuccessful).toBe(false);
expect(result.hasError).toBe(true);
});
it('should handle upload without category', async () => {
const input = {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
type: 'image',
};
const expectedOutput = { success: true, mediaId: 'media-456', url: 'https://example.com/media2.jpg' };
mockApiClient.uploadMedia.mockResolvedValue(expectedOutput);
const result = await service.uploadMedia(input);
expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(UploadMediaViewModel);
expect(result.success).toBe(true);
});
});
describe('getMedia', () => {
it('should call apiClient.getMedia with mediaId and return view model', async () => {
const mediaId = 'media-123';
const expectedOutput = {
id: 'media-123',
url: 'https://example.com/image.jpg',
type: 'image',
category: 'avatar',
uploadedAt: '2023-01-15T00:00:00.000Z',
size: 2048000,
};
mockApiClient.getMedia.mockResolvedValue(expectedOutput);
const result = await service.getMedia(mediaId);
expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId);
expect(result).toBeInstanceOf(MediaViewModel);
expect(result.id).toBe('media-123');
expect(result.url).toBe('https://example.com/image.jpg');
expect(result.type).toBe('image');
expect(result.category).toBe('avatar');
expect(result.uploadedAt).toEqual(new Date('2023-01-15T00:00:00.000Z'));
expect(result.size).toBe(2048000);
expect(result.formattedSize).toBe('1.95 MB');
});
it('should handle media without category and size', async () => {
const mediaId = 'media-456';
const expectedOutput = {
id: 'media-456',
url: 'https://example.com/video.mp4',
type: 'video',
uploadedAt: '2023-02-20T00:00:00.000Z',
};
mockApiClient.getMedia.mockResolvedValue(expectedOutput);
const result = await service.getMedia(mediaId);
expect(result).toBeInstanceOf(MediaViewModel);
expect(result.id).toBe('media-456');
expect(result.type).toBe('video');
expect(result.category).toBeUndefined();
expect(result.size).toBeUndefined();
expect(result.formattedSize).toBe('Unknown');
});
});
describe('deleteMedia', () => {
it('should call apiClient.deleteMedia with mediaId and return view model', async () => {
const mediaId = 'media-123';
const expectedOutput = { success: true };
mockApiClient.deleteMedia.mockResolvedValue(expectedOutput);
const result = await service.deleteMedia(mediaId);
expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId);
expect(result).toBeInstanceOf(DeleteMediaViewModel);
expect(result.success).toBe(true);
expect(result.isSuccessful).toBe(true);
expect(result.hasError).toBe(false);
});
it('should handle error response', async () => {
const mediaId = 'media-456';
const expectedOutput = { success: false, error: 'Deletion failed' };
mockApiClient.deleteMedia.mockResolvedValue(expectedOutput);
const result = await service.deleteMedia(mediaId);
expect(result).toBeInstanceOf(DeleteMediaViewModel);
expect(result.success).toBe(false);
expect(result.error).toBe('Deletion failed');
expect(result.isSuccessful).toBe(false);
expect(result.hasError).toBe(true);
});
it('should have all required methods', () => {
const service = new MediaService();
expect(typeof service.getAvatar).toBe('function');
expect(typeof service.getCategoryIcon).toBe('function');
expect(typeof service.getLeagueCover).toBe('function');
expect(typeof service.getLeagueLogo).toBe('function');
expect(typeof service.getSponsorLogo).toBe('function');
expect(typeof service.getTeamLogo).toBe('function');
expect(typeof service.getTrackImage).toBe('function');
});
});

View File

@@ -1,13 +1,75 @@
/**
* Media Service - DTO Only
* MediaService
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
* Frontend orchestration service for media operations.
* Handles binary media fetching with proper error handling.
*/
export class MediaService {
constructor(private readonly apiClient: any) {}
async getMediaById(mediaId: string): Promise<any> {
return { id: mediaId, url: '' };
import { Result } from '@/lib/contracts/Result';
import { Service, DomainError } from '@/lib/contracts/services/Service';
import { MediaAdapter } from '@/lib/adapters/MediaAdapter';
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
/**
* MediaService
*
* Handles media asset fetching with proper error handling.
* Creates its own dependencies and orchestrates adapter calls.
*/
export class MediaService implements Service {
private adapter: MediaAdapter;
constructor() {
// Service creates its own dependencies
this.adapter = new MediaAdapter();
}
/**
* Get avatar for a driver
*/
async getAvatar(driverId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/avatar/${driverId}`);
}
/**
* Get category icon
*/
async getCategoryIcon(categoryId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/categories/${categoryId}/icon`);
}
/**
* Get league cover
*/
async getLeagueCover(leagueId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/leagues/${leagueId}/cover`);
}
/**
* Get league logo
*/
async getLeagueLogo(leagueId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/leagues/${leagueId}/logo`);
}
/**
* Get sponsor logo
*/
async getSponsorLogo(sponsorId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/sponsors/${sponsorId}/logo`);
}
/**
* Get team logo
*/
async getTeamLogo(teamId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/teams/${teamId}/logo`);
}
/**
* Get track image
*/
async getTrackImage(trackId: string): Promise<Result<MediaBinaryDTO, DomainError>> {
return this.adapter.fetchMedia(`/media/tracks/${trackId}/image`);
}
}

View File

@@ -54,22 +54,12 @@ export class OnboardingService implements Service {
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async generateAvatars(input: RequestAvatarGenerationInputDTO): Promise<Result<RequestAvatarGenerationOutputDTO, DomainError>> {
try {
// TODO: Implement actual API call when available
// For now, return mock result
const mockResult: RequestAvatarGenerationOutputDTO = {
success: true,
avatarUrls: [
`https://api.example.com/avatars/${input.userId}/1.png`,
`https://api.example.com/avatars/${input.userId}/2.png`,
`https://api.example.com/avatars/${input.userId}/3.png`,
],
};
return Result.ok(mockResult);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to generate avatars';
return Result.err({ type: 'unknown', message: errorMessage });
}
// Endpoint not implemented yet - return NotImplemented error
return Result.err({
type: 'notImplemented',
message: 'Avatar generation endpoint is not implemented yet'
});
}
}

View File

@@ -1,13 +1,87 @@
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
/**
* Race Results Service - DTO Only
*
* Race Results Service
*
* Orchestration service for race results operations.
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class RaceResultsService {
constructor(private readonly apiClient: any) {}
private apiClient: RacesApiClient;
async getRaceResults(raceId: string): Promise<any> {
return { raceId, results: [] };
constructor() {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.apiClient = new RacesApiClient(baseUrl, errorReporter, logger);
}
/**
* Get race results detail
* Returns results for a specific race
*/
async getRaceResultsDetail(raceId: string): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getResultsDetail(raceId);
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch race results'
});
}
}
/**
* Get race with strength of field
* Returns race data with SOF calculation
*/
async getWithSOF(raceId: string): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getWithSOF(raceId);
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch race SOF'
});
}
}
private mapApiErrorType(apiErrorType: string): DomainError['type'] {
switch (apiErrorType) {
case 'NOT_FOUND':
return 'notFound';
case 'AUTH_ERROR':
return 'unauthorized';
case 'VALIDATION_ERROR':
return 'validation';
case 'SERVER_ERROR':
return 'serverError';
case 'NETWORK_ERROR':
return 'networkError';
default:
return 'unknown';
}
}
}

View File

@@ -17,4 +17,9 @@ export class RaceService {
async getRacesByLeagueId(leagueId: string): Promise<any> {
return this.apiClient.getPageData(leagueId);
}
async findByLeagueId(leagueId: string): Promise<any[]> {
const result = await this.apiClient.getPageData(leagueId);
return result.races || [];
}
}

View File

@@ -1,13 +1,110 @@
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
import { PenaltiesApiClient } from '@/lib/api/penalties/PenaltiesApiClient';
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
/**
* Race Stewarding Service - DTO Only
*
* Race Stewarding Service
*
* Orchestration service for race stewarding operations.
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class RaceStewardingService {
constructor(private readonly raceApiClient: any, private readonly protestApiClient: any, private readonly penaltyApiClient: any) {}
private racesApiClient: RacesApiClient;
private protestsApiClient: ProtestsApiClient;
private penaltiesApiClient: PenaltiesApiClient;
async getRaceStewarding(raceId: string): Promise<any> {
return { raceId, protests: [], penalties: [] };
constructor() {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
this.protestsApiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
this.penaltiesApiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
}
/**
* Get race stewarding data
* Returns protests and penalties for a race
*/
async getRaceStewarding(raceId: string): Promise<Result<any, DomainError>> {
try {
// Fetch data in parallel
const [raceDetail, protests, penalties] = await Promise.all([
this.racesApiClient.getDetail(raceId, ''),
this.protestsApiClient.getRaceProtests(raceId),
this.penaltiesApiClient.getRacePenalties(raceId),
]);
// Transform data to match view model structure
const protestsData = protests.protests.map((p: any) => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
incident: {
lap: p.lap,
description: p.description,
},
filedAt: p.filedAt,
status: p.status,
}));
const pendingProtests = protestsData.filter((p: any) => p.status === 'pending' || p.status === 'under_review');
const resolvedProtests = protestsData.filter((p: any) =>
p.status === 'upheld' ||
p.status === 'dismissed' ||
p.status === 'withdrawn'
);
const data = {
race: raceDetail.race,
league: raceDetail.league,
protests: protestsData,
penalties: penalties.penalties,
driverMap: { ...protests.driverMap, ...penalties.driverMap },
pendingProtests,
resolvedProtests,
pendingCount: pendingProtests.length,
resolvedCount: resolvedProtests.length,
penaltiesCount: penalties.penalties.length,
};
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch stewarding data'
});
}
}
private mapApiErrorType(apiErrorType: string): DomainError['type'] {
switch (apiErrorType) {
case 'NOT_FOUND':
return 'notFound';
case 'AUTH_ERROR':
return 'unauthorized';
case 'VALIDATION_ERROR':
return 'validation';
case 'SERVER_ERROR':
return 'serverError';
case 'NETWORK_ERROR':
return 'networkError';
default:
return 'unknown';
}
}
}

View File

@@ -0,0 +1,153 @@
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
/**
* Races Service
*
* Orchestration service for race-related operations.
* Returns raw API DTOs. No ViewModels or UX logic.
*/
export class RacesService {
private apiClient: RacesApiClient;
constructor() {
// Service creates its own dependencies
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new ConsoleErrorReporter();
this.apiClient = new RacesApiClient(baseUrl, errorReporter, logger);
}
/**
* Get races page data
* Returns races for the main races page
*/
async getRacesPageData(): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getPageData();
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch races page data'
});
}
}
/**
* Get race detail
* Returns detailed information for a specific race
*/
async getRaceDetail(raceId: string, driverId: string): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getDetail(raceId, driverId);
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch race detail'
});
}
}
/**
* Get race results detail
* Returns results for a specific race
*/
async getRaceResultsDetail(raceId: string): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getResultsDetail(raceId);
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch race results'
});
}
}
/**
* Get race with strength of field
* Returns race data with SOF calculation
*/
async getRaceWithSOF(raceId: string): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getWithSOF(raceId);
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch race SOF'
});
}
}
/**
* Get all races for the all races page
* Returns all races with pagination support
*/
async getAllRacesPageData(): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getPageData();
return Result.ok(data);
} catch (error) {
if (error instanceof ApiError) {
return Result.err({
type: this.mapApiErrorType(error.type),
message: error.message
});
}
return Result.err({
type: 'unknown',
message: 'Failed to fetch all races'
});
}
}
private mapApiErrorType(apiErrorType: string): DomainError['type'] {
switch (apiErrorType) {
case 'NOT_FOUND':
return 'notFound';
case 'AUTH_ERROR':
return 'unauthorized';
case 'VALIDATION_ERROR':
return 'validation';
case 'SERVER_ERROR':
return 'serverError';
case 'NETWORK_ERROR':
return 'networkError';
default:
return 'unknown';
}
}
}

View File

@@ -1,3 +1,6 @@
import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service';
/**
* Sponsor Service - DTO Only
*
@@ -7,7 +10,102 @@
export class SponsorService {
constructor(private readonly apiClient: any) {}
async getSponsorById(sponsorId: string): Promise<any> {
return { id: sponsorId, name: 'Sponsor' };
async getSponsorById(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getSponsor(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSponsorById' });
}
}
async getSponsorDashboard(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getDashboard(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSponsorDashboard' });
}
}
async getSponsorSponsorships(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getSponsorships(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSponsorSponsorships' });
}
}
async getBilling(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getBilling(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getBilling' });
}
}
async getAvailableLeagues(): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getAvailableLeagues();
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getAvailableLeagues' });
}
}
async getLeagueDetail(leagueId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getLeagueDetail(leagueId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getLeagueDetail' });
}
}
async getSettings(sponsorId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getSettings(sponsorId);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getSettings' });
}
}
async updateSettings(sponsorId: string, input: any): Promise<Result<void, DomainError>> {
try {
await this.apiClient.updateSettings(sponsorId, input);
return Result.ok(undefined);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'updateSettings' });
}
}
async acceptSponsorshipRequest(requestId: string, sponsorId: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.acceptSponsorshipRequest(requestId, sponsorId);
return Result.ok(undefined);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'acceptSponsorshipRequest' });
}
}
async rejectSponsorshipRequest(requestId: string, sponsorId: string, reason?: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.rejectSponsorshipRequest(requestId, sponsorId, reason);
return Result.ok(undefined);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'rejectSponsorshipRequest' });
}
}
async getPendingSponsorshipRequests(input: any): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getPendingSponsorshipRequests(input);
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'notImplemented', message: 'getPendingSponsorshipRequests' });
}
}
}

View File

@@ -5,24 +5,15 @@ import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
import type { AcceptSponsorshipRequestInputDTO } from '@/lib/types/generated/AcceptSponsorshipRequestInputDTO';
import type { RejectSponsorshipRequestInputDTO } from '@/lib/types/generated/RejectSponsorshipRequestInputDTO';
export interface AcceptSponsorshipRequestCommand {
requestId: string;
actorDriverId: string;
interface GetPendingRequestsInput {
entityType: string;
entityId: string;
}
export interface RejectSponsorshipRequestCommand {
requestId: string;
actorDriverId: string;
reason: string | null;
}
export type AcceptSponsorshipRequestServiceError =
| 'ACCEPT_SPONSORSHIP_REQUEST_FAILED';
export type RejectSponsorshipRequestServiceError =
| 'REJECT_SPONSORSHIP_REQUEST_FAILED';
export class SponsorshipRequestsService implements Service {
private readonly client: SponsorsApiClient;
@@ -38,13 +29,29 @@ export class SponsorshipRequestsService implements Service {
this.client = new SponsorsApiClient(baseUrl, errorReporter, logger);
}
async acceptRequest(
command: AcceptSponsorshipRequestCommand,
): Promise<Result<void, AcceptSponsorshipRequestServiceError>> {
async getPendingRequests(
input: GetPendingRequestsInput,
): Promise<Result<GetPendingSponsorshipRequestsOutputDTO, 'GET_PENDING_REQUESTS_FAILED'>> {
try {
await this.client.acceptSponsorshipRequest(command.requestId, {
actorDriverId: command.actorDriverId,
} as any);
const result = await this.client.getPendingSponsorshipRequests({
entityType: input.entityType,
entityId: input.entityId,
});
return Result.ok(result);
} catch {
return Result.err('GET_PENDING_REQUESTS_FAILED');
}
}
async acceptRequest(
command: { requestId: string; actorDriverId: string },
): Promise<Result<void, 'ACCEPT_SPONSORSHIP_REQUEST_FAILED'>> {
try {
const input: AcceptSponsorshipRequestInputDTO = {
respondedBy: command.actorDriverId,
};
await this.client.acceptSponsorshipRequest(command.requestId, input);
return Result.ok(undefined);
} catch {
@@ -53,13 +60,14 @@ export class SponsorshipRequestsService implements Service {
}
async rejectRequest(
command: RejectSponsorshipRequestCommand,
): Promise<Result<void, RejectSponsorshipRequestServiceError>> {
command: { requestId: string; actorDriverId: string; reason: string | null },
): Promise<Result<void, 'REJECT_SPONSORSHIP_REQUEST_FAILED'>> {
try {
await this.client.rejectSponsorshipRequest(command.requestId, {
actorDriverId: command.actorDriverId,
reason: command.reason ?? undefined,
} as any);
const input: RejectSponsorshipRequestInputDTO = {
respondedBy: command.actorDriverId,
reason: command.reason || undefined,
};
await this.client.rejectSponsorshipRequest(command.requestId, input);
return Result.ok(undefined);
} catch {

View File

@@ -0,0 +1,48 @@
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
/**
* Team Join Service - ViewModels
*
* Returns ViewModels for team join requests.
* Handles presentation logic for join request management.
*/
export class TeamJoinService implements Service {
private apiClient: TeamsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> {
try {
const result = await this.apiClient.getJoinRequests(teamId);
return result.requests.map(request =>
new TeamJoinRequestViewModel(request, currentDriverId, isOwner)
);
} catch (error) {
// Return empty array on error
return [];
}
}
async approveJoinRequest(): Promise<void> {
throw new Error('Not implemented: API endpoint for approving join requests');
}
async rejectJoinRequest(): Promise<void> {
throw new Error('Not implemented: API endpoint for rejecting join requests');
}
}

View File

@@ -1,17 +1,79 @@
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
/**
* Team 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 TeamService {
constructor(private readonly apiClient: any) {}
export class TeamService implements Service {
private apiClient: TeamsApiClient;
async getTeamById(teamId: string): Promise<any> {
return { id: teamId, name: 'Team' };
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
}
async getTeamsByLeagueId(leagueId: string): Promise<any> {
return { teams: [] };
async getAllTeams(): Promise<Result<TeamSummaryViewModel[], DomainError>> {
try {
const result = await this.apiClient.getAll();
return Result.ok(result.teams.map(team => new TeamSummaryViewModel(team)));
} catch (error) {
return Result.err({ type: 'unknown', message: 'Failed to fetch teams' });
}
}
async getTeamDetails(_teamId: string, _currentDriverId: string): Promise<Result<any, DomainError>> {
try {
const result = await this.apiClient.getDetails(_teamId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Team not found' });
}
return Result.ok(result);
} catch (error) {
return Result.err({ type: 'unknown', message: 'Failed to fetch team details' });
}
}
async getTeamMembers(teamId: string, currentDriverId: string, ownerId: string): Promise<Result<TeamMemberViewModel[], DomainError>> {
try {
const result = await this.apiClient.getMembers(teamId);
return Result.ok(result.members.map(member => new TeamMemberViewModel(member, currentDriverId, ownerId)));
} catch (error) {
return Result.err({ type: 'unknown', message: 'Failed to fetch team members' });
}
}
async getTeamJoinRequests(_teamId: string): Promise<Result<any, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getTeamJoinRequests' });
}
async createTeam(_input: any): Promise<Result<any, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'createTeam' });
}
async updateTeam(_teamId: string, _input: any): Promise<Result<any, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'updateTeam' });
}
async getDriverTeam(_driverId: string): Promise<Result<any, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getDriverTeam' });
}
async getMembership(_teamId: string, _driverId: string): Promise<Result<any, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getMembership' });
}
}