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

@@ -0,0 +1,108 @@
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import type { UserDto, DashboardStats, UserListResponse, ListUsersQuery } from '@/lib/api/admin/AdminApiClient';
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';
/**
* Admin Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by presenters/templates.
* @server-safe
*/
export class AdminService {
private apiClient: AdminApiClient;
constructor() {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
this.apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
}
/**
* Get dashboard statistics
*/
async getDashboardStats(): Promise<Result<DashboardStats, DomainError>> {
try {
const result = await this.apiClient.getDashboardStats();
return Result.ok(result);
} catch (error) {
console.error('AdminService.getDashboardStats failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'notFound', message: 'Access denied' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to fetch dashboard stats' });
}
}
/**
* List users with filtering and pagination
*/
async listUsers(query: ListUsersQuery = {}): Promise<Result<UserListResponse, DomainError>> {
try {
const result = await this.apiClient.listUsers(query);
return Result.ok(result);
} catch (error) {
console.error('AdminService.listUsers failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'notFound', message: 'Access denied' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to fetch users' });
}
}
/**
* Update user status
*/
async updateUserStatus(userId: string, status: string): Promise<Result<UserDto, DomainError>> {
try {
const result = await this.apiClient.updateUserStatus(userId, status);
return Result.ok(result);
} catch (error) {
console.error('AdminService.updateUserStatus failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'forbidden', message: 'Insufficient permissions' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to update user status' });
}
}
/**
* Delete a user (soft delete)
*/
async deleteUser(userId: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.deleteUser(userId);
return Result.ok(undefined);
} catch (error) {
console.error('AdminService.deleteUser failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err({ type: 'forbidden', message: 'Insufficient permissions' });
}
}
return Result.err({ type: 'serverError', message: 'Failed to delete user' });
}
}
}

View File

@@ -0,0 +1,46 @@
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 { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
/**
* DashboardService
*
* Pure service that creates its own API client and returns Result types.
* No business logic, only data fetching and error mapping.
*/
export class DashboardService {
private apiClient: DashboardApiClient;
constructor() {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
this.apiClient = new DashboardApiClient(baseUrl, errorReporter, logger);
}
async getDashboardOverview(): Promise<Result<DashboardOverviewDTO, DomainError>> {
try {
const dto = await this.apiClient.getDashboardOverview();
return Result.ok(dto);
} catch (error) {
console.error('DashboardService.getDashboardOverview failed:', error);
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: 'Dashboard fetch failed' });
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Auth Page Service
*
* Processes URL parameters for auth pages and provides structured data.
* This is a composition service, not an API service.
*/
import { Result } from '@/lib/contracts/Result';
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;
}
export class AuthPageService {
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, string>> {
try {
const returnTo = params.returnTo ?? '/dashboard';
const hasInsufficientPermissions = params.returnTo !== null;
return Result.ok({
returnTo,
hasInsufficientPermissions,
});
} catch (error) {
return Result.err('Failed to process login parameters');
}
}
async processForgotPasswordParams(params: AuthPageParams): Promise<Result<ForgotPasswordPageDTO, string>> {
try {
const returnTo = params.returnTo ?? '/auth/login';
return Result.ok({ returnTo });
} catch (error) {
return Result.err('Failed to process forgot password parameters');
}
}
async processResetPasswordParams(params: AuthPageParams): Promise<Result<ResetPasswordPageDTO, string>> {
try {
const token = params.token;
if (!token) {
return Result.err('Missing reset token');
}
const returnTo = params.returnTo ?? '/auth/login';
return Result.ok({ token, returnTo });
} catch (error) {
return Result.err('Failed to process reset password parameters');
}
}
async processSignupParams(params: AuthPageParams): Promise<Result<SignupPageDTO, string>> {
try {
const returnTo = params.returnTo ?? '/onboarding';
return Result.ok({ returnTo });
} catch (error) {
return Result.err('Failed to process signup parameters');
}
}
}

View File

@@ -0,0 +1,53 @@
/**
* Auth Service
*
* Orchestrates authentication operations.
* Calls AuthApiClient for API calls.
*/
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
import { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
import { LoginParamsDTO } from '@/lib/types/generated/LoginParamsDTO';
import { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
import { ForgotPasswordDTO } from '@/lib/types/generated/ForgotPasswordDTO';
import { ResetPasswordDTO } from '@/lib/types/generated/ResetPasswordDTO';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
export class AuthService {
private apiClient: AuthApiClient;
constructor() {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
this.apiClient = new AuthApiClient(baseUrl, errorReporter, logger);
}
async login(params: LoginParamsDTO): Promise<SessionViewModel> {
const dto = await this.apiClient.login(params);
return new SessionViewModel(dto.user);
}
async signup(params: SignupParamsDTO): Promise<SessionViewModel> {
const dto = await this.apiClient.signup(params);
return new SessionViewModel(dto.user);
}
async logout(): Promise<void> {
await this.apiClient.logout();
}
async forgotPassword(params: ForgotPasswordDTO): Promise<{ message: string; magicLink?: string }> {
return await this.apiClient.forgotPassword(params);
}
async resetPassword(params: ResetPasswordDTO): Promise<{ message: string }> {
return await this.apiClient.resetPassword(params);
}
}

View File

@@ -1,11 +1,11 @@
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
/**
* Session Service
*
* Orchestrates session operations by coordinating API calls and view model creation.
* All dependencies are injected via constructor.
* Returns SessionViewModel for client consumption.
*/
export class SessionService {
constructor(
@@ -13,10 +13,11 @@ export class SessionService {
) {}
/**
* Get current user session with view model transformation
* Get current user session (returns ViewModel)
*/
async getSession(): Promise<SessionViewModel | null> {
const dto = await this.apiClient.getSession();
return dto ? new SessionViewModel(dto.user) : null;
if (!dto) return null;
return new SessionViewModel(dto.user);
}
}
}

View File

@@ -0,0 +1,10 @@
/**
* Forgot Password Page DTO
*
* Data transfer object for forgot password page composition.
* Used by AuthPageService and ForgotPasswordViewDataBuilder.
*/
export interface ForgotPasswordPageDTO {
returnTo: string;
}

View File

@@ -0,0 +1,11 @@
/**
* Login Page DTO
*
* Data transfer object for login page composition.
* Used by AuthPageService and LoginViewDataBuilder.
*/
export interface LoginPageDTO {
returnTo: string;
hasInsufficientPermissions: boolean;
}

View File

@@ -0,0 +1,11 @@
/**
* Reset Password Page DTO
*
* Data transfer object for reset password page composition.
* Used by AuthPageService and ResetPasswordViewDataBuilder.
*/
export interface ResetPasswordPageDTO {
token: string;
returnTo: string;
}

View File

@@ -0,0 +1,10 @@
/**
* Signup Page DTO
*
* Data transfer object for signup page composition.
* Used by AuthPageService and SignupViewDataBuilder.
*/
export interface SignupPageDTO {
returnTo: string;
}

View File

@@ -1,60 +0,0 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { DashboardService } from './DashboardService';
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
describe('DashboardService', () => {
let mockApiClient: Mocked<DashboardApiClient>;
let service: DashboardService;
beforeEach(() => {
mockApiClient = {
getDashboardOverview: vi.fn(),
} as Mocked<DashboardApiClient>;
service = new DashboardService(mockApiClient);
});
describe('getDashboardOverview', () => {
it('should call apiClient.getDashboardOverview and return DashboardOverviewViewModel', async () => {
const mockDto = {
currentDriver: {
id: 'driver-123',
name: 'Test Driver',
avatarUrl: 'https://example.com/avatar.jpg',
country: 'US',
totalRaces: 42,
wins: 10,
podiums: 20,
rating: 1500,
globalRank: 5,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { feedItems: [] },
friends: [],
};
mockApiClient.getDashboardOverview.mockResolvedValue(mockDto);
const result = await service.getDashboardOverview();
expect(mockApiClient.getDashboardOverview).toHaveBeenCalled();
expect(result).toBeInstanceOf(DashboardOverviewViewModel);
expect(result.activeLeaguesCount).toBe(3);
});
it('should throw error when apiClient.getDashboardOverview fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDashboardOverview.mockRejectedValue(error);
await expect(service.getDashboardOverview()).rejects.toThrow('API call failed');
});
});
});

View File

@@ -0,0 +1,56 @@
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 DriverProfileReadServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
export class DriverProfileReadService implements Service {
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfileReadServiceError>> {
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(
'DriverProfileReadService 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,52 @@
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 DriverProfileServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
export class DriverProfileService implements Service {
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfileServiceError>> {
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('DriverProfileService 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,35 @@
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 { DomainError, Service } from '@/lib/contracts/services/Service';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
export class DriverProfileUpdateService implements Service {
private readonly apiClient: DriversApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
}
async updateProfile(
updates: { bio?: string; country?: string },
): Promise<Result<DriverDTO, DomainError>> {
try {
const dto = await this.apiClient.updateProfile(updates);
return Result.ok(dto);
} catch {
return Result.err({ type: 'unknown', message: 'DRIVER_PROFILE_UPDATE_FAILED' });
}
}
}

View File

@@ -1,16 +1,15 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import type { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import { CompleteOnboardingViewModel } from "@/lib/view-models/CompleteOnboardingViewModel";
import { DriverLeaderboardViewModel } from "@/lib/view-models/DriverLeaderboardViewModel";
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
import { DriverProfileViewModel } from "@/lib/view-models/DriverProfileViewModel";
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
/**
* Driver Service
* Driver Service - DTO Only
*
* Orchestrates driver operations by coordinating API calls and view model creation.
* All dependencies are injected via constructor.
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class DriverService {
constructor(
@@ -18,143 +17,49 @@ export class DriverService {
) {}
/**
* Get driver leaderboard with view model transformation
* Get driver leaderboard (returns DTO)
*/
async getDriverLeaderboard(): Promise<DriverLeaderboardViewModel> {
const dto = await this.apiClient.getLeaderboard();
return new DriverLeaderboardViewModel(dto);
async getDriverLeaderboard() {
return this.apiClient.getLeaderboard();
}
/**
* Complete driver onboarding with view model transformation
* Complete driver onboarding (returns DTO)
*/
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingViewModel> {
const dto = await this.apiClient.completeOnboarding(input);
return new CompleteOnboardingViewModel(dto);
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
return this.apiClient.completeOnboarding(input);
}
/**
* Get current driver with view model transformation
*/
async getCurrentDriver(): Promise<DriverViewModel | null> {
const dto = await this.apiClient.getCurrent();
if (!dto) {
return null;
}
return new DriverViewModel({ ...dto, avatarUrl: dto.avatarUrl ?? null });
* Get current driver (returns DTO)
*/
async getCurrentDriver(): Promise<DriverDTO | null> {
return this.apiClient.getCurrent();
}
/**
* Get driver profile with full details and view model transformation
*/
async getDriverProfile(driverId: string): Promise<DriverProfileViewModel> {
const dto = await this.apiClient.getDriverProfile(driverId);
return new DriverProfileViewModel({
currentDriver: dto.currentDriver
? {
id: dto.currentDriver.id,
name: dto.currentDriver.name,
country: dto.currentDriver.country,
avatarUrl: dto.currentDriver.avatarUrl || '',
iracingId: dto.currentDriver.iracingId ?? null,
joinedAt: dto.currentDriver.joinedAt,
rating: dto.currentDriver.rating ?? null,
globalRank: dto.currentDriver.globalRank ?? null,
consistency: dto.currentDriver.consistency ?? null,
bio: dto.currentDriver.bio ?? null,
totalDrivers: dto.currentDriver.totalDrivers ?? null,
}
: null,
stats: dto.stats
? {
totalRaces: dto.stats.totalRaces,
wins: dto.stats.wins,
podiums: dto.stats.podiums,
dnfs: dto.stats.dnfs,
avgFinish: dto.stats.avgFinish ?? null,
bestFinish: dto.stats.bestFinish ?? null,
worstFinish: dto.stats.worstFinish ?? null,
finishRate: dto.stats.finishRate ?? null,
winRate: dto.stats.winRate ?? null,
podiumRate: dto.stats.podiumRate ?? null,
percentile: dto.stats.percentile ?? null,
rating: dto.stats.rating ?? null,
consistency: dto.stats.consistency ?? null,
overallRank: dto.stats.overallRank ?? null,
}
: null,
finishDistribution: dto.finishDistribution
? {
totalRaces: dto.finishDistribution.totalRaces,
wins: dto.finishDistribution.wins,
podiums: dto.finishDistribution.podiums,
topTen: dto.finishDistribution.topTen,
dnfs: dto.finishDistribution.dnfs,
other: dto.finishDistribution.other,
}
: null,
teamMemberships: dto.teamMemberships.map((m) => ({
teamId: m.teamId,
teamName: m.teamName,
teamTag: m.teamTag ?? null,
role: m.role,
joinedAt: m.joinedAt,
isCurrent: m.isCurrent,
})),
socialSummary: {
friendsCount: dto.socialSummary.friendsCount,
friends: dto.socialSummary.friends.map((f) => ({
id: f.id,
name: f.name,
country: f.country,
avatarUrl: f.avatarUrl || '',
})),
},
extendedProfile: dto.extendedProfile
? {
socialHandles: dto.extendedProfile.socialHandles.map((h) => ({
platform: h.platform as 'twitter' | 'youtube' | 'twitch' | 'discord',
handle: h.handle,
url: h.url,
})),
achievements: dto.extendedProfile.achievements.map((a) => ({
id: a.id,
title: a.title,
description: a.description,
icon: a.icon as 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap',
rarity: a.rarity as 'common' | 'rare' | 'epic' | 'legendary',
earnedAt: a.earnedAt,
})),
racingStyle: dto.extendedProfile.racingStyle,
favoriteTrack: dto.extendedProfile.favoriteTrack,
favoriteCar: dto.extendedProfile.favoriteCar,
timezone: dto.extendedProfile.timezone,
availableHours: dto.extendedProfile.availableHours,
lookingForTeam: dto.extendedProfile.lookingForTeam,
openToRequests: dto.extendedProfile.openToRequests,
}
: null,
});
* Get driver profile (returns DTO)
*/
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
return this.apiClient.getDriverProfile(driverId);
}
/**
* Update current driver profile with view model transformation
*/
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverProfileViewModel> {
const dto = await this.apiClient.updateProfile(updates);
// After updating, get the full profile again to return updated view model
return this.getDriverProfile(dto.id);
}
* Update current driver profile (returns DTO)
*/
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverDTO> {
return this.apiClient.updateProfile(updates);
}
/**
* Find driver by ID
* Find driver by ID (returns DTO)
*/
async findById(id: string): Promise<GetDriverOutputDTO | null> {
return this.apiClient.getDriver(id);
}
/**
* Find multiple drivers by IDs
* Find multiple drivers by IDs (returns DTOs)
*/
async findByIds(ids: string[]): Promise<GetDriverOutputDTO[]> {
const drivers = await Promise.all(ids.map(id => this.apiClient.getDriver(id)));

View File

@@ -1,28 +1,68 @@
import { ContainerManager } from '@/lib/di/container';
import { SESSION_SERVICE_TOKEN, LANDING_SERVICE_TOKEN } from '@/lib/di/tokens';
import { LandingService } from '@/lib/services/landing/LandingService';
import { SessionService } from '@/lib/services/auth/SessionService';
import { redirect } from 'next/navigation';
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
export async function getHomeData() {
const container = ContainerManager.getInstance().getContainer();
const sessionService = container.get<SessionService>(SESSION_SERVICE_TOKEN);
const landingService = container.get<LandingService>(LANDING_SERVICE_TOKEN);
// API Clients
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
// Services
import { SessionService } from '@/lib/services/auth/SessionService';
// Infrastructure
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
// DTO types
import type { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO';
import type { LeagueWithCapacityDTO } from '@/lib/types/generated/LeagueWithCapacityDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
export interface HomeDataDTO {
isAlpha: boolean;
upcomingRaces: RacesPageDataRaceDTO[];
topLeagues: LeagueWithCapacityDTO[];
teams: TeamListItemDTO[];
}
export async function getHomeData(): Promise<HomeDataDTO> {
// Manual wiring: construct dependencies explicitly
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
// Construct API clients
const authApiClient = new AuthApiClient(baseUrl, errorReporter, logger);
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
// Construct services
const sessionService = new SessionService(authApiClient);
// Check session and redirect if logged in
const session = await sessionService.getSession();
if (session) {
redirect('/dashboard');
}
// Get feature flags
const featureService = await FeatureFlagService.fromAPI();
const isAlpha = featureService.isEnabled('alpha_features');
const discovery = await landingService.getHomeDiscovery();
// Get home discovery data (manual implementation)
const [racesDto, leaguesDto, teamsDto] = await Promise.all([
racesApiClient.getPageData(),
leaguesApiClient.getAllWithCapacity(),
teamsApiClient.getAll(),
]);
// Return DTOs directly (no ViewModels)
return {
isAlpha,
upcomingRaces: discovery.upcomingRaces,
topLeagues: discovery.topLeagues,
teams: discovery.teams,
upcomingRaces: racesDto.races.slice(0, 4),
topLeagues: leaguesDto.leagues.slice(0, 4),
teams: teamsDto.teams.slice(0, 4),
};
}

View File

@@ -0,0 +1,13 @@
/**
* Landing 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 LandingService {
constructor(private readonly apiClient: any) {}
async getLandingData(): Promise<any> {
return { featuredLeagues: [], stats: {} };
}
}

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);
}
}

View File

@@ -0,0 +1,13 @@
/**
* Media 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 MediaService {
constructor(private readonly apiClient: any) {}
async getMediaById(mediaId: string): Promise<any> {
return { id: mediaId, url: '' };
}
}

View File

@@ -0,0 +1,75 @@
/**
* Onboarding Service
*
* Orchestrates onboarding operations.
* Uses DriversApiClient for onboarding-related API calls.
*
* @server-safe
*/
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
export class OnboardingService implements Service {
private apiClient: DriversApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const { NODE_ENV } = getWebsiteServerEnv();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: NODE_ENV === 'production',
});
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
}
async completeOnboarding(input: CompleteOnboardingInputDTO): Promise<Result<CompleteOnboardingOutputDTO, DomainError>> {
try {
const result = await this.apiClient.completeOnboarding(input);
return Result.ok(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to complete onboarding';
return Result.err({ type: 'unknown', message: errorMessage });
}
}
async checkCurrentDriver(): Promise<Result<unknown, DomainError>> {
try {
const result = await this.apiClient.getCurrent();
return Result.ok(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to check driver status';
return Result.err({ type: 'unknown', message: errorMessage });
}
}
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 });
}
}
}

View File

@@ -0,0 +1,13 @@
/**
* Payment 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 PaymentService {
constructor(private readonly apiClient: any) {}
async getPaymentById(paymentId: string): Promise<any> {
return { id: paymentId, amount: 0 };
}
}

View File

@@ -0,0 +1,13 @@
/**
* Wallet 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 WalletService {
constructor(private readonly apiClient: any) {}
async getWalletBalance(driverId: string): Promise<any> {
return { balance: 0, currency: 'USD' };
}
}

View File

@@ -0,0 +1,41 @@
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
import type { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
import type { RaceViewModel } from '@/lib/view-models/RaceViewModel';
import type { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel';
import type { ApplyPenaltyCommandDTO } from '@/lib/types/generated/ApplyPenaltyCommandDTO';
import type { RequestProtestDefenseCommandDTO } from '@/lib/types/generated/RequestProtestDefenseCommandDTO';
import type { ReviewProtestCommandDTO } from '@/lib/types/generated/ReviewProtestCommandDTO';
/**
* Protest 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 ProtestService {
constructor(private readonly apiClient: ProtestsApiClient) {}
async getLeagueProtests(leagueId: string): Promise<any> {
return this.apiClient.getLeagueProtests(leagueId);
}
async getProtestById(leagueId: string, protestId: string): Promise<any> {
return this.apiClient.getLeagueProtest(leagueId, protestId);
}
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<void> {
return this.apiClient.applyPenalty(input);
}
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
return this.apiClient.requestDefense(input);
}
async reviewProtest(input: ReviewProtestCommandDTO): Promise<void> {
return this.apiClient.reviewProtest(input);
}
async findByRaceId(raceId: string): Promise<any> {
return this.apiClient.getRaceProtests(raceId);
}
}

View File

@@ -0,0 +1,13 @@
/**
* Race Results 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 RaceResultsService {
constructor(private readonly apiClient: any) {}
async getRaceResults(raceId: string): Promise<any> {
return { raceId, results: [] };
}
}

View File

@@ -0,0 +1,20 @@
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
/**
* Race 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 RaceService {
constructor(private readonly apiClient: RacesApiClient) {}
async getRaceById(raceId: string): Promise<any> {
// This would need a driverId, but for now we'll use a placeholder
return this.apiClient.getDetail(raceId, 'placeholder-driver-id');
}
async getRacesByLeagueId(leagueId: string): Promise<any> {
return this.apiClient.getPageData(leagueId);
}
}

View File

@@ -0,0 +1,13 @@
/**
* Race 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 RaceStewardingService {
constructor(private readonly raceApiClient: any, private readonly protestApiClient: any, private readonly penaltyApiClient: any) {}
async getRaceStewarding(raceId: string): Promise<any> {
return { raceId, protests: [], penalties: [] };
}
}

View File

@@ -0,0 +1,13 @@
/**
* Sponsor 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 SponsorService {
constructor(private readonly apiClient: any) {}
async getSponsorById(sponsorId: string): Promise<any> {
return { id: sponsorId, name: 'Sponsor' };
}
}

View File

@@ -0,0 +1,89 @@
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
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';
export interface PendingSponsorshipRequestDto {
requestId: string;
sponsorId: string;
sponsorName: string;
message: string | null;
createdAtIso: string;
}
export interface SponsorshipRequestsReadApiDto {
sections: Array<{
entityType: 'driver' | 'team' | 'season';
entityId: string;
entityName: string;
requests: PendingSponsorshipRequestDto[];
}>;
}
export type SponsorshipRequestsReadServiceError = 'notFound' | 'unauthorized' | 'serverError';
export class SponsorshipRequestsReadService implements Service {
private readonly client: SponsorsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.client = new SponsorsApiClient(baseUrl, errorReporter, logger);
}
async getPendingRequestsForDriver(
driverId: string,
): Promise<Result<SponsorshipRequestsReadApiDto, SponsorshipRequestsReadServiceError>> {
try {
const response = await this.client.getPendingSponsorshipRequests({
entityType: 'driver',
entityId: driverId,
});
const requests = (response.requests ?? []).map((r) => {
const raw = r as unknown as {
id?: string;
requestId?: string;
sponsorId?: string;
sponsorName?: string;
message?: unknown;
createdAt?: string;
createdAtIso?: string;
};
return {
requestId: String(raw.id ?? raw.requestId ?? ''),
sponsorId: String(raw.sponsorId ?? ''),
sponsorName: String(raw.sponsorName ?? 'Sponsor'),
message: typeof raw.message === 'string' ? raw.message : null,
createdAtIso: String(raw.createdAt ?? raw.createdAtIso ?? ''),
};
});
return Result.ok({
sections: [
{
entityType: 'driver',
entityId: driverId,
entityName: 'Your Profile',
requests,
},
],
});
} catch (error) {
const errorAny = error as { statusCode?: number; message?: string };
if (errorAny.statusCode === 401) return Result.err('unauthorized');
if (errorAny.statusCode === 404) return Result.err('notFound');
return Result.err('serverError');
}
}
}

View File

@@ -0,0 +1,69 @@
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
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';
export interface AcceptSponsorshipRequestCommand {
requestId: string;
actorDriverId: 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;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: isProductionEnvironment(),
});
this.client = new SponsorsApiClient(baseUrl, errorReporter, logger);
}
async acceptRequest(
command: AcceptSponsorshipRequestCommand,
): Promise<Result<void, AcceptSponsorshipRequestServiceError>> {
try {
await this.client.acceptSponsorshipRequest(command.requestId, {
actorDriverId: command.actorDriverId,
} as any);
return Result.ok(undefined);
} catch {
return Result.err('ACCEPT_SPONSORSHIP_REQUEST_FAILED');
}
}
async rejectRequest(
command: RejectSponsorshipRequestCommand,
): Promise<Result<void, RejectSponsorshipRequestServiceError>> {
try {
await this.client.rejectSponsorshipRequest(command.requestId, {
actorDriverId: command.actorDriverId,
reason: command.reason ?? undefined,
} as any);
return Result.ok(undefined);
} catch {
return Result.err('REJECT_SPONSORSHIP_REQUEST_FAILED');
}
}
}

View File

@@ -0,0 +1,17 @@
/**
* 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) {}
async getTeamById(teamId: string): Promise<any> {
return { id: teamId, name: 'Team' };
}
async getTeamsByLeagueId(leagueId: string): Promise<any> {
return { teams: [] };
}
}