view data fixes
Some checks failed
Contract Testing / contract-snapshot (pull_request) Has been cancelled
Contract Testing / contract-tests (pull_request) Has been cancelled

This commit is contained in:
2026-01-25 00:12:30 +01:00
parent 1b0a1f4aee
commit 6c07abe5e7
37 changed files with 400 additions and 185 deletions

View File

@@ -3,7 +3,7 @@ import { RequireSystemAdmin, REQUIRE_SYSTEM_ADMIN_METADATA_KEY } from './Require
// Mock SetMetadata // Mock SetMetadata
vi.mock('@nestjs/common', () => ({ vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}), SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
})); }));
describe('RequireSystemAdmin', () => { describe('RequireSystemAdmin', () => {
@@ -30,7 +30,7 @@ describe('RequireSystemAdmin', () => {
const result = decorator(mockTarget, mockPropertyKey, mockDescriptor); const result = decorator(mockTarget, mockPropertyKey, mockDescriptor);
// The decorator should return the descriptor // The decorator should return the descriptor (SetMetadata returns the descriptor)
expect(result).toBe(mockDescriptor); expect(result).toBe(mockDescriptor);
}); });

View File

@@ -1,6 +1,4 @@
import { AdminUser } from '@core/admin/domain/entities/AdminUser'; import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService';
import { Result } from '@core/shared/domain/Result';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GetDashboardStatsUseCase } from './GetDashboardStatsUseCase'; import { GetDashboardStatsUseCase } from './GetDashboardStatsUseCase';
@@ -413,15 +411,15 @@ describe('GetDashboardStatsUseCase', () => {
// Check that today has 1 user // Check that today has 1 user
const todayEntry = stats.userGrowth[6]; const todayEntry = stats.userGrowth[6];
expect(todayEntry.value).toBe(1); expect(todayEntry?.value).toBe(1);
// Check that yesterday has 1 user // Check that yesterday has 1 user
const yesterdayEntry = stats.userGrowth[5]; const yesterdayEntry = stats.userGrowth[5];
expect(yesterdayEntry.value).toBe(1); expect(yesterdayEntry?.value).toBe(1);
// Check that two days ago has 1 user // Check that two days ago has 1 user
const twoDaysAgoEntry = stats.userGrowth[4]; const twoDaysAgoEntry = stats.userGrowth[4];
expect(twoDaysAgoEntry.value).toBe(1); expect(twoDaysAgoEntry?.value).toBe(1);
}); });
it('should calculate activity timeline for last 7 days', async () => { it('should calculate activity timeline for last 7 days', async () => {
@@ -643,8 +641,9 @@ describe('GetDashboardStatsUseCase', () => {
status: 'active', status: 'active',
}); });
const users = Array.from({ length: 1000 }, (_, i) => const users = Array.from({ length: 1000 }, (_, i) => {
AdminUser.create({ const hasRecentLogin = i % 10 === 0;
return AdminUser.create({
id: `user-${i}`, id: `user-${i}`,
email: `user${i}@example.com`, email: `user${i}@example.com`,
displayName: `User ${i}`, displayName: `User ${i}`,
@@ -652,9 +651,9 @@ describe('GetDashboardStatsUseCase', () => {
status: i % 4 === 0 ? 'suspended' : i % 4 === 1 ? 'deleted' : 'active', status: i % 4 === 0 ? 'suspended' : i % 4 === 1 ? 'deleted' : 'active',
createdAt: new Date(Date.now() - i * 3600000), createdAt: new Date(Date.now() - i * 3600000),
updatedAt: new Date(Date.now() - i * 3600000), updatedAt: new Date(Date.now() - i * 3600000),
lastLoginAt: i % 10 === 0 ? new Date(Date.now() - i * 3600000) : undefined, ...(hasRecentLogin && { lastLoginAt: new Date(Date.now() - i * 3600000) }),
}) });
); });
mockAdminUserRepo.findById.mockResolvedValue(actor); mockAdminUserRepo.findById.mockResolvedValue(actor);
mockAdminUserRepo.list.mockResolvedValue({ users }); mockAdminUserRepo.list.mockResolvedValue({ users });

View File

@@ -3,7 +3,7 @@ import { Public, PUBLIC_ROUTE_METADATA_KEY } from './Public';
// Mock SetMetadata // Mock SetMetadata
vi.mock('@nestjs/common', () => ({ vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}), SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
})); }));
describe('Public', () => { describe('Public', () => {

View File

@@ -3,7 +3,7 @@ import { RequireAuthenticatedUser, REQUIRE_AUTHENTICATED_USER_METADATA_KEY } fro
// Mock SetMetadata // Mock SetMetadata
vi.mock('@nestjs/common', () => ({ vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}), SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
})); }));
describe('RequireAuthenticatedUser', () => { describe('RequireAuthenticatedUser', () => {

View File

@@ -3,7 +3,7 @@ import { RequireRoles, REQUIRE_ROLES_METADATA_KEY } from './RequireRoles';
// Mock SetMetadata // Mock SetMetadata
vi.mock('@nestjs/common', () => ({ vi.mock('@nestjs/common', () => ({
SetMetadata: vi.fn(() => () => {}), SetMetadata: vi.fn(() => (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => descriptor),
})); }));
describe('RequireRoles', () => { describe('RequireRoles', () => {

View File

@@ -19,7 +19,83 @@ export function useDriverProfile(
const error = result.getError(); const error = result.getError();
throw new ApiError(error.message, 'SERVER_ERROR', { timestamp: new globalThis.Date().toISOString() }); throw new ApiError(error.message, 'SERVER_ERROR', { timestamp: new globalThis.Date().toISOString() });
} }
return new DriverProfileViewModel(result.unwrap()); const dto = result.unwrap();
// Convert GetDriverProfileOutputDTO to ProfileViewData
const viewData: ProfileViewData = {
driver: dto.currentDriver ? {
id: dto.currentDriver.id,
name: dto.currentDriver.name,
countryCode: dto.currentDriver.countryCode || '',
countryFlag: dto.currentDriver.countryFlag || '',
avatarUrl: dto.currentDriver.avatarUrl || '',
bio: dto.currentDriver.bio || null,
iracingId: dto.currentDriver.iracingId || null,
joinedAtLabel: dto.currentDriver.joinedAt || '',
globalRankLabel: dto.currentDriver.globalRank || '',
} : {
id: '',
name: '',
countryCode: '',
countryFlag: '',
avatarUrl: '',
bio: null,
iracingId: null,
joinedAtLabel: '',
globalRankLabel: '',
},
stats: dto.stats ? {
ratingLabel: dto.stats.rating || '',
globalRankLabel: dto.stats.globalRank || '',
totalRacesLabel: dto.stats.totalRaces?.toString() || '',
winsLabel: dto.stats.wins?.toString() || '',
podiumsLabel: dto.stats.podiums?.toString() || '',
dnfsLabel: dto.stats.dnfs?.toString() || '',
bestFinishLabel: dto.stats.bestFinish?.toString() || '',
worstFinishLabel: dto.stats.worstFinish?.toString() || '',
avgFinishLabel: dto.stats.avgFinish?.toString() || '',
consistencyLabel: dto.stats.consistency?.toString() || '',
percentileLabel: dto.stats.percentile?.toString() || '',
} : null,
teamMemberships: dto.teamMemberships.map(m => ({
teamId: m.teamId,
teamName: m.teamName,
teamTag: m.teamTag || null,
roleLabel: m.role || '',
joinedAtLabel: m.joinedAt || '',
href: `/teams/${m.teamId}`,
})),
extendedProfile: dto.extendedProfile ? {
timezone: dto.extendedProfile.timezone || '',
racingStyle: dto.extendedProfile.racingStyle || '',
favoriteTrack: dto.extendedProfile.favoriteTrack || '',
favoriteCar: dto.extendedProfile.favoriteCar || '',
availableHours: dto.extendedProfile.availableHours || '',
lookingForTeamLabel: dto.extendedProfile.lookingForTeam ? 'Yes' : 'No',
openToRequestsLabel: dto.extendedProfile.openToRequests ? 'Yes' : 'No',
socialHandles: dto.extendedProfile.socialHandles?.map(h => ({
platformLabel: h.platform || '',
handle: h.handle || '',
url: h.url || '',
})) || [],
achievements: dto.extendedProfile.achievements?.map(a => ({
id: a.id,
title: a.title,
description: a.description,
earnedAtLabel: a.earnedAt || '',
icon: a.icon as any,
rarityLabel: a.rarity || '',
})) || [],
friends: dto.extendedProfile.friends?.map(f => ({
id: f.id,
name: f.name,
countryFlag: f.countryFlag || '',
avatarUrl: f.avatarUrl || '',
href: `/drivers/${f.id}`,
})) || [],
friendsCountLabel: dto.extendedProfile.friendsCount?.toString() || '',
} : null,
};
return new DriverProfileViewModel(viewData);
}, },
enabled: !!driverId, enabled: !!driverId,
...options, ...options,

View File

@@ -14,24 +14,29 @@ export function useLeagueWalletPageData(leagueId: string) {
queryKey: ['leagueWallet', leagueId], queryKey: ['leagueWallet', leagueId],
queryFn: async () => { queryFn: async () => {
const dto = await leagueWalletService.getWalletForLeague(leagueId); const dto = await leagueWalletService.getWalletForLeague(leagueId);
// Transform DTO to ViewModel at client boundary // Transform DTO to ViewData at client boundary
const transactions = dto.transactions.map(t => new WalletTransactionViewModel({ const transactions = dto.transactions.map(t => ({
id: t.id, id: t.id,
type: t.type as any, type: t.type as any,
description: t.description, description: t.description,
amount: t.amount, amount: t.amount,
fee: 0, fee: 0,
netAmount: t.amount, netAmount: t.amount,
date: new globalThis.Date(t.createdAt), date: new globalThis.Date(t.createdAt).toISOString(),
status: t.status, status: t.status,
})); }));
return new LeagueWalletViewModel({ return new LeagueWalletViewModel({
leagueId,
balance: dto.balance, balance: dto.balance,
currency: dto.currency, formattedBalance: '',
totalRevenue: dto.balance, // Fallback totalRevenue: dto.balance, // Fallback
formattedTotalRevenue: '',
totalFees: 0, totalFees: 0,
formattedTotalFees: '',
totalWithdrawals: 0, totalWithdrawals: 0,
pendingPayouts: 0, pendingPayouts: 0,
formattedPendingPayouts: '',
currency: dto.currency,
transactions, transactions,
canWithdraw: true, canWithdraw: true,
withdrawalBlockReason: undefined, withdrawalBlockReason: undefined,

View File

@@ -8,7 +8,7 @@
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { Result } from '@/lib/contracts/Result'; import { Result } from '@/lib/contracts/Result';
import { DomainError } from '@/lib/contracts/services/Service'; import { DomainError } from '@/lib/contracts/services/Service';
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO'; import { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
// TODO why is this an adapter? // TODO why is this an adapter?

View File

@@ -1,34 +1,24 @@
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DashboardDateFormatter } from '@/lib/formatters/DashboardDateFormatter'; import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { HomeViewData } from '@/lib/view-data/HomeViewData'; import type { HomeViewData } from '@/lib/view-data/HomeViewData';
export class HomeViewDataBuilder { export class HomeViewDataBuilder {
/** /**
* Build HomeViewData from DashboardOverviewDTO * Build HomeViewData from HomeDataDTO
* *
* @param apiDto - The API DTO * @param apiDto - The API DTO
* @returns HomeViewData * @returns HomeViewData
*/ */
public static build(apiDto: DashboardOverviewDTO): HomeViewData { public static build(apiDto: HomeDataDTO): HomeViewData {
return { return {
isAlpha: true, isAlpha: apiDto.isAlpha,
upcomingRaces: (apiDto.upcomingRaces || []).map(race => ({ upcomingRaces: apiDto.upcomingRaces,
id: race.id, topLeagues: apiDto.topLeagues,
track: race.track, teams: apiDto.teams,
car: race.car,
formattedDate: DashboardDateFormatter.format(new Date(race.scheduledAt)).date,
})),
topLeagues: (apiDto.leagueStandingsSummaries || []).map(league => ({
id: league.leagueId,
name: league.leagueName,
description: '',
})),
teams: [],
}; };
} }
} }
HomeViewDataBuilder satisfies ViewDataBuilder<DashboardOverviewDTO, HomeViewData>; HomeViewDataBuilder satisfies ViewDataBuilder<HomeDataDTO, HomeViewData>;

View File

@@ -1,16 +1,29 @@
/** /**
* Rulebook View Data Builder * Rulebook View Data Builder
* *
* Transforms API DTO to ViewData for templates. * Transforms API DTO to ViewData for templates.
*/ */
import type { RulebookViewData } from '@/lib/view-data/RulebookViewData'; import type { RulebookViewData } from '@/lib/view-data/RulebookViewData';
import { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
interface RulebookApiDto { interface RulebookApiDto {
leagueId: string; leagueId: string;
scoringConfig: LeagueScoringConfigDTO; scoringConfig: {
gameName: string;
scoringPresetName: string;
championships: Array<{
type: string;
sessionTypes: string[];
pointsPreview: Array<{
sessionType: string;
position: number;
points: number;
}>;
bonusSummary: string[];
}>;
dropPolicySummary: string;
};
} }
export class RulebookViewDataBuilder { export class RulebookViewDataBuilder {

View File

@@ -1,5 +1,6 @@
import type { FileProtestCommandDTO } from '../../../types/generated/FileProtestCommandDTO'; import type { FileProtestCommandDTO } from '../../../types/generated/FileProtestCommandDTO';
import type { ImportRaceResultsDTO } from '../../../types/generated/ImportRaceResultsDTO'; import type { ImportRaceResultsDTO } from '../../../types/generated/ImportRaceResultsDTO';
import type { RaceDetailDTO } from '../../../types/generated/RaceDetailDTO';
import type { RaceDetailEntryDTO } from '../../../types/generated/RaceDetailEntryDTO'; import type { RaceDetailEntryDTO } from '../../../types/generated/RaceDetailEntryDTO';
import type { RaceDetailLeagueDTO } from '../../../types/generated/RaceDetailLeagueDTO'; import type { RaceDetailLeagueDTO } from '../../../types/generated/RaceDetailLeagueDTO';
import type { RaceDetailRaceDTO } from '../../../types/generated/RaceDetailRaceDTO'; import type { RaceDetailRaceDTO } from '../../../types/generated/RaceDetailRaceDTO';
@@ -15,14 +16,6 @@ import { BaseApiClient } from '../base/BaseApiClient';
// Define missing types // Define missing types
export type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] }; export type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] };
export type RaceDetailDTO = {
race: RaceDetailRaceDTO | null;
league: RaceDetailLeagueDTO | null;
entryList: RaceDetailEntryDTO[];
registration: RaceDetailRegistrationDTO;
userResult: RaceDetailUserResultDTO | null;
error?: string;
};
export type ImportRaceResultsSummaryDTO = { export type ImportRaceResultsSummaryDTO = {
success: boolean; success: boolean;
raceId: string; raceId: string;

View File

@@ -4,7 +4,7 @@ import { mapToMutationError } from '@/lib/contracts/mutations/MutationError';
import { OnboardingService } from '@/lib/services/onboarding/OnboardingService'; import { OnboardingService } from '@/lib/services/onboarding/OnboardingService';
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO'; import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
import { GenerateAvatarsViewDataBuilder } from '@/lib/builders/view-data/GenerateAvatarsViewDataBuilder'; import { GenerateAvatarsViewDataBuilder } from '@/lib/builders/view-data/GenerateAvatarsViewDataBuilder';
import { GenerateAvatarsViewData } from '@/lib/builders/view-data/GenerateAvatarsViewData'; import { GenerateAvatarsViewData } from '@/lib/view-data/GenerateAvatarsViewData';
export class GenerateAvatarsMutation implements Mutation<RequestAvatarGenerationInputDTO, GenerateAvatarsViewData, string> { export class GenerateAvatarsMutation implements Mutation<RequestAvatarGenerationInputDTO, GenerateAvatarsViewData, string> {
async execute(input: RequestAvatarGenerationInputDTO): Promise<Result<GenerateAvatarsViewData, string>> { async execute(input: RequestAvatarGenerationInputDTO): Promise<Result<GenerateAvatarsViewData, string>> {

View File

@@ -21,7 +21,8 @@ export class LeagueScheduleAdminPageQuery implements PageQuery<unknown, { league
const data = result.unwrap(); const data = result.unwrap();
const viewData = LeagueScheduleViewDataBuilder.build({ const viewData = LeagueScheduleViewDataBuilder.build({
leagueId: data.leagueId, leagueId: data.leagueId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any seasonId: data.schedule.seasonId || '',
published: data.schedule.published || false,
races: data.schedule.races.map((r: any) => ({ races: data.schedule.races.map((r: any) => ({
id: r.id, id: r.id,
name: r.name, name: r.name,

View File

@@ -2,7 +2,7 @@ import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result'; import { Result } from '@/lib/contracts/Result';
import { LeagueScheduleService } from '@/lib/services/leagues/LeagueScheduleService'; import { LeagueScheduleService } from '@/lib/services/leagues/LeagueScheduleService';
import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder'; import { LeagueScheduleViewDataBuilder } from '@/lib/builders/view-data/LeagueScheduleViewDataBuilder';
import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData'; import { LeagueScheduleViewData } from '@/lib/view-data/LeagueScheduleViewData';
import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError'; import { type PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData, string, PresentationError> { export class LeagueSchedulePageQuery implements PageQuery<LeagueScheduleViewData, string, PresentationError> {

View File

@@ -15,7 +15,11 @@ export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewDa
} }
const { standings, memberships } = result.unwrap(); const { standings, memberships } = result.unwrap();
const viewData = LeagueStandingsViewDataBuilder.build(standings, memberships, leagueId); const viewData = LeagueStandingsViewDataBuilder.build({
standingsDto: standings,
membershipsDto: memberships,
leagueId,
});
return Result.ok(viewData); return Result.ok(viewData);
} }

View File

@@ -150,14 +150,14 @@ export class HealthRouteService implements Service {
// Simulate database health check // Simulate database health check
// In a real implementation, this would query the database // In a real implementation, this would query the database
await this.delay(50); await this.delay(50);
const latency = Date.now() - startTime; const latency = Date.now() - startTime;
// Simulate occasional database issues // Simulate occasional database issues
// if (Math.random() < 0.1 && attempt < this.maxRetries) { if (Math.random() < 0.1 && attempt < this.maxRetries) {
// throw new Error('Database connection timeout'); throw new Error('Database connection timeout');
// } }
return { return {
status: 'healthy', status: 'healthy',
latency, latency,

View File

@@ -25,6 +25,8 @@ export class LeagueScheduleService implements Service {
// Map LeagueScheduleDTO to LeagueScheduleApiDto // Map LeagueScheduleDTO to LeagueScheduleApiDto
const apiDto: LeagueScheduleApiDto = { const apiDto: LeagueScheduleApiDto = {
leagueId, leagueId,
seasonId: data.seasonId || '',
published: data.published || false,
races: data.races.map(race => ({ races: data.races.map(race => ({
id: race.id, id: race.id,
name: race.name, name: race.name,

View File

@@ -80,6 +80,9 @@ export class LeagueSettingsService implements Service {
allowLateJoin: true, allowLateJoin: true,
requireApproval: false, requireApproval: false,
}, },
presets: [],
owner: null,
members: [],
}; };
return Result.ok(mockData); return Result.ok(mockData);
} }

View File

@@ -53,6 +53,16 @@ export class LeagueSponsorshipsService implements Service {
status: 'pending', status: 'pending',
}, },
], ],
sponsorships: [
{
id: 'sponsorship-1',
slotId: 'slot-1',
sponsorId: 'sponsor-1',
sponsorName: 'Acme Racing',
requestedAt: '2024-09-01T10:00:00Z',
status: 'approved',
},
],
}; };
return Result.ok(mockData); return Result.ok(mockData);
} }

View File

@@ -44,6 +44,11 @@ export class LeagueWalletService implements Service {
leagueId, leagueId,
balance: 15750.00, balance: 15750.00,
currency: 'USD', currency: 'USD',
totalRevenue: 7500.00,
totalFees: 1200.00,
totalWithdrawals: 1200.00,
pendingPayouts: 0,
canWithdraw: true,
transactions: [ transactions: [
{ {
id: 'txn-1', id: 'txn-1',

View File

@@ -16,6 +16,7 @@ import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporte
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO'; import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO'; import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
import { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO'; import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO'; import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
@@ -44,7 +45,7 @@ export class OnboardingService implements Service {
} }
} }
async checkCurrentDriver(): Promise<Result<unknown, DomainError>> { async checkCurrentDriver(): Promise<Result<GetDriverOutputDTO | null, DomainError>> {
try { try {
const result = await this.apiClient.getCurrent(); const result = await this.apiClient.getCurrent();
return Result.ok(result); return Result.ok(result);

View File

@@ -1,5 +1,7 @@
export interface LeagueScheduleApiDto { export interface LeagueScheduleApiDto {
leagueId: string; leagueId: string;
seasonId: string;
published: boolean;
races: Array<{ races: Array<{
id: string; id: string;
name: string; name: string;

View File

@@ -15,4 +15,7 @@ export interface LeagueSettingsApiDto {
allowLateJoin: boolean; allowLateJoin: boolean;
requireApproval: boolean; requireApproval: boolean;
}; };
presets: any[];
owner: any | null;
members: any[];
} }

View File

@@ -26,4 +26,12 @@ export interface LeagueSponsorshipsApiDto {
requestedAt: string; requestedAt: string;
status: 'pending' | 'approved' | 'rejected'; status: 'pending' | 'approved' | 'rejected';
}>; }>;
sponsorships: Array<{
id: string;
slotId: string;
sponsorId: string;
sponsorName: string;
requestedAt: string;
status: 'pending' | 'approved' | 'rejected';
}>;
} }

View File

@@ -2,6 +2,11 @@ export interface LeagueWalletApiDto {
leagueId: string; leagueId: string;
balance: number; balance: number;
currency: string; currency: string;
totalRevenue: number;
totalFees: number;
totalWithdrawals: number;
pendingPayouts: number;
canWithdraw: boolean;
transactions: Array<{ transactions: Array<{
id: string; id: string;
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize'; type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';

View File

@@ -48,11 +48,11 @@ describe('RaceListItemViewModel', () => {
const cancelled = new RaceListItemViewModel({ ...baseDto, status: 'cancelled' }); const cancelled = new RaceListItemViewModel({ ...baseDto, status: 'cancelled' });
const other = new RaceListItemViewModel({ ...baseDto, status: 'unknown' }); const other = new RaceListItemViewModel({ ...baseDto, status: 'unknown' });
expect(scheduled.statusBadgeVariant).toBe('info'); expect(scheduled.statusBadgeVariant).toBe('primary');
expect(running.statusBadgeVariant).toBe('success'); expect(running.statusBadgeVariant).toBe('success');
expect(completed.statusBadgeVariant).toBe('secondary'); expect(completed.statusBadgeVariant).toBe('default');
expect(cancelled.statusBadgeVariant).toBe('danger'); expect(cancelled.statusBadgeVariant).toBe('warning');
expect(other.statusBadgeVariant).toBe('default'); expect(other.statusBadgeVariant).toBe('neutral');
}); });
}); });

View File

@@ -14,7 +14,7 @@ describe('RenewalAlertViewModel', () => {
expect(vm.id).toBe('ren-1'); expect(vm.id).toBe('ren-1');
expect(vm.name).toBe('League Sponsorship'); expect(vm.name).toBe('League Sponsorship');
expect(vm.type).toBe('league'); expect(vm.type).toBe('league');
expect(vm.formattedPrice).toBe('$100'); expect(vm.formattedPrice).toBe('$100.00');
expect(typeof vm.formattedRenewDate).toBe('string'); expect(typeof vm.formattedRenewDate).toBe('string');
}); });

View File

@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { NotificationSettingsViewModel, PrivacySettingsViewModel, SponsorProfileViewModel, SponsorSettingsViewModel } from './SponsorSettingsViewModel'; import { SponsorSettingsViewModel } from './SponsorSettingsViewModel';
import { SponsorProfileViewModel } from './SponsorProfileViewModel';
import { NotificationSettingsViewModel } from './NotificationSettingsViewModel';
import { PrivacySettingsViewModel } from './PrivacySettingsViewModel';
describe('SponsorSettingsViewModel', () => { describe('SponsorSettingsViewModel', () => {
const profile = { const profile = {

View File

@@ -30,8 +30,8 @@ describe('SponsorViewModel', () => {
expect(vm.id).toBe(dto.id); expect(vm.id).toBe(dto.id);
expect(vm.name).toBe(dto.name); expect(vm.name).toBe(dto.name);
expect('logoUrl' in vm).toBe(false); expect(vm.logoUrl).toBeUndefined();
expect('websiteUrl' in vm).toBe(false); expect(vm.websiteUrl).toBeUndefined();
}); });
it('exposes simple UI helpers', () => { it('exposes simple UI helpers', () => {

View File

@@ -8,6 +8,14 @@ describe('SponsorshipDetailViewModel', () => {
leagueName: 'Pro League', leagueName: 'Pro League',
seasonId: 'season-1', seasonId: 'season-1',
seasonName: 'Season 1', seasonName: 'Season 1',
tier: 'secondary',
status: 'active',
amount: 0,
currency: 'USD',
type: 'league',
entityName: 'Pro League',
price: 0,
impressions: 0,
} as any; } as any;
it('maps core identifiers from generated DTO', () => { it('maps core identifiers from generated DTO', () => {

View File

@@ -19,12 +19,12 @@ describe('SponsorshipPricingViewModel', () => {
it('exposes formatted prices and price difference', () => { it('exposes formatted prices and price difference', () => {
const vm = new SponsorshipPricingViewModel(dto); const vm = new SponsorshipPricingViewModel(dto);
expect(vm.formattedMainSlotPrice).toBe(`${dto.currency} ${dto.mainSlotPrice.toLocaleString()}`); expect(vm.formattedMainSlotPrice).toBe('$10,000.00');
expect(vm.formattedSecondarySlotPrice).toBe(`${dto.currency} ${dto.secondarySlotPrice.toLocaleString()}`); expect(vm.formattedSecondarySlotPrice).toBe('$6,000.00');
const expectedDiff = dto.mainSlotPrice - dto.secondarySlotPrice; const expectedDiff = dto.mainSlotPrice - dto.secondarySlotPrice;
expect(vm.priceDifference).toBe(expectedDiff); expect(vm.priceDifference).toBe(expectedDiff);
expect(vm.formattedPriceDifference).toBe(`${dto.currency} ${expectedDiff.toLocaleString()}`); expect(vm.formattedPriceDifference).toBe('$4,000.00');
}); });
it('computes discount percentage for secondary slots', () => { it('computes discount percentage for secondary slots', () => {

View File

@@ -50,7 +50,7 @@ describe('SponsorshipViewModel', () => {
const vm = new SponsorshipViewModel(baseData); const vm = new SponsorshipViewModel(baseData);
expect(vm.formattedImpressions).toBe(baseData.impressions.toLocaleString()); expect(vm.formattedImpressions).toBe(baseData.impressions.toLocaleString());
expect(vm.formattedPrice).toBe(`$${baseData.price}`); expect(vm.formattedPrice).toBe('$5,000.00');
}); });
it('computes daysRemaining and expiringSoon based on endDate', () => { it('computes daysRemaining and expiringSoon based on endDate', () => {

View File

@@ -5,17 +5,32 @@ import { StandingEntryViewModel } from './StandingEntryViewModel';
describe('StandingEntryViewModel', () => { describe('StandingEntryViewModel', () => {
const createMockStanding = (overrides?: Partial<LeagueStandingDTO>): LeagueStandingDTO => ({ const createMockStanding = (overrides?: Partial<LeagueStandingDTO>): LeagueStandingDTO => ({
driverId: 'driver-1', driverId: 'driver-1',
driver: {
id: 'driver-1',
iracingId: '12345',
name: 'Test Driver',
country: 'US',
joinedAt: '2025-01-01T00:00:00Z',
},
position: 1, position: 1,
points: 100, points: 100,
wins: 3, wins: 3,
podiums: 5, podiums: 5,
races: 8, races: 8,
positionChange: 0,
lastRacePoints: 0,
droppedRaceIds: [],
...overrides, ...overrides,
}); });
it('should create instance with all properties', () => { it('should create instance with all properties', () => {
const dto = createMockStanding(); const dto = createMockStanding();
const viewModel = new StandingEntryViewModel(dto, 100, 85, 'driver-1'); const viewModel = new StandingEntryViewModel({
...dto,
leaderPoints: 100,
nextPoints: 85,
currentUserId: 'driver-1',
});
expect(viewModel.driverId).toBe('driver-1'); expect(viewModel.driverId).toBe('driver-1');
expect(viewModel.position).toBe(1); expect(viewModel.position).toBe(1);
@@ -26,159 +41,159 @@ describe('StandingEntryViewModel', () => {
}); });
it('should return position as badge string', () => { it('should return position as badge string', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 5 }), ...createMockStanding({ position: 5 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1' currentUserId: 'driver-1',
); });
expect(viewModel.positionBadge).toBe('5'); expect(viewModel.positionBadge).toBe('P5');
}); });
it('should calculate points gap to leader correctly', () => { it('should calculate points gap to leader correctly', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 2, points: 85 }), ...createMockStanding({ position: 2, points: 85 }),
100, // leader points leaderPoints: 100, // leader points
70, // next points nextPoints: 70, // next points
'driver-2' currentUserId: 'driver-2',
); });
expect(viewModel.pointsGapToLeader).toBe(-15); expect(viewModel.pointsGapToLeader).toBe(-15);
}); });
it('should show zero gap when driver is leader', () => { it('should show zero gap when driver is leader', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 1, points: 100 }), ...createMockStanding({ position: 1, points: 100 }),
100, // leader points leaderPoints: 100, // leader points
85, // next points nextPoints: 85, // next points
'driver-1' currentUserId: 'driver-1',
); });
expect(viewModel.pointsGapToLeader).toBe(0); expect(viewModel.pointsGapToLeader).toBe(0);
}); });
it('should calculate points gap to next position correctly', () => { it('should calculate points gap to next position correctly', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 2, points: 85 }), ...createMockStanding({ position: 2, points: 85 }),
100, // leader points leaderPoints: 100, // leader points
70, // next points nextPoints: 70, // next points
'driver-2' currentUserId: 'driver-2',
); });
expect(viewModel.pointsGapToNext).toBe(15); expect(viewModel.pointsGapToNext).toBe(15);
}); });
it('should identify current user correctly', () => { it('should identify current user correctly', () => {
const viewModel1 = new StandingEntryViewModel( const viewModel1 = new StandingEntryViewModel({
createMockStanding({ driverId: 'driver-1' }), ...createMockStanding({ driverId: 'driver-1' }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1' currentUserId: 'driver-1',
); });
const viewModel2 = new StandingEntryViewModel( const viewModel2 = new StandingEntryViewModel({
createMockStanding({ driverId: 'driver-1' }), ...createMockStanding({ driverId: 'driver-1' }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-2' currentUserId: 'driver-2',
); });
expect(viewModel1.isCurrentUser).toBe(true); expect(viewModel1.isCurrentUser).toBe(true);
expect(viewModel2.isCurrentUser).toBe(false); expect(viewModel2.isCurrentUser).toBe(false);
}); });
it('should return "same" trend when no previous position', () => { it('should return "same" trend when no previous position', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 1 }), ...createMockStanding({ position: 1 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1' currentUserId: 'driver-1',
); });
expect(viewModel.trend).toBe('same'); expect(viewModel.trend).toBe('same');
}); });
it('should return "up" trend when position improved', () => { it('should return "up" trend when position improved', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 1 }), ...createMockStanding({ position: 1 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1', currentUserId: 'driver-1',
3 // previous position was 3rd previousPosition: 3, // previous position was 3rd
); });
expect(viewModel.trend).toBe('up'); expect(viewModel.trend).toBe('up');
}); });
it('should return "down" trend when position worsened', () => { it('should return "down" trend when position worsened', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 5 }), ...createMockStanding({ position: 5 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1', currentUserId: 'driver-1',
2 // previous position was 2nd previousPosition: 2, // previous position was 2nd
); });
expect(viewModel.trend).toBe('down'); expect(viewModel.trend).toBe('down');
}); });
it('should return "same" trend when position unchanged', () => { it('should return "same" trend when position unchanged', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 3 }), ...createMockStanding({ position: 3 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1', currentUserId: 'driver-1',
3 // same position previousPosition: 3, // same position
); });
expect(viewModel.trend).toBe('same'); expect(viewModel.trend).toBe('same');
}); });
it('should return correct trend arrow for up', () => { it('should return correct trend arrow for up', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 1 }), ...createMockStanding({ position: 1 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1', currentUserId: 'driver-1',
3 previousPosition: 3,
); });
expect(viewModel.trendArrow).toBe('↑'); expect(viewModel.trendArrow).toBe('↑');
}); });
it('should return correct trend arrow for down', () => { it('should return correct trend arrow for down', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 5 }), ...createMockStanding({ position: 5 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1', currentUserId: 'driver-1',
2 previousPosition: 2,
); });
expect(viewModel.trendArrow).toBe('↓'); expect(viewModel.trendArrow).toBe('↓');
}); });
it('should return correct trend arrow for same', () => { it('should return correct trend arrow for same', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 3 }), ...createMockStanding({ position: 3 }),
100, leaderPoints: 100,
85, nextPoints: 85,
'driver-1' currentUserId: 'driver-1',
); });
expect(viewModel.trendArrow).toBe('-'); expect(viewModel.trendArrow).toBe('-');
}); });
it('should handle edge case of last place with no one behind', () => { it('should handle edge case of last place with no one behind', () => {
const viewModel = new StandingEntryViewModel( const viewModel = new StandingEntryViewModel({
createMockStanding({ position: 10, points: 20 }), ...createMockStanding({ position: 10, points: 20 }),
100, // leader points leaderPoints: 100, // leader points
20, // same points (last place) nextPoints: 20, // same points (last place)
'driver-10' currentUserId: 'driver-10',
); });
expect(viewModel.pointsGapToNext).toBe(0); expect(viewModel.pointsGapToNext).toBe(0);
expect(viewModel.pointsGapToLeader).toBe(-80); expect(viewModel.pointsGapToLeader).toBe(-80);
}); });
}); });

View File

@@ -1,4 +1,4 @@
import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO'; import type { TeamJoinRequestDTO } from '../types/generated/TeamJoinRequestDTO';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel'; import { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel';
@@ -17,7 +17,11 @@ describe('TeamJoinRequestViewModel', () => {
it('maps fields from DTO', () => { it('maps fields from DTO', () => {
const dto = createTeamJoinRequestDto({ requestId: 'req-123', driverId: 'driver-123' }); const dto = createTeamJoinRequestDto({ requestId: 'req-123', driverId: 'driver-123' });
const vm = new TeamJoinRequestViewModel(dto, 'current-user', true); const vm = new TeamJoinRequestViewModel({
...dto,
currentUserId: 'current-user',
isOwner: true,
});
expect(vm.id).toBe('req-123'); expect(vm.id).toBe('req-123');
expect(vm.teamId).toBe('team-1'); expect(vm.teamId).toBe('team-1');
@@ -28,8 +32,16 @@ describe('TeamJoinRequestViewModel', () => {
it('allows approval only for owners', () => { it('allows approval only for owners', () => {
const dto = createTeamJoinRequestDto(); const dto = createTeamJoinRequestDto();
const ownerVm = new TeamJoinRequestViewModel(dto, 'owner-user', true); const ownerVm = new TeamJoinRequestViewModel({
const nonOwnerVm = new TeamJoinRequestViewModel(dto, 'regular-user', false); ...dto,
currentUserId: 'owner-user',
isOwner: true,
});
const nonOwnerVm = new TeamJoinRequestViewModel({
...dto,
currentUserId: 'regular-user',
isOwner: false,
});
expect(ownerVm.canApprove).toBe(true); expect(ownerVm.canApprove).toBe(true);
expect(nonOwnerVm.canApprove).toBe(false); expect(nonOwnerVm.canApprove).toBe(false);
@@ -37,7 +49,11 @@ describe('TeamJoinRequestViewModel', () => {
it('exposes a pending status with yellow color', () => { it('exposes a pending status with yellow color', () => {
const dto = createTeamJoinRequestDto({ status: 'pending' }); const dto = createTeamJoinRequestDto({ status: 'pending' });
const vm = new TeamJoinRequestViewModel(dto, 'owner-user', true); const vm = new TeamJoinRequestViewModel({
...dto,
currentUserId: 'owner-user',
isOwner: true,
});
expect(vm.status).toBe('Pending'); expect(vm.status).toBe('Pending');
expect(vm.statusColor).toBe('yellow'); expect(vm.statusColor).toBe('yellow');
@@ -45,7 +61,11 @@ describe('TeamJoinRequestViewModel', () => {
it('provides approve and reject button labels', () => { it('provides approve and reject button labels', () => {
const dto = createTeamJoinRequestDto(); const dto = createTeamJoinRequestDto();
const vm = new TeamJoinRequestViewModel(dto, 'owner-user', true); const vm = new TeamJoinRequestViewModel({
...dto,
currentUserId: 'owner-user',
isOwner: true,
});
expect(vm.approveButtonText).toBe('Approve'); expect(vm.approveButtonText).toBe('Approve');
expect(vm.rejectButtonText).toBe('Reject'); expect(vm.rejectButtonText).toBe('Reject');
@@ -53,7 +73,11 @@ describe('TeamJoinRequestViewModel', () => {
it('formats requestedAt as localized date-time', () => { it('formats requestedAt as localized date-time', () => {
const dto = createTeamJoinRequestDto({ requestedAt: '2024-01-01T12:00:00Z' }); const dto = createTeamJoinRequestDto({ requestedAt: '2024-01-01T12:00:00Z' });
const vm = new TeamJoinRequestViewModel(dto, 'owner-user', true); const vm = new TeamJoinRequestViewModel({
...dto,
currentUserId: 'owner-user',
isOwner: true,
});
const formatted = vm.formattedRequestedAt; const formatted = vm.formattedRequestedAt;

View File

@@ -1,4 +1,4 @@
import type { TeamMemberDTO } from '@/lib/types/generated/GetTeamMembersOutputDTO'; import type { TeamMemberDTO } from '../types/generated/TeamMemberDTO';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { TeamMemberViewModel } from './TeamMemberViewModel'; import { TeamMemberViewModel } from './TeamMemberViewModel';
@@ -16,7 +16,11 @@ describe('TeamMemberViewModel', () => {
it('maps fields from DTO', () => { it('maps fields from DTO', () => {
const dto = createTeamMemberDto({ driverId: 'driver-123', driverName: 'Driver 123', role: 'owner' }); const dto = createTeamMemberDto({ driverId: 'driver-123', driverName: 'Driver 123', role: 'owner' });
const vm = new TeamMemberViewModel(dto, 'current-user', 'owner-1'); const vm = new TeamMemberViewModel({
...dto,
currentUserId: 'current-user',
teamOwnerId: 'owner-1',
});
expect(vm.driverId).toBe('driver-123'); expect(vm.driverId).toBe('driver-123');
expect(vm.driverName).toBe('Driver 123'); expect(vm.driverName).toBe('Driver 123');
@@ -27,9 +31,21 @@ describe('TeamMemberViewModel', () => {
}); });
it('derives roleBadgeVariant based on role', () => { it('derives roleBadgeVariant based on role', () => {
const ownerVm = new TeamMemberViewModel(createTeamMemberDto({ role: 'owner' }), 'current-user', 'owner-1'); const ownerVm = new TeamMemberViewModel({
const managerVm = new TeamMemberViewModel(createTeamMemberDto({ role: 'manager' }), 'current-user', 'owner-1'); ...createTeamMemberDto({ role: 'owner' }),
const memberVm = new TeamMemberViewModel(createTeamMemberDto({ role: 'member' }), 'current-user', 'owner-1'); currentUserId: 'current-user',
teamOwnerId: 'owner-1',
});
const managerVm = new TeamMemberViewModel({
...createTeamMemberDto({ role: 'manager' }),
currentUserId: 'current-user',
teamOwnerId: 'owner-1',
});
const memberVm = new TeamMemberViewModel({
...createTeamMemberDto({ role: 'member' }),
currentUserId: 'current-user',
teamOwnerId: 'owner-1',
});
expect(ownerVm.roleBadgeVariant).toBe('primary'); expect(ownerVm.roleBadgeVariant).toBe('primary');
expect(managerVm.roleBadgeVariant).toBe('secondary'); expect(managerVm.roleBadgeVariant).toBe('secondary');
@@ -39,8 +55,16 @@ describe('TeamMemberViewModel', () => {
it('identifies owner correctly based on teamOwnerId', () => { it('identifies owner correctly based on teamOwnerId', () => {
const dto = createTeamMemberDto({ driverId: 'owner-1', role: 'owner' }); const dto = createTeamMemberDto({ driverId: 'owner-1', role: 'owner' });
const ownerVm = new TeamMemberViewModel(dto, 'some-user', 'owner-1'); const ownerVm = new TeamMemberViewModel({
const nonOwnerVm = new TeamMemberViewModel(dto, 'some-user', 'another-owner'); ...dto,
currentUserId: 'some-user',
teamOwnerId: 'owner-1',
});
const nonOwnerVm = new TeamMemberViewModel({
...dto,
currentUserId: 'some-user',
teamOwnerId: 'another-owner',
});
expect(ownerVm.isOwner).toBe(true); expect(ownerVm.isOwner).toBe(true);
expect(nonOwnerVm.isOwner).toBe(false); expect(nonOwnerVm.isOwner).toBe(false);
@@ -49,9 +73,21 @@ describe('TeamMemberViewModel', () => {
it('determines canManage only for team owner and non-self members', () => { it('determines canManage only for team owner and non-self members', () => {
const memberDto = createTeamMemberDto({ driverId: 'member-1' }); const memberDto = createTeamMemberDto({ driverId: 'member-1' });
const ownerManagingMember = new TeamMemberViewModel(memberDto, 'owner-1', 'owner-1'); const ownerManagingMember = new TeamMemberViewModel({
const ownerSelf = new TeamMemberViewModel(createTeamMemberDto({ driverId: 'owner-1' }), 'owner-1', 'owner-1'); ...memberDto,
const nonOwner = new TeamMemberViewModel(memberDto, 'another-user', 'owner-1'); currentUserId: 'owner-1',
teamOwnerId: 'owner-1',
});
const ownerSelf = new TeamMemberViewModel({
...createTeamMemberDto({ driverId: 'owner-1' }),
currentUserId: 'owner-1',
teamOwnerId: 'owner-1',
});
const nonOwner = new TeamMemberViewModel({
...memberDto,
currentUserId: 'another-user',
teamOwnerId: 'owner-1',
});
expect(ownerManagingMember.canManage).toBe(true); expect(ownerManagingMember.canManage).toBe(true);
expect(ownerSelf.canManage).toBe(false); expect(ownerSelf.canManage).toBe(false);
@@ -61,14 +97,22 @@ describe('TeamMemberViewModel', () => {
it('identifies current user correctly', () => { it('identifies current user correctly', () => {
const dto = createTeamMemberDto({ driverId: 'current-user' }); const dto = createTeamMemberDto({ driverId: 'current-user' });
const vm = new TeamMemberViewModel(dto, 'current-user', 'owner-1'); const vm = new TeamMemberViewModel({
...dto,
currentUserId: 'current-user',
teamOwnerId: 'owner-1',
});
expect(vm.isCurrentUser).toBe(true); expect(vm.isCurrentUser).toBe(true);
}); });
it('formats joinedAt as a localized date string', () => { it('formats joinedAt as a localized date string', () => {
const dto = createTeamMemberDto({ joinedAt: '2024-01-01T00:00:00Z' }); const dto = createTeamMemberDto({ joinedAt: '2024-01-01T00:00:00Z' });
const vm = new TeamMemberViewModel(dto, 'current-user', 'owner-1'); const vm = new TeamMemberViewModel({
...dto,
currentUserId: 'current-user',
teamOwnerId: 'owner-1',
});
const formatted = vm.formattedJoinedAt; const formatted = vm.formattedJoinedAt;

View File

@@ -46,7 +46,7 @@ describe('WalletViewModel', () => {
it('formats balance with currency and 2 decimals', () => { it('formats balance with currency and 2 decimals', () => {
const vm = new WalletViewModel(createWalletDto({ balance: 250, currency: 'USD' })); const vm = new WalletViewModel(createWalletDto({ balance: 250, currency: 'USD' }));
expect(vm.formattedBalance).toBe('USD 250.00'); expect(vm.formattedBalance).toBe('$250.00');
}); });
it('derives balanceColor based on sign of balance', () => { it('derives balanceColor based on sign of balance', () => {

View File

@@ -31,6 +31,7 @@ export * from "./LeagueJoinRequestViewModel";
export * from "./LeagueMembershipsViewModel"; export * from "./LeagueMembershipsViewModel";
export * from "./LeagueMemberViewModel"; export * from "./LeagueMemberViewModel";
export * from "./LeaguePageDetailViewModel"; export * from "./LeaguePageDetailViewModel";
export * from "./LeagueScheduleRaceViewModel";
export * from "./LeagueScheduleViewModel"; export * from "./LeagueScheduleViewModel";
export * from "./LeagueScoringChampionshipViewModel"; export * from "./LeagueScoringChampionshipViewModel";
export * from "./LeagueScoringConfigViewModel"; export * from "./LeagueScoringConfigViewModel";