website refactor
This commit is contained in:
108
apps/website/lib/services/admin/AdminService.ts
Normal file
108
apps/website/lib/services/admin/AdminService.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
|
||||
import type { UserDto, DashboardStats, UserListResponse, ListUsersQuery } from '@/lib/api/admin/AdminApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
|
||||
/**
|
||||
* Admin Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by presenters/templates.
|
||||
* @server-safe
|
||||
*/
|
||||
export class AdminService {
|
||||
private apiClient: AdminApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
this.apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard statistics
|
||||
*/
|
||||
async getDashboardStats(): Promise<Result<DashboardStats, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.getDashboardStats();
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
console.error('AdminService.getDashboardStats failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err({ type: 'notFound', message: 'Access denied' });
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err({ type: 'serverError', message: 'Failed to fetch dashboard stats' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List users with filtering and pagination
|
||||
*/
|
||||
async listUsers(query: ListUsersQuery = {}): Promise<Result<UserListResponse, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.listUsers(query);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
console.error('AdminService.listUsers failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err({ type: 'notFound', message: 'Access denied' });
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err({ type: 'serverError', message: 'Failed to fetch users' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user status
|
||||
*/
|
||||
async updateUserStatus(userId: string, status: string): Promise<Result<UserDto, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.updateUserStatus(userId, status);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
console.error('AdminService.updateUserStatus failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err({ type: 'forbidden', message: 'Insufficient permissions' });
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err({ type: 'serverError', message: 'Failed to update user status' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user (soft delete)
|
||||
*/
|
||||
async deleteUser(userId: string): Promise<Result<void, DomainError>> {
|
||||
try {
|
||||
await this.apiClient.deleteUser(userId);
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('AdminService.deleteUser failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err({ type: 'forbidden', message: 'Insufficient permissions' });
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err({ type: 'serverError', message: 'Failed to delete user' });
|
||||
}
|
||||
}
|
||||
}
|
||||
46
apps/website/lib/services/analytics/DashboardService.ts
Normal file
46
apps/website/lib/services/analytics/DashboardService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
|
||||
import { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError } from '@/lib/contracts/services/Service';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
/**
|
||||
* DashboardService
|
||||
*
|
||||
* Pure service that creates its own API client and returns Result types.
|
||||
* No business logic, only data fetching and error mapping.
|
||||
*/
|
||||
export class DashboardService {
|
||||
private apiClient: DashboardApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
this.apiClient = new DashboardApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getDashboardOverview(): Promise<Result<DashboardOverviewDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getDashboardOverview();
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
console.error('DashboardService.getDashboardOverview failed:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('403') || error.message.includes('401')) {
|
||||
return Result.err({ type: 'unauthorized', message: error.message });
|
||||
}
|
||||
if (error.message.includes('404')) {
|
||||
return Result.err({ type: 'notFound', message: error.message });
|
||||
}
|
||||
if (error.message.includes('5') || error.message.includes('server')) {
|
||||
return Result.err({ type: 'serverError', message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return Result.err({ type: 'unknown', message: 'Dashboard fetch failed' });
|
||||
}
|
||||
}
|
||||
}
|
||||
64
apps/website/lib/services/auth/AuthPageService.ts
Normal file
64
apps/website/lib/services/auth/AuthPageService.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Auth Page Service
|
||||
*
|
||||
* Processes URL parameters for auth pages and provides structured data.
|
||||
* This is a composition service, not an API service.
|
||||
*/
|
||||
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { LoginPageDTO } from './types/LoginPageDTO';
|
||||
import { ForgotPasswordPageDTO } from './types/ForgotPasswordPageDTO';
|
||||
import { ResetPasswordPageDTO } from './types/ResetPasswordPageDTO';
|
||||
import { SignupPageDTO } from './types/SignupPageDTO';
|
||||
|
||||
export interface AuthPageParams {
|
||||
returnTo?: string | null;
|
||||
token?: string | null;
|
||||
}
|
||||
|
||||
export class AuthPageService {
|
||||
async processLoginParams(params: AuthPageParams): Promise<Result<LoginPageDTO, string>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/dashboard';
|
||||
const hasInsufficientPermissions = params.returnTo !== null;
|
||||
|
||||
return Result.ok({
|
||||
returnTo,
|
||||
hasInsufficientPermissions,
|
||||
});
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process login parameters');
|
||||
}
|
||||
}
|
||||
|
||||
async processForgotPasswordParams(params: AuthPageParams): Promise<Result<ForgotPasswordPageDTO, string>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/auth/login';
|
||||
return Result.ok({ returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process forgot password parameters');
|
||||
}
|
||||
}
|
||||
|
||||
async processResetPasswordParams(params: AuthPageParams): Promise<Result<ResetPasswordPageDTO, string>> {
|
||||
try {
|
||||
const token = params.token;
|
||||
if (!token) {
|
||||
return Result.err('Missing reset token');
|
||||
}
|
||||
const returnTo = params.returnTo ?? '/auth/login';
|
||||
return Result.ok({ token, returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process reset password parameters');
|
||||
}
|
||||
}
|
||||
|
||||
async processSignupParams(params: AuthPageParams): Promise<Result<SignupPageDTO, string>> {
|
||||
try {
|
||||
const returnTo = params.returnTo ?? '/onboarding';
|
||||
return Result.ok({ returnTo });
|
||||
} catch (error) {
|
||||
return Result.err('Failed to process signup parameters');
|
||||
}
|
||||
}
|
||||
}
|
||||
53
apps/website/lib/services/auth/AuthService.ts
Normal file
53
apps/website/lib/services/auth/AuthService.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Auth Service
|
||||
*
|
||||
* Orchestrates authentication operations.
|
||||
* Calls AuthApiClient for API calls.
|
||||
*/
|
||||
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import { LoginParamsDTO } from '@/lib/types/generated/LoginParamsDTO';
|
||||
import { SignupParamsDTO } from '@/lib/types/generated/SignupParamsDTO';
|
||||
import { ForgotPasswordDTO } from '@/lib/types/generated/ForgotPasswordDTO';
|
||||
import { ResetPasswordDTO } from '@/lib/types/generated/ResetPasswordDTO';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
|
||||
export class AuthService {
|
||||
private apiClient: AuthApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
this.apiClient = new AuthApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async login(params: LoginParamsDTO): Promise<SessionViewModel> {
|
||||
const dto = await this.apiClient.login(params);
|
||||
return new SessionViewModel(dto.user);
|
||||
}
|
||||
|
||||
async signup(params: SignupParamsDTO): Promise<SessionViewModel> {
|
||||
const dto = await this.apiClient.signup(params);
|
||||
return new SessionViewModel(dto.user);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.apiClient.logout();
|
||||
}
|
||||
|
||||
async forgotPassword(params: ForgotPasswordDTO): Promise<{ message: string; magicLink?: string }> {
|
||||
return await this.apiClient.forgotPassword(params);
|
||||
}
|
||||
|
||||
async resetPassword(params: ResetPasswordDTO): Promise<{ message: string }> {
|
||||
return await this.apiClient.resetPassword(params);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import type { AuthSessionDTO } from '@/lib/types/generated/AuthSessionDTO';
|
||||
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
|
||||
/**
|
||||
* Session Service
|
||||
*
|
||||
* Orchestrates session operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
* Returns SessionViewModel for client consumption.
|
||||
*/
|
||||
export class SessionService {
|
||||
constructor(
|
||||
@@ -13,10 +13,11 @@ export class SessionService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get current user session with view model transformation
|
||||
* Get current user session (returns ViewModel)
|
||||
*/
|
||||
async getSession(): Promise<SessionViewModel | null> {
|
||||
const dto = await this.apiClient.getSession();
|
||||
return dto ? new SessionViewModel(dto.user) : null;
|
||||
if (!dto) return null;
|
||||
return new SessionViewModel(dto.user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Forgot Password Page DTO
|
||||
*
|
||||
* Data transfer object for forgot password page composition.
|
||||
* Used by AuthPageService and ForgotPasswordViewDataBuilder.
|
||||
*/
|
||||
|
||||
export interface ForgotPasswordPageDTO {
|
||||
returnTo: string;
|
||||
}
|
||||
11
apps/website/lib/services/auth/types/LoginPageDTO.ts
Normal file
11
apps/website/lib/services/auth/types/LoginPageDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Login Page DTO
|
||||
*
|
||||
* Data transfer object for login page composition.
|
||||
* Used by AuthPageService and LoginViewDataBuilder.
|
||||
*/
|
||||
|
||||
export interface LoginPageDTO {
|
||||
returnTo: string;
|
||||
hasInsufficientPermissions: boolean;
|
||||
}
|
||||
11
apps/website/lib/services/auth/types/ResetPasswordPageDTO.ts
Normal file
11
apps/website/lib/services/auth/types/ResetPasswordPageDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Reset Password Page DTO
|
||||
*
|
||||
* Data transfer object for reset password page composition.
|
||||
* Used by AuthPageService and ResetPasswordViewDataBuilder.
|
||||
*/
|
||||
|
||||
export interface ResetPasswordPageDTO {
|
||||
token: string;
|
||||
returnTo: string;
|
||||
}
|
||||
10
apps/website/lib/services/auth/types/SignupPageDTO.ts
Normal file
10
apps/website/lib/services/auth/types/SignupPageDTO.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Signup Page DTO
|
||||
*
|
||||
* Data transfer object for signup page composition.
|
||||
* Used by AuthPageService and SignupViewDataBuilder.
|
||||
*/
|
||||
|
||||
export interface SignupPageDTO {
|
||||
returnTo: string;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { DashboardService } from './DashboardService';
|
||||
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
|
||||
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
|
||||
|
||||
describe('DashboardService', () => {
|
||||
let mockApiClient: Mocked<DashboardApiClient>;
|
||||
let service: DashboardService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getDashboardOverview: vi.fn(),
|
||||
} as Mocked<DashboardApiClient>;
|
||||
|
||||
service = new DashboardService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getDashboardOverview', () => {
|
||||
it('should call apiClient.getDashboardOverview and return DashboardOverviewViewModel', async () => {
|
||||
const mockDto = {
|
||||
currentDriver: {
|
||||
id: 'driver-123',
|
||||
name: 'Test Driver',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
country: 'US',
|
||||
totalRaces: 42,
|
||||
wins: 10,
|
||||
podiums: 20,
|
||||
rating: 1500,
|
||||
globalRank: 5,
|
||||
consistency: 85,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 3,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: { feedItems: [] },
|
||||
friends: [],
|
||||
};
|
||||
|
||||
mockApiClient.getDashboardOverview.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getDashboardOverview();
|
||||
|
||||
expect(mockApiClient.getDashboardOverview).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(DashboardOverviewViewModel);
|
||||
expect(result.activeLeaguesCount).toBe(3);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getDashboardOverview fails', async () => {
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getDashboardOverview.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getDashboardOverview()).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
|
||||
type DriverProfileReadServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
|
||||
|
||||
export class DriverProfileReadService implements Service {
|
||||
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfileReadServiceError>> {
|
||||
const logger = new ConsoleLogger();
|
||||
|
||||
try {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
const dto = await apiClient.getDriverProfile(driverId);
|
||||
|
||||
if (!dto.currentDriver) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
|
||||
if (errorAny.statusCode === 401 || errorAny.message?.toLowerCase().includes('unauthorized')) {
|
||||
return Result.err('unauthorized');
|
||||
}
|
||||
|
||||
if (errorAny.statusCode === 404 || errorAny.message?.toLowerCase().includes('not found')) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
logger.error(
|
||||
'DriverProfileReadService failed',
|
||||
error instanceof Error ? error : undefined,
|
||||
{ error: errorAny }
|
||||
);
|
||||
|
||||
if (errorAny.statusCode && errorAny.statusCode >= 500) {
|
||||
return Result.err('serverError');
|
||||
}
|
||||
|
||||
return Result.err('unknown');
|
||||
}
|
||||
}
|
||||
}
|
||||
52
apps/website/lib/services/drivers/DriverProfileService.ts
Normal file
52
apps/website/lib/services/drivers/DriverProfileService.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
|
||||
type DriverProfileServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
|
||||
|
||||
export class DriverProfileService implements Service {
|
||||
async getDriverProfile(driverId: string): Promise<Result<GetDriverProfileOutputDTO, DriverProfileServiceError>> {
|
||||
const logger = new ConsoleLogger();
|
||||
|
||||
try {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
const dto = await apiClient.getDriverProfile(driverId);
|
||||
|
||||
if (!dto.currentDriver) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
return Result.ok(dto);
|
||||
} catch (error) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
|
||||
if (errorAny.statusCode === 401 || errorAny.message?.toLowerCase().includes('unauthorized')) {
|
||||
return Result.err('unauthorized');
|
||||
}
|
||||
|
||||
if (errorAny.statusCode === 404 || errorAny.message?.toLowerCase().includes('not found')) {
|
||||
return Result.err('notFound');
|
||||
}
|
||||
|
||||
logger.error('DriverProfileService failed', error instanceof Error ? error : undefined, { error: errorAny });
|
||||
|
||||
if (errorAny.statusCode && errorAny.statusCode >= 500) {
|
||||
return Result.err('serverError');
|
||||
}
|
||||
|
||||
return Result.err('unknown');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
|
||||
export class DriverProfileUpdateService implements Service {
|
||||
private readonly apiClient: DriversApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async updateProfile(
|
||||
updates: { bio?: string; country?: string },
|
||||
): Promise<Result<DriverDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.updateProfile(updates);
|
||||
return Result.ok(dto);
|
||||
} catch {
|
||||
return Result.err({ type: 'unknown', message: 'DRIVER_PROFILE_UPDATE_FAILED' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import type { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
|
||||
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
|
||||
import { CompleteOnboardingViewModel } from "@/lib/view-models/CompleteOnboardingViewModel";
|
||||
import { DriverLeaderboardViewModel } from "@/lib/view-models/DriverLeaderboardViewModel";
|
||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||
import { DriverProfileViewModel } from "@/lib/view-models/DriverProfileViewModel";
|
||||
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||
|
||||
/**
|
||||
* Driver Service
|
||||
* Driver Service - DTO Only
|
||||
*
|
||||
* Orchestrates driver operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class DriverService {
|
||||
constructor(
|
||||
@@ -18,143 +17,49 @@ export class DriverService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get driver leaderboard with view model transformation
|
||||
* Get driver leaderboard (returns DTO)
|
||||
*/
|
||||
async getDriverLeaderboard(): Promise<DriverLeaderboardViewModel> {
|
||||
const dto = await this.apiClient.getLeaderboard();
|
||||
return new DriverLeaderboardViewModel(dto);
|
||||
async getDriverLeaderboard() {
|
||||
return this.apiClient.getLeaderboard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete driver onboarding with view model transformation
|
||||
* Complete driver onboarding (returns DTO)
|
||||
*/
|
||||
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingViewModel> {
|
||||
const dto = await this.apiClient.completeOnboarding(input);
|
||||
return new CompleteOnboardingViewModel(dto);
|
||||
async completeDriverOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
|
||||
return this.apiClient.completeOnboarding(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current driver with view model transformation
|
||||
*/
|
||||
async getCurrentDriver(): Promise<DriverViewModel | null> {
|
||||
const dto = await this.apiClient.getCurrent();
|
||||
if (!dto) {
|
||||
return null;
|
||||
}
|
||||
return new DriverViewModel({ ...dto, avatarUrl: dto.avatarUrl ?? null });
|
||||
* Get current driver (returns DTO)
|
||||
*/
|
||||
async getCurrentDriver(): Promise<DriverDTO | null> {
|
||||
return this.apiClient.getCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver profile with full details and view model transformation
|
||||
*/
|
||||
async getDriverProfile(driverId: string): Promise<DriverProfileViewModel> {
|
||||
const dto = await this.apiClient.getDriverProfile(driverId);
|
||||
return new DriverProfileViewModel({
|
||||
currentDriver: dto.currentDriver
|
||||
? {
|
||||
id: dto.currentDriver.id,
|
||||
name: dto.currentDriver.name,
|
||||
country: dto.currentDriver.country,
|
||||
avatarUrl: dto.currentDriver.avatarUrl || '',
|
||||
iracingId: dto.currentDriver.iracingId ?? null,
|
||||
joinedAt: dto.currentDriver.joinedAt,
|
||||
rating: dto.currentDriver.rating ?? null,
|
||||
globalRank: dto.currentDriver.globalRank ?? null,
|
||||
consistency: dto.currentDriver.consistency ?? null,
|
||||
bio: dto.currentDriver.bio ?? null,
|
||||
totalDrivers: dto.currentDriver.totalDrivers ?? null,
|
||||
}
|
||||
: null,
|
||||
stats: dto.stats
|
||||
? {
|
||||
totalRaces: dto.stats.totalRaces,
|
||||
wins: dto.stats.wins,
|
||||
podiums: dto.stats.podiums,
|
||||
dnfs: dto.stats.dnfs,
|
||||
avgFinish: dto.stats.avgFinish ?? null,
|
||||
bestFinish: dto.stats.bestFinish ?? null,
|
||||
worstFinish: dto.stats.worstFinish ?? null,
|
||||
finishRate: dto.stats.finishRate ?? null,
|
||||
winRate: dto.stats.winRate ?? null,
|
||||
podiumRate: dto.stats.podiumRate ?? null,
|
||||
percentile: dto.stats.percentile ?? null,
|
||||
rating: dto.stats.rating ?? null,
|
||||
consistency: dto.stats.consistency ?? null,
|
||||
overallRank: dto.stats.overallRank ?? null,
|
||||
}
|
||||
: null,
|
||||
finishDistribution: dto.finishDistribution
|
||||
? {
|
||||
totalRaces: dto.finishDistribution.totalRaces,
|
||||
wins: dto.finishDistribution.wins,
|
||||
podiums: dto.finishDistribution.podiums,
|
||||
topTen: dto.finishDistribution.topTen,
|
||||
dnfs: dto.finishDistribution.dnfs,
|
||||
other: dto.finishDistribution.other,
|
||||
}
|
||||
: null,
|
||||
teamMemberships: dto.teamMemberships.map((m) => ({
|
||||
teamId: m.teamId,
|
||||
teamName: m.teamName,
|
||||
teamTag: m.teamTag ?? null,
|
||||
role: m.role,
|
||||
joinedAt: m.joinedAt,
|
||||
isCurrent: m.isCurrent,
|
||||
})),
|
||||
socialSummary: {
|
||||
friendsCount: dto.socialSummary.friendsCount,
|
||||
friends: dto.socialSummary.friends.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
country: f.country,
|
||||
avatarUrl: f.avatarUrl || '',
|
||||
})),
|
||||
},
|
||||
extendedProfile: dto.extendedProfile
|
||||
? {
|
||||
socialHandles: dto.extendedProfile.socialHandles.map((h) => ({
|
||||
platform: h.platform as 'twitter' | 'youtube' | 'twitch' | 'discord',
|
||||
handle: h.handle,
|
||||
url: h.url,
|
||||
})),
|
||||
achievements: dto.extendedProfile.achievements.map((a) => ({
|
||||
id: a.id,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
icon: a.icon as 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap',
|
||||
rarity: a.rarity as 'common' | 'rare' | 'epic' | 'legendary',
|
||||
earnedAt: a.earnedAt,
|
||||
})),
|
||||
racingStyle: dto.extendedProfile.racingStyle,
|
||||
favoriteTrack: dto.extendedProfile.favoriteTrack,
|
||||
favoriteCar: dto.extendedProfile.favoriteCar,
|
||||
timezone: dto.extendedProfile.timezone,
|
||||
availableHours: dto.extendedProfile.availableHours,
|
||||
lookingForTeam: dto.extendedProfile.lookingForTeam,
|
||||
openToRequests: dto.extendedProfile.openToRequests,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
* Get driver profile (returns DTO)
|
||||
*/
|
||||
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
|
||||
return this.apiClient.getDriverProfile(driverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current driver profile with view model transformation
|
||||
*/
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverProfileViewModel> {
|
||||
const dto = await this.apiClient.updateProfile(updates);
|
||||
// After updating, get the full profile again to return updated view model
|
||||
return this.getDriverProfile(dto.id);
|
||||
}
|
||||
* Update current driver profile (returns DTO)
|
||||
*/
|
||||
async updateProfile(updates: { bio?: string; country?: string }): Promise<DriverDTO> {
|
||||
return this.apiClient.updateProfile(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find driver by ID
|
||||
* Find driver by ID (returns DTO)
|
||||
*/
|
||||
async findById(id: string): Promise<GetDriverOutputDTO | null> {
|
||||
return this.apiClient.getDriver(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple drivers by IDs
|
||||
* Find multiple drivers by IDs (returns DTOs)
|
||||
*/
|
||||
async findByIds(ids: string[]): Promise<GetDriverOutputDTO[]> {
|
||||
const drivers = await Promise.all(ids.map(id => this.apiClient.getDriver(id)));
|
||||
|
||||
@@ -1,28 +1,68 @@
|
||||
import { ContainerManager } from '@/lib/di/container';
|
||||
import { SESSION_SERVICE_TOKEN, LANDING_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import { LandingService } from '@/lib/services/landing/LandingService';
|
||||
import { SessionService } from '@/lib/services/auth/SessionService';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
|
||||
|
||||
export async function getHomeData() {
|
||||
const container = ContainerManager.getInstance().getContainer();
|
||||
const sessionService = container.get<SessionService>(SESSION_SERVICE_TOKEN);
|
||||
const landingService = container.get<LandingService>(LANDING_SERVICE_TOKEN);
|
||||
// API Clients
|
||||
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
|
||||
|
||||
// Services
|
||||
import { SessionService } from '@/lib/services/auth/SessionService';
|
||||
|
||||
// Infrastructure
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
|
||||
|
||||
// DTO types
|
||||
import type { RacesPageDataRaceDTO } from '@/lib/types/generated/RacesPageDataRaceDTO';
|
||||
import type { LeagueWithCapacityDTO } from '@/lib/types/generated/LeagueWithCapacityDTO';
|
||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||
|
||||
export interface HomeDataDTO {
|
||||
isAlpha: boolean;
|
||||
upcomingRaces: RacesPageDataRaceDTO[];
|
||||
topLeagues: LeagueWithCapacityDTO[];
|
||||
teams: TeamListItemDTO[];
|
||||
}
|
||||
|
||||
export async function getHomeData(): Promise<HomeDataDTO> {
|
||||
// Manual wiring: construct dependencies explicitly
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
const errorReporter = new ConsoleErrorReporter();
|
||||
const logger = new ConsoleLogger();
|
||||
|
||||
// Construct API clients
|
||||
const authApiClient = new AuthApiClient(baseUrl, errorReporter, logger);
|
||||
const racesApiClient = new RacesApiClient(baseUrl, errorReporter, logger);
|
||||
const leaguesApiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
|
||||
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Construct services
|
||||
const sessionService = new SessionService(authApiClient);
|
||||
|
||||
// Check session and redirect if logged in
|
||||
const session = await sessionService.getSession();
|
||||
if (session) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
// Get feature flags
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const isAlpha = featureService.isEnabled('alpha_features');
|
||||
const discovery = await landingService.getHomeDiscovery();
|
||||
|
||||
|
||||
// Get home discovery data (manual implementation)
|
||||
const [racesDto, leaguesDto, teamsDto] = await Promise.all([
|
||||
racesApiClient.getPageData(),
|
||||
leaguesApiClient.getAllWithCapacity(),
|
||||
teamsApiClient.getAll(),
|
||||
]);
|
||||
|
||||
// Return DTOs directly (no ViewModels)
|
||||
return {
|
||||
isAlpha,
|
||||
upcomingRaces: discovery.upcomingRaces,
|
||||
topLeagues: discovery.topLeagues,
|
||||
teams: discovery.teams,
|
||||
upcomingRaces: racesDto.races.slice(0, 4),
|
||||
topLeagues: leaguesDto.leagues.slice(0, 4),
|
||||
teams: teamsDto.teams.slice(0, 4),
|
||||
};
|
||||
}
|
||||
13
apps/website/lib/services/landing/LandingService.ts
Normal file
13
apps/website/lib/services/landing/LandingService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Landing Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class LandingService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getLandingData(): Promise<any> {
|
||||
return { featuredLeagues: [], stats: {} };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
|
||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
|
||||
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
|
||||
@@ -25,10 +25,9 @@ export class LeagueMembershipService {
|
||||
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> {
|
||||
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsDTO> {
|
||||
const dto = await this.getClient().getMemberships(leagueId);
|
||||
const members: LeagueMemberDTO[] = dto.members ?? [];
|
||||
return members.map((m) => new LeagueMemberViewModel(m, currentUserId));
|
||||
return dto;
|
||||
}
|
||||
|
||||
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||
@@ -164,4 +163,4 @@ export class LeagueMembershipService {
|
||||
clearLeagueMemberships(leagueId: string): void {
|
||||
LeagueMembershipService.clearLeagueMemberships(leagueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { LeagueStandingsViewModel } from '@/lib/view-models/LeagueStandingsViewModel';
|
||||
import { LeagueStatsViewModel } from '@/lib/view-models/LeagueStatsViewModel';
|
||||
import { LeagueScheduleViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
||||
import { LeagueMembershipsViewModel } from '@/lib/view-models/LeagueMembershipsViewModel';
|
||||
import { RemoveMemberViewModel } from '@/lib/view-models/RemoveMemberViewModel';
|
||||
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
|
||||
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||
import type { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
|
||||
import type { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO';
|
||||
@@ -24,14 +18,15 @@ describe('LeagueService', () => {
|
||||
getSchedule: vi.fn(),
|
||||
getMemberships: vi.fn(),
|
||||
create: vi.fn(),
|
||||
removeMember: vi.fn(),
|
||||
removeRosterMember: vi.fn(),
|
||||
updateRosterMemberRole: vi.fn(),
|
||||
} as unknown as Mocked<LeaguesApiClient>;
|
||||
|
||||
service = new LeagueService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getAllLeagues', () => {
|
||||
it('should call apiClient.getAllWithCapacityAndScoring and return array of LeagueSummaryViewModel', async () => {
|
||||
it('should call apiClient.getAllWithCapacityAndScoring and return DTO', async () => {
|
||||
const mockDto = {
|
||||
totalCount: 2,
|
||||
leagues: [
|
||||
@@ -45,8 +40,7 @@ describe('LeagueService', () => {
|
||||
const result = await service.getAllLeagues();
|
||||
|
||||
expect(mockApiClient.getAllWithCapacityAndScoring).toHaveBeenCalled();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((l) => l.id)).toEqual(['league-1', 'league-2']);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should handle empty leagues array', async () => {
|
||||
@@ -56,7 +50,7 @@ describe('LeagueService', () => {
|
||||
|
||||
const result = await service.getAllLeagues();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getAllWithCapacityAndScoring fails', async () => {
|
||||
@@ -68,32 +62,29 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
describe('getLeagueStandings', () => {
|
||||
it('should call apiClient.getStandings and return LeagueStandingsViewModel', async () => {
|
||||
it('should call apiClient.getStandings and return DTO', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const currentUserId = 'user-456';
|
||||
const mockDto = { standings: [] } as any;
|
||||
|
||||
mockApiClient.getStandings.mockResolvedValue({ standings: [] } as any);
|
||||
mockApiClient.getMemberships.mockResolvedValue({ members: [] } as any);
|
||||
mockApiClient.getStandings.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getLeagueStandings(leagueId, currentUserId);
|
||||
const result = await service.getLeagueStandings(leagueId);
|
||||
|
||||
expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
|
||||
expect(result).toBeInstanceOf(LeagueStandingsViewModel);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getStandings fails', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getStandings.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('API call failed');
|
||||
await expect(service.getLeagueStandings(leagueId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeagueStats', () => {
|
||||
it('should call apiClient.getTotal and return LeagueStatsViewModel', async () => {
|
||||
it('should call apiClient.getTotal and return DTO', async () => {
|
||||
const mockDto = { totalLeagues: 42 };
|
||||
|
||||
mockApiClient.getTotal.mockResolvedValue(mockDto);
|
||||
@@ -101,8 +92,7 @@ describe('LeagueService', () => {
|
||||
const result = await service.getLeagueStats();
|
||||
|
||||
expect(mockApiClient.getTotal).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(LeagueStatsViewModel);
|
||||
expect(result.totalLeagues).toBe(42);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getTotal fails', async () => {
|
||||
@@ -114,7 +104,7 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
describe('getLeagueSchedule', () => {
|
||||
it('should call apiClient.getSchedule and return LeagueScheduleViewModel with Date parsing', async () => {
|
||||
it('should call apiClient.getSchedule and return DTO', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const mockDto = {
|
||||
races: [
|
||||
@@ -128,8 +118,7 @@ describe('LeagueService', () => {
|
||||
const result = await service.getLeagueSchedule(leagueId);
|
||||
|
||||
expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
|
||||
expect(result).toBeInstanceOf(LeagueScheduleViewModel);
|
||||
expect(result.raceCount).toBe(2);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should handle empty races array', async () => {
|
||||
@@ -140,13 +129,11 @@ describe('LeagueService', () => {
|
||||
|
||||
const result = await service.getLeagueSchedule(leagueId);
|
||||
|
||||
expect(result.races).toEqual([]);
|
||||
expect(result.hasRaces).toBe(false);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getSchedule fails', async () => {
|
||||
const leagueId = 'league-123';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getSchedule.mockRejectedValue(error);
|
||||
|
||||
@@ -155,49 +142,37 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
describe('getLeagueMemberships', () => {
|
||||
it('should call apiClient.getMemberships and return LeagueMembershipsViewModel', async () => {
|
||||
it('should call apiClient.getMemberships and return DTO', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const mockDto = {
|
||||
members: [{ driverId: 'driver-1' }, { driverId: 'driver-2' }],
|
||||
} as any;
|
||||
|
||||
mockApiClient.getMemberships.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getLeagueMemberships(leagueId, currentUserId);
|
||||
const result = await service.getLeagueMemberships(leagueId);
|
||||
|
||||
expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
|
||||
expect(result).toBeInstanceOf(LeagueMembershipsViewModel);
|
||||
expect(result.memberships).toHaveLength(2);
|
||||
|
||||
const first = result.memberships[0]!;
|
||||
expect(first).toBeInstanceOf(LeagueMemberViewModel);
|
||||
expect(first.driverId).toBe('driver-1');
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should handle empty memberships array', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const mockDto = { members: [] } as any;
|
||||
|
||||
mockApiClient.getMemberships.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getLeagueMemberships(leagueId, currentUserId);
|
||||
const result = await service.getLeagueMemberships(leagueId);
|
||||
|
||||
expect(result.memberships).toHaveLength(0);
|
||||
expect(result.hasMembers).toBe(false);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getMemberships fails', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const currentUserId = 'user-456';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getMemberships.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('API call failed');
|
||||
await expect(service.getLeagueMemberships(leagueId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,46 +213,41 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
describe('removeMember', () => {
|
||||
it('should call apiClient.removeMember and return RemoveMemberViewModel', async () => {
|
||||
it('should call apiClient.removeRosterMember and return DTO', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const performerDriverId = 'performer-456';
|
||||
const targetDriverId = 'target-789';
|
||||
|
||||
const mockDto: RemoveLeagueMemberOutputDTO = { success: true };
|
||||
|
||||
mockApiClient.removeMember.mockResolvedValue(mockDto);
|
||||
mockApiClient.removeRosterMember.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
const result = await service.removeMember(leagueId, targetDriverId);
|
||||
|
||||
expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
|
||||
expect(result).toBeInstanceOf(RemoveMemberViewModel);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockApiClient.removeRosterMember).toHaveBeenCalledWith(leagueId, targetDriverId);
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should handle unsuccessful removal', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const performerDriverId = 'performer-456';
|
||||
const targetDriverId = 'target-789';
|
||||
|
||||
const mockDto: RemoveLeagueMemberOutputDTO = { success: false };
|
||||
|
||||
mockApiClient.removeMember.mockResolvedValue(mockDto);
|
||||
mockApiClient.removeRosterMember.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
const result = await service.removeMember(leagueId, targetDriverId);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.successMessage).toBe('Failed to remove member.');
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.removeMember fails', async () => {
|
||||
it('should throw error when apiClient.removeRosterMember fails', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const performerDriverId = 'performer-456';
|
||||
const targetDriverId = 'target-789';
|
||||
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.removeMember.mockRejectedValue(error);
|
||||
mockApiClient.removeRosterMember.mockRejectedValue(error);
|
||||
|
||||
await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
|
||||
await expect(service.removeMember(leagueId, targetDriverId)).rejects.toThrow('API call failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
28
apps/website/lib/services/leagues/LeagueSettingsService.ts
Normal file
28
apps/website/lib/services/leagues/LeagueSettingsService.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
|
||||
/**
|
||||
* League Settings Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class LeagueSettingsService {
|
||||
constructor(
|
||||
private readonly leagueApiClient: LeaguesApiClient,
|
||||
private readonly driverApiClient: DriversApiClient
|
||||
) {}
|
||||
|
||||
async getLeagueSettings(leagueId: string): Promise<any> {
|
||||
// This would typically call multiple endpoints to gather all settings data
|
||||
// For now, return a basic structure
|
||||
return {
|
||||
league: await this.leagueApiClient.getAllWithCapacityAndScoring(),
|
||||
config: { /* config data */ }
|
||||
};
|
||||
}
|
||||
|
||||
async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<{ success: boolean }> {
|
||||
return this.leagueApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId);
|
||||
}
|
||||
}
|
||||
41
apps/website/lib/services/leagues/LeagueStewardingService.ts
Normal file
41
apps/website/lib/services/leagues/LeagueStewardingService.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { RaceService } from '@/lib/services/races/RaceService';
|
||||
import { ProtestService } from '@/lib/services/protests/ProtestService';
|
||||
import { PenaltyService } from '@/lib/services/penalties/PenaltyService';
|
||||
import { DriverService } from '@/lib/services/drivers/DriverService';
|
||||
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
|
||||
|
||||
/**
|
||||
* League Stewarding Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class LeagueStewardingService {
|
||||
constructor(
|
||||
private readonly raceService: RaceService,
|
||||
private readonly protestService: ProtestService,
|
||||
private readonly penaltyService: PenaltyService,
|
||||
private readonly driverService: DriverService,
|
||||
private readonly membershipService: LeagueMembershipService
|
||||
) {}
|
||||
|
||||
async getLeagueProtests(leagueId: string): Promise<any> {
|
||||
return this.protestService.getLeagueProtests(leagueId);
|
||||
}
|
||||
|
||||
async getProtestById(leagueId: string, protestId: string): Promise<any> {
|
||||
return this.protestService.getProtestById(leagueId, protestId);
|
||||
}
|
||||
|
||||
async applyPenalty(input: any): Promise<void> {
|
||||
return this.protestService.applyPenalty(input);
|
||||
}
|
||||
|
||||
async requestDefense(input: any): Promise<void> {
|
||||
return this.protestService.requestDefense(input);
|
||||
}
|
||||
|
||||
async reviewProtest(input: any): Promise<void> {
|
||||
return this.protestService.reviewProtest(input);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest';
|
||||
import { LeagueWalletService } from './LeagueWalletService';
|
||||
import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient';
|
||||
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
|
||||
|
||||
describe('LeagueWalletService', () => {
|
||||
let mockApiClient: Mocked<WalletsApiClient>;
|
||||
@@ -17,7 +16,7 @@ describe('LeagueWalletService', () => {
|
||||
});
|
||||
|
||||
describe('getWalletForLeague', () => {
|
||||
it('should call apiClient.getLeagueWallet and return LeagueWalletViewModel', async () => {
|
||||
it('should call apiClient.getLeagueWallet and return DTO', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const mockDto = {
|
||||
balance: 1000,
|
||||
@@ -47,11 +46,7 @@ describe('LeagueWalletService', () => {
|
||||
const result = await service.getWalletForLeague(leagueId);
|
||||
|
||||
expect(mockApiClient.getLeagueWallet).toHaveBeenCalledWith(leagueId);
|
||||
expect(result).toBeInstanceOf(LeagueWalletViewModel);
|
||||
expect(result.balance).toBe(1000);
|
||||
expect(result.currency).toBe('USD');
|
||||
expect(result.transactions).toHaveLength(1);
|
||||
expect(result.formattedBalance).toBe('$1000.00');
|
||||
expect(result).toEqual(mockDto);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getLeagueWallet fails', async () => {
|
||||
@@ -98,4 +93,4 @@ describe('LeagueWalletService', () => {
|
||||
await expect(service.withdraw(leagueId, amount, currency, seasonId, destinationAccount)).rejects.toThrow('Withdrawal failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
apps/website/lib/services/leagues/LeagueWalletService.ts
Normal file
40
apps/website/lib/services/leagues/LeagueWalletService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { WalletsApiClient } from '@/lib/api/wallets/WalletsApiClient';
|
||||
import type { LeagueWalletDTO, WithdrawRequestDTO, WithdrawResponseDTO } from '@/lib/api/wallets/WalletsApiClient';
|
||||
|
||||
/**
|
||||
* LeagueWalletService - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class LeagueWalletService {
|
||||
constructor(
|
||||
private readonly apiClient: WalletsApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get wallet for a league
|
||||
*/
|
||||
async getWalletForLeague(leagueId: string): Promise<LeagueWalletDTO> {
|
||||
return this.apiClient.getLeagueWallet(leagueId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw from league wallet
|
||||
*/
|
||||
async withdraw(
|
||||
leagueId: string,
|
||||
amount: number,
|
||||
currency: string,
|
||||
seasonId: string,
|
||||
destinationAccount: string
|
||||
): Promise<WithdrawResponseDTO> {
|
||||
const payload: WithdrawRequestDTO = {
|
||||
amount,
|
||||
currency,
|
||||
seasonId,
|
||||
destinationAccount,
|
||||
};
|
||||
return this.apiClient.withdrawFromLeagueWallet(leagueId, payload);
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/services/media/MediaService.ts
Normal file
13
apps/website/lib/services/media/MediaService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Media Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class MediaService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getMediaById(mediaId: string): Promise<any> {
|
||||
return { id: mediaId, url: '' };
|
||||
}
|
||||
}
|
||||
75
apps/website/lib/services/onboarding/OnboardingService.ts
Normal file
75
apps/website/lib/services/onboarding/OnboardingService.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Onboarding Service
|
||||
*
|
||||
* Orchestrates onboarding operations.
|
||||
* Uses DriversApiClient for onboarding-related API calls.
|
||||
*
|
||||
* @server-safe
|
||||
*/
|
||||
|
||||
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { DomainError, Service } from '@/lib/contracts/services/Service';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { getWebsiteServerEnv } from '@/lib/config/env';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
|
||||
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
|
||||
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
|
||||
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
|
||||
|
||||
export class OnboardingService implements Service {
|
||||
private apiClient: DriversApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const { NODE_ENV } = getWebsiteServerEnv();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: false,
|
||||
logToConsole: true,
|
||||
reportToExternal: NODE_ENV === 'production',
|
||||
});
|
||||
this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async completeOnboarding(input: CompleteOnboardingInputDTO): Promise<Result<CompleteOnboardingOutputDTO, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.completeOnboarding(input);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to complete onboarding';
|
||||
return Result.err({ type: 'unknown', message: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
async checkCurrentDriver(): Promise<Result<unknown, DomainError>> {
|
||||
try {
|
||||
const result = await this.apiClient.getCurrent();
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to check driver status';
|
||||
return Result.err({ type: 'unknown', message: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
async generateAvatars(input: RequestAvatarGenerationInputDTO): Promise<Result<RequestAvatarGenerationOutputDTO, DomainError>> {
|
||||
try {
|
||||
// TODO: Implement actual API call when available
|
||||
// For now, return mock result
|
||||
const mockResult: RequestAvatarGenerationOutputDTO = {
|
||||
success: true,
|
||||
avatarUrls: [
|
||||
`https://api.example.com/avatars/${input.userId}/1.png`,
|
||||
`https://api.example.com/avatars/${input.userId}/2.png`,
|
||||
`https://api.example.com/avatars/${input.userId}/3.png`,
|
||||
],
|
||||
};
|
||||
return Result.ok(mockResult);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to generate avatars';
|
||||
return Result.err({ type: 'unknown', message: errorMessage });
|
||||
}
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/services/payments/PaymentService.ts
Normal file
13
apps/website/lib/services/payments/PaymentService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Payment Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class PaymentService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getPaymentById(paymentId: string): Promise<any> {
|
||||
return { id: paymentId, amount: 0 };
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/services/payments/WalletService.ts
Normal file
13
apps/website/lib/services/payments/WalletService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Wallet Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class WalletService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getWalletBalance(driverId: string): Promise<any> {
|
||||
return { balance: 0, currency: 'USD' };
|
||||
}
|
||||
}
|
||||
41
apps/website/lib/services/protests/ProtestService.ts
Normal file
41
apps/website/lib/services/protests/ProtestService.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ProtestsApiClient } from '@/lib/api/protests/ProtestsApiClient';
|
||||
import type { ProtestViewModel } from '@/lib/view-models/ProtestViewModel';
|
||||
import type { RaceViewModel } from '@/lib/view-models/RaceViewModel';
|
||||
import type { ProtestDriverViewModel } from '@/lib/view-models/ProtestDriverViewModel';
|
||||
import type { ApplyPenaltyCommandDTO } from '@/lib/types/generated/ApplyPenaltyCommandDTO';
|
||||
import type { RequestProtestDefenseCommandDTO } from '@/lib/types/generated/RequestProtestDefenseCommandDTO';
|
||||
import type { ReviewProtestCommandDTO } from '@/lib/types/generated/ReviewProtestCommandDTO';
|
||||
|
||||
/**
|
||||
* Protest Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class ProtestService {
|
||||
constructor(private readonly apiClient: ProtestsApiClient) {}
|
||||
|
||||
async getLeagueProtests(leagueId: string): Promise<any> {
|
||||
return this.apiClient.getLeagueProtests(leagueId);
|
||||
}
|
||||
|
||||
async getProtestById(leagueId: string, protestId: string): Promise<any> {
|
||||
return this.apiClient.getLeagueProtest(leagueId, protestId);
|
||||
}
|
||||
|
||||
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
return this.apiClient.applyPenalty(input);
|
||||
}
|
||||
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
return this.apiClient.requestDefense(input);
|
||||
}
|
||||
|
||||
async reviewProtest(input: ReviewProtestCommandDTO): Promise<void> {
|
||||
return this.apiClient.reviewProtest(input);
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<any> {
|
||||
return this.apiClient.getRaceProtests(raceId);
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/services/races/RaceResultsService.ts
Normal file
13
apps/website/lib/services/races/RaceResultsService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Race Results Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class RaceResultsService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getRaceResults(raceId: string): Promise<any> {
|
||||
return { raceId, results: [] };
|
||||
}
|
||||
}
|
||||
20
apps/website/lib/services/races/RaceService.ts
Normal file
20
apps/website/lib/services/races/RaceService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
|
||||
|
||||
/**
|
||||
* Race Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class RaceService {
|
||||
constructor(private readonly apiClient: RacesApiClient) {}
|
||||
|
||||
async getRaceById(raceId: string): Promise<any> {
|
||||
// This would need a driverId, but for now we'll use a placeholder
|
||||
return this.apiClient.getDetail(raceId, 'placeholder-driver-id');
|
||||
}
|
||||
|
||||
async getRacesByLeagueId(leagueId: string): Promise<any> {
|
||||
return this.apiClient.getPageData(leagueId);
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/services/races/RaceStewardingService.ts
Normal file
13
apps/website/lib/services/races/RaceStewardingService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Race Stewarding Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class RaceStewardingService {
|
||||
constructor(private readonly raceApiClient: any, private readonly protestApiClient: any, private readonly penaltyApiClient: any) {}
|
||||
|
||||
async getRaceStewarding(raceId: string): Promise<any> {
|
||||
return { raceId, protests: [], penalties: [] };
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/services/sponsors/SponsorService.ts
Normal file
13
apps/website/lib/services/sponsors/SponsorService.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Sponsor Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class SponsorService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getSponsorById(sponsorId: string): Promise<any> {
|
||||
return { id: sponsorId, name: 'Sponsor' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
export interface PendingSponsorshipRequestDto {
|
||||
requestId: string;
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
message: string | null;
|
||||
createdAtIso: string;
|
||||
}
|
||||
|
||||
export interface SponsorshipRequestsReadApiDto {
|
||||
sections: Array<{
|
||||
entityType: 'driver' | 'team' | 'season';
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
requests: PendingSponsorshipRequestDto[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SponsorshipRequestsReadServiceError = 'notFound' | 'unauthorized' | 'serverError';
|
||||
|
||||
export class SponsorshipRequestsReadService implements Service {
|
||||
private readonly client: SponsorsApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
this.client = new SponsorsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async getPendingRequestsForDriver(
|
||||
driverId: string,
|
||||
): Promise<Result<SponsorshipRequestsReadApiDto, SponsorshipRequestsReadServiceError>> {
|
||||
try {
|
||||
const response = await this.client.getPendingSponsorshipRequests({
|
||||
entityType: 'driver',
|
||||
entityId: driverId,
|
||||
});
|
||||
|
||||
const requests = (response.requests ?? []).map((r) => {
|
||||
const raw = r as unknown as {
|
||||
id?: string;
|
||||
requestId?: string;
|
||||
sponsorId?: string;
|
||||
sponsorName?: string;
|
||||
message?: unknown;
|
||||
createdAt?: string;
|
||||
createdAtIso?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
requestId: String(raw.id ?? raw.requestId ?? ''),
|
||||
sponsorId: String(raw.sponsorId ?? ''),
|
||||
sponsorName: String(raw.sponsorName ?? 'Sponsor'),
|
||||
message: typeof raw.message === 'string' ? raw.message : null,
|
||||
createdAtIso: String(raw.createdAt ?? raw.createdAtIso ?? ''),
|
||||
};
|
||||
});
|
||||
|
||||
return Result.ok({
|
||||
sections: [
|
||||
{
|
||||
entityType: 'driver',
|
||||
entityId: driverId,
|
||||
entityName: 'Your Profile',
|
||||
requests,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
const errorAny = error as { statusCode?: number; message?: string };
|
||||
if (errorAny.statusCode === 401) return Result.err('unauthorized');
|
||||
if (errorAny.statusCode === 404) return Result.err('notFound');
|
||||
return Result.err('serverError');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { isProductionEnvironment } from '@/lib/config/env';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import type { Service } from '@/lib/contracts/services/Service';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
|
||||
export interface AcceptSponsorshipRequestCommand {
|
||||
requestId: string;
|
||||
actorDriverId: string;
|
||||
}
|
||||
|
||||
export interface RejectSponsorshipRequestCommand {
|
||||
requestId: string;
|
||||
actorDriverId: string;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
export type AcceptSponsorshipRequestServiceError =
|
||||
| 'ACCEPT_SPONSORSHIP_REQUEST_FAILED';
|
||||
|
||||
export type RejectSponsorshipRequestServiceError =
|
||||
| 'REJECT_SPONSORSHIP_REQUEST_FAILED';
|
||||
|
||||
export class SponsorshipRequestsService implements Service {
|
||||
private readonly client: SponsorsApiClient;
|
||||
|
||||
constructor() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
const logger = new ConsoleLogger();
|
||||
const errorReporter = new EnhancedErrorReporter(logger, {
|
||||
showUserNotifications: true,
|
||||
logToConsole: true,
|
||||
reportToExternal: isProductionEnvironment(),
|
||||
});
|
||||
|
||||
this.client = new SponsorsApiClient(baseUrl, errorReporter, logger);
|
||||
}
|
||||
|
||||
async acceptRequest(
|
||||
command: AcceptSponsorshipRequestCommand,
|
||||
): Promise<Result<void, AcceptSponsorshipRequestServiceError>> {
|
||||
try {
|
||||
await this.client.acceptSponsorshipRequest(command.requestId, {
|
||||
actorDriverId: command.actorDriverId,
|
||||
} as any);
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch {
|
||||
return Result.err('ACCEPT_SPONSORSHIP_REQUEST_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
async rejectRequest(
|
||||
command: RejectSponsorshipRequestCommand,
|
||||
): Promise<Result<void, RejectSponsorshipRequestServiceError>> {
|
||||
try {
|
||||
await this.client.rejectSponsorshipRequest(command.requestId, {
|
||||
actorDriverId: command.actorDriverId,
|
||||
reason: command.reason ?? undefined,
|
||||
} as any);
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch {
|
||||
return Result.err('REJECT_SPONSORSHIP_REQUEST_FAILED');
|
||||
}
|
||||
}
|
||||
}
|
||||
17
apps/website/lib/services/teams/TeamService.ts
Normal file
17
apps/website/lib/services/teams/TeamService.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Team Service - DTO Only
|
||||
*
|
||||
* Returns raw API DTOs. No ViewModels or UX logic.
|
||||
* All client-side presentation logic must be handled by hooks/components.
|
||||
*/
|
||||
export class TeamService {
|
||||
constructor(private readonly apiClient: any) {}
|
||||
|
||||
async getTeamById(teamId: string): Promise<any> {
|
||||
return { id: teamId, name: 'Team' };
|
||||
}
|
||||
|
||||
async getTeamsByLeagueId(leagueId: string): Promise<any> {
|
||||
return { teams: [] };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user