website refactor
This commit is contained in:
@@ -6,14 +6,15 @@
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { LoginPageDTO } from './types/LoginPageDTO';
|
||||
import { ForgotPasswordPageDTO } from './types/ForgotPasswordPageDTO';
|
||||
import { ResetPasswordPageDTO } from './types/ResetPasswordPageDTO';
|
||||
import { SignupPageDTO } from './types/SignupPageDTO';
|
||||
import { AuthPageParams } from './AuthPageParams';
|
||||
|
||||
export class AuthPageService {
|
||||
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, string>> {
|
||||
export class AuthPageService implements Service {
|
||||
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, DomainError>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/dashboard';
|
||||
const hasInsufficientPermissions = params.returnTo !== null;
|
||||
@@ -23,38 +24,38 @@ export class AuthPageService {
|
||||
hasInsufficientPermissions,
|
||||
});
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process login parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process login parameters' });
|
||||
}
|
||||
}
|
||||
|
||||
async processForgotPasswordParams(params: AuthPageParams): Promise<Result<ForgotPasswordPageDTO, string>> {
|
||||
async processForgotPasswordParams(params: AuthPageParams): Promise<Result<ForgotPasswordPageDTO, DomainError>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/auth/login';
|
||||
return Result.ok({ returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process forgot password parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process forgot password parameters' });
|
||||
}
|
||||
}
|
||||
|
||||
async processResetPasswordParams(params: AuthPageParams): Promise<Result<ResetPasswordPageDTO, string>> {
|
||||
async processResetPasswordParams(params: AuthPageParams): Promise<Result<ResetPasswordPageDTO, DomainError>> {
|
||||
try {
|
||||
const token = params.token;
|
||||
if (!token) {
|
||||
return Result.err('Missing reset token');
|
||||
return Result.err({ type: 'validation', message: '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');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process reset password parameters' });
|
||||
}
|
||||
}
|
||||
|
||||
async processSignupParams(params: AuthPageParams): Promise<Result<SignupPageDTO, string>> {
|
||||
async processSignupParams(params: AuthPageParams): Promise<Result<SignupPageDTO, DomainError>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/onboarding';
|
||||
return Result.ok({ returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process signup parameters');
|
||||
return Result.err({ type: 'unknown', message: 'Failed to process signup parameters' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,87 @@
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
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';
|
||||
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import type { LoginParamsDTO } from '@/lib/types/generated/LoginParamsDTO';
|
||||
import type { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
|
||||
import type { ForgotPasswordDTO } from '@/lib/types/generated/ForgotPasswordDTO';
|
||||
import type { ResetPasswordDTO } from '@/lib/types/generated/ResetPasswordDTO';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
|
||||
/**
|
||||
* Auth Service
|
||||
*
|
||||
* Orchestrates authentication operations.
|
||||
* Calls AuthApiClient for API calls.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
|
||||
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 {
|
||||
export class AuthService implements Service {
|
||||
private apiClient: AuthApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
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 login(params: LoginParamsDTO): Promise<Result<AuthSessionDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.login(params);
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unauthorized', message: (error as Error).message || 'Login failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async signup(params: SignupParamsDTO): Promise<SessionViewModel> {
|
||||
const dto = await this.apiClient.signup(params);
|
||||
return new SessionViewModel(dto.user);
|
||||
async signup(params: SignupParamsDTO): Promise<Result<AuthSessionDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.signup(params);
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Signup failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.apiClient.logout();
|
||||
async logout(): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.apiClient.logout();
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Logout failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async forgotPassword(params: ForgotPasswordDTO): Promise<{ message: string; magicLink?: string }> {
|
||||
return await this.apiClient.forgotPassword(params);
|
||||
async forgotPassword(params: ForgotPasswordDTO): Promise<Result<{ message: string; magicLink?: string }, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.forgotPassword(params);
|
||||
return Result.ok(result);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Forgot password request failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(params: ResetPasswordDTO): Promise<{ message: string }> {
|
||||
return await this.apiClient.resetPassword(params);
|
||||
async resetPassword(params: ResetPasswordDTO): Promise<Result<{ message: string }, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.resetPassword(params);
|
||||
return Result.ok(result);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Reset password failed' });
|
||||
}
|
||||
}
|
||||
|
||||
async getSession(): Promise<Result<AuthSessionDTO | null, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getSession();
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch session' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { AuthService } from './AuthService';
|
||||
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
|
||||
/**
|
||||
* Session Service
|
||||
*
|
||||
* Returns SessionViewModel for client consumption.
|
||||
* Orchestrates session-related operations.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
export class SessionService {
|
||||
constructor(
|
||||
private readonly apiClient: AuthApiClient
|
||||
) {}
|
||||
export class SessionService implements Service {
|
||||
private authService: AuthService;
|
||||
|
||||
constructor() {
|
||||
this.authService = new AuthService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user session (returns ViewModel)
|
||||
* Get current user session
|
||||
*/
|
||||
async getSession(): Promise<SessionViewModel | null> {
|
||||
const dto = await this.apiClient.getSession();
|
||||
if (!dto) return null;
|
||||
return new SessionViewModel(dto.user);
|
||||
async getSession(): Promise<Result<AuthSessionDTO | null, DomainError>> {
|
||||
return this.authService.getSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDT
|
||||
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
/**
|
||||
* Driver Service - DTO Only
|
||||
@@ -11,58 +16,97 @@ import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverP
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class DriverService {
|
||||
constructor(
|
||||
private readonly apiClient: DriversApiClient
|
||||
) {}
|
||||
export class DriverService implements Service {
|
||||
private readonly apiClient: DriversApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger);
|
||||
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver leaderboard (returns DTO)
|
||||
*/
|
||||
async getDriverLeaderboard() {
|
||||
return this.apiClient.getLeaderboard();
|
||||
async getDriverLeaderboard(): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getLeaderboard();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get leaderboard' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete driver onboarding (returns DTO)
|
||||
*/
|
||||
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
|
||||
return this.apiClient.completeOnboarding(input);
|
||||
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<Result<CompleteOnboardingOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.completeOnboarding(input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to complete onboarding' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current driver (returns DTO)
|
||||
*/
|
||||
async getCurrentDriver(): Promise<DriverDTO | null> {
|
||||
return this.apiClient.getCurrent();
|
||||
async getCurrentDriver(): Promise<Result<DriverDTO | null, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getCurrent();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get current driver' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver profile (returns DTO)
|
||||
*/
|
||||
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
|
||||
return this.apiClient.getDriverProfile(driverId);
|
||||
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getDriverProfile(driverId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get driver profile' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current driver profile (returns DTO)
|
||||
*/
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverDTO> {
|
||||
return this.apiClient.updateProfile(updates);
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<Result<DriverDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.updateProfile(updates);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update profile' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find driver by ID (returns DTO)
|
||||
*/
|
||||
async findById(id: string): Promise<GetDriverOutputDTO | null> {
|
||||
return this.apiClient.getDriver(id);
|
||||
async findById(id: string): Promise<Result<GetDriverOutputDTO | null, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getDriver(id);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find driver' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)));
|
||||
return drivers.filter((d): d is GetDriverOutputDTO => d !== null);
|
||||
async findByIds(ids: string[]): Promise<Result<GetDriverOutputDTO[], DomainError>> {
|
||||
try {
|
||||
const drivers = await Promise.all(ids.map(id => this.apiClient.getDriver(id)));
|
||||
return Result.ok(drivers.filter((d): d is GetDriverOutputDTO => d !== null));
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find drivers' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import type { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import type { GetLiveriesOutputDTO } from '@/lib/types/tbd/GetLiveriesOutputDTO';
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import type { GetLiveriesOutputDTO } from '@/lib/types/tbd/GetLiveriesOutputDTO'
|
||||
* Provides livery management functionality.
|
||||
*/
|
||||
export class LiveryService implements Service {
|
||||
async getLiveries(driverId: string): Promise<Result<GetLiveriesOutputDTO, string>> {
|
||||
async getLiveries(_: string): Promise<Result<GetLiveriesOutputDTO, DomainError>> {
|
||||
// Mock data for now
|
||||
const mockLiveries: GetLiveriesOutputDTO = {
|
||||
liveries: [
|
||||
@@ -31,7 +31,7 @@ export class LiveryService implements Service {
|
||||
return Result.ok(mockLiveries);
|
||||
}
|
||||
|
||||
async uploadLivery(driverId: string, file: File): Promise<Result<{ liveryId: string }, string>> {
|
||||
async uploadLivery(__: string, ___: File): Promise<Result<{ liveryId: string }, DomainError>> {
|
||||
// Mock implementation
|
||||
return Result.ok({ liveryId: 'new-livery-id' });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteServerEnv } from '@/lib/config/env';
|
||||
|
||||
export interface ErrorStats {
|
||||
totalErrors: number;
|
||||
@@ -26,59 +29,71 @@ export interface ErrorStats {
|
||||
};
|
||||
}
|
||||
|
||||
export function getErrorAnalyticsStats(): ErrorStats {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
export class ErrorAnalyticsService implements Service {
|
||||
static getErrorAnalyticsStats(): ErrorStats {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const errorHistory = globalHandler.getErrorHistory();
|
||||
const errorStats = globalHandler.getStats();
|
||||
const apiStats = apiLogger.getStats();
|
||||
const errorHistory = globalHandler.getErrorHistory();
|
||||
const errorStats = globalHandler.getStats();
|
||||
const apiStats = apiLogger.getStats();
|
||||
|
||||
// Group errors by time (last 10 minutes)
|
||||
const timeGroups = new Map<string, number>();
|
||||
const now = Date.now();
|
||||
const tenMinutesAgo = now - (10 * 60 * 1000);
|
||||
// Group errors by time (last 10 minutes)
|
||||
const timeGroups = new Map<string, number>();
|
||||
const now = Date.now();
|
||||
const tenMinutesAgo = now - (10 * 60 * 1000);
|
||||
|
||||
errorHistory.forEach(entry => {
|
||||
const entryTime = new Date(entry.timestamp).getTime();
|
||||
if (entryTime >= tenMinutesAgo) {
|
||||
const timeKey = new Date(entry.timestamp).toLocaleTimeString();
|
||||
timeGroups.set(timeKey, (timeGroups.get(timeKey) || 0) + 1);
|
||||
errorHistory.forEach(entry => {
|
||||
const entryTime = new Date(entry.timestamp).getTime();
|
||||
if (entryTime >= tenMinutesAgo) {
|
||||
const timeKey = new Date(entry.timestamp).toLocaleTimeString();
|
||||
timeGroups.set(timeKey, (timeGroups.get(timeKey) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
const errorsByTime = Array.from(timeGroups.entries())
|
||||
.map(([time, count]) => ({ time, count }))
|
||||
.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
const recentErrors = errorHistory.slice(-10).reverse().map(entry => ({
|
||||
timestamp: entry.timestamp,
|
||||
message: entry.error.message,
|
||||
type: entry.error instanceof ApiError ? entry.error.type : entry.error.name || 'Error',
|
||||
context: entry.context,
|
||||
}));
|
||||
|
||||
const slowestRequests = apiLogger.getSlowestRequests(5).map(log => ({
|
||||
url: log.url,
|
||||
duration: log.response?.duration || 0,
|
||||
}));
|
||||
|
||||
const env = getWebsiteServerEnv();
|
||||
|
||||
return {
|
||||
totalErrors: errorStats.total,
|
||||
errorsByType: errorStats.byType,
|
||||
errorsByTime,
|
||||
recentErrors,
|
||||
apiStats: {
|
||||
totalRequests: apiStats.total,
|
||||
successful: apiStats.successful,
|
||||
failed: apiStats.failed,
|
||||
averageDuration: apiStats.averageDuration,
|
||||
slowestRequests,
|
||||
},
|
||||
environment: {
|
||||
mode: env.NODE_ENV || 'unknown',
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getErrorAnalyticsStats(): Promise<Result<ErrorStats, DomainError>> {
|
||||
try {
|
||||
return Result.ok(ErrorAnalyticsService.getErrorAnalyticsStats());
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get error analytics stats' });
|
||||
}
|
||||
});
|
||||
|
||||
const errorsByTime = Array.from(timeGroups.entries())
|
||||
.map(([time, count]) => ({ time, count }))
|
||||
.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
const recentErrors = errorHistory.slice(-10).reverse().map(entry => ({
|
||||
timestamp: entry.timestamp,
|
||||
message: entry.error.message,
|
||||
type: entry.error instanceof ApiError ? entry.error.type : entry.error.name || 'Error',
|
||||
context: entry.context,
|
||||
}));
|
||||
|
||||
const slowestRequests = apiLogger.getSlowestRequests(5).map(log => ({
|
||||
url: log.url,
|
||||
duration: log.response?.duration || 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
totalErrors: errorStats.total,
|
||||
errorsByType: errorStats.byType,
|
||||
errorsByTime,
|
||||
recentErrors,
|
||||
apiStats: {
|
||||
totalRequests: apiStats.total,
|
||||
successful: apiStats.successful,
|
||||
failed: apiStats.failed,
|
||||
averageDuration: apiStats.averageDuration,
|
||||
slowestRequests,
|
||||
},
|
||||
environment: {
|
||||
mode: process.env.NODE_ENV || 'unknown',
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
75
apps/website/lib/services/home/HomeService.ts
Normal file
75
apps/website/lib/services/home/HomeService.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
|
||||
|
||||
// API Clients
|
||||
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';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
|
||||
// DTO types
|
||||
import type { HomeViewData } from '@/templates/HomeTemplate';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
|
||||
export class HomeService {
|
||||
async getHomeData(): Promise<Result<HomeViewData, Error>> {
|
||||
try {
|
||||
// Manual wiring: construct dependencies explicitly
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
|
||||
// Construct API clients
|
||||
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Get feature flags
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const isAlpha = featureService.isEnabled('alpha_features');
|
||||
|
||||
// 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 Result.ok({
|
||||
isAlpha,
|
||||
upcomingRaces: racesDto.races.slice(0, 4).map(r => ({
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
formattedDate: new Date(r.scheduledAt).toLocaleDateString(),
|
||||
})),
|
||||
topLeagues: leaguesDto.leagues.slice(0, 4).map(l => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
description: l.description,
|
||||
})),
|
||||
teams: teamsDto.teams.slice(0, 4).map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
logoUrl: t.logoUrl,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
return Result.err(err instanceof Error ? err : new Error('Failed to get home data'));
|
||||
}
|
||||
}
|
||||
|
||||
async shouldRedirectToDashboard(): Promise<boolean> {
|
||||
const sessionService = new SessionService();
|
||||
const sessionResult = await sessionService.getSession();
|
||||
return sessionResult.isOk() && !!sessionResult.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
|
||||
|
||||
// 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(routes.protected.dashboard);
|
||||
}
|
||||
|
||||
// Get feature flags
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const isAlpha = featureService.isEnabled('alpha_features');
|
||||
|
||||
// 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: racesDto.races.slice(0, 4),
|
||||
topLeagues: leaguesDto.leagues.slice(0, 4),
|
||||
teams: teamsDto.teams.slice(0, 4),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
|
||||
/**
|
||||
* Landing Service - DTO Only
|
||||
@@ -7,14 +15,34 @@ import { DomainError } from '@/lib/contracts/services/Service';
|
||||
* 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) {}
|
||||
export class LandingService implements Service {
|
||||
private racesApi: RacesApiClient;
|
||||
private leaguesApi: LeaguesApiClient;
|
||||
private teamsApi: TeamsApiClient;
|
||||
private authApi: AuthApiClient;
|
||||
|
||||
async getLandingData(): Promise<any> {
|
||||
return { featuredLeagues: [], stats: {} };
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
this.racesApi = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
this.leaguesApi = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
this.teamsApi = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
this.authApi = new AuthApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async signup(email: string): Promise<Result<void, DomainError>> {
|
||||
async getLandingData(): Promise<Result<{ featuredLeagues: unknown[]; stats: Record<string, unknown> }, DomainError>> {
|
||||
return Result.ok({ featuredLeagues: [], stats: {} });
|
||||
}
|
||||
|
||||
async signup(_email: string): Promise<Result<void, DomainError>> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const email = _email;
|
||||
return Result.err({ type: 'notImplemented', message: 'Email signup endpoint' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import type { LeagueActivity } from '@/components/leagues/LeagueActivityFeed';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
|
||||
export function processLeagueActivities(raceList: any[], limit: number): LeagueActivity[] {
|
||||
const activities: LeagueActivity[] = [];
|
||||
|
||||
const completedRaces = raceList
|
||||
.filter((r) => r.status === 'completed')
|
||||
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
||||
.slice(0, 5);
|
||||
export class LeagueActivityService implements Service {
|
||||
processLeagueActivities(raceList: any[], limit: number): Result<LeagueActivity[], DomainError> {
|
||||
const activities: LeagueActivity[] = [];
|
||||
|
||||
const completedRaces = raceList
|
||||
.filter((r) => r.status === 'completed')
|
||||
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
||||
.slice(0, 5);
|
||||
|
||||
const upcomingRaces = raceList
|
||||
.filter((r) => r.status === 'scheduled')
|
||||
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
||||
.slice(0, 3);
|
||||
const upcomingRaces = raceList
|
||||
.filter((r) => r.status === 'scheduled')
|
||||
.sort((a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
for (const race of completedRaces) {
|
||||
activities.push({
|
||||
type: 'race_completed',
|
||||
raceId: race.id,
|
||||
raceName: `${race.track} - ${race.car}`,
|
||||
timestamp: new Date(race.scheduledAt),
|
||||
});
|
||||
for (const race of completedRaces) {
|
||||
activities.push({
|
||||
type: 'race_completed',
|
||||
raceId: race.id,
|
||||
raceName: `${race.track} - ${race.car}`,
|
||||
timestamp: new Date(race.scheduledAt),
|
||||
});
|
||||
}
|
||||
|
||||
for (const race of upcomingRaces) {
|
||||
activities.push({
|
||||
type: 'race_scheduled',
|
||||
raceId: race.id,
|
||||
raceName: `${race.track} - ${race.car}`,
|
||||
timestamp: new Date(new Date(race.scheduledAt).getTime() - 7 * 24 * 60 * 60 * 1000), // Simulate schedule announcement
|
||||
});
|
||||
}
|
||||
|
||||
// Sort all activities by timestamp
|
||||
activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
if (activities.length > limit) {
|
||||
activities.splice(limit);
|
||||
}
|
||||
|
||||
return Result.ok(activities);
|
||||
}
|
||||
|
||||
for (const race of upcomingRaces) {
|
||||
activities.push({
|
||||
type: 'race_scheduled',
|
||||
raceId: race.id,
|
||||
raceName: `${race.track} - ${race.car}`,
|
||||
timestamp: new Date(new Date(race.scheduledAt).getTime() - 7 * 24 * 60 * 60 * 1000), // Simulate schedule announcement
|
||||
});
|
||||
}
|
||||
|
||||
// Sort all activities by timestamp
|
||||
activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
if (activities.length > limit) {
|
||||
activities.splice(limit);
|
||||
}
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
@@ -1,166 +1,130 @@
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
||||
import type { LeagueMembershipDTO } from '@/lib/types/generated/LeagueMembershipDTO';
|
||||
|
||||
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
|
||||
|
||||
function getDefaultLeaguesApiClient(): LeaguesApiClient {
|
||||
if (cachedLeaguesApiClient) return cachedLeaguesApiClient;
|
||||
|
||||
const api = new ApiClient(getWebsiteApiBaseUrl());
|
||||
cachedLeaguesApiClient = api.leagues;
|
||||
return cachedLeaguesApiClient;
|
||||
export interface LeagueRosterAdminData {
|
||||
leagueId: string;
|
||||
members: LeagueRosterMemberDTO[];
|
||||
joinRequests: LeagueRosterJoinRequestDTO[];
|
||||
}
|
||||
|
||||
export class LeagueMembershipService {
|
||||
// In-memory cache for memberships (populated via API calls)
|
||||
private static leagueMemberships = new Map<string, LeagueMembership[]>();
|
||||
export class LeagueMembershipService implements Service {
|
||||
private apiClient: LeaguesApiClient;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static cachedMemberships = new Map<string, any[]>();
|
||||
|
||||
constructor(private readonly leaguesApiClient?: LeaguesApiClient) {}
|
||||
|
||||
private getClient(): LeaguesApiClient {
|
||||
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsDTO> {
|
||||
const dto = await this.getClient().getMemberships(leagueId);
|
||||
return dto;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getMembership(leagueId: string, driverId: string): any | null {
|
||||
const members = this.cachedMemberships.get(leagueId);
|
||||
if (!members) return null;
|
||||
return members.find(m => m.driverId === driverId) || null;
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||
return this.getClient().removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getLeagueMembers(leagueId: string): any[] {
|
||||
return this.cachedMemberships.get(leagueId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific membership from cache.
|
||||
*/
|
||||
static getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||
const list = this.leagueMemberships.get(leagueId);
|
||||
if (!list) return null;
|
||||
return list.find((m) => m.driverId === driverId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of a league from cache.
|
||||
*/
|
||||
static getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||
return [...(this.leagueMemberships.get(leagueId) ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache memberships for a league via API.
|
||||
*/
|
||||
static async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||
try {
|
||||
const result = await getDefaultLeaguesApiClient().getMemberships(leagueId);
|
||||
const memberships: LeagueMembership[] = (result.members ?? []).map((member) => ({
|
||||
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
|
||||
leagueId,
|
||||
driverId: member.driverId,
|
||||
role: member.role as 'owner' | 'admin' | 'steward' | 'member',
|
||||
status: 'active', // Assume active since API returns current members
|
||||
joinedAt: member.joinedAt,
|
||||
}));
|
||||
this.setLeagueMemberships(leagueId, memberships);
|
||||
return memberships;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch league memberships:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a league.
|
||||
*
|
||||
* NOTE: The API currently exposes membership mutations via league member management endpoints.
|
||||
* For now we keep the website decoupled by consuming only the API through this service.
|
||||
*/
|
||||
async joinLeague(leagueId: string, driverId: string): Promise<void> {
|
||||
// Temporary: no join endpoint exposed yet in API.
|
||||
// Keep behavior predictable for UI.
|
||||
throw new Error('Joining leagues is not available in this build.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave a league.
|
||||
*/
|
||||
async leaveLeague(leagueId: string, driverId: string): Promise<void> {
|
||||
// Temporary: no leave endpoint exposed yet in API.
|
||||
throw new Error('Leaving leagues is not available in this build.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set memberships in cache (for use after API calls).
|
||||
*/
|
||||
static setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
|
||||
this.leagueMemberships.set(leagueId, memberships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached memberships for a league.
|
||||
*/
|
||||
static clearLeagueMemberships(leagueId: string): void {
|
||||
this.leagueMemberships.delete(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get iterator for cached memberships (for utility functions).
|
||||
*/
|
||||
static getCachedMembershipsIterator(): IterableIterator<[string, LeagueMembership[]]> {
|
||||
return this.leagueMemberships.entries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all memberships for a specific driver across all leagues.
|
||||
*/
|
||||
static getAllMembershipsForDriver(driverId: string): LeagueMembership[] {
|
||||
const allMemberships: LeagueMembership[] = [];
|
||||
for (const [leagueId, memberships] of this.leagueMemberships.entries()) {
|
||||
const driverMembership = memberships.find((m) => m.driverId === driverId);
|
||||
if (driverMembership) {
|
||||
allMemberships.push(driverMembership);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getAllMembershipsForDriver(driverId: string): any[] {
|
||||
const allMemberships: any[] = [];
|
||||
for (const [leagueId, members] of this.cachedMemberships.entries()) {
|
||||
const membership = members.find(m => m.driverId === driverId);
|
||||
if (membership) {
|
||||
allMemberships.push({ ...membership, leagueId });
|
||||
}
|
||||
}
|
||||
return allMemberships;
|
||||
}
|
||||
|
||||
// Instance methods that delegate to static methods for consistency with service pattern
|
||||
async fetchLeagueMemberships(leagueId: string): Promise<void> {
|
||||
try {
|
||||
const members = await this.apiClient.getMemberships(leagueId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
LeagueMembershipService.cachedMemberships.set(leagueId, (members as any).members || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch memberships', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific membership from cache.
|
||||
*/
|
||||
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getMembership(leagueId: string, driverId: string): any | null {
|
||||
return LeagueMembershipService.getMembership(leagueId, driverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of a league from cache.
|
||||
*/
|
||||
getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getLeagueMembers(leagueId: string): any[] {
|
||||
return LeagueMembershipService.getLeagueMembers(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache memberships for a league via API.
|
||||
*/
|
||||
async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||
return LeagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
async joinLeague(_: string, __: string): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'joinLeague' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set memberships in cache (for use after API calls).
|
||||
*/
|
||||
setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
|
||||
LeagueMembershipService.setLeagueMemberships(leagueId, memberships);
|
||||
async leaveLeague(_: string, __: string): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'leaveLeague' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached memberships for a league.
|
||||
*/
|
||||
clearLeagueMemberships(leagueId: string): void {
|
||||
LeagueMembershipService.clearLeagueMemberships(leagueId);
|
||||
async getRosterAdminData(leagueId: string): Promise<Result<LeagueRosterAdminData, DomainError>> {
|
||||
try {
|
||||
const [members, joinRequests] = await Promise.all([
|
||||
this.apiClient.getAdminRosterMembers(leagueId),
|
||||
this.apiClient.getAdminRosterJoinRequests(leagueId),
|
||||
]);
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueMembershipService.getRosterAdminData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
|
||||
}
|
||||
}
|
||||
|
||||
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto';
|
||||
|
||||
export class LeagueRulebookService implements Service {
|
||||
async getRulebookData(leagueId: string): Promise<Result<RulebookApiDto, never>> {
|
||||
async getRulebookData(leagueId: string): Promise<Result<RulebookApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: RulebookApiDto = {
|
||||
leagueId,
|
||||
@@ -27,4 +27,4 @@ export class LeagueRulebookService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto';
|
||||
|
||||
export class LeagueScheduleService implements Service {
|
||||
async getScheduleData(leagueId: string): Promise<Result<LeagueScheduleApiDto, never>> {
|
||||
async getScheduleData(leagueId: string): Promise<Result<LeagueScheduleApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueScheduleApiDto = {
|
||||
leagueId,
|
||||
@@ -36,4 +36,4 @@ export class LeagueScheduleService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,27 @@ 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';
|
||||
import { getWebsiteServerEnv } from '@/lib/config/env';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/AllLeaguesWithCapacityAndScoringDTO';
|
||||
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
|
||||
|
||||
export interface LeagueScheduleAdminData {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
seasons: LeagueSeasonSummaryDTO[];
|
||||
schedule: LeagueScheduleDTO;
|
||||
}
|
||||
|
||||
export interface LeagueRosterAdminData {
|
||||
leagueId: string;
|
||||
members: LeagueRosterMemberDTO[];
|
||||
joinRequests: LeagueRosterJoinRequestDTO[];
|
||||
}
|
||||
|
||||
export interface LeagueDetailData {
|
||||
league: LeagueWithCapacityAndScoringDTO;
|
||||
apiDto: AllLeaguesWithCapacityAndScoringDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* League Service - DTO Only
|
||||
@@ -40,11 +59,10 @@ export class LeagueService implements Service {
|
||||
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',
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
// Optional clients can be initialized if needed
|
||||
@@ -54,9 +72,73 @@ export class LeagueService implements Service {
|
||||
try {
|
||||
const dto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getAllLeagues failed:', error);
|
||||
return Result.err({ type: 'serverError', message: 'Failed to fetch leagues' });
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch leagues' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueDetailData(leagueId: string): Promise<Result<LeagueDetailData, DomainError>> {
|
||||
try {
|
||||
const apiDto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
|
||||
if (!apiDto || !apiDto.leagues) {
|
||||
return Result.err({ type: 'notFound', message: 'Leagues not found' });
|
||||
}
|
||||
|
||||
const league = apiDto.leagues.find(l => l.id === leagueId);
|
||||
if (!league) {
|
||||
return Result.err({ type: 'notFound', message: 'League not found' });
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
league,
|
||||
apiDto,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getLeagueDetailData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league detail' });
|
||||
}
|
||||
}
|
||||
|
||||
async getScheduleAdminData(leagueId: string, seasonId?: string): Promise<Result<LeagueScheduleAdminData, DomainError>> {
|
||||
try {
|
||||
const seasons = await this.apiClient.getSeasons(leagueId);
|
||||
|
||||
if (!seasons || seasons.length === 0) {
|
||||
return Result.err({ type: 'notFound', message: 'No seasons found for league' });
|
||||
}
|
||||
|
||||
const targetSeasonId = seasonId || (seasons.find(s => s.status === 'active')?.seasonId || seasons[0].seasonId);
|
||||
const schedule = await this.apiClient.getSchedule(leagueId, targetSeasonId);
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
seasonId: targetSeasonId,
|
||||
seasons,
|
||||
schedule,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getScheduleAdminData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch schedule admin data' });
|
||||
}
|
||||
}
|
||||
|
||||
async getRosterAdminData(leagueId: string): Promise<Result<LeagueRosterAdminData, DomainError>> {
|
||||
try {
|
||||
const [members, joinRequests] = await Promise.all([
|
||||
this.apiClient.getAdminRosterMembers(leagueId),
|
||||
this.apiClient.getAdminRosterJoinRequests(leagueId),
|
||||
]);
|
||||
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
members,
|
||||
joinRequests,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('LeagueService.getRosterAdminData failed:', error);
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster data' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,41 +146,76 @@ export class LeagueService implements Service {
|
||||
return Result.err({ type: 'notImplemented', message: 'League standings endpoint not implemented' });
|
||||
}
|
||||
|
||||
async getLeagueStats(): Promise<TotalLeaguesDTO> {
|
||||
return this.apiClient.getTotal();
|
||||
async getLeagueStats(): Promise<Result<TotalLeaguesDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getTotal();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league stats' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
|
||||
return this.apiClient.getSchedule(leagueId);
|
||||
async getLeagueSchedule(leagueId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSchedule(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueSeasons(leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
return this.apiClient.getSeasons(leagueId);
|
||||
async getLeagueSeasons(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSeasons(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch league seasons' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueSeasonSummaries(leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
return this.apiClient.getSeasons(leagueId);
|
||||
async getLeagueSeasonSummaries(leagueId: string): Promise<Result<LeagueSeasonSummaryDTO[], DomainError>> {
|
||||
return this.getLeagueSeasons(leagueId);
|
||||
}
|
||||
|
||||
async getAdminSchedule(leagueId: string, seasonId: string): Promise<LeagueScheduleDTO> {
|
||||
return this.apiClient.getSchedule(leagueId, seasonId);
|
||||
async getAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSchedule(leagueId, seasonId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch admin schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async publishAdminSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
||||
async publishAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to publish schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async unpublishAdminSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
||||
async unpublishAdminSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to unpublish schedule' });
|
||||
}
|
||||
}
|
||||
|
||||
async createAdminScheduleRace(
|
||||
leagueId: string,
|
||||
seasonId: string,
|
||||
input: { track: string; car: string; scheduledAtIso: string },
|
||||
): Promise<CreateLeagueScheduleRaceOutputDTO> {
|
||||
const payload: CreateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
return this.apiClient.createSeasonScheduleRace(leagueId, seasonId, payload);
|
||||
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
|
||||
try {
|
||||
const payload: CreateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, payload);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateAdminScheduleRace(
|
||||
@@ -106,33 +223,48 @@ export class LeagueService implements Service {
|
||||
seasonId: string,
|
||||
raceId: string,
|
||||
input: Partial<{ track: string; car: string; scheduledAtIso: string }>,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
const payload: UpdateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
return this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, payload);
|
||||
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
try {
|
||||
const payload: UpdateLeagueScheduleRaceInputDTO = { ...input, example: '' };
|
||||
const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, payload);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAdminScheduleRace(leagueId: string, seasonId: string, raceId: string): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
return this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
||||
async deleteAdminScheduleRace(leagueId: string, seasonId: string, raceId: string): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to delete race' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueScheduleDto(leagueId: string, seasonId: string): Promise<LeagueScheduleDTO> {
|
||||
return this.apiClient.getSchedule(leagueId, seasonId);
|
||||
async getLeagueScheduleDto(leagueId: string, seasonId: string): Promise<Result<LeagueScheduleDTO, DomainError>> {
|
||||
return this.getAdminSchedule(leagueId, seasonId);
|
||||
}
|
||||
|
||||
async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.publishSeasonSchedule(leagueId, seasonId);
|
||||
async publishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
return this.publishAdminSchedule(leagueId, seasonId);
|
||||
}
|
||||
|
||||
async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<LeagueSeasonSchedulePublishOutputDTO> {
|
||||
return this.apiClient.unpublishSeasonSchedule(leagueId, seasonId);
|
||||
async unpublishLeagueSeasonSchedule(leagueId: string, seasonId: string): Promise<Result<LeagueSeasonSchedulePublishOutputDTO, DomainError>> {
|
||||
return this.unpublishAdminSchedule(leagueId, seasonId);
|
||||
}
|
||||
|
||||
async createLeagueSeasonScheduleRace(
|
||||
leagueId: string,
|
||||
seasonId: string,
|
||||
input: CreateLeagueScheduleRaceInputDTO,
|
||||
): Promise<CreateLeagueScheduleRaceOutputDTO> {
|
||||
return this.apiClient.createSeasonScheduleRace(leagueId, seasonId, input);
|
||||
): Promise<Result<CreateLeagueScheduleRaceOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.createSeasonScheduleRace(leagueId, seasonId, input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create race' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateLeagueSeasonScheduleRace(
|
||||
@@ -140,52 +272,93 @@ export class LeagueService implements Service {
|
||||
seasonId: string,
|
||||
raceId: string,
|
||||
input: UpdateLeagueScheduleRaceInputDTO,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
return this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, input);
|
||||
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.updateSeasonScheduleRace(leagueId, seasonId, raceId, input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update race' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteLeagueSeasonScheduleRace(
|
||||
leagueId: string,
|
||||
seasonId: string,
|
||||
raceId: string,
|
||||
): Promise<LeagueScheduleRaceMutationSuccessDTO> {
|
||||
return this.apiClient.deleteSeasonScheduleRace(leagueId, seasonId, raceId);
|
||||
): Promise<Result<LeagueScheduleRaceMutationSuccessDTO, DomainError>> {
|
||||
return this.deleteAdminScheduleRace(leagueId, seasonId, raceId);
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
|
||||
return this.apiClient.getMemberships(leagueId);
|
||||
async getLeagueMemberships(leagueId: string): Promise<Result<LeagueMembershipsDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getMemberships(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch memberships' });
|
||||
}
|
||||
}
|
||||
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueOutputDTO> {
|
||||
return this.apiClient.create(input);
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<Result<CreateLeagueOutputDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.create(input);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to create league' });
|
||||
}
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
||||
return { success: dto.success };
|
||||
async removeMember(leagueId: string, targetDriverId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.removeRosterMember(leagueId, targetDriverId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to remove member' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole);
|
||||
return { success: dto.success };
|
||||
async updateMemberRole(leagueId: string, targetDriverId: string, newRole: MembershipRole): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.updateRosterMemberRole(leagueId, targetDriverId, newRole);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to update member role' });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdminRosterMembers(leagueId: string): Promise<LeagueRosterMemberDTO[]> {
|
||||
return this.apiClient.getAdminRosterMembers(leagueId);
|
||||
async getAdminRosterMembers(leagueId: string): Promise<Result<LeagueRosterMemberDTO[], DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getAdminRosterMembers(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch roster members' });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdminRosterJoinRequests(leagueId: string): Promise<LeagueRosterJoinRequestDTO[]> {
|
||||
return this.apiClient.getAdminRosterJoinRequests(leagueId);
|
||||
async getAdminRosterJoinRequests(leagueId: string): Promise<Result<LeagueRosterJoinRequestDTO[], DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getAdminRosterJoinRequests(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch join requests' });
|
||||
}
|
||||
}
|
||||
|
||||
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
||||
return { success: dto.success };
|
||||
async approveJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.approveRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to approve join request' });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<{ success: boolean }> {
|
||||
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
||||
return { success: dto.success };
|
||||
async rejectJoinRequest(leagueId: string, joinRequestId: string): Promise<Result<{ success: boolean }, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.rejectRosterJoinRequest(leagueId, joinRequestId);
|
||||
return Result.ok({ success: dto.success });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to reject join request' });
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueDetail(): Promise<Result<never, DomainError>> {
|
||||
@@ -199,4 +372,4 @@ export class LeagueService implements Service {
|
||||
async getScoringPresets(): Promise<Result<never, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'Scoring presets endpoint not implemented' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { type LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
|
||||
|
||||
export class LeagueSettingsService implements Service {
|
||||
async getSettingsData(leagueId: string): Promise<Result<LeagueSettingsApiDto, never>> {
|
||||
private static cachedMemberships = new Map<string, unknown[]>();
|
||||
|
||||
async getSettingsData(leagueId: string): Promise<Result<LeagueSettingsApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueSettingsApiDto = {
|
||||
leagueId,
|
||||
@@ -25,4 +27,14 @@ export class LeagueSettingsService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
|
||||
static getCachedMembershipsIterator(): IterableIterator<[string, unknown[]]> {
|
||||
return this.cachedMemberships.entries();
|
||||
}
|
||||
|
||||
static getMembership(leagueId: string, driverId: string): unknown | null {
|
||||
const members = this.cachedMemberships.get(leagueId);
|
||||
if (!members) return null;
|
||||
return members.find((m: any) => m.driverId === driverId) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
|
||||
|
||||
export class LeagueSponsorshipsService implements Service {
|
||||
async getSponsorshipsData(leagueId: string): Promise<Result<LeagueSponsorshipsApiDto, never>> {
|
||||
async getSponsorshipsData(leagueId: string): Promise<Result<LeagueSponsorshipsApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueSponsorshipsApiDto = {
|
||||
leagueId,
|
||||
@@ -56,4 +56,4 @@ export class LeagueSponsorshipsService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { LeagueStandingsApiDto, LeagueMembershipsApiDto } from '@/lib/types/tbd/LeagueStandingsApiDto';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { type LeagueStandingsApiDto, type LeagueMembershipsApiDto } from '@/lib/types/tbd/LeagueStandingsApiDto';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
@@ -18,7 +18,7 @@ export class LeagueStandingsService implements Service {
|
||||
);
|
||||
}
|
||||
|
||||
async getStandingsData(leagueId: string): Promise<Result<{ standings: LeagueStandingsApiDto; memberships: LeagueMembershipsApiDto }, never>> {
|
||||
async getStandingsData(_: string): Promise<Result<{ standings: LeagueStandingsApiDto; memberships: LeagueMembershipsApiDto }, DomainError>> {
|
||||
// Mock data since backend may not be implemented
|
||||
const mockStandings: LeagueStandingsApiDto = {
|
||||
standings: [
|
||||
@@ -86,4 +86,4 @@ export class LeagueStandingsService implements Service {
|
||||
|
||||
return Result.ok({ standings: mockStandings, memberships: mockMemberships });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
import { type StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
|
||||
|
||||
export class LeagueStewardingService implements Service {
|
||||
async getStewardingData(leagueId: string): Promise<Result<StewardingApiDto, never>> {
|
||||
async getStewardingData(leagueId: string): Promise<Result<StewardingApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: StewardingApiDto = {
|
||||
leagueId,
|
||||
@@ -15,4 +15,8 @@ export class LeagueStewardingService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
|
||||
async getProtestDetailViewModel(_: string, __: string): Promise<Result<any, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'getProtestDetailViewModel' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
|
||||
|
||||
export class LeagueWalletService implements Service {
|
||||
async getWalletData(leagueId: string): Promise<Result<LeagueWalletApiDto, never>> {
|
||||
async getWalletForLeague(leagueId: string): Promise<LeagueWalletApiDto> {
|
||||
const result = await this.getWalletData(leagueId);
|
||||
if (result.isErr()) throw new Error(result.getError().message);
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
async withdraw(
|
||||
leagueId: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
seasonId: string,
|
||||
destinationId: string
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
// Mock implementation
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getWalletData(leagueId: string): Promise<Result<LeagueWalletApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: LeagueWalletApiDto = {
|
||||
leagueId,
|
||||
@@ -46,4 +63,4 @@ export class LeagueWalletService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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';
|
||||
import { Service, type DomainError } from '@/lib/contracts/services/Service';
|
||||
|
||||
type ProfileLeaguesServiceError = 'notFound' | 'redirect' | 'unknown';
|
||||
|
||||
interface ProfileLeaguesPageDto {
|
||||
export interface ProfileLeaguesPageDto {
|
||||
ownedLeagues: Array<{
|
||||
leagueId: string;
|
||||
name: string;
|
||||
@@ -27,7 +25,7 @@ interface MembershipDTO {
|
||||
}
|
||||
|
||||
export class ProfileLeaguesService implements Service {
|
||||
async getProfileLeagues(driverId: string): Promise<Result<ProfileLeaguesPageDto, ProfileLeaguesServiceError>> {
|
||||
async getProfileLeagues(driverId: string): Promise<Result<ProfileLeaguesPageDto, DomainError>> {
|
||||
try {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const apiClient = new ApiClient(baseUrl);
|
||||
@@ -35,7 +33,7 @@ export class ProfileLeaguesService implements Service {
|
||||
const leaguesDto = await apiClient.leagues.getAllWithCapacity();
|
||||
|
||||
if (!leaguesDto?.leagues) {
|
||||
return Result.err('notFound');
|
||||
return Result.err({ type: 'notFound', message: 'Leagues not found' });
|
||||
}
|
||||
|
||||
// Fetch all memberships in parallel
|
||||
@@ -80,18 +78,18 @@ export class ProfileLeaguesService implements Service {
|
||||
ownedLeagues,
|
||||
memberLeagues,
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
|
||||
if (errorAny.statusCode === 404 || errorAny.message?.toLowerCase().includes('not found')) {
|
||||
return Result.err('notFound');
|
||||
return Result.err({ type: 'notFound', message: 'Profile leagues not found' });
|
||||
}
|
||||
|
||||
if (errorAny.statusCode === 302 || errorAny.message?.toLowerCase().includes('redirect')) {
|
||||
return Result.err('redirect');
|
||||
return Result.err({ type: 'unauthorized', message: 'Unauthorized access' });
|
||||
}
|
||||
|
||||
return Result.err('unknown');
|
||||
return Result.err({ type: 'unknown', message: error.message || 'Failed to fetch profile leagues' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { Service } from '@/lib/contracts/services/Service';
|
||||
import { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { ProtestDetailApiDto } from '@/lib/types/tbd/ProtestDetailApiDto';
|
||||
|
||||
export class ProtestDetailService implements Service {
|
||||
async getProtestDetail(leagueId: string, protestId: string): Promise<Result<ProtestDetailApiDto, never>> {
|
||||
async getProtestDetail(leagueId: string, protestId: string): Promise<Result<ProtestDetailApiDto, DomainError>> {
|
||||
// Mock data since backend not implemented
|
||||
const mockData: ProtestDetailApiDto = {
|
||||
id: protestId,
|
||||
@@ -35,4 +35,4 @@ export class ProtestDetailService implements Service {
|
||||
};
|
||||
return Result.ok(mockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
|
||||
/**
|
||||
* 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) {}
|
||||
export class PaymentService implements Service {
|
||||
constructor() {}
|
||||
|
||||
async getPaymentById(paymentId: string): Promise<any> {
|
||||
return { id: paymentId, amount: 0 };
|
||||
async getPaymentById(paymentId: string): Promise<Result<{ id: string; amount: number }, DomainError>> {
|
||||
return Result.ok({ id: paymentId, amount: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
|
||||
/**
|
||||
* 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) {}
|
||||
export class WalletService implements Service {
|
||||
constructor() {}
|
||||
|
||||
async getWalletBalance(driverId: string): Promise<any> {
|
||||
return { balance: 0, currency: 'USD' };
|
||||
async getWalletBalance(_: string): Promise<Result<{ balance: number; currency: string }, DomainError>> {
|
||||
return Result.ok({ balance: 0, currency: 'USD' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { PenaltiesApiClient } from '@/lib/api/penalties/PenaltiesApiClient';
|
||||
import type { PenaltyTypesReferenceDTO } from '@/lib/types/PenaltyTypesReferenceDTO';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
/**
|
||||
* Penalty Service
|
||||
@@ -7,30 +12,50 @@ import type { PenaltyTypesReferenceDTO } from '@/lib/types/PenaltyTypesReference
|
||||
* Orchestrates penalty operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class PenaltyService {
|
||||
constructor(
|
||||
private readonly apiClient: PenaltiesApiClient
|
||||
) {}
|
||||
export class PenaltyService implements Service {
|
||||
private readonly apiClient: PenaltiesApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger);
|
||||
this.apiClient = new PenaltiesApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find penalties by race ID
|
||||
*/
|
||||
async findByRaceId(raceId: string): Promise<any[]> {
|
||||
const dto = await this.apiClient.getRacePenalties(raceId);
|
||||
return dto.penalties;
|
||||
async findByRaceId(raceId: string): Promise<Result<unknown[], DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getRacePenalties(raceId);
|
||||
return Result.ok(dto.penalties);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find penalties' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed penalty types and semantics
|
||||
*/
|
||||
async getPenaltyTypesReference(): Promise<PenaltyTypesReferenceDTO> {
|
||||
return this.apiClient.getPenaltyTypesReference();
|
||||
async getPenaltyTypesReference(): Promise<Result<PenaltyTypesReferenceDTO, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getPenaltyTypesReference();
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get penalty types' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a penalty
|
||||
*/
|
||||
async applyPenalty(input: any): Promise<void> {
|
||||
await this.apiClient.applyPenalty(input);
|
||||
async applyPenalty(input: unknown): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await this.apiClient.applyPenalty(input as any);
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to apply penalty' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { FeatureState, PolicyApiClient, PolicySnapshotDto } from '@/lib/api/policy/PolicyApiClient';
|
||||
import { PolicyApiClient, type FeatureState, type PolicySnapshotDto } from '@/lib/api/policy/PolicyApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
export interface CapabilityEvaluationResult {
|
||||
isLoading: boolean;
|
||||
@@ -8,11 +13,23 @@ export interface CapabilityEvaluationResult {
|
||||
shouldShowComingSoon: boolean;
|
||||
}
|
||||
|
||||
export class PolicyService {
|
||||
constructor(private readonly apiClient: PolicyApiClient) {}
|
||||
export class PolicyService implements Service {
|
||||
private readonly apiClient: PolicyApiClient;
|
||||
|
||||
getSnapshot(): Promise<PolicySnapshotDto> {
|
||||
return this.apiClient.getSnapshot();
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger);
|
||||
this.apiClient = new PolicyApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getSnapshot(): Promise<Result<PolicySnapshotDto, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getSnapshot();
|
||||
return Result.ok(data);
|
||||
} catch (error: any) {
|
||||
return Result.err({ type: 'serverError', message: error.message || 'Failed to get policy snapshot' });
|
||||
}
|
||||
}
|
||||
|
||||
getCapabilityState(snapshot: PolicySnapshotDto, capabilityKey: string): FeatureState {
|
||||
@@ -79,4 +96,4 @@ export class PolicyService {
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
/**
|
||||
* Protest Service - DTO Only
|
||||
@@ -12,30 +14,67 @@ import type { ReviewProtestCommandDTO } from '@/lib/types/generated/ReviewProtes
|
||||
* 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) {}
|
||||
export class ProtestService implements Service {
|
||||
private readonly apiClient: ProtestsApiClient;
|
||||
|
||||
async getLeagueProtests(leagueId: string): Promise<any> {
|
||||
return this.apiClient.getLeagueProtests(leagueId);
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger);
|
||||
this.apiClient = new ProtestsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getProtestById(leagueId: string, protestId: string): Promise<any> {
|
||||
return this.apiClient.getLeagueProtest(leagueId, protestId);
|
||||
async getLeagueProtests(leagueId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getLeagueProtests(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get league protests' });
|
||||
}
|
||||
}
|
||||
|
||||
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
return this.apiClient.applyPenalty(input);
|
||||
async getProtestById(leagueId: string, protestId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getLeagueProtest(leagueId, protestId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to get protest' });
|
||||
}
|
||||
}
|
||||
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
return this.apiClient.requestDefense(input);
|
||||
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.apiClient.applyPenalty(input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to apply penalty' });
|
||||
}
|
||||
}
|
||||
|
||||
async reviewProtest(input: ReviewProtestCommandDTO): Promise<void> {
|
||||
return this.apiClient.reviewProtest(input);
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.apiClient.requestDefense(input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to request defense' });
|
||||
}
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<any> {
|
||||
return this.apiClient.getRaceProtests(raceId);
|
||||
async reviewProtest(input: ReviewProtestCommandDTO): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.apiClient.reviewProtest(input);
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to review protest' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getRaceProtests(raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to find protests' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
import { DomainError, Service } 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';
|
||||
@@ -12,7 +12,7 @@ import { ApiError } from '@/lib/api/base/ApiError';
|
||||
* Orchestration service for race results operations.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
export class RaceResultsService {
|
||||
export class RaceResultsService implements Service {
|
||||
private apiClient: RacesApiClient;
|
||||
|
||||
constructor() {
|
||||
@@ -28,11 +28,11 @@ export class RaceResultsService {
|
||||
* Get race results detail
|
||||
* Returns results for a specific race
|
||||
*/
|
||||
async getRaceResultsDetail(raceId: string): Promise<Result<any, DomainError>> {
|
||||
async getRaceResultsDetail(raceId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getResultsDetail(raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
@@ -41,7 +41,7 @@ export class RaceResultsService {
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch race results'
|
||||
message: (error as Error).message || 'Failed to fetch race results'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,11 @@ export class RaceResultsService {
|
||||
* Get race with strength of field
|
||||
* Returns race data with SOF calculation
|
||||
*/
|
||||
async getWithSOF(raceId: string): Promise<Result<any, DomainError>> {
|
||||
async getWithSOF(raceId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getWithSOF(raceId);
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
@@ -63,7 +63,7 @@ export class RaceResultsService {
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch race SOF'
|
||||
message: (error as Error).message || 'Failed to fetch race SOF'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -84,4 +84,4 @@ export class RaceResultsService {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } 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 Service - DTO Only
|
||||
@@ -6,20 +12,83 @@ import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
* 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) {}
|
||||
export class RaceService implements Service {
|
||||
private 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');
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
this.apiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getRacesByLeagueId(leagueId: string): Promise<any> {
|
||||
return this.apiClient.getPageData(leagueId);
|
||||
async getRaceById(raceId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
// This would need a driverId, but for now we'll use a placeholder
|
||||
const data = await this.apiClient.getDetail(raceId, 'placeholder-driver-id');
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err(this.mapError(error, 'Failed to fetch race by ID'));
|
||||
}
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<any[]> {
|
||||
const result = await this.apiClient.getPageData(leagueId);
|
||||
return result.races || [];
|
||||
async getRacesByLeagueId(leagueId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const data = await this.apiClient.getPageData(leagueId);
|
||||
return Result.ok(data);
|
||||
} catch (error: unknown) {
|
||||
return Result.err(this.mapError(error, 'Failed to fetch races by league ID'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Result<unknown[], DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.getPageData(leagueId);
|
||||
return Result.ok(result.races || []);
|
||||
} catch (error: unknown) {
|
||||
return Result.err(this.mapError(error, 'Failed to find races by league ID'));
|
||||
}
|
||||
}
|
||||
|
||||
async registerForRace(_: string, __: string, ___: string): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'registerForRace' });
|
||||
}
|
||||
|
||||
async withdrawFromRace(_: string, __: string): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'withdrawFromRace' });
|
||||
}
|
||||
|
||||
async fileProtest(__: unknown): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'fileProtest' });
|
||||
}
|
||||
|
||||
private mapError(error: unknown, defaultMessage: string): DomainError {
|
||||
if (error instanceof ApiError) {
|
||||
return {
|
||||
type: this.mapApiErrorType(error.type),
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'unknown',
|
||||
message: defaultMessage
|
||||
};
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { DomainError, Service } 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';
|
||||
@@ -14,7 +14,7 @@ import { ApiError } from '@/lib/api/base/ApiError';
|
||||
* Orchestration service for race stewarding operations.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
*/
|
||||
export class RaceStewardingService {
|
||||
export class RaceStewardingService implements Service {
|
||||
private racesApiClient: RacesApiClient;
|
||||
private protestsApiClient: ProtestsApiClient;
|
||||
private penaltiesApiClient: PenaltiesApiClient;
|
||||
@@ -34,7 +34,7 @@ export class RaceStewardingService {
|
||||
* Get race stewarding data
|
||||
* Returns protests and penalties for a race
|
||||
*/
|
||||
async getRaceStewarding(raceId: string): Promise<Result<any, DomainError>> {
|
||||
async getRaceStewarding(raceId: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
// Fetch data in parallel
|
||||
const [raceDetail, protests, penalties] = await Promise.all([
|
||||
@@ -44,6 +44,7 @@ export class RaceStewardingService {
|
||||
]);
|
||||
|
||||
// Transform data to match view model structure
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const protestsData = protests.protests.map((p: any) => ({
|
||||
id: p.id,
|
||||
protestingDriverId: p.protestingDriverId,
|
||||
@@ -56,7 +57,9 @@ export class RaceStewardingService {
|
||||
status: p.status,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pendingProtests = protestsData.filter((p: any) => p.status === 'pending' || p.status === 'under_review');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resolvedProtests = protestsData.filter((p: any) =>
|
||||
p.status === 'upheld' ||
|
||||
p.status === 'dismissed' ||
|
||||
@@ -77,7 +80,7 @@ export class RaceStewardingService {
|
||||
};
|
||||
|
||||
return Result.ok(data);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ApiError) {
|
||||
return Result.err({
|
||||
type: this.mapApiErrorType(error.type),
|
||||
@@ -86,7 +89,7 @@ export class RaceStewardingService {
|
||||
}
|
||||
return Result.err({
|
||||
type: 'unknown',
|
||||
message: 'Failed to fetch stewarding data'
|
||||
message: (error as Error).message || 'Failed to fetch stewarding data'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -107,4 +110,4 @@ export class RaceStewardingService {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export class SponsorService implements Service {
|
||||
return Result.err({ type: 'notFound', message: 'Sponsor not found' });
|
||||
}
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'unknown', message: 'Failed to get sponsor' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to get sponsor' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ export class SponsorService implements Service {
|
||||
return Result.err({ type: 'notFound', message: 'Dashboard not found' });
|
||||
}
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'notImplemented', message: 'getSponsorDashboard' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'notImplemented', message: (error as Error).message || 'getSponsorDashboard' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +66,12 @@ export class SponsorService implements Service {
|
||||
return Result.err({ type: 'notFound', message: 'Sponsorships not found' });
|
||||
}
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'notImplemented', message: 'getSponsorSponsorships' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'notImplemented', message: (error as Error).message || 'getSponsorSponsorships' });
|
||||
}
|
||||
}
|
||||
|
||||
async getBilling(): Promise<Result<SponsorBillingDTO, DomainError>> {
|
||||
async getBilling(_: string): Promise<Result<SponsorBillingDTO, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'getBilling' });
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@ export class SponsorService implements Service {
|
||||
try {
|
||||
await this.apiClient.acceptSponsorshipRequest(requestId, { respondedBy: sponsorId });
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'unknown', message: 'Failed to accept sponsorship request' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to accept sponsorship request' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ export class SponsorService implements Service {
|
||||
try {
|
||||
await this.apiClient.rejectSponsorshipRequest(requestId, { respondedBy: sponsorId, reason });
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'unknown', message: 'Failed to reject sponsorship request' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to reject sponsorship request' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ export class SponsorService implements Service {
|
||||
try {
|
||||
const result = await this.apiClient.getPendingSponsorshipRequests(input);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'notImplemented', message: 'getPendingSponsorshipRequests' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'notImplemented', message: (error as Error).message || 'getPendingSponsorshipRequests' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 type { Service, DomainError } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
@@ -23,8 +23,6 @@ export interface SponsorshipRequestsReadApiDto {
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SponsorshipRequestsReadServiceError = 'notFound' | 'unauthorized' | 'serverError';
|
||||
|
||||
export class SponsorshipRequestsReadService implements Service {
|
||||
private readonly client: SponsorsApiClient;
|
||||
|
||||
@@ -42,7 +40,7 @@ export class SponsorshipRequestsReadService implements Service {
|
||||
|
||||
async getPendingRequestsForDriver(
|
||||
driverId: string,
|
||||
): Promise<Result<SponsorshipRequestsReadApiDto, SponsorshipRequestsReadServiceError>> {
|
||||
): Promise<Result<SponsorshipRequestsReadApiDto, DomainError>> {
|
||||
try {
|
||||
const response = await this.client.getPendingSponsorshipRequests({
|
||||
entityType: 'driver',
|
||||
@@ -79,11 +77,11 @@ export class SponsorshipRequestsReadService implements Service {
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
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');
|
||||
if (errorAny.statusCode === 401) return Result.err({ type: 'unauthorized', message: 'Unauthorized' });
|
||||
if (errorAny.statusCode === 404) return Result.err({ type: 'notFound', message: 'Not found' });
|
||||
return Result.err({ type: 'serverError', message: error.message || 'Server error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { Service, DomainError } 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';
|
||||
@@ -31,7 +31,7 @@ export class SponsorshipRequestsService implements Service {
|
||||
|
||||
async getPendingRequests(
|
||||
input: GetPendingRequestsInput,
|
||||
): Promise<Result<GetPendingSponsorshipRequestsOutputDTO, 'GET_PENDING_REQUESTS_FAILED'>> {
|
||||
): Promise<Result<GetPendingSponsorshipRequestsOutputDTO, DomainError>> {
|
||||
try {
|
||||
const result = await this.client.getPendingSponsorshipRequests({
|
||||
entityType: input.entityType,
|
||||
@@ -40,13 +40,13 @@ export class SponsorshipRequestsService implements Service {
|
||||
|
||||
return Result.ok(result);
|
||||
} catch {
|
||||
return Result.err('GET_PENDING_REQUESTS_FAILED');
|
||||
return Result.err({ type: 'serverError', message: 'Failed to fetch pending requests' });
|
||||
}
|
||||
}
|
||||
|
||||
async acceptRequest(
|
||||
command: { requestId: string; actorDriverId: string },
|
||||
): Promise<Result<void, 'ACCEPT_SPONSORSHIP_REQUEST_FAILED'>> {
|
||||
): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
const input: AcceptSponsorshipRequestInputDTO = {
|
||||
respondedBy: command.actorDriverId,
|
||||
@@ -55,13 +55,13 @@ export class SponsorshipRequestsService implements Service {
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch {
|
||||
return Result.err('ACCEPT_SPONSORSHIP_REQUEST_FAILED');
|
||||
return Result.err({ type: 'serverError', message: 'Failed to accept sponsorship request' });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectRequest(
|
||||
command: { requestId: string; actorDriverId: string; reason: string | null },
|
||||
): Promise<Result<void, 'REJECT_SPONSORSHIP_REQUEST_FAILED'>> {
|
||||
): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
const input: RejectSponsorshipRequestInputDTO = {
|
||||
respondedBy: command.actorDriverId,
|
||||
@@ -71,7 +71,7 @@ export class SponsorshipRequestsService implements Service {
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch {
|
||||
return Result.err('REJECT_SPONSORSHIP_REQUEST_FAILED');
|
||||
return Result.err({ type: 'serverError', message: 'Failed to reject sponsorship request' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
|
||||
/**
|
||||
* Team Join Service - ViewModels
|
||||
@@ -21,28 +22,27 @@ export class TeamJoinService implements Service {
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> {
|
||||
async getJoinRequests(teamId: string, currentDriverId: string, isOwner: boolean): Promise<Result<TeamJoinRequestViewModel[], DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.getJoinRequests(teamId);
|
||||
return result.requests.map(request =>
|
||||
return Result.ok(result.requests.map(request =>
|
||||
new TeamJoinRequestViewModel(request, currentDriverId, isOwner)
|
||||
);
|
||||
} catch (error) {
|
||||
// Return empty array on error
|
||||
return [];
|
||||
));
|
||||
} catch (error: any) {
|
||||
return Result.err({ type: 'serverError', message: error.message || 'Failed to fetch join requests' });
|
||||
}
|
||||
}
|
||||
|
||||
async approveJoinRequest(): Promise<void> {
|
||||
throw new Error('Not implemented: API endpoint for approving join requests');
|
||||
async approveJoinRequest(): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'Not implemented: API endpoint for approving join requests' });
|
||||
}
|
||||
|
||||
async rejectJoinRequest(): Promise<void> {
|
||||
throw new Error('Not implemented: API endpoint for rejecting join requests');
|
||||
async rejectJoinRequest(): Promise<Result<void, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'Not implemented: API endpoint for rejecting join requests' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
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';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
|
||||
/**
|
||||
* Team Service - DTO Only
|
||||
@@ -22,7 +23,7 @@ export class TeamService implements Service {
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
this.apiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
@@ -31,20 +32,20 @@ export class TeamService implements Service {
|
||||
try {
|
||||
const result = await this.apiClient.getAll();
|
||||
return Result.ok(result.teams);
|
||||
} catch (error) {
|
||||
return Result.err({ type: 'unknown', message: 'Failed to fetch teams' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch teams' });
|
||||
}
|
||||
}
|
||||
|
||||
async getTeamDetails(_teamId: string, _currentDriverId: string): Promise<Result<any, DomainError>> {
|
||||
async getTeamDetails(teamId: string, _: string): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.getDetails(_teamId);
|
||||
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' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team details' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,28 +53,28 @@ export class TeamService implements Service {
|
||||
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' });
|
||||
} catch (error: unknown) {
|
||||
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to fetch team members' });
|
||||
}
|
||||
}
|
||||
|
||||
async getTeamJoinRequests(_teamId: string): Promise<Result<any, DomainError>> {
|
||||
async getTeamJoinRequests(_: string): Promise<Result<unknown, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'getTeamJoinRequests' });
|
||||
}
|
||||
|
||||
async createTeam(_input: any): Promise<Result<any, DomainError>> {
|
||||
async createTeam(__: unknown): Promise<Result<unknown, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'createTeam' });
|
||||
}
|
||||
|
||||
async updateTeam(_teamId: string, _input: any): Promise<Result<any, DomainError>> {
|
||||
async updateTeam(_: string, __: unknown): Promise<Result<unknown, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'updateTeam' });
|
||||
}
|
||||
|
||||
async getDriverTeam(_driverId: string): Promise<Result<any, DomainError>> {
|
||||
async getDriverTeam(_: string): Promise<Result<unknown, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'getDriverTeam' });
|
||||
}
|
||||
|
||||
async getMembership(_teamId: string, _driverId: string): Promise<Result<any, DomainError>> {
|
||||
async getMembership(_: string, __: string): Promise<Result<unknown, DomainError>> {
|
||||
return Result.err({ type: 'notImplemented', message: 'getMembership' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user