diff --git a/adapters/bootstrap/racing/RacingMembershipFactory.ts b/adapters/bootstrap/racing/RacingMembershipFactory.ts index 4747d9fad..cb861164f 100644 --- a/adapters/bootstrap/racing/RacingMembershipFactory.ts +++ b/adapters/bootstrap/racing/RacingMembershipFactory.ts @@ -105,16 +105,22 @@ export class RacingMembershipFactory { } // Spread remaining drivers across remaining leagues to create realistic overlap. + // IMPORTANT: in postgres mode league ids are UUIDs, so never parse numeric suffixes from ids. for (const driver of drivers) { const driverId = driver.id.toString(); - const driverNumber = Number(driverId.split('-')[1]); for (const league of leagues) { const leagueId = league.id.toString(); if (leagueId === seedId('league-5', this.persistence)) continue; if (emptyLeagueId && leagueId === emptyLeagueId) continue; - if (driverNumber % 11 === 0 && leagueId === seedId('league-3', this.persistence)) { + // Deterministic membership distribution that works with UUID ids. + // Use stable hash of (driverId + leagueId) rather than parsing numeric suffixes. + const distKey = `${driverId}:${leagueId}`; + const dist = this.stableHash(distKey); + + // Some inactive memberships for league-3 to exercise edge cases. + if (leagueId === seedId('league-3', this.persistence) && dist % 11 === 0) { add({ leagueId, driverId, @@ -126,7 +132,7 @@ export class RacingMembershipFactory { } // Sparse membership distribution (not every driver in every league) - if ((driverNumber + Number(leagueId.split('-')[1] ?? 0)) % 9 === 0) { + if (dist % 9 === 0) { add({ leagueId, driverId, @@ -330,6 +336,15 @@ export class RacingMembershipFactory { return registrations; } + private stableHash(input: string): number { + // Simple deterministic string hash (non-crypto), stable across runs. + let hash = 0; + for (let i = 0; i < input.length; i++) { + hash = (hash * 31 + input.charCodeAt(i)) | 0; + } + return Math.abs(hash); + } + private addDays(date: Date, days: number): Date { return new Date(date.getTime() + days * 24 * 60 * 60 * 1000); } diff --git a/adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper.ts b/adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper.ts index a8cb9c5c5..03554a53d 100644 --- a/adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper.ts +++ b/adapters/racing/persistence/typeorm/mappers/PointsTableJsonMapper.ts @@ -14,9 +14,9 @@ export class PointsTableJsonMapper { } fromJson(serialized: SerializedPointsTable): PointsTable { - const record: Record = {}; + const record: Map = new Map(); for (const [position, points] of Object.entries(serialized.pointsByPosition)) { - record[Number(position)] = points; + record.set(Number(position), points); } return new PointsTable(record); } diff --git a/apps/api/src/domain/league/LeagueConfig.integration.test.ts b/apps/api/src/domain/league/LeagueConfig.integration.test.ts new file mode 100644 index 000000000..f698b371d --- /dev/null +++ b/apps/api/src/domain/league/LeagueConfig.integration.test.ts @@ -0,0 +1,253 @@ +import 'reflect-metadata'; + +import { ValidationPipe } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { AuthenticationGuard } from '../auth/AuthenticationGuard'; +import { AuthorizationGuard } from '../auth/AuthorizationGuard'; +import type { AuthorizationService } from '../auth/AuthorizationService'; +import { LeagueModule } from './LeagueModule'; + +import { requestContextMiddleware } from '@adapters/http/RequestContext'; + +import { + DRIVER_REPOSITORY_TOKEN, + LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, + LEAGUE_REPOSITORY_TOKEN, + SEASON_REPOSITORY_TOKEN, + LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN, +} from '../../persistence/inmemory/InMemoryRacingPersistenceModule'; + +import { Driver } from '@core/racing/domain/entities/Driver'; +import { League } from '@core/racing/domain/entities/League'; +import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership'; +import { Season } from '@core/racing/domain/entities/season/Season'; +import { LeagueScoringConfig } from '@core/racing/domain/entities/LeagueScoringConfig'; +import { PointsTable } from '@core/racing/domain/value-objects/PointsTable'; + +describe('League Config API - Integration', () => { + let app: import("@nestjs/common").INestApplication; + + const sessionPort: { getCurrentSession: () => Promise } = { + getCurrentSession: vi.fn(async () => null), + }; + + const authorizationService: Pick = { + getRolesForUser: vi.fn(() => []), + }; + + async function seedLeagueWithConfig(params: { + leagueId: string; + ownerId: string; + seasonId: string; + scoringConfigId: string; + gameId: string; + }): Promise { + const leagueRepo = app.get(LEAGUE_REPOSITORY_TOKEN); + const driverRepo = app.get(DRIVER_REPOSITORY_TOKEN); + const membershipRepo = app.get(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN); + const seasonRepo = app.get(SEASON_REPOSITORY_TOKEN); + const scoringRepo = app.get(LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN); + + // Game is seeded by default, no need to create + + // Create league + await leagueRepo.create( + League.create({ + id: params.leagueId, + name: 'Test League', + description: 'Test league', + ownerId: params.ownerId, + settings: { visibility: 'ranked' }, + }), + ); + + // Create owner driver + await driverRepo.create( + Driver.create({ + id: params.ownerId, + iracingId: '2001', + name: 'Owner Driver', + country: 'DE', + }), + ); + + // Create membership + await membershipRepo.saveMembership( + LeagueMembership.create({ + leagueId: params.leagueId, + driverId: params.ownerId, + role: 'owner', + status: 'active', + }), + ); + + // Create season + await seasonRepo.create( + Season.create({ + id: params.seasonId, + leagueId: params.leagueId, + gameId: params.gameId, + name: 'Test Season', + startDate: new Date('2025-01-01'), + endDate: new Date('2025-12-31'), + status: 'active', + }), + ); + + // Create scoring config + await scoringRepo.save( + LeagueScoringConfig.create({ + id: params.scoringConfigId, + seasonId: params.seasonId, + scoringPresetId: 'f1-2024', + championships: [ + { + id: 'championship-1', + name: 'Main Championship', + type: 'driver', + sessionTypes: ['main'], + pointsTableBySessionType: { + practice: new PointsTable({}), + qualifying: new PointsTable({}), + q1: new PointsTable({}), + q2: new PointsTable({}), + q3: new PointsTable({}), + sprint: new PointsTable({}), + main: new PointsTable({ 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 }), + timeTrial: new PointsTable({}), + }, + dropScorePolicy: { strategy: 'none' }, + }, + ], + }), + ); + } + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [LeagueModule], + }).compile(); + + app = module.createNestApplication(); + + // Required for getActorFromRequestContext() used by requireLeagueAdminOrOwner(). + app.use(requestContextMiddleware as never); + + // Test-only auth injection: emulate an authenticated session by setting request.user. + app.use((req: unknown, _res: unknown, next: unknown) => { + const r = req as { headers: Record, user?: { userId: string } }; + const userId = r.headers['x-test-user-id']; + if (typeof userId === 'string' && userId.length > 0) { + r.user = { userId }; + } + (next as () => void)(); + }); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + + const reflector = new Reflector(); + app.useGlobalGuards( + new AuthenticationGuard(sessionPort as never), + new AuthorizationGuard(reflector, authorizationService as never), + ); + + await app.init(); + }); + + afterEach(async () => { + await app?.close(); + vi.clearAllMocks(); + }); + + it('should return league config with all dependencies injected', async () => { + const leagueId = 'league-1'; + const ownerId = 'owner-1'; + const seasonId = 'season-1'; + const scoringConfigId = 'scoring-1'; + const gameId = 'game-1'; + + await seedLeagueWithConfig({ leagueId, ownerId, seasonId, scoringConfigId, gameId }); + + const response = await request(app.getHttpServer()) + .get(`/leagues/${leagueId}/config`) + .expect(200); + + // Verify the response contains expected data + expect(response.body).toBeDefined(); + expect(response.body.leagueId).toBeDefined(); + expect(response.body.leagueId.value).toBe(leagueId); + expect(response.body.basics).toBeDefined(); + expect(response.body.basics.name.value).toBe('Test League'); + expect(response.body.basics.visibility).toBe('ranked'); + expect(response.body.scoring).toBeDefined(); + expect(response.body.scoring.type).toBe('f1-2024'); + }); + + it('should return null for non-existent league', async () => { + const response = await request(app.getHttpServer()) + .get('/leagues/non-existent-league/config') + .expect(200); + + expect(response.body).toBeNull(); + }); + + it('should handle league with no season or scoring config', async () => { + const leagueId = 'league-2'; + const ownerId = 'owner-2'; + + const leagueRepo = app.get(LEAGUE_REPOSITORY_TOKEN); + const driverRepo = app.get(DRIVER_REPOSITORY_TOKEN); + const membershipRepo = app.get(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN); + + // Create league without season or scoring config + await leagueRepo.create( + League.create({ + id: leagueId, + name: 'League Without Config', + description: 'Test league', + ownerId: ownerId, + settings: { visibility: 'unranked' }, + }), + ); + + await driverRepo.create( + Driver.create({ + id: ownerId, + iracingId: '2002', + name: 'Owner Driver 2', + country: 'US', + }), + ); + + await membershipRepo.saveMembership( + LeagueMembership.create({ + leagueId: leagueId, + driverId: ownerId, + role: 'owner', + status: 'active', + }), + ); + + const response = await request(app.getHttpServer()) + .get(`/leagues/${leagueId}/config`) + .expect(200); + + // Should still return basic league info + expect(response.body).toBeDefined(); + expect(response.body.leagueId).toBeDefined(); + expect(response.body.leagueId.value).toBe(leagueId); + expect(response.body.basics).toBeDefined(); + expect(response.body.basics.name.value).toBe('League Without Config'); + expect(response.body.basics.visibility).toBe('unranked'); + }); +}); diff --git a/apps/api/src/domain/league/LeagueController.ts b/apps/api/src/domain/league/LeagueController.ts index f809a65a1..73de6fbe4 100644 --- a/apps/api/src/domain/league/LeagueController.ts +++ b/apps/api/src/domain/league/LeagueController.ts @@ -192,6 +192,7 @@ export class LeagueController { return this.leagueService.getLeagueOwnerSummary(query); } + @Public() @Get(':leagueId/config') @ApiOperation({ summary: 'Get league full configuration' }) @ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO }) diff --git a/apps/api/src/domain/league/LeagueProviders.test.ts b/apps/api/src/domain/league/LeagueProviders.test.ts new file mode 100644 index 000000000..2a37848e7 --- /dev/null +++ b/apps/api/src/domain/league/LeagueProviders.test.ts @@ -0,0 +1,70 @@ +import 'reflect-metadata'; + +import { Test } from '@nestjs/testing'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { LeagueModule } from './LeagueModule'; +import { + GET_LEAGUE_FULL_CONFIG_USE_CASE, + LEAGUE_REPOSITORY_TOKEN, + SEASON_REPOSITORY_TOKEN, + LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN, + GAME_REPOSITORY_TOKEN, +} from './LeagueProviders'; + +import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase'; +import type { LeagueRepository } from '@core/racing/domain/repositories/LeagueRepository'; +import type { SeasonRepository } from '@core/racing/domain/repositories/SeasonRepository'; +import type { LeagueScoringConfigRepository } from '@core/racing/domain/repositories/LeagueScoringConfigRepository'; +import type { GameRepository } from '@core/racing/domain/repositories/GameRepository'; + +describe('LeagueProviders - Dependency Injection', () => { + let module: import('@nestjs/testing').TestingModule; + + beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [LeagueModule], + }).compile(); + }); + + afterEach(async () => { + await module.close(); + }); + + it('should inject GetLeagueFullConfigUseCase with all required dependencies', () => { + // Get the use case instance + const useCase = module.get(GET_LEAGUE_FULL_CONFIG_USE_CASE); + + // Verify the use case is defined + expect(useCase).toBeDefined(); + expect(useCase).toBeInstanceOf(GetLeagueFullConfigUseCase); + + // Get the repositories to verify they exist + const leagueRepo = module.get(LEAGUE_REPOSITORY_TOKEN); + const seasonRepo = module.get(SEASON_REPOSITORY_TOKEN); + const scoringRepo = module.get(LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN); + const gameRepo = module.get(GAME_REPOSITORY_TOKEN); + + // Verify all repositories are defined + expect(leagueRepo).toBeDefined(); + expect(seasonRepo).toBeDefined(); + expect(scoringRepo).toBeDefined(); + expect(gameRepo).toBeDefined(); + + // Verify the use case has the repositories injected + // We can't directly access private properties, but we can verify the use case is functional + // by checking that it has the execute method + expect(typeof useCase.execute).toBe('function'); + }); + + it('should have the correct token for GetLeagueFullConfigUseCase', () => { + expect(GET_LEAGUE_FULL_CONFIG_USE_CASE).toBe('GetLeagueFullConfigUseCase'); + }); + + it('should have all required repository tokens defined', () => { + expect(LEAGUE_REPOSITORY_TOKEN).toBe('ILeagueRepository'); + expect(SEASON_REPOSITORY_TOKEN).toBe('ISeasonRepository'); + expect(LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN).toBe('ILeagueScoringConfigRepository'); + expect(GAME_REPOSITORY_TOKEN).toBe('IGameRepository'); + }); +}); diff --git a/apps/api/src/domain/league/LeagueProviders.ts b/apps/api/src/domain/league/LeagueProviders.ts index 4fe67c666..9adf06d35 100644 --- a/apps/api/src/domain/league/LeagueProviders.ts +++ b/apps/api/src/domain/league/LeagueProviders.ts @@ -251,7 +251,19 @@ export const LeagueProviders: Provider[] = [ }, { provide: GET_LEAGUE_FULL_CONFIG_USE_CASE, - useClass: GetLeagueFullConfigUseCase, + useFactory: ( + leagueRepo: LeagueRepository, + seasonRepo: SeasonRepository, + scoringRepo: import('@core/racing/domain/repositories/LeagueScoringConfigRepository').LeagueScoringConfigRepository, + gameRepo: import('@core/racing/domain/repositories/GameRepository').GameRepository, + ) => + new GetLeagueFullConfigUseCase(leagueRepo, seasonRepo, scoringRepo, gameRepo), + inject: [ + LEAGUE_REPOSITORY_TOKEN, + SEASON_REPOSITORY_TOKEN, + LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN, + GAME_REPOSITORY_TOKEN, + ], }, { provide: CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE, diff --git a/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts b/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts index 847cfa5c5..8b2b02afd 100644 --- a/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts +++ b/apps/api/src/domain/league/presenters/LeagueConfigPresenter.ts @@ -11,7 +11,7 @@ export class LeagueConfigPresenter implements UseCaseOutputPort { - const status = (r as any).status; - return status === 'completed' || status === 'past'; - }).length; - + + // League overview wants total races, not just completed. + // (In seed/demo data many races are `status: running`, which should still count.) + const racesCount = races.length; + // Compute real avgSOF from races const racesWithSOF = races.filter(r => { const sof = (r as any).strengthOfField; @@ -48,12 +48,25 @@ export class LeagueDetailViewDataBuilder { const avgSOF = racesWithSOF.length > 0 ? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as any).strengthOfField || 0), 0) / racesWithSOF.length) : null; + + if (process.env.NODE_ENV !== 'production') { + const race0 = races.length > 0 ? races[0] : null; + console.info( + '[LeagueDetailViewDataBuilder] leagueId=%s members=%d races=%d racesWithSOF=%d avgSOF=%s race0=%o', + league.id, + membersCount, + racesCount, + racesWithSOF.length, + String(avgSOF), + race0, + ); + } const info: LeagueInfoData = { name: league.name, description: league.description || '', membersCount, - racesCount: completedRacesCount, + racesCount, avgSOF, structure: `Solo • ${league.settings?.maxDrivers ?? 32} max`, scoring: scoringConfig?.scoringPresetId || 'Standard', diff --git a/apps/website/lib/services/analytics/DashboardService.test.ts b/apps/website/lib/services/analytics/DashboardService.test.ts index 6851ee4da..3448623f3 100644 --- a/apps/website/lib/services/analytics/DashboardService.test.ts +++ b/apps/website/lib/services/analytics/DashboardService.test.ts @@ -8,6 +8,8 @@ describe('DashboardService', () => { let service: DashboardService; beforeEach(() => { + process.env.API_BASE_URL = 'http://localhost:3001'; + mockApiClient = { getDashboardOverview: vi.fn(), getAnalyticsMetrics: vi.fn(), diff --git a/apps/website/lib/services/landing/LandingService.test.ts b/apps/website/lib/services/landing/LandingService.test.ts index e3cd52486..9ebebeecb 100644 --- a/apps/website/lib/services/landing/LandingService.test.ts +++ b/apps/website/lib/services/landing/LandingService.test.ts @@ -12,6 +12,8 @@ describe('LandingService', () => { let service: LandingService; beforeEach(() => { + process.env.API_BASE_URL = 'http://localhost:3001'; + mockRacesApi = { getPageData: vi.fn(), } as unknown as Mocked; diff --git a/apps/website/lib/services/leagues/LeagueService.test.ts b/apps/website/lib/services/leagues/LeagueService.test.ts index e0f1ce908..39f2d0ca7 100644 --- a/apps/website/lib/services/leagues/LeagueService.test.ts +++ b/apps/website/lib/services/leagues/LeagueService.test.ts @@ -1,15 +1,18 @@ import { describe, it, expect, vi, Mocked, beforeEach } from 'vitest'; import { LeagueService } from './LeagueService'; import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient'; +import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO'; import type { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO'; import type { RemoveLeagueMemberOutputDTO } from '@/lib/types/generated/RemoveLeagueMemberOutputDTO'; describe('LeagueService', () => { let mockApiClient: Mocked; + let mockRacesApiClient: Mocked; let service: LeagueService; beforeEach(() => { + process.env.API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:3001'; mockApiClient = { getAllWithCapacity: vi.fn(), getAllWithCapacityAndScoring: vi.fn(), @@ -17,12 +20,20 @@ describe('LeagueService', () => { getTotal: vi.fn(), getSchedule: vi.fn(), getMemberships: vi.fn(), + getLeagueConfig: vi.fn(), create: vi.fn(), removeRosterMember: vi.fn(), updateRosterMemberRole: vi.fn(), } as unknown as Mocked; + mockRacesApiClient = { + getPageData: vi.fn(), + } as unknown as Mocked; + + mockApiClient.getLeagueConfig.mockResolvedValue({ form: null } as any); + service = new LeagueService(mockApiClient); + (service as any).racesApiClient = mockRacesApiClient; }); describe('getAllLeagues', () => { @@ -145,6 +156,43 @@ describe('LeagueService', () => { }); }); + describe('getLeagueDetailData', () => { + it('should use races page-data to enrich races with status/strengthOfField', async () => { + const leagueId = 'league-123'; + const league = { id: leagueId, name: 'League One', createdAt: '2024-01-01T00:00:00Z' } as any; + + mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue({ totalCount: 1, leagues: [league] } as any); + mockApiClient.getMemberships.mockResolvedValue({ members: [] } as any); + mockRacesApiClient.getPageData.mockResolvedValue({ + races: [ + { + id: 'race-1', + track: 'Monza', + car: 'GT3', + scheduledAt: '2026-01-01T00:00:00.000Z', + status: 'running', + leagueId, + leagueName: 'League One', + strengthOfField: 2500, + isUpcoming: false, + isLive: true, + isPast: false, + }, + ], + } as any); + + const result = await service.getLeagueDetailData(leagueId); + + expect(result.isOk()).toBe(true); + const dto = result.unwrap(); + expect(dto.races).toHaveLength(1); + expect((dto.races[0] as any).status).toBe('running'); + expect((dto.races[0] as any).strengthOfField).toBe(2500); + expect(dto.races[0]!.name).toBe('Monza - GT3'); + expect(dto.races[0]!.date).toBe('2026-01-01T00:00:00.000Z'); + }); + }); + describe('getLeagueMemberships', () => { it('should call apiClient.getMemberships and return DTO', async () => { const leagueId = 'league-123'; diff --git a/apps/website/lib/services/leagues/LeagueService.ts b/apps/website/lib/services/leagues/LeagueService.ts index 86553a7e9..d958e9db6 100644 --- a/apps/website/lib/services/leagues/LeagueService.ts +++ b/apps/website/lib/services/leagues/LeagueService.ts @@ -58,6 +58,7 @@ export interface LeagueDetailData { */ @injectable() export class LeagueService implements Service { + private readonly baseUrl: string; private apiClient: LeaguesApiClient; private driversApiClient: DriversApiClient; private sponsorsApiClient: SponsorsApiClient; @@ -65,6 +66,7 @@ export class LeagueService implements Service { constructor(@unmanaged() apiClient?: LeaguesApiClient) { const baseUrl = getWebsiteApiBaseUrl(); + this.baseUrl = baseUrl; const logger = new ConsoleLogger(); const errorReporter = new EnhancedErrorReporter(logger, { showUserNotifications: false, @@ -148,11 +150,27 @@ export class LeagueService implements Service { async getLeagueDetailData(leagueId: string): Promise> { try { - const [apiDto, memberships, racesResponse] = await Promise.all([ + const [apiDto, memberships, racesPageData] = await Promise.all([ this.apiClient.getAllWithCapacityAndScoring(), this.apiClient.getMemberships(leagueId), - this.apiClient.getRaces(leagueId), + this.racesApiClient.getPageData(leagueId), ]); + + if (process.env.NODE_ENV !== 'production') { + const membershipCount = Array.isArray(memberships?.members) ? memberships.members.length : 0; + const racesCount = Array.isArray(racesPageData?.races) ? racesPageData.races.length : 0; + const race0 = racesCount > 0 ? racesPageData.races[0] : null; + + console.info( + '[LeagueService.getLeagueDetailData] baseUrl=%s leagueId=%s memberships=%d races=%d race0=%o', + this.baseUrl, + leagueId, + membershipCount, + racesCount, + race0, + ); + + } if (!apiDto || !apiDto.leagues) { return Result.err({ type: 'notFound', message: 'Leagues not found' }); @@ -189,12 +207,21 @@ export class LeagueService implements Service { console.warn('Failed to fetch league scoring config', e); } + const races: RaceDTO[] = (racesPageData.races || []).map((r) => ({ + id: r.id, + name: `${r.track} - ${r.car}`, + date: r.scheduledAt, + leagueName: r.leagueName, + status: r.status, + strengthOfField: r.strengthOfField, + })) as unknown as RaceDTO[]; + return Result.ok({ league, owner, scoringConfig, memberships, - races: racesResponse.races, + races, sponsors: [], // Sponsors integration can be added here }); } catch (error: unknown) { diff --git a/apps/website/lib/services/sponsors/SponsorService.test.ts b/apps/website/lib/services/sponsors/SponsorService.test.ts index 632056456..a1a8bc013 100644 --- a/apps/website/lib/services/sponsors/SponsorService.test.ts +++ b/apps/website/lib/services/sponsors/SponsorService.test.ts @@ -11,6 +11,8 @@ describe('SponsorService', () => { let mockApiClientInstance: any; beforeEach(() => { + process.env.API_BASE_URL = 'http://localhost:3001'; + vi.clearAllMocks(); service = new SponsorService(); // @ts-ignore - accessing private property for testing diff --git a/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts b/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts index 687910775..7de0341f3 100644 --- a/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts +++ b/core/racing/application/use-cases/GetLeagueFullConfigUseCase.ts @@ -38,7 +38,9 @@ export class GetLeagueFullConfigUseCase { const { leagueId } = input; try { + console.log('Getting league with ID:', leagueId); const league = await this.leagueRepository.findById(leagueId); + console.log('League result:', league); if (!league) { return Result.err({ code: 'LEAGUE_NOT_FOUND', @@ -46,20 +48,40 @@ export class GetLeagueFullConfigUseCase { }); } + console.log('League settings:', JSON.stringify(league.settings, null, 2)); + const seasons = await this.seasonRepository.findByLeagueId(leagueId); + console.log('Seasons found:', seasons?.length); const activeSeason = seasons && seasons.length > 0 ? seasons.find((s) => s.status.isActive()) ?? seasons[0] : undefined; + console.log('Active season:', activeSeason?.id); const scoringConfig = await (async () => { if (!activeSeason) return null; - return (await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id)) ?? null; + console.log('Getting scoring config for season:', activeSeason.id); + try { + const result = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id); + console.log('Scoring config result:', result); + return result ?? null; + } catch (error) { + console.error('Error getting scoring config for season:', activeSeason.id, error); + throw error; + } })(); const game = await (async () => { if (!activeSeason || !activeSeason.gameId) return null; - return (await this.gameRepository.findById(activeSeason.gameId)) ?? null; + console.log('Getting game for game ID:', activeSeason.gameId); + try { + const result = await this.gameRepository.findById(activeSeason.gameId); + console.log('Game result:', result); + return result ?? null; + } catch (error) { + console.error('Error getting game for game ID:', activeSeason.gameId, error); + throw error; + } })(); const config: LeagueFullConfig = { @@ -80,6 +102,9 @@ export class GetLeagueFullConfigUseCase { ? error.message : 'Failed to load league full configuration'; + console.error('GetLeagueFullConfigUseCase error:', error); + console.error('Error stack:', error instanceof Error ? error.stack : 'No stack trace'); + return Result.err({ code: 'REPOSITORY_ERROR', details: { message },