view models

This commit is contained in:
2025-12-18 13:48:35 +01:00
parent cc2553876a
commit 91adbb9c83
71 changed files with 3119 additions and 359 deletions

View File

@@ -8,6 +8,9 @@
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"noEmit": false, "noEmit": false,
"noEmitOnError": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"removeComments": true, "removeComments": true,

View File

@@ -1,6 +1,6 @@
{ {
"extends": ["next/core-web-vitals", "plugin:import/recommended", "plugin:import/typescript"], "extends": ["next/core-web-vitals", "plugin:import/recommended", "plugin:import/typescript"],
"plugins": ["boundaries", "import"], "plugins": ["boundaries", "import", "@typescript-eslint", "unused-imports", "no-unused-vars"],
"settings": { "settings": {
"import/resolver": { "import/resolver": {
"typescript": {} "typescript": {}
@@ -11,6 +11,17 @@
"@next/next/no-img-element": "warn", "@next/next/no-img-element": "warn",
"react-hooks/exhaustive-deps": "warn", "react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "all",
"argsIgnorePattern": "^$",
"vars": "all",
"varsIgnorePattern": "^$",
"caughtErrors": "all",
"ignoreTypeImports": false
}
],
"boundaries/element-types": [ "boundaries/element-types": [
2, 2,
{ {
@@ -22,6 +33,16 @@
} }
] ]
} }
],
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
] ]
} }
} }

View File

@@ -1,9 +1,9 @@
import { BaseApiClient } from '../base/BaseApiClient'; import { BaseApiClient } from '../base/BaseApiClient';
import type { import { AuthSessionDTO } from '../../types/generated/AuthSessionDTO';
LoginParamsDto,
SignupParamsDto, // TODO: Create DTOs for login/signup params in apps/website/lib/types/generated
SessionDataDto, type LoginParamsDto = { email: string; password: string };
} from '../../dtos'; type SignupParamsDto = { email: string; password: string; displayName: string };
/** /**
* Auth API Client * Auth API Client
@@ -12,18 +12,18 @@ import type {
*/ */
export class AuthApiClient extends BaseApiClient { export class AuthApiClient extends BaseApiClient {
/** Sign up with email */ /** Sign up with email */
signup(params: SignupParamsDto): Promise<SessionDataDto> { signup(params: SignupParamsDto): Promise<AuthSessionDTO> {
return this.post<SessionDataDto>('/auth/signup', params); return this.post<AuthSessionDTO>('/auth/signup', params);
} }
/** Login with email */ /** Login with email */
login(params: LoginParamsDto): Promise<SessionDataDto> { login(params: LoginParamsDto): Promise<AuthSessionDTO> {
return this.post<SessionDataDto>('/auth/login', params); return this.post<AuthSessionDTO>('/auth/login', params);
} }
/** Get current session */ /** Get current session */
getSession(): Promise<SessionDataDto | null> { getSession(): Promise<AuthSessionDTO | null> {
return this.get<SessionDataDto | null>('/auth/session'); return this.get<AuthSessionDTO | null>('/auth/session');
} }
/** Logout */ /** Logout */

View File

@@ -1,10 +1,19 @@
import { BaseApiClient } from '../base/BaseApiClient'; import { BaseApiClient } from '../base/BaseApiClient';
import type {
DriversLeaderboardDto,
DriverRegistrationStatusDto,
} from '../../dtos';
// Import generated types // Import generated types
import type { DriverDTO, CompleteOnboardingInputDTO, CompleteOnboardingOutputDTO } from '../../types/api-helpers'; import type { CompleteOnboardingInputDTO, CompleteOnboardingOutputDTO, DriverRegistrationStatusDTO, DriverLeaderboardItemDTO } from '../../types/generated';
// TODO: Create proper DriverDTO in generated types
type DriverDTO = {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
};
type DriversLeaderboardDto = {
drivers: DriverLeaderboardItemDTO[];
};
/** /**
* Drivers API Client * Drivers API Client
@@ -19,16 +28,16 @@ export class DriversApiClient extends BaseApiClient {
/** Complete driver onboarding */ /** Complete driver onboarding */
completeOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> { completeOnboarding(input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingOutputDTO> {
return this.post<CompleteOnboardingOutputDto>('/drivers/complete-onboarding', input); return this.post<CompleteOnboardingOutputDTO>('/drivers/complete-onboarding', input);
} }
/** Get current driver (based on session) */ /** Get current driver (based on session) */
getCurrent(): Promise<DriverDTO | null> { getCurrent(): Promise<DriverDTO | null> {
return this.get<DriverDto | null>('/drivers/current'); return this.get<DriverDTO | null>('/drivers/current');
} }
/** Get driver registration status for a specific race */ /** Get driver registration status for a specific race */
getRegistrationStatus(driverId: string, raceId: string): Promise<DriverRegistrationStatusDto> { getRegistrationStatus(driverId: string, raceId: string): Promise<DriverRegistrationStatusDTO> {
return this.get<DriverRegistrationStatusDto>(`/drivers/${driverId}/races/${raceId}/registration-status`); return this.get<DriverRegistrationStatusDTO>(`/drivers/${driverId}/races/${raceId}/registration-status`);
} }
} }

View File

@@ -1,17 +1,45 @@
import { BaseApiClient } from '../base/BaseApiClient'; import { BaseApiClient } from '../base/BaseApiClient';
import type {
GetPaymentsOutputDto, // TODO: Import these types from apps/website/lib/types/generated when available
CreatePaymentInputDto, type GetPaymentsOutputDto = { payments: import('../types/generated').PaymentDto[] };
CreatePaymentOutputDto, type CreatePaymentInputDto = {
GetMembershipFeesOutputDto, type: 'sponsorship' | 'membership_fee';
GetPrizesOutputDto, amount: number;
GetWalletOutputDto, payerId: string;
ProcessWalletTransactionInputDto, payerType: 'sponsor' | 'driver';
ProcessWalletTransactionOutputDto, leagueId: string;
UpdateMemberPaymentInputDto, seasonId?: string;
UpdateMemberPaymentOutputDto, };
GetWalletTransactionsOutputDto, type CreatePaymentOutputDto = { payment: import('../types/generated').PaymentDto };
} from '../../dtos'; type GetMembershipFeesOutputDto = {
fee: import('../types/generated').MembershipFeeDto | null;
payments: import('../types/generated').MemberPaymentDto[]
};
type GetPrizesOutputDto = { prizes: import('../types/generated').PrizeDto[] };
type GetWalletOutputDto = {
wallet: import('../types/generated').WalletDto;
transactions: import('../types/generated').TransactionDto[]
};
type ProcessWalletTransactionInputDto = {
leagueId: string;
type: 'deposit' | 'withdrawal' | 'platform_fee';
amount: number;
description: string;
referenceId?: string;
referenceType?: 'sponsorship' | 'membership_fee' | 'prize';
};
type ProcessWalletTransactionOutputDto = {
wallet: import('../types/generated').WalletDto;
transaction: import('../types/generated').TransactionDto
};
type UpdateMemberPaymentInputDto = {
feeId: string;
driverId: string;
status?: 'pending' | 'paid' | 'overdue';
paidAt?: Date | string;
};
type UpdateMemberPaymentOutputDto = { payment: import('../types/generated').MemberPaymentDto };
type GetWalletTransactionsOutputDto = { transactions: import('../types/generated').TransactionDto[] };
/** /**
* Payments API Client * Payments API Client

View File

@@ -1,12 +1,13 @@
import { BaseApiClient } from '../base/BaseApiClient'; import { BaseApiClient } from '../base/BaseApiClient';
import type { import type { CreateSponsorInputDTO } from '../../types/generated/CreateSponsorInputDTO';
GetEntitySponsorshipPricingResultDto, import type { SponsorDashboardDTO } from '../../types/generated/SponsorDashboardDTO';
GetSponsorsOutputDto, import type { SponsorSponsorshipsDTO } from '../../types/generated/SponsorSponsorshipsDTO';
CreateSponsorInputDto,
CreateSponsorOutputDto, // TODO: Move these types to apps/website/lib/types/generated when available
SponsorDashboardDto, export type CreateSponsorOutputDto = { id: string; name: string };
SponsorSponsorshipsDto, export type GetEntitySponsorshipPricingResultDto = { pricing: Array<{ entityType: string; price: number }> };
} from '../../dtos'; export type SponsorDTO = { id: string; name: string; logoUrl?: string; websiteUrl?: string };
export type GetSponsorsOutputDto = { sponsors: SponsorDTO[] };
/** /**
* Sponsors API Client * Sponsors API Client
@@ -25,17 +26,17 @@ export class SponsorsApiClient extends BaseApiClient {
} }
/** Create a new sponsor */ /** Create a new sponsor */
create(input: CreateSponsorInputDto): Promise<CreateSponsorOutputDto> { create(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDto> {
return this.post<CreateSponsorOutputDto>('/sponsors', input); return this.post<CreateSponsorOutputDto>('/sponsors', input);
} }
/** Get sponsor dashboard */ /** Get sponsor dashboard */
getDashboard(sponsorId: string): Promise<SponsorDashboardDto | null> { getDashboard(sponsorId: string): Promise<SponsorDashboardDTO | null> {
return this.get<SponsorDashboardDto | null>(`/sponsors/dashboard/${sponsorId}`); return this.get<SponsorDashboardDTO | null>(`/sponsors/dashboard/${sponsorId}`);
} }
/** Get sponsor sponsorships */ /** Get sponsor sponsorships */
getSponsorships(sponsorId: string): Promise<SponsorSponsorshipsDto | null> { getSponsorships(sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
return this.get<SponsorSponsorshipsDto | null>(`/sponsors/${sponsorId}/sponsorships`); return this.get<SponsorSponsorshipsDTO | null>(`/sponsors/${sponsorId}/sponsorships`);
} }
} }

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, vi, Mocked } from 'vitest'; import { describe, it, expect, vi, Mocked } from 'vitest';
import { AnalyticsService } from './AnalyticsService'; import { AnalyticsService } from './AnalyticsService';
import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient';
import { RecordPageViewInputViewModel } from '../../view-models/RecordPageViewInputViewModel'; import { RecordPageViewOutputViewModel } from '../../view-models/RecordPageViewOutputViewModel';
import { RecordEngagementInputViewModel } from '../../view-models/RecordEngagementInputViewModel'; import { RecordEngagementOutputViewModel } from '../../view-models/RecordEngagementOutputViewModel';
describe('AnalyticsService', () => { describe('AnalyticsService', () => {
let mockApiClient: Mocked<AnalyticsApiClient>; let mockApiClient: Mocked<AnalyticsApiClient>;
@@ -19,10 +19,10 @@ describe('AnalyticsService', () => {
describe('recordPageView', () => { describe('recordPageView', () => {
it('should call apiClient.recordPageView with correct input', async () => { it('should call apiClient.recordPageView with correct input', async () => {
const input = new RecordPageViewInputViewModel({ const input = {
path: '/dashboard', path: '/dashboard',
userId: 'user-123', userId: 'user-123',
}); };
const expectedOutput = { pageViewId: 'pv-123' }; const expectedOutput = { pageViewId: 'pv-123' };
mockApiClient.recordPageView.mockResolvedValue(expectedOutput); mockApiClient.recordPageView.mockResolvedValue(expectedOutput);
@@ -33,13 +33,14 @@ describe('AnalyticsService', () => {
path: '/dashboard', path: '/dashboard',
userId: 'user-123', userId: 'user-123',
}); });
expect(result).toEqual(expectedOutput); expect(result).toBeInstanceOf(RecordPageViewOutputViewModel);
expect(result.pageViewId).toEqual('pv-123');
}); });
it('should call apiClient.recordPageView without userId when not provided', async () => { it('should call apiClient.recordPageView without userId when not provided', async () => {
const input = new RecordPageViewInputViewModel({ const input = {
path: '/home', path: '/home',
}); };
const expectedOutput = { pageViewId: 'pv-456' }; const expectedOutput = { pageViewId: 'pv-456' };
mockApiClient.recordPageView.mockResolvedValue(expectedOutput); mockApiClient.recordPageView.mockResolvedValue(expectedOutput);
@@ -49,17 +50,18 @@ describe('AnalyticsService', () => {
expect(mockApiClient.recordPageView).toHaveBeenCalledWith({ expect(mockApiClient.recordPageView).toHaveBeenCalledWith({
path: '/home', path: '/home',
}); });
expect(result).toEqual(expectedOutput); expect(result).toBeInstanceOf(RecordPageViewOutputViewModel);
expect(result.pageViewId).toEqual('pv-456');
}); });
}); });
describe('recordEngagement', () => { describe('recordEngagement', () => {
it('should call apiClient.recordEngagement with correct input', async () => { it('should call apiClient.recordEngagement with correct input', async () => {
const input = new RecordEngagementInputViewModel({ const input = {
eventType: 'button_click', eventType: 'button_click',
userId: 'user-123', userId: 'user-123',
metadata: { buttonId: 'submit', page: '/form' }, metadata: { buttonId: 'submit', page: '/form' },
}); };
const expectedOutput = { eventId: 'event-123', engagementWeight: 1.5 }; const expectedOutput = { eventId: 'event-123', engagementWeight: 1.5 };
mockApiClient.recordEngagement.mockResolvedValue(expectedOutput); mockApiClient.recordEngagement.mockResolvedValue(expectedOutput);
@@ -71,13 +73,15 @@ describe('AnalyticsService', () => {
userId: 'user-123', userId: 'user-123',
metadata: { buttonId: 'submit', page: '/form' }, metadata: { buttonId: 'submit', page: '/form' },
}); });
expect(result).toEqual(expectedOutput); expect(result).toBeInstanceOf(RecordEngagementOutputViewModel);
expect(result.eventId).toEqual('event-123');
expect(result.engagementWeight).toEqual(1.5);
}); });
it('should call apiClient.recordEngagement without optional fields', async () => { it('should call apiClient.recordEngagement without optional fields', async () => {
const input = new RecordEngagementInputViewModel({ const input = {
eventType: 'page_load', eventType: 'page_load',
}); };
const expectedOutput = { eventId: 'event-456', engagementWeight: 0.5 }; const expectedOutput = { eventId: 'event-456', engagementWeight: 0.5 };
mockApiClient.recordEngagement.mockResolvedValue(expectedOutput); mockApiClient.recordEngagement.mockResolvedValue(expectedOutput);
@@ -87,7 +91,9 @@ describe('AnalyticsService', () => {
expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({ expect(mockApiClient.recordEngagement).toHaveBeenCalledWith({
eventType: 'page_load', eventType: 'page_load',
}); });
expect(result).toEqual(expectedOutput); expect(result).toBeInstanceOf(RecordEngagementOutputViewModel);
expect(result.eventId).toEqual('event-456');
expect(result.engagementWeight).toEqual(0.5);
}); });
}); });
}); });

View File

@@ -1,7 +1,18 @@
import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient';
import { RecordPageViewOutputDTO, RecordEngagementOutputDTO } from '../../types/generated'; import { RecordPageViewOutputViewModel } from '../../view-models/RecordPageViewOutputViewModel';
import { RecordPageViewInputViewModel } from '../../view-models/RecordPageViewInputViewModel'; import { RecordEngagementOutputViewModel } from '../../view-models/RecordEngagementOutputViewModel';
import { RecordEngagementInputViewModel } from '../../view-models/RecordEngagementInputViewModel';
// TODO: Create proper DTOs in generated types
interface RecordPageViewInputDTO {
path: string;
userId?: string;
}
interface RecordEngagementInputDTO {
eventType: string;
userId?: string;
metadata?: Record<string, unknown>;
}
/** /**
* Analytics Service * Analytics Service
@@ -15,31 +26,18 @@ export class AnalyticsService {
) {} ) {}
/** /**
* Record a page view * Record a page view
*/ */
async recordPageView(input: RecordPageViewInputViewModel): Promise<RecordPageViewOutputDTO> { async recordPageView(input: RecordPageViewInputDTO): Promise<RecordPageViewOutputViewModel> {
const apiInput: { path: string; userId?: string } = { const result = await this.apiClient.recordPageView(input);
path: input.path, return new RecordPageViewOutputViewModel(result);
}; }
if (input.userId) {
apiInput.userId = input.userId;
}
return await this.apiClient.recordPageView(apiInput);
}
/** /**
* Record an engagement event * Record an engagement event
*/ */
async recordEngagement(input: RecordEngagementInputViewModel): Promise<RecordEngagementOutputDTO> { async recordEngagement(input: RecordEngagementInputDTO): Promise<RecordEngagementOutputViewModel> {
const apiInput: { eventType: string; userId?: string; metadata?: Record<string, unknown> } = { const result = await this.apiClient.recordEngagement(input);
eventType: input.eventType, return new RecordEngagementOutputViewModel(result);
}; }
if (input.userId) {
apiInput.userId = input.userId;
}
if (input.metadata) {
apiInput.metadata = input.metadata;
}
return await this.apiClient.recordEngagement(apiInput);
}
} }

View File

@@ -60,21 +60,4 @@ describe('DashboardService', () => {
}); });
}); });
describe('getDashboardOverview', () => {
it('should call getDashboardData and return the result', async () => {
const dto = {
totalUsers: 200,
activeUsers: 100,
totalRaces: 40,
totalLeagues: 10,
};
mockApiClient.getDashboardData.mockResolvedValue(dto);
const result = await service.getDashboardOverview();
expect(mockApiClient.getDashboardData).toHaveBeenCalled();
expect(result).toBeInstanceOf(AnalyticsDashboardViewModel);
expect(result.totalUsers).toBe(200);
});
});
}); });

View File

@@ -1,5 +1,6 @@
import { AnalyticsDashboardViewModel } from '@/lib/view-models/AnalyticsDashboardViewModel';
import { AnalyticsMetricsViewModel } from '@/lib/view-models/AnalyticsMetricsViewModel';
import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient'; import { AnalyticsApiClient } from '../../api/analytics/AnalyticsApiClient';
import { AnalyticsDashboardViewModel, AnalyticsMetricsViewModel } from '../../view-models';
/** /**
* Dashboard Service * Dashboard Service

View File

@@ -0,0 +1,146 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { AuthService } from './AuthService';
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
describe('AuthService', () => {
let mockApiClient: Mocked<AuthApiClient>;
let service: AuthService;
beforeEach(() => {
mockApiClient = {
signup: vi.fn(),
login: vi.fn(),
logout: vi.fn(),
getIracingAuthUrl: vi.fn(),
} as Mocked<AuthApiClient>;
service = new AuthService(mockApiClient);
});
describe('signup', () => {
it('should call apiClient.signup and return SessionViewModel', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
displayName: 'Test User',
};
const mockResponse = {
token: 'jwt-token',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
},
};
mockApiClient.signup.mockResolvedValue(mockResponse);
const result = await service.signup(params);
expect(mockApiClient.signup).toHaveBeenCalledWith(params);
expect(result).toBeInstanceOf(SessionViewModel);
expect(result.userId).toBe('user-123');
expect(result.email).toBe('test@example.com');
expect(result.displayName).toBe('Test User');
expect(result.isAuthenticated).toBe(true);
});
it('should throw error when apiClient.signup fails', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
displayName: 'Test User',
};
const error = new Error('Signup failed');
mockApiClient.signup.mockRejectedValue(error);
await expect(service.signup(params)).rejects.toThrow('Signup failed');
});
});
describe('login', () => {
it('should call apiClient.login and return SessionViewModel', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
};
const mockResponse = {
token: 'jwt-token',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
},
};
mockApiClient.login.mockResolvedValue(mockResponse);
const result = await service.login(params);
expect(mockApiClient.login).toHaveBeenCalledWith(params);
expect(result).toBeInstanceOf(SessionViewModel);
expect(result.userId).toBe('user-123');
expect(result.email).toBe('test@example.com');
expect(result.displayName).toBe('Test User');
expect(result.isAuthenticated).toBe(true);
});
it('should throw error when apiClient.login fails', async () => {
const params = {
email: 'test@example.com',
password: 'password123',
};
const error = new Error('Login failed');
mockApiClient.login.mockRejectedValue(error);
await expect(service.login(params)).rejects.toThrow('Login failed');
});
});
describe('logout', () => {
it('should call apiClient.logout', async () => {
mockApiClient.logout.mockResolvedValue(undefined);
await service.logout();
expect(mockApiClient.logout).toHaveBeenCalled();
});
it('should throw error when apiClient.logout fails', async () => {
const error = new Error('Logout failed');
mockApiClient.logout.mockRejectedValue(error);
await expect(service.logout()).rejects.toThrow('Logout failed');
});
});
describe('getIracingAuthUrl', () => {
it('should call apiClient.getIracingAuthUrl with returnTo', () => {
const returnTo = '/dashboard';
const expectedUrl = 'https://api.example.com/auth/iracing/start?returnTo=%2Fdashboard';
mockApiClient.getIracingAuthUrl.mockReturnValue(expectedUrl);
const result = service.getIracingAuthUrl(returnTo);
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(returnTo);
expect(result).toBe(expectedUrl);
});
it('should call apiClient.getIracingAuthUrl without returnTo', () => {
const expectedUrl = 'https://api.example.com/auth/iracing/start';
mockApiClient.getIracingAuthUrl.mockReturnValue(expectedUrl);
const result = service.getIracingAuthUrl();
expect(mockApiClient.getIracingAuthUrl).toHaveBeenCalledWith(undefined);
expect(result).toBe(expectedUrl);
});
});
});

View File

@@ -1,9 +1,9 @@
import { AuthApiClient } from '../../api/auth/AuthApiClient'; import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Create DTOs for login/signup params in apps/website/lib/types/generated
type LoginParamsDto = { email: string; password: string }; type LoginParamsDto = { email: string; password: string };
type SignupParamsDto = { email: string; password: string; displayName: string }; type SignupParamsDto = { email: string; password: string; displayName: string };
type SessionDataDto = { userId: string; email: string; displayName: string; token: string };
/** /**
* Auth Service * Auth Service
@@ -19,9 +19,10 @@ export class AuthService {
/** /**
* Sign up a new user * Sign up a new user
*/ */
async signup(params: SignupParamsDto): Promise<SessionDataDto> { async signup(params: SignupParamsDto): Promise<SessionViewModel> {
try { try {
return await this.apiClient.signup(params); const dto = await this.apiClient.signup(params);
return new SessionViewModel(dto.user);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -30,9 +31,10 @@ export class AuthService {
/** /**
* Log in an existing user * Log in an existing user
*/ */
async login(params: LoginParamsDto): Promise<SessionDataDto> { async login(params: LoginParamsDto): Promise<SessionViewModel> {
try { try {
return await this.apiClient.login(params); const dto = await this.apiClient.login(params);
return new SessionViewModel(dto.user);
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -0,0 +1,57 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { SessionService } from './SessionService';
import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models/SessionViewModel';
describe('SessionService', () => {
let mockApiClient: Mocked<AuthApiClient>;
let service: SessionService;
beforeEach(() => {
mockApiClient = {
getSession: vi.fn(),
} as Mocked<AuthApiClient>;
service = new SessionService(mockApiClient);
});
describe('getSession', () => {
it('should call apiClient.getSession and return SessionViewModel when session exists', async () => {
const mockResponse = {
token: 'jwt-token',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
},
};
mockApiClient.getSession.mockResolvedValue(mockResponse);
const result = await service.getSession();
expect(mockApiClient.getSession).toHaveBeenCalled();
expect(result).toBeInstanceOf(SessionViewModel);
expect(result?.userId).toBe('user-123');
expect(result?.email).toBe('test@example.com');
expect(result?.displayName).toBe('Test User');
expect(result?.isAuthenticated).toBe(true);
});
it('should return null when apiClient.getSession returns null', async () => {
mockApiClient.getSession.mockResolvedValue(null);
const result = await service.getSession();
expect(mockApiClient.getSession).toHaveBeenCalled();
expect(result).toBeNull();
});
it('should throw error when apiClient.getSession fails', async () => {
const error = new Error('Get session failed');
mockApiClient.getSession.mockRejectedValue(error);
await expect(service.getSession()).rejects.toThrow('Get session failed');
});
});
});

View File

@@ -1,5 +1,5 @@
import { SessionViewModel } from '@/lib/view-models/SessionViewModel';
import { AuthApiClient } from '../../api/auth/AuthApiClient'; import { AuthApiClient } from '../../api/auth/AuthApiClient';
import { SessionViewModel } from '../../view-models';
/** /**
* Session Service * Session Service
@@ -17,6 +17,6 @@ export class SessionService {
*/ */
async getSession(): Promise<SessionViewModel | null> { async getSession(): Promise<SessionViewModel | null> {
const dto = await this.apiClient.getSession(); const dto = await this.apiClient.getSession();
return dto ? new SessionViewModel(dto) : null; return dto ? new SessionViewModel(dto.user) : null;
} }
} }

View File

@@ -0,0 +1,77 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { DriverRegistrationService } from './DriverRegistrationService';
import { DriversApiClient } from '../../api/drivers/DriversApiClient';
import { DriverRegistrationStatusViewModel } from '../../view-models/DriverRegistrationStatusViewModel';
describe('DriverRegistrationService', () => {
let mockApiClient: Mocked<DriversApiClient>;
let service: DriverRegistrationService;
beforeEach(() => {
mockApiClient = {
getRegistrationStatus: vi.fn(),
} as Mocked<DriversApiClient>;
service = new DriverRegistrationService(mockApiClient);
});
describe('getDriverRegistrationStatus', () => {
it('should call apiClient.getRegistrationStatus and return DriverRegistrationStatusViewModel', async () => {
const driverId = 'driver-123';
const raceId = 'race-456';
const mockDto = {
isRegistered: true,
raceId: 'race-456',
driverId: 'driver-123',
};
mockApiClient.getRegistrationStatus.mockResolvedValue(mockDto);
const result = await service.getDriverRegistrationStatus(driverId, raceId);
expect(mockApiClient.getRegistrationStatus).toHaveBeenCalledWith(driverId, raceId);
expect(result).toBeInstanceOf(DriverRegistrationStatusViewModel);
expect(result.isRegistered).toBe(true);
expect(result.raceId).toBe('race-456');
expect(result.driverId).toBe('driver-123');
expect(result.statusMessage).toBe('Registered for this race');
expect(result.statusColor).toBe('green');
expect(result.statusBadgeVariant).toBe('success');
expect(result.registrationButtonText).toBe('Withdraw');
expect(result.canRegister).toBe(false);
});
it('should handle unregistered driver', async () => {
const driverId = 'driver-123';
const raceId = 'race-456';
const mockDto = {
isRegistered: false,
raceId: 'race-456',
driverId: 'driver-123',
};
mockApiClient.getRegistrationStatus.mockResolvedValue(mockDto);
const result = await service.getDriverRegistrationStatus(driverId, raceId);
expect(result.isRegistered).toBe(false);
expect(result.statusMessage).toBe('Not registered');
expect(result.statusColor).toBe('red');
expect(result.statusBadgeVariant).toBe('warning');
expect(result.registrationButtonText).toBe('Register');
expect(result.canRegister).toBe(true);
});
it('should throw error when apiClient.getRegistrationStatus fails', async () => {
const driverId = 'driver-123';
const raceId = 'race-456';
const error = new Error('API call failed');
mockApiClient.getRegistrationStatus.mockRejectedValue(error);
await expect(service.getDriverRegistrationStatus(driverId, raceId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -0,0 +1,130 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { DriverService } from './DriverService';
import { DriversApiClient } from '../../api/drivers/DriversApiClient';
import { DriverLeaderboardViewModel } from '../../view-models/DriverLeaderboardViewModel';
import { DriverViewModel } from '../../view-models/DriverViewModel';
import { CompleteOnboardingViewModel } from '../../view-models/CompleteOnboardingViewModel';
describe('DriverService', () => {
let mockApiClient: Mocked<DriversApiClient>;
let service: DriverService;
beforeEach(() => {
mockApiClient = {
getLeaderboard: vi.fn(),
completeOnboarding: vi.fn(),
getCurrent: vi.fn(),
} as Mocked<DriversApiClient>;
service = new DriverService(mockApiClient);
});
describe('getDriverLeaderboard', () => {
it('should call apiClient.getLeaderboard and return DriverLeaderboardViewModel', async () => {
const mockDto = {
drivers: [
{
id: 'driver-1',
name: 'John Doe',
rating: 2500,
wins: 10,
podiums: 15,
totalRaces: 50,
country: 'US',
avatarUrl: 'https://example.com/avatar.jpg',
},
],
};
mockApiClient.getLeaderboard.mockResolvedValue(mockDto);
const result = await service.getDriverLeaderboard();
expect(mockApiClient.getLeaderboard).toHaveBeenCalled();
expect(result).toBeInstanceOf(DriverLeaderboardViewModel);
expect(result.drivers).toHaveLength(1);
expect(result.drivers[0].name).toBe('John Doe');
});
it('should throw error when apiClient.getLeaderboard fails', async () => {
const error = new Error('API call failed');
mockApiClient.getLeaderboard.mockRejectedValue(error);
await expect(service.getDriverLeaderboard()).rejects.toThrow('API call failed');
});
});
describe('completeDriverOnboarding', () => {
it('should call apiClient.completeOnboarding and return CompleteOnboardingViewModel', async () => {
const input = {
iracingId: '123456',
country: 'US',
};
const mockDto = {
success: true,
driverId: 'driver-123',
};
mockApiClient.completeOnboarding.mockResolvedValue(mockDto);
const result = await service.completeDriverOnboarding(input);
expect(mockApiClient.completeOnboarding).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(CompleteOnboardingViewModel);
expect(result.success).toBe(true);
expect(result.isSuccessful).toBe(true);
});
it('should throw error when apiClient.completeOnboarding fails', async () => {
const input = {
iracingId: '123456',
country: 'US',
};
const error = new Error('API call failed');
mockApiClient.completeOnboarding.mockRejectedValue(error);
await expect(service.completeDriverOnboarding(input)).rejects.toThrow('API call failed');
});
});
describe('getCurrentDriver', () => {
it('should call apiClient.getCurrent and return DriverViewModel when driver exists', async () => {
const mockDto = {
id: 'driver-123',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
iracingId: '123456',
rating: 2500,
};
mockApiClient.getCurrent.mockResolvedValue(mockDto);
const result = await service.getCurrentDriver();
expect(mockApiClient.getCurrent).toHaveBeenCalled();
expect(result).toBeInstanceOf(DriverViewModel);
expect(result?.id).toBe('driver-123');
expect(result?.name).toBe('John Doe');
expect(result?.hasIracingId).toBe(true);
expect(result?.formattedRating).toBe('2500');
});
it('should return null when apiClient.getCurrent returns null', async () => {
mockApiClient.getCurrent.mockResolvedValue(null);
const result = await service.getCurrentDriver();
expect(mockApiClient.getCurrent).toHaveBeenCalled();
expect(result).toBeNull();
});
it('should throw error when apiClient.getCurrent fails', async () => {
const error = new Error('API call failed');
mockApiClient.getCurrent.mockRejectedValue(error);
await expect(service.getCurrentDriver()).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,8 +1,17 @@
import type { DriversApiClient } from '../../api/drivers/DriversApiClient'; import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
import { DriverLeaderboardViewModel } from '../../view-models'; import { CompleteOnboardingInputDTO } from "@/lib/types/generated/CompleteOnboardingInputDTO";
import { DriverViewModel } from '../../view-models/DriverViewModel'; import { CompleteOnboardingViewModel } from "@/lib/view-models/CompleteOnboardingViewModel";
import { CompleteOnboardingViewModel } from '../../view-models/CompleteOnboardingViewModel'; import { DriverLeaderboardViewModel } from "@/lib/view-models/DriverLeaderboardViewModel";
import type { CompleteOnboardingInputDTO } from '../../types/generated'; import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
// TODO: Create proper DriverDTO in generated types
type DriverDTO = {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
};
/** /**
* Driver Service * Driver Service

View File

@@ -0,0 +1,111 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueMembershipService } from './LeagueMembershipService';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueMemberViewModel } from '../../view-models';
import type { LeagueMemberDTO } from '../../types/generated';
describe('LeagueMembershipService', () => {
let mockApiClient: Mocked<LeaguesApiClient>;
let service: LeagueMembershipService;
beforeEach(() => {
mockApiClient = {
getMemberships: vi.fn(),
removeMember: vi.fn(),
} as Mocked<LeaguesApiClient>;
service = new LeagueMembershipService(mockApiClient);
});
describe('getLeagueMemberships', () => {
it('should call apiClient.getMemberships and return array of LeagueMemberViewModel', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
members: [
{ driverId: 'driver-1' },
{ driverId: 'driver-2' },
] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(LeagueMemberViewModel);
expect(result[0].driverId).toBe('driver-1');
expect(result[0].currentUserId).toBe(currentUserId);
expect(result[1]).toBeInstanceOf(LeagueMemberViewModel);
expect(result[1].driverId).toBe('driver-2');
expect(result[1].currentUserId).toBe(currentUserId);
});
it('should handle empty members array', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
members: [] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(result).toHaveLength(0);
});
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');
});
});
describe('removeMember', () => {
it('should call apiClient.removeMember and return the result', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockResult = { success: true };
mockApiClient.removeMember.mockResolvedValue(mockResult);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
expect(result).toEqual(mockResult);
});
it('should handle unsuccessful removal', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockResult = { success: false };
mockApiClient.removeMember.mockResolvedValue(mockResult);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(result.success).toBe(false);
});
it('should throw error when apiClient.removeMember 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);
await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,6 +1,11 @@
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueMemberViewModel } from '../../view-models';
import type { LeagueMemberDTO } from '../../types/generated'; // TODO: Move to generated types when available
type LeagueMembershipsDTO = {
members: LeagueMemberDTO[];
};
/** /**
* League Membership Service * League Membership Service
@@ -17,7 +22,7 @@ export class LeagueMembershipService {
* Get league memberships with view model transformation * Get league memberships with view model transformation
*/ */
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> { async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> {
const dto = await this.apiClient.getMemberships(leagueId); const dto: LeagueMembershipsDTO = await this.apiClient.getMemberships(leagueId);
return dto.members.map((member: LeagueMemberDTO) => new LeagueMemberViewModel(member, currentUserId)); return dto.members.map((member: LeagueMemberDTO) => new LeagueMemberViewModel(member, currentUserId));
} }

View File

@@ -0,0 +1,299 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { LeagueService } from './LeagueService';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueSummaryViewModel, LeagueStandingsViewModel, LeagueStatsViewModel, LeagueScheduleViewModel, LeagueMembershipsViewModel, CreateLeagueViewModel, RemoveMemberViewModel, LeagueMemberViewModel } from '../../view-models';
import type { LeagueWithCapacityDTO, CreateLeagueInputDTO, CreateLeagueOutputDTO, RemoveLeagueMemberOutputDTO, LeagueMemberDTO } from '../../types/generated';
describe('LeagueService', () => {
let mockApiClient: Mocked<LeaguesApiClient>;
let service: LeagueService;
beforeEach(() => {
mockApiClient = {
getAllWithCapacity: vi.fn(),
getStandings: vi.fn(),
getTotal: vi.fn(),
getSchedule: vi.fn(),
getMemberships: vi.fn(),
create: vi.fn(),
removeMember: vi.fn(),
} as Mocked<LeaguesApiClient>;
service = new LeagueService(mockApiClient);
});
describe('getAllLeagues', () => {
it('should call apiClient.getAllWithCapacity and return array of LeagueSummaryViewModel', async () => {
const mockDto = {
leagues: [
{ id: 'league-1', name: 'League One' },
{ id: 'league-2', name: 'League Two' },
] as LeagueWithCapacityDTO[],
};
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
const result = await service.getAllLeagues();
expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(LeagueSummaryViewModel);
expect(result[0].id).toBe('league-1');
expect(result[1]).toBeInstanceOf(LeagueSummaryViewModel);
expect(result[1].id).toBe('league-2');
});
it('should handle empty leagues array', async () => {
const mockDto = {
leagues: [] as LeagueWithCapacityDTO[],
};
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
const result = await service.getAllLeagues();
expect(result).toHaveLength(0);
});
it('should throw error when apiClient.getAllWithCapacity fails', async () => {
const error = new Error('API call failed');
mockApiClient.getAllWithCapacity.mockRejectedValue(error);
await expect(service.getAllLeagues()).rejects.toThrow('API call failed');
});
});
describe('getLeagueStandings', () => {
it('should call apiClient.getStandings and return LeagueStandingsViewModel', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
id: leagueId,
name: 'Test League',
};
mockApiClient.getStandings.mockResolvedValue(mockDto);
const result = await service.getLeagueStandings(leagueId, currentUserId);
expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueStandingsViewModel);
});
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');
});
});
describe('getLeagueStats', () => {
it('should call apiClient.getTotal and return LeagueStatsViewModel', async () => {
const mockDto = { totalLeagues: 42 };
mockApiClient.getTotal.mockResolvedValue(mockDto);
const result = await service.getLeagueStats();
expect(mockApiClient.getTotal).toHaveBeenCalled();
expect(result).toBeInstanceOf(LeagueStatsViewModel);
expect(result.totalLeagues).toBe(42);
});
it('should throw error when apiClient.getTotal fails', async () => {
const error = new Error('API call failed');
mockApiClient.getTotal.mockRejectedValue(error);
await expect(service.getLeagueStats()).rejects.toThrow('API call failed');
});
});
describe('getLeagueSchedule', () => {
it('should call apiClient.getSchedule and return LeagueScheduleViewModel', async () => {
const leagueId = 'league-123';
const mockDto = { races: [{ id: 'race-1' }, { id: 'race-2' }] };
mockApiClient.getSchedule.mockResolvedValue(mockDto);
const result = await service.getLeagueSchedule(leagueId);
expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueScheduleViewModel);
expect(result.races).toEqual(mockDto.races);
});
it('should handle empty races array', async () => {
const leagueId = 'league-123';
const mockDto = { races: [] };
mockApiClient.getSchedule.mockResolvedValue(mockDto);
const result = await service.getLeagueSchedule(leagueId);
expect(result.races).toEqual([]);
expect(result.hasRaces).toBe(false);
});
it('should throw error when apiClient.getSchedule fails', async () => {
const leagueId = 'league-123';
const error = new Error('API call failed');
mockApiClient.getSchedule.mockRejectedValue(error);
await expect(service.getLeagueSchedule(leagueId)).rejects.toThrow('API call failed');
});
});
describe('getLeagueMemberships', () => {
it('should call apiClient.getMemberships and return LeagueMembershipsViewModel', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
memberships: [
{ driverId: 'driver-1' },
{ driverId: 'driver-2' },
] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
expect(result).toBeInstanceOf(LeagueMembershipsViewModel);
expect(result.memberships).toHaveLength(2);
expect(result.memberships[0]).toBeInstanceOf(LeagueMemberViewModel);
expect(result.memberships[0].driverId).toBe('driver-1');
});
it('should handle empty memberships array', async () => {
const leagueId = 'league-123';
const currentUserId = 'user-456';
const mockDto = {
memberships: [] as LeagueMemberDTO[],
};
mockApiClient.getMemberships.mockResolvedValue(mockDto);
const result = await service.getLeagueMemberships(leagueId, currentUserId);
expect(result.memberships).toHaveLength(0);
expect(result.hasMembers).toBe(false);
});
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');
});
});
describe('createLeague', () => {
it('should call apiClient.create and return CreateLeagueViewModel', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const mockDto: CreateLeagueOutputDTO = {
leagueId: 'new-league-id',
success: true,
};
mockApiClient.create.mockResolvedValue(mockDto);
const result = await service.createLeague(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(CreateLeagueViewModel);
expect(result.leagueId).toBe('new-league-id');
expect(result.success).toBe(true);
});
it('should handle unsuccessful creation', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const mockDto: CreateLeagueOutputDTO = {
leagueId: '',
success: false,
};
mockApiClient.create.mockResolvedValue(mockDto);
const result = await service.createLeague(input);
expect(result.success).toBe(false);
expect(result.successMessage).toBe('Failed to create league.');
});
it('should throw error when apiClient.create fails', async () => {
const input: CreateLeagueInputDTO = {
name: 'New League',
description: 'A new league',
};
const error = new Error('API call failed');
mockApiClient.create.mockRejectedValue(error);
await expect(service.createLeague(input)).rejects.toThrow('API call failed');
});
});
describe('removeMember', () => {
it('should call apiClient.removeMember and return RemoveMemberViewModel', async () => {
const leagueId = 'league-123';
const performerDriverId = 'performer-456';
const targetDriverId = 'target-789';
const mockDto: RemoveLeagueMemberOutputDTO = { success: true };
mockApiClient.removeMember.mockResolvedValue(mockDto);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
expect(result).toBeInstanceOf(RemoveMemberViewModel);
expect(result.success).toBe(true);
});
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);
const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);
expect(result.success).toBe(false);
expect(result.successMessage).toBe('Failed to remove member.');
});
it('should throw error when apiClient.removeMember 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);
await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,11 +1,14 @@
import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient'; import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient";
import { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models'; import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO";
import type { CreateLeagueInputDTO, CreateLeagueOutputDTO, LeagueWithCapacityDTO } from '../../types/generated'; import { LeagueWithCapacityDTO } from "@/lib/types/generated/LeagueWithCapacityDTO";
import { CreateLeagueViewModel } from "@/lib/view-models/CreateLeagueViewModel";
import { LeagueMembershipsViewModel } from "@/lib/view-models/LeagueMembershipsViewModel";
import { LeagueScheduleViewModel } from "@/lib/view-models/LeagueScheduleViewModel";
import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewModel";
import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel";
import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel";
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
// TODO: Move these types to apps/website/lib/types/generated when available
type LeagueStatsDto = { totalLeagues: number };
type LeagueScheduleDto = { races: Array<unknown> };
type LeagueMembershipsDto = { memberships: Array<unknown> };
/** /**
* League Service * League Service
@@ -43,35 +46,40 @@ export class LeagueService {
/** /**
* Get league statistics * Get league statistics
*/ */
async getLeagueStats(): Promise<LeagueStatsDto> { async getLeagueStats(): Promise<LeagueStatsViewModel> {
return await this.apiClient.getTotal(); const dto = await this.apiClient.getTotal();
return new LeagueStatsViewModel(dto);
} }
/** /**
* Get league schedule * Get league schedule
*/ */
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDto> { async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
return await this.apiClient.getSchedule(leagueId); const dto = await this.apiClient.getSchedule(leagueId);
return new LeagueScheduleViewModel(dto);
} }
/** /**
* Get league memberships * Get league memberships
*/ */
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDto> { async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMembershipsViewModel> {
return await this.apiClient.getMemberships(leagueId); const dto = await this.apiClient.getMemberships(leagueId);
return new LeagueMembershipsViewModel(dto, currentUserId);
} }
/** /**
* Create a new league * Create a new league
*/ */
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueOutputDTO> { async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
return await this.apiClient.create(input); const dto = await this.apiClient.create(input);
return new CreateLeagueViewModel(dto);
} }
/** /**
* Remove a member from league * Remove a member from league
*/ */
async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> { async removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<RemoveMemberViewModel> {
return await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId); const dto = await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId);
return new RemoveMemberViewModel(dto);
} }
} }

View File

@@ -0,0 +1,121 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { AvatarService } from './AvatarService';
import { MediaApiClient } from '../../api/media/MediaApiClient';
import { RequestAvatarGenerationViewModel } from '../../view-models/RequestAvatarGenerationViewModel';
import { AvatarViewModel } from '../../view-models/AvatarViewModel';
import { UpdateAvatarViewModel } from '../../view-models/UpdateAvatarViewModel';
describe('AvatarService', () => {
let mockApiClient: Mocked<MediaApiClient>;
let service: AvatarService;
beforeEach(() => {
mockApiClient = {
requestAvatarGeneration: vi.fn(),
getAvatar: vi.fn(),
updateAvatar: vi.fn(),
} as Mocked<MediaApiClient>;
service = new AvatarService(mockApiClient);
});
describe('requestAvatarGeneration', () => {
it('should call apiClient.requestAvatarGeneration with correct input and return view model', async () => {
const input = {
userId: 'user-123',
facePhotoData: 'base64data',
suitColor: 'red' as const,
};
const expectedOutput = { success: true, avatarUrl: 'https://example.com/avatar.jpg' };
mockApiClient.requestAvatarGeneration.mockResolvedValue(expectedOutput);
const result = await service.requestAvatarGeneration(input);
expect(mockApiClient.requestAvatarGeneration).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(RequestAvatarGenerationViewModel);
expect(result.success).toBe(true);
expect(result.avatarUrl).toBe('https://example.com/avatar.jpg');
});
it('should handle error response', async () => {
const input = {
userId: 'user-123',
facePhotoData: 'base64data',
suitColor: 'blue' as const,
};
const expectedOutput = { success: false, error: 'Generation failed' };
mockApiClient.requestAvatarGeneration.mockResolvedValue(expectedOutput);
const result = await service.requestAvatarGeneration(input);
expect(result).toBeInstanceOf(RequestAvatarGenerationViewModel);
expect(result.success).toBe(false);
expect(result.error).toBe('Generation failed');
});
});
describe('getAvatar', () => {
it('should call apiClient.getAvatar with driverId and return view model', async () => {
const driverId = 'driver-123';
const expectedOutput = { driverId: 'driver-123', avatarUrl: 'https://example.com/avatar.jpg' };
mockApiClient.getAvatar.mockResolvedValue(expectedOutput);
const result = await service.getAvatar(driverId);
expect(mockApiClient.getAvatar).toHaveBeenCalledWith(driverId);
expect(result).toBeInstanceOf(AvatarViewModel);
expect(result.driverId).toBe('driver-123');
expect(result.avatarUrl).toBe('https://example.com/avatar.jpg');
expect(result.hasAvatar).toBe(true);
});
it('should handle driver without avatar', async () => {
const driverId = 'driver-456';
const expectedOutput = { driverId: 'driver-456' };
mockApiClient.getAvatar.mockResolvedValue(expectedOutput);
const result = await service.getAvatar(driverId);
expect(result).toBeInstanceOf(AvatarViewModel);
expect(result.driverId).toBe('driver-456');
expect(result.avatarUrl).toBeUndefined();
expect(result.hasAvatar).toBe(false);
});
});
describe('updateAvatar', () => {
it('should call apiClient.updateAvatar with correct input and return view model', async () => {
const input = { driverId: 'driver-123', avatarUrl: 'https://example.com/new-avatar.jpg' };
const expectedOutput = { success: true };
mockApiClient.updateAvatar.mockResolvedValue(expectedOutput);
const result = await service.updateAvatar(input);
expect(mockApiClient.updateAvatar).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(UpdateAvatarViewModel);
expect(result.success).toBe(true);
expect(result.isSuccessful).toBe(true);
expect(result.hasError).toBe(false);
});
it('should handle error response', async () => {
const input = { driverId: 'driver-123', avatarUrl: 'https://example.com/new-avatar.jpg' };
const expectedOutput = { success: false, error: 'Update failed' };
mockApiClient.updateAvatar.mockResolvedValue(expectedOutput);
const result = await service.updateAvatar(input);
expect(result).toBeInstanceOf(UpdateAvatarViewModel);
expect(result.success).toBe(false);
expect(result.error).toBe('Update failed');
expect(result.isSuccessful).toBe(false);
expect(result.hasError).toBe(true);
});
});
});

View File

@@ -1,13 +1,11 @@
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
import { AvatarViewModel } from '@/lib/view-models/AvatarViewModel';
import { RequestAvatarGenerationViewModel } from '@/lib/view-models/RequestAvatarGenerationViewModel';
import { UpdateAvatarViewModel } from '@/lib/view-models/UpdateAvatarViewModel';
import type { MediaApiClient } from '../../api/media/MediaApiClient'; import type { MediaApiClient } from '../../api/media/MediaApiClient';
import type { RequestAvatarGenerationInputDTO } from '../../types/generated';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Move these types to apps/website/lib/types/generated when available
type UpdateAvatarInputDto = { driverId: string; avatarUrl: string }; type UpdateAvatarInputDto = { driverId: string; avatarUrl: string };
import {
RequestAvatarGenerationViewModel,
AvatarViewModel,
UpdateAvatarViewModel
} from '../../view-models';
/** /**
* Avatar Service * Avatar Service

View File

@@ -0,0 +1,159 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { MediaService } from './MediaService';
import { MediaApiClient } from '../../api/media/MediaApiClient';
import { MediaViewModel } from '../../view-models/MediaViewModel';
import { UploadMediaViewModel } from '../../view-models/UploadMediaViewModel';
import { DeleteMediaViewModel } from '../../view-models/DeleteMediaViewModel';
describe('MediaService', () => {
let mockApiClient: Mocked<MediaApiClient>;
let service: MediaService;
beforeEach(() => {
mockApiClient = {
uploadMedia: vi.fn(),
getMedia: vi.fn(),
deleteMedia: vi.fn(),
} as Mocked<MediaApiClient>;
service = new MediaService(mockApiClient);
});
describe('uploadMedia', () => {
it('should call apiClient.uploadMedia with correct input and return view model', async () => {
const input = {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
type: 'image',
category: 'avatar' as const,
};
const expectedOutput = { success: true, mediaId: 'media-123', url: 'https://example.com/media.jpg' };
mockApiClient.uploadMedia.mockResolvedValue(expectedOutput);
const result = await service.uploadMedia(input);
expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(UploadMediaViewModel);
expect(result.success).toBe(true);
expect(result.mediaId).toBe('media-123');
expect(result.url).toBe('https://example.com/media.jpg');
expect(result.isSuccessful).toBe(true);
expect(result.hasError).toBe(false);
});
it('should handle error response', async () => {
const input = {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
type: 'image',
};
const expectedOutput = { success: false, error: 'Upload failed' };
mockApiClient.uploadMedia.mockResolvedValue(expectedOutput);
const result = await service.uploadMedia(input);
expect(result).toBeInstanceOf(UploadMediaViewModel);
expect(result.success).toBe(false);
expect(result.error).toBe('Upload failed');
expect(result.isSuccessful).toBe(false);
expect(result.hasError).toBe(true);
});
it('should handle upload without category', async () => {
const input = {
file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
type: 'image',
};
const expectedOutput = { success: true, mediaId: 'media-456', url: 'https://example.com/media2.jpg' };
mockApiClient.uploadMedia.mockResolvedValue(expectedOutput);
const result = await service.uploadMedia(input);
expect(mockApiClient.uploadMedia).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(UploadMediaViewModel);
expect(result.success).toBe(true);
});
});
describe('getMedia', () => {
it('should call apiClient.getMedia with mediaId and return view model', async () => {
const mediaId = 'media-123';
const expectedOutput = {
id: 'media-123',
url: 'https://example.com/image.jpg',
type: 'image' as const,
category: 'avatar' as const,
uploadedAt: new Date('2023-01-15'),
size: 2048000,
};
mockApiClient.getMedia.mockResolvedValue(expectedOutput);
const result = await service.getMedia(mediaId);
expect(mockApiClient.getMedia).toHaveBeenCalledWith(mediaId);
expect(result).toBeInstanceOf(MediaViewModel);
expect(result.id).toBe('media-123');
expect(result.url).toBe('https://example.com/image.jpg');
expect(result.type).toBe('image');
expect(result.category).toBe('avatar');
expect(result.uploadedAt).toEqual(new Date('2023-01-15'));
expect(result.size).toBe(2048000);
expect(result.formattedSize).toBe('2000.00 KB');
});
it('should handle media without category and size', async () => {
const mediaId = 'media-456';
const expectedOutput = {
id: 'media-456',
url: 'https://example.com/video.mp4',
type: 'video' as const,
uploadedAt: new Date('2023-02-20'),
};
mockApiClient.getMedia.mockResolvedValue(expectedOutput);
const result = await service.getMedia(mediaId);
expect(result).toBeInstanceOf(MediaViewModel);
expect(result.id).toBe('media-456');
expect(result.type).toBe('video');
expect(result.category).toBeUndefined();
expect(result.size).toBeUndefined();
expect(result.formattedSize).toBe('Unknown');
});
});
describe('deleteMedia', () => {
it('should call apiClient.deleteMedia with mediaId and return view model', async () => {
const mediaId = 'media-123';
const expectedOutput = { success: true };
mockApiClient.deleteMedia.mockResolvedValue(expectedOutput);
const result = await service.deleteMedia(mediaId);
expect(mockApiClient.deleteMedia).toHaveBeenCalledWith(mediaId);
expect(result).toBeInstanceOf(DeleteMediaViewModel);
expect(result.success).toBe(true);
expect(result.isSuccessful).toBe(true);
expect(result.hasError).toBe(false);
});
it('should handle error response', async () => {
const mediaId = 'media-456';
const expectedOutput = { success: false, error: 'Deletion failed' };
mockApiClient.deleteMedia.mockResolvedValue(expectedOutput);
const result = await service.deleteMedia(mediaId);
expect(result).toBeInstanceOf(DeleteMediaViewModel);
expect(result.success).toBe(false);
expect(result.error).toBe('Deletion failed');
expect(result.isSuccessful).toBe(false);
expect(result.hasError).toBe(true);
});
});
});

View File

@@ -1,8 +1,10 @@
import { DeleteMediaViewModel } from '@/lib/view-models/DeleteMediaViewModel';
import { MediaViewModel } from '@/lib/view-models/MediaViewModel';
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
import type { MediaApiClient } from '../../api/media/MediaApiClient'; import type { MediaApiClient } from '../../api/media/MediaApiClient';
import { MediaViewModel, UploadMediaViewModel, DeleteMediaViewModel } from '../../view-models';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Move these types to apps/website/lib/types/generated when available
type UploadMediaInputDto = { url: string; mediaType: string; entityType: string; entityId: string }; type UploadMediaInputDto = { file: File; type: string; category?: string };
/** /**
* Media Service * Media Service

View File

@@ -0,0 +1,52 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { MembershipFeeService, GetMembershipFeesOutputDto } from './MembershipFeeService';
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import { MembershipFeeViewModel } from '../../view-models';
import type { MembershipFeeDto } from '../../types/generated';
describe('MembershipFeeService', () => {
let mockApiClient: Mocked<PaymentsApiClient>;
let service: MembershipFeeService;
beforeEach(() => {
mockApiClient = {
getMembershipFees: vi.fn(),
} as Mocked<PaymentsApiClient>;
service = new MembershipFeeService(mockApiClient);
});
describe('getMembershipFees', () => {
it('should call apiClient.getMembershipFees with correct leagueId and return mapped view models', async () => {
const leagueId = 'league-123';
const mockFees: MembershipFeeDto[] = [
{ id: 'fee-1', leagueId: 'league-123' },
{ id: 'fee-2', leagueId: 'league-123' },
];
const mockOutput: GetMembershipFeesOutputDto = { fees: mockFees };
mockApiClient.getMembershipFees.mockResolvedValue(mockOutput);
const result = await service.getMembershipFees(leagueId);
expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId);
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(MembershipFeeViewModel);
expect(result[0].id).toEqual('fee-1');
expect(result[0].leagueId).toEqual('league-123');
expect(result[1]).toBeInstanceOf(MembershipFeeViewModel);
expect(result[1].id).toEqual('fee-2');
expect(result[1].leagueId).toEqual('league-123');
});
it('should return empty array when no fees are returned', async () => {
const leagueId = 'league-456';
const mockOutput: GetMembershipFeesOutputDto = { fees: [] };
mockApiClient.getMembershipFees.mockResolvedValue(mockOutput);
const result = await service.getMembershipFees(leagueId);
expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId);
expect(result).toEqual([]);
});
});
});

View File

@@ -1,6 +1,11 @@
import { MembershipFeeDto } from '@/lib/types/generated/MembershipFeeDto';
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import { MembershipFeeViewModel } from '../../view-models';
import type { MembershipFeeDto } from '../../types/generated'; // TODO: This DTO should be generated from OpenAPI spec when the endpoint is added
export interface GetMembershipFeesOutputDto {
fees: MembershipFeeDto[];
}
/** /**
* Membership Fee Service * Membership Fee Service

View File

@@ -0,0 +1,244 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { PaymentService } from './PaymentService';
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import { PaymentViewModel, MembershipFeeViewModel, PrizeViewModel, WalletViewModel } from '../../view-models';
describe('PaymentService', () => {
let mockApiClient: Mocked<PaymentsApiClient>;
let service: PaymentService;
beforeEach(() => {
mockApiClient = {
getPayments: vi.fn(),
createPayment: vi.fn(),
getMembershipFees: vi.fn(),
getPrizes: vi.fn(),
getWallet: vi.fn(),
} as Mocked<PaymentsApiClient>;
service = new PaymentService(mockApiClient);
});
describe('getPayments', () => {
it('should call apiClient.getPayments and return PaymentViewModel array', async () => {
const mockResponse = {
payments: [
{
id: 'payment-1',
type: 'sponsorship' as const,
amount: 100,
platformFee: 10,
netAmount: 90,
payerId: 'user-1',
payerType: 'sponsor' as const,
leagueId: 'league-1',
status: 'completed' as const,
createdAt: new Date(),
},
],
};
mockApiClient.getPayments.mockResolvedValue(mockResponse);
const result = await service.getPayments();
expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, undefined);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(PaymentViewModel);
expect(result[0].id).toBe('payment-1');
});
it('should call apiClient.getPayments with filters', async () => {
const mockResponse = { payments: [] };
mockApiClient.getPayments.mockResolvedValue(mockResponse);
await service.getPayments('league-1', 'user-1');
expect(mockApiClient.getPayments).toHaveBeenCalledWith('league-1', 'user-1');
});
});
describe('getPayment', () => {
it('should return PaymentViewModel when payment exists', async () => {
const mockResponse = {
payments: [
{
id: 'payment-1',
type: 'sponsorship' as const,
amount: 100,
platformFee: 10,
netAmount: 90,
payerId: 'user-1',
payerType: 'sponsor' as const,
leagueId: 'league-1',
status: 'completed' as const,
createdAt: new Date(),
},
],
};
mockApiClient.getPayments.mockResolvedValue(mockResponse);
const result = await service.getPayment('payment-1');
expect(result).toBeInstanceOf(PaymentViewModel);
expect(result.id).toBe('payment-1');
});
it('should throw error when payment does not exist', async () => {
const mockResponse = { payments: [] };
mockApiClient.getPayments.mockResolvedValue(mockResponse);
await expect(service.getPayment('non-existent')).rejects.toThrow(
'Payment with ID non-existent not found'
);
});
});
describe('createPayment', () => {
it('should call apiClient.createPayment and return PaymentViewModel', async () => {
const input = {
type: 'sponsorship' as const,
amount: 100,
payerId: 'user-1',
payerType: 'sponsor' as const,
leagueId: 'league-1',
};
const mockResponse = {
payment: {
id: 'payment-1',
type: 'sponsorship' as const,
amount: 100,
platformFee: 10,
netAmount: 90,
payerId: 'user-1',
payerType: 'sponsor' as const,
leagueId: 'league-1',
status: 'pending' as const,
createdAt: new Date(),
},
};
mockApiClient.createPayment.mockResolvedValue(mockResponse);
const result = await service.createPayment(input);
expect(mockApiClient.createPayment).toHaveBeenCalledWith(input);
expect(result).toBeInstanceOf(PaymentViewModel);
expect(result.id).toBe('payment-1');
});
});
describe('getMembershipFees', () => {
it('should return MembershipFeeViewModel when fee exists', async () => {
const mockResponse = {
fee: {
id: 'fee-1',
leagueId: 'league-1',
type: 'season' as const,
amount: 50,
enabled: true,
createdAt: new Date(),
updatedAt: new Date(),
},
payments: [],
};
mockApiClient.getMembershipFees.mockResolvedValue(mockResponse);
const result = await service.getMembershipFees('league-1');
expect(result).toBeInstanceOf(MembershipFeeViewModel);
expect(result!.id).toBe('fee-1');
});
it('should return null when fee does not exist', async () => {
const mockResponse = {
fee: null,
payments: [],
};
mockApiClient.getMembershipFees.mockResolvedValue(mockResponse);
const result = await service.getMembershipFees('league-1');
expect(result).toBeNull();
});
});
describe('getPrizes', () => {
it('should call apiClient.getPrizes and return PrizeViewModel array', async () => {
const mockResponse = {
prizes: [
{
id: 'prize-1',
leagueId: 'league-1',
seasonId: 'season-1',
position: 1,
name: 'First Place',
amount: 100,
type: 'cash' as const,
awarded: false,
createdAt: new Date(),
},
],
};
mockApiClient.getPrizes.mockResolvedValue(mockResponse);
const result = await service.getPrizes();
expect(mockApiClient.getPrizes).toHaveBeenCalledWith(undefined, undefined);
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(PrizeViewModel);
expect(result[0].id).toBe('prize-1');
});
it('should call apiClient.getPrizes with filters', async () => {
const mockResponse = { prizes: [] };
mockApiClient.getPrizes.mockResolvedValue(mockResponse);
await service.getPrizes('league-1', 'season-1');
expect(mockApiClient.getPrizes).toHaveBeenCalledWith('league-1', 'season-1');
});
});
describe('getWallet', () => {
it('should call apiClient.getWallet and return WalletViewModel', async () => {
const mockResponse = {
wallet: {
id: 'wallet-1',
leagueId: 'league-1',
balance: 1000,
totalRevenue: 1500,
totalPlatformFees: 100,
totalWithdrawn: 400,
createdAt: new Date(),
currency: 'EUR',
},
transactions: [
{
id: 'tx-1',
walletId: 'wallet-1',
type: 'deposit' as const,
amount: 500,
description: 'Deposit',
createdAt: new Date(),
},
],
};
mockApiClient.getWallet.mockResolvedValue(mockResponse);
const result = await service.getWallet('user-1');
expect(mockApiClient.getWallet).toHaveBeenCalledWith('user-1');
expect(result).toBeInstanceOf(WalletViewModel);
expect(result.id).toBe('wallet-1');
expect(result.balance).toBe(1000);
expect(result.transactions).toHaveLength(1);
});
});
});

View File

@@ -1,15 +1,21 @@
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
import { PaymentViewModel } from '@/lib/view-models/PaymentViewModel';
import { PrizeViewModel } from '@/lib/view-models/PrizeViewModel';
import { WalletViewModel } from '@/lib/view-models/WalletViewModel';
import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import type { PaymentDto, MembershipFeeDto, PrizeDto } from '../../types/generated'; import type { PaymentDTO } from '../../types/generated/PaymentDto';
import type { PrizeDto } from '../../types/generated/PrizeDto';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Move these types to apps/website/lib/types/generated when available
type CreatePaymentInputDto = { amount: number; leagueId: string; driverId: string; description: string }; type CreatePaymentInputDto = {
type CreatePaymentOutputDto = { id: string; success: boolean }; type: 'sponsorship' | 'membership_fee';
import { amount: number;
PaymentViewModel, payerId: string;
MembershipFeeViewModel, payerType: 'sponsor' | 'driver';
PrizeViewModel, leagueId: string;
WalletViewModel, seasonId?: string;
} from '../../view-models'; };
/** /**
* Payment Service * Payment Service
@@ -27,7 +33,7 @@ export class PaymentService {
*/ */
async getPayments(leagueId?: string, driverId?: string): Promise<PaymentViewModel[]> { async getPayments(leagueId?: string, driverId?: string): Promise<PaymentViewModel[]> {
const dto = await this.apiClient.getPayments(leagueId, driverId); const dto = await this.apiClient.getPayments(leagueId, driverId);
return dto.payments.map((payment: PaymentDto) => new PaymentViewModel(payment)); return dto.payments.map((payment: PaymentDTO) => new PaymentViewModel(payment));
} }
/** /**
@@ -36,7 +42,7 @@ export class PaymentService {
async getPayment(paymentId: string): Promise<PaymentViewModel> { async getPayment(paymentId: string): Promise<PaymentViewModel> {
// Note: Assuming the API returns a single payment from the list // Note: Assuming the API returns a single payment from the list
const dto = await this.apiClient.getPayments(); const dto = await this.apiClient.getPayments();
const payment = dto.payments.find((p: PaymentDto) => p.id === paymentId); const payment = dto.payments.find((p: PaymentDTO) => p.id === paymentId);
if (!payment) { if (!payment) {
throw new Error(`Payment with ID ${paymentId} not found`); throw new Error(`Payment with ID ${paymentId} not found`);
} }
@@ -46,16 +52,17 @@ export class PaymentService {
/** /**
* Create a new payment * Create a new payment
*/ */
async createPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> { async createPayment(input: CreatePaymentInputDto): Promise<PaymentViewModel> {
return await this.apiClient.createPayment(input); const dto = await this.apiClient.createPayment(input);
return new PaymentViewModel(dto.payment);
} }
/** /**
* Get membership fees for a league * Get membership fees for a league
*/ */
async getMembershipFees(leagueId: string): Promise<MembershipFeeViewModel[]> { async getMembershipFees(leagueId: string): Promise<MembershipFeeViewModel | null> {
const dto = await this.apiClient.getMembershipFees(leagueId); const dto = await this.apiClient.getMembershipFees(leagueId);
return dto.fees.map((fee: MembershipFeeDto) => new MembershipFeeViewModel(fee)); return dto.fee ? new MembershipFeeViewModel(dto.fee) : null;
} }
/** /**
@@ -71,14 +78,7 @@ export class PaymentService {
*/ */
async getWallet(driverId: string): Promise<WalletViewModel> { async getWallet(driverId: string): Promise<WalletViewModel> {
const dto = await this.apiClient.getWallet(driverId); const dto = await this.apiClient.getWallet(driverId);
return new WalletViewModel(dto); return new WalletViewModel({ ...dto.wallet, transactions: dto.transactions });
}
/**
* Process a payment (alias for createPayment)
*/
async processPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> {
return await this.createPayment(input);
} }
/** /**

View File

@@ -0,0 +1,77 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { WalletService } from './WalletService';
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import { WalletViewModel } from '../../view-models';
describe('WalletService', () => {
let mockApiClient: Mocked<PaymentsApiClient>;
let service: WalletService;
beforeEach(() => {
mockApiClient = {
getWallet: vi.fn(),
} as Mocked<PaymentsApiClient>;
service = new WalletService(mockApiClient);
});
describe('getWallet', () => {
it('should call apiClient.getWallet and return WalletViewModel', async () => {
const mockResponse = {
wallet: {
id: 'wallet-1',
leagueId: 'league-1',
balance: 1000,
totalRevenue: 1500,
totalPlatformFees: 100,
totalWithdrawn: 400,
createdAt: '2023-01-01T00:00:00Z',
currency: 'EUR',
},
transactions: [
{
id: 'tx-1',
walletId: 'wallet-1',
amount: 500,
description: 'Deposit',
createdAt: '2023-01-01T00:00:00Z',
type: 'deposit' as const,
},
],
};
mockApiClient.getWallet.mockResolvedValue(mockResponse);
const result = await service.getWallet('user-1');
expect(mockApiClient.getWallet).toHaveBeenCalledWith('user-1');
expect(result).toBeInstanceOf(WalletViewModel);
expect(result.id).toBe('wallet-1');
expect(result.balance).toBe(1000);
expect(result.transactions).toHaveLength(1);
expect(result.transactions[0].id).toBe('tx-1');
});
it('should handle wallet without transactions', async () => {
const mockResponse = {
wallet: {
id: 'wallet-1',
leagueId: 'league-1',
balance: 1000,
totalRevenue: 1500,
totalPlatformFees: 100,
totalWithdrawn: 400,
createdAt: '2023-01-01T00:00:00Z',
currency: 'EUR',
},
transactions: [],
};
mockApiClient.getWallet.mockResolvedValue(mockResponse);
const result = await service.getWallet('user-1');
expect(result.transactions).toHaveLength(0);
});
});
});

View File

@@ -1,5 +1,6 @@
import { WalletViewModel } from '@/lib/view-models/WalletViewModel';
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
import { WalletViewModel } from '../../view-models'; import { FullTransactionDto } from '../../view-models/WalletTransactionViewModel';
/** /**
* Wallet Service * Wallet Service
@@ -16,7 +17,7 @@ export class WalletService {
* Get wallet by driver ID with view model transformation * Get wallet by driver ID with view model transformation
*/ */
async getWallet(driverId: string): Promise<WalletViewModel> { async getWallet(driverId: string): Promise<WalletViewModel> {
const dto = await this.apiClient.getWallet(driverId); const { wallet, transactions } = await this.apiClient.getWallet(driverId);
return new WalletViewModel(dto); return new WalletViewModel({ ...wallet, transactions: transactions as FullTransactionDto[] });
} }
} }

View File

@@ -0,0 +1,149 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { RaceResultsService } from './RaceResultsService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel';
import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel';
import type { RaceResultsDetailDTO, RaceWithSOFDTO } from '../../types/generated';
describe('RaceResultsService', () => {
let mockApiClient: Mocked<RacesApiClient>;
let service: RaceResultsService;
beforeEach(() => {
mockApiClient = {
getResultsDetail: vi.fn(),
getWithSOF: vi.fn(),
importResults: vi.fn(),
} as Mocked<RacesApiClient>;
service = new RaceResultsService(mockApiClient);
});
describe('getResultsDetail', () => {
it('should call apiClient.getResultsDetail and return RaceResultsDetailViewModel', async () => {
const raceId = 'race-123';
const currentUserId = 'user-456';
const mockDto: RaceResultsDetailDTO = {
raceId,
track: 'Test Track',
};
mockApiClient.getResultsDetail.mockResolvedValue(mockDto);
const result = await service.getResultsDetail(raceId, currentUserId);
expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId);
expect(result).toBeInstanceOf(RaceResultsDetailViewModel);
expect(result.raceId).toBe(raceId);
expect(result.track).toBe('Test Track');
});
it('should handle undefined currentUserId', async () => {
const raceId = 'race-123';
const mockDto: RaceResultsDetailDTO = {
raceId,
track: 'Test Track',
};
mockApiClient.getResultsDetail.mockResolvedValue(mockDto);
const result = await service.getResultsDetail(raceId);
expect(result).toBeInstanceOf(RaceResultsDetailViewModel);
expect(result.currentUserId).toBe('');
});
it('should throw error when apiClient.getResultsDetail fails', async () => {
const raceId = 'race-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getResultsDetail.mockRejectedValue(error);
await expect(service.getResultsDetail(raceId, currentUserId)).rejects.toThrow('API call failed');
});
});
describe('getWithSOF', () => {
it('should call apiClient.getWithSOF and return RaceWithSOFViewModel', async () => {
const raceId = 'race-123';
const mockDto: RaceWithSOFDTO = {
id: raceId,
track: 'Test Track',
};
mockApiClient.getWithSOF.mockResolvedValue(mockDto);
const result = await service.getWithSOF(raceId);
expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId);
expect(result).toBeInstanceOf(RaceWithSOFViewModel);
expect(result.id).toBe(raceId);
expect(result.track).toBe('Test Track');
});
it('should throw error when apiClient.getWithSOF fails', async () => {
const raceId = 'race-123';
const error = new Error('API call failed');
mockApiClient.getWithSOF.mockRejectedValue(error);
await expect(service.getWithSOF(raceId)).rejects.toThrow('API call failed');
});
});
describe('importResults', () => {
it('should call apiClient.importResults and return ImportRaceResultsSummaryViewModel', async () => {
const raceId = 'race-123';
const input = { raceId, results: [{ position: 1 }] };
const mockDto = {
raceId,
importedCount: 10,
errors: ['Error 1'],
};
mockApiClient.importResults.mockResolvedValue(mockDto as any);
const result = await service.importResults(raceId, input);
expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input);
expect(result).toBeInstanceOf(ImportRaceResultsSummaryViewModel);
expect(result.raceId).toBe(raceId);
expect(result.importedCount).toBe(10);
expect(result.errors).toEqual(['Error 1']);
});
it('should handle successful import with no errors', async () => {
const raceId = 'race-123';
const input = { raceId, results: [] };
const mockDto = {
raceId,
importedCount: 5,
errors: [],
};
mockApiClient.importResults.mockResolvedValue(mockDto as any);
const result = await service.importResults(raceId, input);
expect(result.importedCount).toBe(5);
expect(result.errors).toEqual([]);
});
it('should throw error when apiClient.importResults fails', async () => {
const raceId = 'race-123';
const input = { raceId, results: [] };
const error = new Error('API call failed');
mockApiClient.importResults.mockRejectedValue(error);
await expect(service.importResults(raceId, input)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,13 +1,17 @@
import { RacesApiClient } from '../../api/races/RacesApiClient'; import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel'; import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel';
import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Move this type to apps/website/lib/types/generated when available
type ImportRaceResultsInputDto = { raceId: string; results: Array<any> }; type ImportRaceResultsInputDto = { raceId: string; results: Array<unknown> };
// Note: RaceWithSOFViewModel and ImportRaceResultsSummaryViewModel are defined in presenters // TODO: Move this type to apps/website/lib/types/generated when available
// These will need to be converted to proper view models type ImportRaceResultsSummaryDto = {
type RaceWithSOFViewModel = any; // TODO: Create proper view model raceId: string;
type ImportRaceResultsSummaryViewModel = any; // TODO: Create proper view model importedCount: number;
errors: string[];
};
/** /**
* Race Results Service * Race Results Service
@@ -29,20 +33,18 @@ export class RaceResultsService {
} }
/** /**
* Get race with strength of field calculation * Get race with strength of field calculation
* TODO: Create RaceWithSOFViewModel and use it here */
*/ async getWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
async getWithSOF(raceId: string): Promise<RaceWithSOFViewModel> { const dto = await this.apiClient.getWithSOF(raceId);
const dto = await this.apiClient.getWithSOF(raceId); return new RaceWithSOFViewModel(dto);
return dto; // TODO: return new RaceWithSOFViewModel(dto); }
}
/** /**
* Import race results and get summary * Import race results and get summary
* TODO: Create ImportRaceResultsSummaryViewModel and use it here */
*/ async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryViewModel> {
async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryViewModel> { const dto = await this.apiClient.importResults(raceId, input) as ImportRaceResultsSummaryDto;
const dto = await this.apiClient.importResults(raceId, input); return new ImportRaceResultsSummaryViewModel(dto);
return dto; // TODO: return new ImportRaceResultsSummaryViewModel(dto); }
}
} }

View File

@@ -0,0 +1,134 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { RaceService } from './RaceService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
describe('RaceService', () => {
let mockApiClient: Mocked<RacesApiClient>;
let service: RaceService;
beforeEach(() => {
mockApiClient = {
getDetail: vi.fn(),
getPageData: vi.fn(),
getTotal: vi.fn(),
} as Mocked<RacesApiClient>;
service = new RaceService(mockApiClient);
});
describe('getRaceDetail', () => {
it('should call apiClient.getDetail and return RaceDetailViewModel', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const mockDto = {
race: { id: raceId, track: 'Test Track' },
league: { id: 'league-1', name: 'Test League' },
entryList: [],
registration: { isRegistered: true, canRegister: false },
userResult: null,
};
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
const result = await service.getRaceDetail(raceId, driverId);
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
expect(result).toBeInstanceOf(RaceDetailViewModel);
expect(result.race?.id).toBe(raceId);
});
it('should throw error when apiClient.getDetail fails', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const error = new Error('API call failed');
mockApiClient.getDetail.mockRejectedValue(error);
await expect(service.getRaceDetail(raceId, driverId)).rejects.toThrow('API call failed');
});
});
describe('getRacesPageData', () => {
it('should call apiClient.getPageData and return RacesPageViewModel with transformed data', async () => {
const mockDto = {
races: [
{
id: 'race-1',
track: 'Monza',
car: 'Ferrari',
scheduledAt: '2023-10-01T10:00:00Z',
status: 'upcoming',
leagueId: 'league-1',
leagueName: 'Test League',
},
{
id: 'race-2',
track: 'Silverstone',
car: 'Mercedes',
scheduledAt: '2023-09-15T10:00:00Z',
status: 'completed',
leagueId: 'league-1',
leagueName: 'Test League',
},
],
};
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
const result = await service.getRacesPageData();
expect(mockApiClient.getPageData).toHaveBeenCalled();
expect(result).toBeInstanceOf(RacesPageViewModel);
expect(result.upcomingRaces).toHaveLength(1);
expect(result.completedRaces).toHaveLength(1);
expect(result.totalCount).toBe(2);
expect(result.upcomingRaces[0].title).toBe('Monza - Ferrari');
expect(result.completedRaces[0].title).toBe('Silverstone - Mercedes');
});
it('should handle empty races array', async () => {
const mockDto = { races: [] };
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
const result = await service.getRacesPageData();
expect(result.upcomingRaces).toHaveLength(0);
expect(result.completedRaces).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
it('should throw error when apiClient.getPageData fails', async () => {
const error = new Error('API call failed');
mockApiClient.getPageData.mockRejectedValue(error);
await expect(service.getRacesPageData()).rejects.toThrow('API call failed');
});
});
describe('getRacesTotal', () => {
it('should call apiClient.getTotal and return RaceStatsViewModel', async () => {
const mockDto = { totalRaces: 42 };
mockApiClient.getTotal.mockResolvedValue(mockDto as any);
const result = await service.getRacesTotal();
expect(mockApiClient.getTotal).toHaveBeenCalled();
expect(result).toBeInstanceOf(RaceStatsViewModel);
expect(result.totalRaces).toBe(42);
expect(result.formattedTotalRaces).toBe('42');
});
it('should throw error when apiClient.getTotal fails', async () => {
const error = new Error('API call failed');
mockApiClient.getTotal.mockRejectedValue(error);
await expect(service.getRacesTotal()).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,9 +1,20 @@
import { RacesApiClient } from '../../api/races/RacesApiClient'; import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel'; import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Move these types to apps/website/lib/types/generated when available
type RacesPageDataDto = { races: Array<any> }; type RacesPageDataRaceDTO = {
type RaceStatsDto = { totalRaces: number }; id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
};
type RacesPageDataDto = { races: RacesPageDataRaceDTO[] };
type RaceStatsDTO = { totalRaces: number };
/** /**
* Race Service * Race Service
@@ -28,18 +39,51 @@ export class RaceService {
} }
/** /**
* Get races page data * Get races page data with view model transformation
* TODO: Add view model transformation when view model is available
*/ */
async getRacesPageData(): Promise<RacesPageDataDto> { async getRacesPageData(): Promise<RacesPageViewModel> {
return this.apiClient.getPageData(); const dto = await this.apiClient.getPageData();
return new RacesPageViewModel(this.transformRacesPageData(dto));
} }
/** /**
* Get total races statistics * Get total races statistics with view model transformation
* TODO: Add view model transformation when view model is available
*/ */
async getRacesTotal(): Promise<RaceStatsDto> { async getRacesTotal(): Promise<RaceStatsViewModel> {
return this.apiClient.getTotal(); const dto: RaceStatsDTO = await this.apiClient.getTotal();
return new RaceStatsViewModel(dto);
}
/**
* Transform API races page data to view model format
*/
private transformRacesPageData(dto: RacesPageDataDto): {
upcomingRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
completedRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
totalCount: number;
} {
const upcomingRaces = dto.races
.filter(race => race.status !== 'completed')
.map(race => ({
id: race.id,
title: `${race.track} - ${race.car}`,
scheduledTime: race.scheduledAt,
status: race.status,
}));
const completedRaces = dto.races
.filter(race => race.status === 'completed')
.map(race => ({
id: race.id,
title: `${race.track} - ${race.car}`,
scheduledTime: race.scheduledAt,
status: race.status,
}));
return {
upcomingRaces,
completedRaces,
totalCount: dto.races.length,
};
} }
} }

View File

@@ -0,0 +1,180 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { SponsorService } from './SponsorService';
import { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient';
import { SponsorViewModel } from '../../view-models/SponsorViewModel';
import { SponsorDashboardViewModel } from '../../view-models/SponsorDashboardViewModel';
import { SponsorSponsorshipsViewModel } from '../../view-models/SponsorSponsorshipsViewModel';
describe('SponsorService', () => {
let mockApiClient: Mocked<SponsorsApiClient>;
let service: SponsorService;
beforeEach(() => {
mockApiClient = {
getAll: vi.fn(),
getDashboard: vi.fn(),
getSponsorships: vi.fn(),
create: vi.fn(),
getPricing: vi.fn(),
} as Mocked<SponsorsApiClient>;
service = new SponsorService(mockApiClient);
});
describe('getAllSponsors', () => {
it('should call apiClient.getAll and return array of SponsorViewModel', async () => {
const mockDto = {
sponsors: [
{
id: 'sponsor-1',
name: 'Test Sponsor',
logoUrl: 'https://example.com/logo.png',
websiteUrl: 'https://example.com',
},
],
};
mockApiClient.getAll.mockResolvedValue(mockDto);
const result = await service.getAllSponsors();
expect(mockApiClient.getAll).toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(SponsorViewModel);
expect(result[0].id).toBe('sponsor-1');
expect(result[0].name).toBe('Test Sponsor');
expect(result[0].hasWebsite).toBe(true);
});
it('should throw error when apiClient.getAll fails', async () => {
const error = new Error('API call failed');
mockApiClient.getAll.mockRejectedValue(error);
await expect(service.getAllSponsors()).rejects.toThrow('API call failed');
});
});
describe('getSponsorDashboard', () => {
it('should call apiClient.getDashboard and return SponsorDashboardViewModel when data exists', async () => {
const mockDto = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
};
mockApiClient.getDashboard.mockResolvedValue(mockDto);
const result = await service.getSponsorDashboard('sponsor-1');
expect(mockApiClient.getDashboard).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeInstanceOf(SponsorDashboardViewModel);
expect(result?.sponsorId).toBe('sponsor-1');
expect(result?.sponsorName).toBe('Test Sponsor');
});
it('should return null when apiClient.getDashboard returns null', async () => {
mockApiClient.getDashboard.mockResolvedValue(null);
const result = await service.getSponsorDashboard('sponsor-1');
expect(mockApiClient.getDashboard).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getDashboard fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDashboard.mockRejectedValue(error);
await expect(service.getSponsorDashboard('sponsor-1')).rejects.toThrow('API call failed');
});
});
describe('getSponsorSponsorships', () => {
it('should call apiClient.getSponsorships and return SponsorSponsorshipsViewModel when data exists', async () => {
const mockDto = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
};
mockApiClient.getSponsorships.mockResolvedValue(mockDto);
const result = await service.getSponsorSponsorships('sponsor-1');
expect(mockApiClient.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeInstanceOf(SponsorSponsorshipsViewModel);
expect(result?.sponsorId).toBe('sponsor-1');
expect(result?.sponsorName).toBe('Test Sponsor');
});
it('should return null when apiClient.getSponsorships returns null', async () => {
mockApiClient.getSponsorships.mockResolvedValue(null);
const result = await service.getSponsorSponsorships('sponsor-1');
expect(mockApiClient.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getSponsorships fails', async () => {
const error = new Error('API call failed');
mockApiClient.getSponsorships.mockRejectedValue(error);
await expect(service.getSponsorSponsorships('sponsor-1')).rejects.toThrow('API call failed');
});
});
describe('createSponsor', () => {
it('should call apiClient.create and return the result', async () => {
const input = {
name: 'New Sponsor',
};
const mockOutput = {
id: 'sponsor-123',
name: 'New Sponsor',
};
mockApiClient.create.mockResolvedValue(mockOutput);
const result = await service.createSponsor(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.create fails', async () => {
const input = {
name: 'New Sponsor',
};
const error = new Error('API call failed');
mockApiClient.create.mockRejectedValue(error);
await expect(service.createSponsor(input)).rejects.toThrow('API call failed');
});
});
describe('getSponsorshipPricing', () => {
it('should call apiClient.getPricing and return the result', async () => {
const mockPricing = {
pricing: [
{ entityType: 'league', price: 100 },
{ entityType: 'driver', price: 50 },
],
};
mockApiClient.getPricing.mockResolvedValue(mockPricing);
const result = await service.getSponsorshipPricing();
expect(mockApiClient.getPricing).toHaveBeenCalled();
expect(result).toEqual(mockPricing);
});
it('should throw error when apiClient.getPricing fails', async () => {
const error = new Error('API call failed');
mockApiClient.getPricing.mockRejectedValue(error);
await expect(service.getSponsorshipPricing()).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,11 +1,8 @@
import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient'; import type { SponsorsApiClient, CreateSponsorOutputDto, GetEntitySponsorshipPricingResultDto, SponsorDTO } from '../../api/sponsors/SponsorsApiClient';
import { SponsorViewModel, SponsorDashboardViewModel, SponsorSponsorshipsViewModel } from '../../view-models'; import { SponsorViewModel } from '../../view-models/SponsorViewModel';
import type { CreateSponsorInputDTO } from '../../types/generated'; import { SponsorDashboardViewModel } from '../../view-models/SponsorDashboardViewModel';
import { SponsorSponsorshipsViewModel } from '../../view-models/SponsorSponsorshipsViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available import type { CreateSponsorInputDTO } from '../../types/generated/CreateSponsorInputDTO';
type CreateSponsorOutputDto = { id: string; name: string };
type GetEntitySponsorshipPricingResultDto = { pricing: Array<{ entityType: string; price: number }> };
type SponsorDTO = { id: string; name: string; logoUrl?: string; websiteUrl?: string };
/** /**
* Sponsor Service * Sponsor Service

View File

@@ -0,0 +1,95 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { SponsorshipService } from './SponsorshipService';
import { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient';
import { SponsorshipPricingViewModel } from '../../view-models/SponsorshipPricingViewModel';
import { SponsorSponsorshipsViewModel } from '../../view-models/SponsorSponsorshipsViewModel';
describe('SponsorshipService', () => {
let mockApiClient: Mocked<SponsorsApiClient>;
let service: SponsorshipService;
beforeEach(() => {
mockApiClient = {
getPricing: vi.fn(),
getSponsorships: vi.fn(),
} as Mocked<SponsorsApiClient>;
service = new SponsorshipService(mockApiClient);
});
describe('getSponsorshipPricing', () => {
it('should call apiClient.getPricing and return SponsorshipPricingViewModel', async () => {
const mockDto = {
pricing: [
{ entityType: 'league', price: 100 },
{ entityType: 'driver', price: 50 },
],
};
mockApiClient.getPricing.mockResolvedValue(mockDto);
const result = await service.getSponsorshipPricing();
expect(mockApiClient.getPricing).toHaveBeenCalled();
expect(result).toBeInstanceOf(SponsorshipPricingViewModel);
expect(result.mainSlotPrice).toBe(100);
expect(result.secondarySlotPrice).toBe(50);
expect(result.currency).toBe('USD');
});
it('should handle missing entity types with default prices', async () => {
const mockDto = {
pricing: [],
};
mockApiClient.getPricing.mockResolvedValue(mockDto);
const result = await service.getSponsorshipPricing();
expect(result.mainSlotPrice).toBe(0);
expect(result.secondarySlotPrice).toBe(0);
expect(result.currency).toBe('USD');
});
it('should throw error when apiClient.getPricing fails', async () => {
const error = new Error('API call failed');
mockApiClient.getPricing.mockRejectedValue(error);
await expect(service.getSponsorshipPricing()).rejects.toThrow('API call failed');
});
});
describe('getSponsorSponsorships', () => {
it('should call apiClient.getSponsorships and return SponsorSponsorshipsViewModel when data exists', async () => {
const mockDto = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
};
mockApiClient.getSponsorships.mockResolvedValue(mockDto);
const result = await service.getSponsorSponsorships('sponsor-1');
expect(mockApiClient.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeInstanceOf(SponsorSponsorshipsViewModel);
expect(result?.sponsorId).toBe('sponsor-1');
expect(result?.sponsorName).toBe('Test Sponsor');
});
it('should return null when apiClient.getSponsorships returns null', async () => {
mockApiClient.getSponsorships.mockResolvedValue(null);
const result = await service.getSponsorSponsorships('sponsor-1');
expect(mockApiClient.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getSponsorships fails', async () => {
const error = new Error('API call failed');
mockApiClient.getSponsorships.mockRejectedValue(error);
await expect(service.getSponsorSponsorships('sponsor-1')).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,13 +1,11 @@
import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient'; import type { SponsorsApiClient } from '../../api/sponsors/SponsorsApiClient';
import type { GetEntitySponsorshipPricingResultDto } from '../../api/sponsors/SponsorsApiClient';
import { import {
SponsorshipPricingViewModel, SponsorshipPricingViewModel,
SponsorSponsorshipsViewModel SponsorSponsorshipsViewModel
} from '../../view-models'; } from '../../view-models';
import type { SponsorSponsorshipsDTO } from '../../types/generated'; import type { SponsorSponsorshipsDTO } from '../../types/generated';
// TODO: Move these types to apps/website/lib/types/generated when available
type GetEntitySponsorshipPricingResultDto = { pricing: Array<{ entityType: string; price: number }> };
/** /**
* Sponsorship Service * Sponsorship Service
* *

View File

@@ -0,0 +1,80 @@
import { describe, it, expect, vi } from 'vitest';
import { TeamJoinService } from './TeamJoinService';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
describe('TeamJoinService', () => {
let service: TeamJoinService;
let mockApiClient: TeamsApiClient;
beforeEach(() => {
mockApiClient = {
getJoinRequests: vi.fn(),
} as any;
service = new TeamJoinService(mockApiClient);
});
describe('getJoinRequests', () => {
it('should return view models for join requests', async () => {
const mockDto = {
requests: [
{
id: 'request-1',
teamId: 'team-1',
driverId: 'driver-1',
requestedAt: '2023-01-01T00:00:00Z',
message: 'Please accept me',
},
{
id: 'request-2',
teamId: 'team-1',
driverId: 'driver-2',
requestedAt: '2023-01-02T00:00:00Z',
},
],
};
mockApiClient.getJoinRequests = vi.fn().mockResolvedValue(mockDto);
const result = await service.getJoinRequests('team-1', 'user-1', true);
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
expect(result).toHaveLength(2);
expect(result[0].id).toBe('request-1');
expect(result[0].canApprove).toBe(true);
expect(result[1].id).toBe('request-2');
expect(result[1].canApprove).toBe(true);
});
it('should pass correct parameters to view model constructor', async () => {
const mockDto = {
requests: [
{
id: 'request-1',
teamId: 'team-1',
driverId: 'driver-1',
requestedAt: '2023-01-01T00:00:00Z',
},
],
};
mockApiClient.getJoinRequests = vi.fn().mockResolvedValue(mockDto);
const result = await service.getJoinRequests('team-1', 'user-1', false);
expect(result[0].canApprove).toBe(false);
});
});
describe('approveJoinRequest', () => {
it('should throw not implemented error', async () => {
await expect(service.approveJoinRequest()).rejects.toThrow('Not implemented: API endpoint for approving join requests');
});
});
describe('rejectJoinRequest', () => {
it('should throw not implemented error', async () => {
await expect(service.rejectJoinRequest()).rejects.toThrow('Not implemented: API endpoint for rejecting join requests');
});
});
});

View File

@@ -1,12 +1,9 @@
import { TeamJoinRequestViewModel, type TeamJoinRequestDTO } from '@/lib/view-models/TeamJoinRequestViewModel';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient'; import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { TeamJoinRequestViewModel } from '../../view-models';
type TeamJoinRequestDTO = { // TODO: Create generated DTO when API spec is available
id: string; type TeamJoinRequestsDto = {
teamId: string; requests: TeamJoinRequestDTO[];
driverId: string;
requestedAt: string;
message?: string;
}; };
/** /**
@@ -24,14 +21,14 @@ export class TeamJoinService {
* Get team join requests with view model transformation * Get team join requests with view model transformation
*/ */
async getJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> { async getJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> {
const dto = await this.apiClient.getJoinRequests(teamId); const dto = await this.apiClient.getJoinRequests(teamId) as TeamJoinRequestsDto;
return dto.requests.map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner)); return dto.requests.map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner));
} }
/** /**
* Approve a team join request * Approve a team join request
*/ */
async approveJoinRequest(teamId: string, requestId: string): Promise<void> { async approveJoinRequest(): Promise<void> {
// TODO: implement API call when endpoint is available // TODO: implement API call when endpoint is available
throw new Error('Not implemented: API endpoint for approving join requests'); throw new Error('Not implemented: API endpoint for approving join requests');
} }
@@ -39,7 +36,7 @@ export class TeamJoinService {
/** /**
* Reject a team join request * Reject a team join request
*/ */
async rejectJoinRequest(teamId: string, requestId: string): Promise<void> { async rejectJoinRequest(): Promise<void> {
// TODO: implement API call when endpoint is available // TODO: implement API call when endpoint is available
throw new Error('Not implemented: API endpoint for rejecting join requests'); throw new Error('Not implemented: API endpoint for rejecting join requests');
} }

View File

@@ -0,0 +1,205 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { TeamService } from './TeamService';
import { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { TeamSummaryViewModel } from '../../view-models/TeamSummaryViewModel';
import { TeamDetailsViewModel } from '../../view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '../../view-models/TeamMemberViewModel';
describe('TeamService', () => {
let mockApiClient: Mocked<TeamsApiClient>;
let service: TeamService;
beforeEach(() => {
mockApiClient = {
getAll: vi.fn(),
getDetails: vi.fn(),
getMembers: vi.fn(),
create: vi.fn(),
update: vi.fn(),
getDriverTeam: vi.fn(),
} as Mocked<TeamsApiClient>;
service = new TeamService(mockApiClient);
});
describe('getAllTeams', () => {
it('should call apiClient.getAll and return array of TeamSummaryViewModel', async () => {
const mockDto = {
teams: [
{
id: 'team-1',
name: 'Test Team',
logoUrl: 'https://example.com/logo.png',
memberCount: 5,
rating: 1500,
},
],
};
mockApiClient.getAll.mockResolvedValue(mockDto);
const result = await service.getAllTeams();
expect(mockApiClient.getAll).toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(TeamSummaryViewModel);
expect(result[0].id).toBe('team-1');
expect(result[0].name).toBe('Test Team');
});
it('should throw error when apiClient.getAll fails', async () => {
const error = new Error('API call failed');
mockApiClient.getAll.mockRejectedValue(error);
await expect(service.getAllTeams()).rejects.toThrow('API call failed');
});
});
describe('getTeamDetails', () => {
it('should call apiClient.getDetails and return TeamDetailsViewModel when data exists', async () => {
const mockDto = {
id: 'team-1',
name: 'Test Team',
description: 'A test team',
logoUrl: 'https://example.com/logo.png',
memberCount: 5,
ownerId: 'owner-1',
members: [],
};
mockApiClient.getDetails.mockResolvedValue(mockDto);
const result = await service.getTeamDetails('team-1', 'user-1');
expect(mockApiClient.getDetails).toHaveBeenCalledWith('team-1');
expect(result).toBeInstanceOf(TeamDetailsViewModel);
expect(result?.id).toBe('team-1');
expect(result?.name).toBe('Test Team');
});
it('should return null when apiClient.getDetails returns null', async () => {
mockApiClient.getDetails.mockResolvedValue(null);
const result = await service.getTeamDetails('team-1', 'user-1');
expect(mockApiClient.getDetails).toHaveBeenCalledWith('team-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getDetails fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDetails.mockRejectedValue(error);
await expect(service.getTeamDetails('team-1', 'user-1')).rejects.toThrow('API call failed');
});
});
describe('getTeamMembers', () => {
it('should call apiClient.getMembers and return array of TeamMemberViewModel', async () => {
const mockDto = {
members: [
{
driverId: 'driver-1',
driver: { id: 'driver-1', name: 'Driver One', avatarUrl: 'avatar.png', iracingId: '123', rating: 1400 },
role: 'member',
joinedAt: '2023-01-01T00:00:00Z',
},
],
};
mockApiClient.getMembers.mockResolvedValue(mockDto);
const result = await service.getTeamMembers('team-1', 'user-1', 'owner-1');
expect(mockApiClient.getMembers).toHaveBeenCalledWith('team-1');
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(TeamMemberViewModel);
expect(result[0].driverId).toBe('driver-1');
expect(result[0].role).toBe('member');
});
it('should throw error when apiClient.getMembers fails', async () => {
const error = new Error('API call failed');
mockApiClient.getMembers.mockRejectedValue(error);
await expect(service.getTeamMembers('team-1', 'user-1', 'owner-1')).rejects.toThrow('API call failed');
});
});
describe('createTeam', () => {
it('should call apiClient.create and return the result', async () => {
const input = { name: 'New Team', tag: 'NT', description: 'A new team' };
const mockOutput = { id: 'team-123', success: true };
mockApiClient.create.mockResolvedValue(mockOutput);
const result = await service.createTeam(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.create fails', async () => {
const input = { name: 'New Team', tag: 'NT' };
const error = new Error('API call failed');
mockApiClient.create.mockRejectedValue(error);
await expect(service.createTeam(input)).rejects.toThrow('API call failed');
});
});
describe('updateTeam', () => {
it('should call apiClient.update and return the result', async () => {
const input = { name: 'Updated Team', description: 'Updated description' };
const mockOutput = { success: true };
mockApiClient.update.mockResolvedValue(mockOutput);
const result = await service.updateTeam('team-1', input);
expect(mockApiClient.update).toHaveBeenCalledWith('team-1', input);
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.update fails', async () => {
const input = { name: 'Updated Team' };
const error = new Error('API call failed');
mockApiClient.update.mockRejectedValue(error);
await expect(service.updateTeam('team-1', input)).rejects.toThrow('API call failed');
});
});
describe('getDriverTeam', () => {
it('should call apiClient.getDriverTeam and return the result', async () => {
const mockOutput = { teamId: 'team-1', teamName: 'Test Team', role: 'member' };
mockApiClient.getDriverTeam.mockResolvedValue(mockOutput);
const result = await service.getDriverTeam('driver-1');
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith('driver-1');
expect(result).toEqual(mockOutput);
});
it('should return null when apiClient.getDriverTeam returns null', async () => {
mockApiClient.getDriverTeam.mockResolvedValue(null);
const result = await service.getDriverTeam('driver-1');
expect(mockApiClient.getDriverTeam).toHaveBeenCalledWith('driver-1');
expect(result).toBeNull();
});
it('should throw error when apiClient.getDriverTeam fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDriverTeam.mockRejectedValue(error);
await expect(service.getDriverTeam('driver-1')).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,14 +1,20 @@
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { CreateTeamViewModel } from '@/lib/view-models/CreateTeamViewModel';
import { UpdateTeamViewModel } from '@/lib/view-models/UpdateTeamViewModel';
import { DriverTeamViewModel } from '@/lib/view-models/DriverTeamViewModel';
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient'; import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
import { TeamSummaryViewModel, TeamDetailsViewModel, TeamMemberViewModel } from '../../view-models';
// TODO: Move these types to apps/website/lib/types/generated when available // TODO: Move these types to apps/website/lib/types/generated when available
type DriverDTO = { id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number };
type CreateTeamInputDto = { name: string; tag: string; description?: string }; type CreateTeamInputDto = { name: string; tag: string; description?: string };
type CreateTeamOutputDto = { id: string; success: boolean }; type CreateTeamOutputDto = { id: string; success: boolean };
type UpdateTeamInputDto = { name?: string; tag?: string; description?: string }; type UpdateTeamInputDto = { name?: string; tag?: string; description?: string };
type UpdateTeamOutputDto = { success: boolean }; type UpdateTeamOutputDto = { success: boolean };
type DriverTeamDto = { teamId: string; teamName: string; role: string }; type DriverTeamDto = { teamId: string; teamName: string; role: string };
type TeamSummaryDTO = { id: string; name: string; logoUrl?: string; memberCount: number; rating: number }; type TeamSummaryDTO = { id: string; name: string; logoUrl?: string; memberCount: number; rating: number };
type TeamMemberDTO = { driverId: string; driver?: any; role: string; joinedAt: string }; type TeamMemberDTO = { driverId: string; driver?: DriverDTO; role: string; joinedAt: string };
/** /**
* Team Service * Team Service
@@ -49,23 +55,26 @@ export class TeamService {
} }
/** /**
* Create a new team * Create a new team with view model transformation
*/ */
async createTeam(input: CreateTeamInputDto): Promise<CreateTeamOutputDto> { async createTeam(input: CreateTeamInputDto): Promise<CreateTeamViewModel> {
return await this.apiClient.create(input); const dto = await this.apiClient.create(input);
return new CreateTeamViewModel(dto);
} }
/** /**
* Update team * Update team with view model transformation
*/ */
async updateTeam(teamId: string, input: UpdateTeamInputDto): Promise<UpdateTeamOutputDto> { async updateTeam(teamId: string, input: UpdateTeamInputDto): Promise<UpdateTeamViewModel> {
return await this.apiClient.update(teamId, input); const dto = await this.apiClient.update(teamId, input);
return new UpdateTeamViewModel(dto);
} }
/** /**
* Get driver's team * Get driver's team with view model transformation
*/ */
async getDriverTeam(driverId: string): Promise<DriverTeamDto | null> { async getDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
return await this.apiClient.getDriverTeam(driverId); const dto = await this.apiClient.getDriverTeam(driverId);
return dto ? new DriverTeamViewModel(dto) : null;
} }
} }

View File

@@ -0,0 +1,21 @@
import { CreateLeagueOutputDTO } from '../types/generated/CreateLeagueOutputDTO';
/**
* View Model for Create League Result
*
* Represents the result of creating a league in a UI-ready format.
*/
export class CreateLeagueViewModel implements CreateLeagueOutputDTO {
leagueId: string;
success: boolean;
constructor(dto: CreateLeagueOutputDTO) {
this.leagueId = dto.leagueId;
this.success = dto.success;
}
/** UI-specific: Success message */
get successMessage(): string {
return this.success ? 'League created successfully!' : 'Failed to create league.';
}
}

View File

@@ -0,0 +1,19 @@
/**
* View Model for Create Team Result
*
* Represents the result of creating a team in a UI-ready format.
*/
export class CreateTeamViewModel {
id: string;
success: boolean;
constructor(dto: { id: string; success: boolean }) {
this.id = dto.id;
this.success = dto.success;
}
/** UI-specific: Success message */
get successMessage(): string {
return this.success ? 'Team created successfully!' : 'Failed to create team.';
}
}

View File

@@ -0,0 +1,26 @@
/**
* View Model for Driver's Team
*
* Represents a driver's team membership in a UI-ready format.
*/
export class DriverTeamViewModel {
teamId: string;
teamName: string;
role: string;
constructor(dto: { teamId: string; teamName: string; role: string }) {
this.teamId = dto.teamId;
this.teamName = dto.teamName;
this.role = dto.role;
}
/** UI-specific: Display role */
get displayRole(): string {
return this.role.charAt(0).toUpperCase() + this.role.slice(1);
}
/** UI-specific: Is owner */
get isOwner(): boolean {
return this.role === 'owner';
}
}

View File

@@ -0,0 +1,20 @@
// TODO: Create ImportRaceResultsSummaryDTO in apps/website/lib/types/generated when available
interface ImportRaceResultsSummaryDTO {
raceId: string;
importedCount: number;
errors: string[];
}
export class ImportRaceResultsSummaryViewModel implements ImportRaceResultsSummaryDTO {
raceId: string;
importedCount: number;
errors: string[];
constructor(dto: ImportRaceResultsSummaryDTO) {
this.raceId = dto.raceId;
this.importedCount = dto.importedCount;
this.errors = dto.errors;
}
// TODO: Add additional UI-specific fields when DTO is available
}

View File

@@ -1,4 +1,5 @@
import { LeagueMemberDTO } from '../types/generated/LeagueMemberDTO'; import { LeagueMemberDTO } from '../types/generated/LeagueMemberDTO';
import { DriverViewModel } from './DriverViewModel';
export class LeagueMemberViewModel implements LeagueMemberDTO { export class LeagueMemberViewModel implements LeagueMemberDTO {
driverId: string; driverId: string;
@@ -12,7 +13,7 @@ export class LeagueMemberViewModel implements LeagueMemberDTO {
// Note: The generated DTO is incomplete // Note: The generated DTO is incomplete
// These fields will need to be added when the OpenAPI spec is updated // These fields will need to be added when the OpenAPI spec is updated
driver?: any; driver?: DriverViewModel;
role: string = 'member'; role: string = 'member';
joinedAt: string = new Date().toISOString(); joinedAt: string = new Date().toISOString();

View File

@@ -0,0 +1,25 @@
import { LeagueMemberViewModel } from './LeagueMemberViewModel';
import type { LeagueMemberDTO } from '../types/generated/LeagueMemberDTO';
/**
* View Model for League Memberships
*
* Represents the league's memberships in a UI-ready format.
*/
export class LeagueMembershipsViewModel {
memberships: LeagueMemberViewModel[];
constructor(dto: { memberships: LeagueMemberDTO[] }, currentUserId: string) {
this.memberships = dto.memberships.map(membership => new LeagueMemberViewModel(membership, currentUserId));
}
/** UI-specific: Number of members */
get memberCount(): number {
return this.memberships.length;
}
/** UI-specific: Whether the league has members */
get hasMembers(): boolean {
return this.memberCount > 0;
}
}

View File

@@ -0,0 +1,22 @@
/**
* View Model for League Schedule
*
* Represents the league's race schedule in a UI-ready format.
*/
export class LeagueScheduleViewModel {
races: Array<unknown>;
constructor(dto: { races: Array<unknown> }) {
this.races = dto.races;
}
/** UI-specific: Number of races in the schedule */
get raceCount(): number {
return this.races.length;
}
/** UI-specific: Whether the schedule has races */
get hasRaces(): boolean {
return this.raceCount > 0;
}
}

View File

@@ -0,0 +1,17 @@
/**
* View Model for League Statistics
*
* Represents the total number of leagues in a UI-ready format.
*/
export class LeagueStatsViewModel {
totalLeagues: number;
constructor(dto: { totalLeagues: number }) {
this.totalLeagues = dto.totalLeagues;
}
/** UI-specific: Formatted total leagues display */
get formattedTotalLeagues(): string {
return this.totalLeagues.toLocaleString();
}
}

View File

@@ -1,37 +1,51 @@
import { MembershipFeeDto } from '../types/generated/MembershipFeeDto'; import type { MembershipFeeDto } from '../types/generated';
export class MembershipFeeViewModel implements MembershipFeeDto { export class MembershipFeeViewModel {
id: string; id: string;
leagueId: string; leagueId: string;
seasonId?: string;
type: string;
amount: number;
enabled: boolean;
createdAt: Date;
updatedAt: Date;
constructor(dto: MembershipFeeDto) { constructor(dto: MembershipFeeDto) {
this.id = dto.id; Object.assign(this, dto);
this.leagueId = dto.leagueId;
} }
// Note: The generated DTO is incomplete
// These fields will need to be added when the OpenAPI spec is updated
amount: number = 0;
currency: string = 'USD';
period: string = 'monthly';
/** UI-specific: Formatted amount */ /** UI-specific: Formatted amount */
get formattedAmount(): string { get formattedAmount(): string {
return `${this.currency} ${this.amount.toFixed(2)}`; return `${this.amount.toFixed(2)}`; // Assuming EUR
} }
/** UI-specific: Period display */ /** UI-specific: Type display */
get periodDisplay(): string { get typeDisplay(): string {
switch (this.period) { switch (this.type) {
case 'monthly': return 'Monthly';
case 'yearly': return 'Yearly';
case 'season': return 'Per Season'; case 'season': return 'Per Season';
default: return this.period; case 'monthly': return 'Monthly';
case 'per_race': return 'Per Race';
default: return this.type;
} }
} }
/** UI-specific: Amount per period */ /** UI-specific: Status display */
get amountPerPeriod(): string { get statusDisplay(): string {
return `${this.formattedAmount} ${this.periodDisplay.toLowerCase()}`; return this.enabled ? 'Enabled' : 'Disabled';
}
/** UI-specific: Status color */
get statusColor(): string {
return this.enabled ? 'green' : 'red';
}
/** UI-specific: Formatted created date */
get formattedCreatedAt(): string {
return this.createdAt.toLocaleString();
}
/** UI-specific: Formatted updated date */
get formattedUpdatedAt(): string {
return this.updatedAt.toLocaleString();
} }
} }

View File

@@ -1,19 +1,31 @@
import { PaymentDTO } from '../types/generated/PaymentDto'; import type { PaymentDto } from '../types/generated';
export class PaymentViewModel implements PaymentDTO { export class PaymentViewModel {
id: string; id: string;
type: string;
amount: number; amount: number;
currency: string; platformFee: number;
netAmount: number;
payerId: string;
payerType: string;
leagueId: string;
seasonId?: string;
status: string; status: string;
createdAt: string; createdAt: Date;
completedAt?: Date;
constructor(dto: PaymentDTO) { constructor(dto: PaymentDto) {
Object.assign(this, dto); Object.assign(this, dto);
} }
/** UI-specific: Formatted amount */ /** UI-specific: Formatted amount */
get formattedAmount(): string { get formattedAmount(): string {
return `${this.currency} ${this.amount.toFixed(2)}`; return `${this.amount.toFixed(2)}`; // Assuming EUR currency
}
/** UI-specific: Formatted net amount */
get formattedNetAmount(): string {
return `${this.netAmount.toFixed(2)}`;
} }
/** UI-specific: Status color */ /** UI-specific: Status color */
@@ -22,17 +34,33 @@ export class PaymentViewModel implements PaymentDTO {
case 'completed': return 'green'; case 'completed': return 'green';
case 'pending': return 'yellow'; case 'pending': return 'yellow';
case 'failed': return 'red'; case 'failed': return 'red';
case 'refunded': return 'orange';
default: return 'gray'; default: return 'gray';
} }
} }
/** UI-specific: Formatted created date */ /** UI-specific: Formatted created date */
get formattedCreatedAt(): string { get formattedCreatedAt(): string {
return new Date(this.createdAt).toLocaleString(); return this.createdAt.toLocaleString();
}
/** UI-specific: Formatted completed date */
get formattedCompletedAt(): string {
return this.completedAt ? this.completedAt.toLocaleString() : 'Not completed';
} }
/** UI-specific: Status display */ /** UI-specific: Status display */
get statusDisplay(): string { get statusDisplay(): string {
return this.status.charAt(0).toUpperCase() + this.status.slice(1); return this.status.charAt(0).toUpperCase() + this.status.slice(1);
} }
/** UI-specific: Type display */
get typeDisplay(): string {
return this.type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
}
/** UI-specific: Payer type display */
get payerTypeDisplay(): string {
return this.payerType.charAt(0).toUpperCase() + this.payerType.slice(1);
}
} }

View File

@@ -1,29 +1,26 @@
import { PrizeDto } from '../types/generated/PrizeDto'; import type { PrizeDto } from '../types/generated';
export class PrizeViewModel implements PrizeDto { export class PrizeViewModel {
id: string; id: string;
leagueId: string; leagueId: string;
seasonId: string; seasonId: string;
position: number; position: number;
name: string; name: string;
amount: number; amount: number;
type: string;
description?: string;
awarded: boolean;
awardedTo?: string;
awardedAt?: Date;
createdAt: Date;
constructor(dto: PrizeDto) { constructor(dto: PrizeDto) {
this.id = dto.id; Object.assign(this, dto);
this.leagueId = dto.leagueId;
this.seasonId = dto.seasonId;
this.position = dto.position;
this.name = dto.name;
this.amount = dto.amount;
} }
// Note: The generated DTO doesn't have currency
// This will need to be added when the OpenAPI spec is updated
currency: string = 'USD';
/** UI-specific: Formatted amount */ /** UI-specific: Formatted amount */
get formattedAmount(): string { get formattedAmount(): string {
return `${this.currency} ${this.amount.toFixed(2)}`; return `${this.amount.toFixed(2)}`; // Assuming EUR
} }
/** UI-specific: Position display */ /** UI-specific: Position display */
@@ -36,8 +33,38 @@ export class PrizeViewModel implements PrizeDto {
} }
} }
/** UI-specific: Type display */
get typeDisplay(): string {
switch (this.type) {
case 'cash': return 'Cash Prize';
case 'merchandise': return 'Merchandise';
case 'other': return 'Other';
default: return this.type;
}
}
/** UI-specific: Status display */
get statusDisplay(): string {
return this.awarded ? 'Awarded' : 'Available';
}
/** UI-specific: Status color */
get statusColor(): string {
return this.awarded ? 'green' : 'blue';
}
/** UI-specific: Prize description */ /** UI-specific: Prize description */
get prizeDescription(): string { get prizeDescription(): string {
return `${this.name} - ${this.formattedAmount}`; return `${this.name} - ${this.formattedAmount}`;
} }
/** UI-specific: Formatted awarded date */
get formattedAwardedAt(): string {
return this.awardedAt ? this.awardedAt.toLocaleString() : 'Not awarded';
}
/** UI-specific: Formatted created date */
get formattedCreatedAt(): string {
return this.createdAt.toLocaleString();
}
} }

View File

@@ -0,0 +1,18 @@
import { RaceStatsDTO } from '../types/generated';
/**
* Race stats view model
* Represents race statistics for display
*/
export class RaceStatsViewModel {
totalRaces: number;
constructor(dto: RaceStatsDTO) {
this.totalRaces = dto.totalRaces;
}
/** UI-specific: Formatted total races */
get formattedTotalRaces(): string {
return this.totalRaces.toLocaleString();
}
}

View File

@@ -0,0 +1,15 @@
import { RaceWithSOFDTO } from '../types/generated/RaceWithSOFDTO';
export class RaceWithSOFViewModel implements RaceWithSOFDTO {
id: string;
track: string;
constructor(dto: RaceWithSOFDTO) {
this.id = dto.id;
this.track = dto.track;
}
// TODO: Add additional fields when RaceWithSOFDTO is updated in OpenAPI spec
// sof?: number;
// results?: RaceResultViewModel[];
}

View File

@@ -0,0 +1,30 @@
import { RecordEngagementOutputDTO } from '../types/generated';
/**
* Record engagement output view model
* Represents the result of recording an engagement event for UI consumption
*/
export class RecordEngagementOutputViewModel {
eventId: string;
engagementWeight: number;
constructor(dto: RecordEngagementOutputDTO) {
this.eventId = dto.eventId;
this.engagementWeight = dto.engagementWeight;
}
/** UI-specific: Formatted event ID for display */
get displayEventId(): string {
return `Event: ${this.eventId}`;
}
/** UI-specific: Formatted engagement weight */
get displayEngagementWeight(): string {
return `${this.engagementWeight.toFixed(2)}`;
}
/** UI-specific: Is high engagement */
get isHighEngagement(): boolean {
return this.engagementWeight > 1.0;
}
}

View File

@@ -0,0 +1,18 @@
import { RecordPageViewOutputDTO } from '../types/generated';
/**
* Record page view output view model
* Represents the result of recording a page view for UI consumption
*/
export class RecordPageViewOutputViewModel {
pageViewId: string;
constructor(dto: RecordPageViewOutputDTO) {
this.pageViewId = dto.pageViewId;
}
/** UI-specific: Formatted page view ID for display */
get displayPageViewId(): string {
return `Page View: ${this.pageViewId}`;
}
}

View File

@@ -0,0 +1,19 @@
import { RemoveLeagueMemberOutputDTO } from '../types/generated/RemoveLeagueMemberOutputDTO';
/**
* View Model for Remove Member Result
*
* Represents the result of removing a member from a league in a UI-ready format.
*/
export class RemoveMemberViewModel implements RemoveLeagueMemberOutputDTO {
success: boolean;
constructor(dto: RemoveLeagueMemberOutputDTO) {
this.success = dto.success;
}
/** UI-specific: Success message */
get successMessage(): string {
return this.success ? 'Member removed successfully!' : 'Failed to remove member.';
}
}

View File

@@ -1,6 +1,19 @@
import { TeamMemberViewModel } from './TeamMemberViewModel'; import { TeamMemberViewModel } from './TeamMemberViewModel';
// Note: No generated DTO available for TeamDetails yet // Note: No generated DTO available for TeamDetails yet
interface DriverDTO {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
}
interface TeamMemberDTO {
driverId: string;
driver?: DriverDTO;
role: string;
joinedAt: string;
}
interface TeamDetailsDTO { interface TeamDetailsDTO {
id: string; id: string;
name: string; name: string;
@@ -8,7 +21,7 @@ interface TeamDetailsDTO {
logoUrl?: string; logoUrl?: string;
memberCount: number; memberCount: number;
ownerId: string; ownerId: string;
members: any[]; members: TeamMemberDTO[];
} }
export class TeamDetailsViewModel { export class TeamDetailsViewModel {

View File

@@ -1,5 +1,5 @@
// Note: No generated DTO available for TeamJoinRequest yet // Note: No generated DTO available for TeamJoinRequest yet
interface TeamJoinRequestDTO { export interface TeamJoinRequestDTO {
id: string; id: string;
teamId: string; teamId: string;
driverId: string; driverId: string;

View File

@@ -1,7 +1,15 @@
// Note: No generated DTO available for TeamMember yet // Note: No generated DTO available for TeamMember yet
interface DriverDTO {
id: string;
name: string;
avatarUrl?: string;
iracingId?: string;
rating?: number;
}
interface TeamMemberDTO { interface TeamMemberDTO {
driverId: string; driverId: string;
driver?: any; driver?: DriverDTO;
role: string; role: string;
joinedAt: string; joinedAt: string;
} }

View File

@@ -0,0 +1,17 @@
/**
* View Model for Update Team Result
*
* Represents the result of updating a team in a UI-ready format.
*/
export class UpdateTeamViewModel {
success: boolean;
constructor(dto: { success: boolean }) {
this.success = dto.success;
}
/** UI-specific: Success message */
get successMessage(): string {
return this.success ? 'Team updated successfully!' : 'Failed to update team.';
}
}

View File

@@ -1,24 +1,30 @@
import { TransactionDto } from '../types/generated/TransactionDto'; import { TransactionDto } from '../types/generated/TransactionDto';
export class WalletTransactionViewModel implements TransactionDto { // TODO: Use generated TransactionDto when it includes all required fields
export type FullTransactionDto = TransactionDto & {
amount: number;
description: string;
createdAt: string;
type: 'deposit' | 'withdrawal';
};
export class WalletTransactionViewModel implements FullTransactionDto {
id: string; id: string;
walletId: string; walletId: string;
amount: number; amount: number;
description: string; description: string;
createdAt: string; createdAt: string;
type: 'deposit' | 'withdrawal';
constructor(dto: TransactionDto) { constructor(dto: FullTransactionDto) {
this.id = dto.id; this.id = dto.id;
this.walletId = dto.walletId; this.walletId = dto.walletId;
this.amount = dto.amount; this.amount = dto.amount;
this.description = dto.description; this.description = dto.description;
this.createdAt = dto.createdAt; this.createdAt = dto.createdAt;
this.type = dto.type;
} }
// Note: The generated DTO doesn't have type field
// This will need to be added when the OpenAPI spec is updated
type: 'deposit' | 'withdrawal' = 'deposit';
/** UI-specific: Formatted amount with sign */ /** UI-specific: Formatted amount with sign */
get formattedAmount(): string { get formattedAmount(): string {
const sign = this.type === 'deposit' ? '+' : '-'; const sign = this.type === 'deposit' ? '+' : '-';

View File

@@ -1,5 +1,5 @@
import { WalletDto } from '../types/generated/WalletDto'; import { WalletDto } from '../types/generated/WalletDto';
import { WalletTransactionViewModel } from './WalletTransactionViewModel'; import { WalletTransactionViewModel, FullTransactionDto } from './WalletTransactionViewModel';
export class WalletViewModel implements WalletDto { export class WalletViewModel implements WalletDto {
id: string; id: string;
@@ -11,7 +11,7 @@ export class WalletViewModel implements WalletDto {
createdAt: string; createdAt: string;
currency: string; currency: string;
constructor(dto: WalletDto & { transactions?: any[] }) { constructor(dto: WalletDto & { transactions?: FullTransactionDto[] }) {
this.id = dto.id; this.id = dto.id;
this.leagueId = dto.leagueId; this.leagueId = dto.leagueId;
this.balance = dto.balance; this.balance = dto.balance;
@@ -20,16 +20,11 @@ export class WalletViewModel implements WalletDto {
this.totalWithdrawn = dto.totalWithdrawn; this.totalWithdrawn = dto.totalWithdrawn;
this.createdAt = dto.createdAt; this.createdAt = dto.createdAt;
this.currency = dto.currency; this.currency = dto.currency;
// Map transactions if provided // Map transactions if provided
if (dto.transactions) { this.transactions = dto.transactions?.map(t => new WalletTransactionViewModel(t)) || [];
this.transactions = dto.transactions.map(t => new WalletTransactionViewModel(t));
}
} }
// Note: The generated DTO doesn't have driverId or transactions
// These will need to be added when the OpenAPI spec is updated
driverId: string = '';
transactions: WalletTransactionViewModel[] = []; transactions: WalletTransactionViewModel[] = [];
/** UI-specific: Formatted balance */ /** UI-specific: Formatted balance */

View File

@@ -1,48 +0,0 @@
/**
* View Models Index
*
* Central export file for all view models.
* View models represent fully prepared UI state and should only be consumed by UI components.
*/
export { AnalyticsDashboardViewModel } from './AnalyticsDashboardViewModel';
export { AnalyticsMetricsViewModel } from './AnalyticsMetricsViewModel';
export { AvatarViewModel } from './AvatarViewModel';
export { CompleteOnboardingViewModel } from './CompleteOnboardingViewModel';
export { DeleteMediaViewModel } from './DeleteMediaViewModel';
export { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
export { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel';
export { DriverRegistrationStatusViewModel } from './DriverRegistrationStatusViewModel';
export { DriverViewModel } from './DriverViewModel';
export { LeagueAdminViewModel } from './LeagueAdminViewModel';
export { LeagueJoinRequestViewModel } from './LeagueJoinRequestViewModel';
export { LeagueMemberViewModel } from './LeagueMemberViewModel';
export { LeagueStandingsViewModel } from './LeagueStandingsViewModel';
export { LeagueSummaryViewModel } from './LeagueSummaryViewModel';
export { MediaViewModel } from './MediaViewModel';
export { MembershipFeeViewModel } from './MembershipFeeViewModel';
export { PaymentViewModel } from './PaymentViewModel';
export { PrizeViewModel } from './PrizeViewModel';
export { ProtestViewModel } from './ProtestViewModel';
export { RaceDetailViewModel } from './RaceDetailViewModel';
export { RaceListItemViewModel } from './RaceListItemViewModel';
export { RaceResultsDetailViewModel } from './RaceResultsDetailViewModel';
export { RaceResultViewModel } from './RaceResultViewModel';
export { RacesPageViewModel } from './RacesPageViewModel';
export { RequestAvatarGenerationViewModel } from './RequestAvatarGenerationViewModel';
export { SessionViewModel } from './SessionViewModel';
export { SponsorDashboardViewModel } from './SponsorDashboardViewModel';
export { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';
export { SponsorshipPricingViewModel } from './SponsorshipPricingViewModel';
export { SponsorSponsorshipsViewModel } from './SponsorSponsorshipsViewModel';
export { SponsorViewModel } from './SponsorViewModel';
export { StandingEntryViewModel } from './StandingEntryViewModel';
export { TeamDetailsViewModel } from './TeamDetailsViewModel';
export { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel';
export { TeamMemberViewModel } from './TeamMemberViewModel';
export { TeamSummaryViewModel } from './TeamSummaryViewModel';
export { UpdateAvatarViewModel } from './UpdateAvatarViewModel';
export { UploadMediaViewModel } from './UploadMediaViewModel';
export { UserProfileViewModel } from './UserProfileViewModel';
export { WalletTransactionViewModel } from './WalletTransactionViewModel';
export { WalletViewModel } from './WalletViewModel';

View File

@@ -7,7 +7,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"type-check": "tsc --noEmit", "type-check": "npx tsc --noEmit",
"clean": "rm -rf .next" "clean": "rm -rf .next"
}, },
"dependencies": { "dependencies": {
@@ -35,6 +35,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-next": "15.5.7", "eslint-config-next": "15.5.7",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-unused-imports": "^3.0.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^3.4.18", "tailwindcss": "^3.4.18",
"typescript": "^5.6.0" "typescript": "^5.6.0"

View File

@@ -8,6 +8,9 @@
"baseUrl": ".", "baseUrl": ".",
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noEmitOnError": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"

View File

@@ -1,10 +1,10 @@
Form Models Command Models
This document defines Form Models as a first-class concept in the frontend architecture. This document defines Command Models as a first-class concept in the frontend architecture.
Form Models are UX-only write models used to collect, validate, and prepare user input Command Models are UX-only write models used to collect, validate, and prepare user input
before it is sent to the backend as a Command DTO. before it is sent to the backend as a Command DTO.
Form Models are not View Models and not Domain Models. Command Models are not View Models and not Domain Models.
@@ -14,7 +14,7 @@ A Form Model answers the question:
“What does the UI need in order to safely submit user input?” “What does the UI need in order to safely submit user input?”
Form Models exist to: Command Models exist to:
• centralize form state • centralize form state
• reduce logic inside components • reduce logic inside components
• provide consistent client-side validation • provide consistent client-side validation
@@ -24,13 +24,13 @@ Form Models exist to:
Core Rules Core Rules
Form Models: Command Models:
• exist only in the frontend • exist only in the frontend
• are write-only (never reused for reads) • are write-only (never reused for reads)
• are created per form • are created per form
• are discarded after submission • are discarded after submission
Form Models MUST NOT: Command Models MUST NOT:
• contain business logic • contain business logic
• enforce domain rules • enforce domain rules
• reference View Models • reference View Models
@@ -46,7 +46,7 @@ API DTO (read) → ViewModel → UI
UI Input → FormModel → Command DTO → API UI Input → FormModel → Command DTO → API
• View Models are read-only • View Models are read-only
Form Models are write-only Command Models are write-only
• No model is reused across read/write boundaries • No model is reused across read/write boundaries
@@ -137,20 +137,20 @@ The component:
Testing Testing
Form Models SHOULD be tested when they contain: Command Models SHOULD be tested when they contain:
• validation rules • validation rules
• non-trivial state transitions • non-trivial state transitions
• command construction logic • command construction logic
Form Models do NOT need tests if they only hold fields without logic. Command Models do NOT need tests if they only hold fields without logic.
Summary Summary
Form Models are UX helpers for writes Command Models are UX helpers for writes
• They protect components from complexity • They protect components from complexity
• They never replace backend validation • They never replace backend validation
• They never leak into read flows • They never leak into read flows
Form Models help users. Command Models help users.
Use Cases protect the system. Use Cases protect the system.

View File

@@ -40,7 +40,7 @@
"smoke:website": "npm run website:build && npx playwright test -c playwright.website.config.ts", "smoke:website": "npm run website:build && npx playwright test -c playwright.website.config.ts",
"test:hosted-real": "vitest run --config vitest.e2e.config.ts tests/e2e/hosted-real/", "test:hosted-real": "vitest run --config vitest.e2e.config.ts tests/e2e/hosted-real/",
"test:companion-hosted": "vitest run --config vitest.e2e.config.ts tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts", "test:companion-hosted": "vitest run --config vitest.e2e.config.ts tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts",
"typecheck": "tsc --noEmit", "typecheck": "npx tsc --noEmit",
"test:types": "tsc --noEmit -p tsconfig.tests.json", "test:types": "tsc --noEmit -p tsconfig.tests.json",
"companion:dev": "npm run dev --workspace=@gridpilot/companion", "companion:dev": "npm run dev --workspace=@gridpilot/companion",
"companion:build": "npm run build --workspace=@gridpilot/companion", "companion:build": "npm run build --workspace=@gridpilot/companion",

View File

@@ -24,6 +24,8 @@
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"paths": { "paths": {
"@core/*": ["./core/*"], "@core/*": ["./core/*"],
"@adapters/*": ["./adapters/*"], "@adapters/*": ["./adapters/*"],