fix issues in core

This commit is contained in:
2025-12-23 14:43:49 +01:00
parent 11492d1ff2
commit df5c20c5cc
62 changed files with 480 additions and 334 deletions

View File

@@ -139,6 +139,7 @@ export const DashboardProviders: Provider[] = [
feedRepo: IFeedRepository,
socialRepo: ISocialGraphRepository,
imageService: ImageServicePort,
output: DashboardOverviewPresenter,
) =>
new DashboardOverviewUseCase(
driverRepo,
@@ -152,6 +153,7 @@ export const DashboardProviders: Provider[] = [
socialRepo,
async (driverId: string) => imageService.getDriverAvatar(driverId),
() => null,
output,
),
inject: [
DRIVER_REPOSITORY_TOKEN,
@@ -164,6 +166,7 @@ export const DashboardProviders: Provider[] = [
FEED_REPOSITORY_TOKEN,
SOCIAL_GRAPH_REPOSITORY_TOKEN,
IMAGE_SERVICE_TOKEN,
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
],
},
];

View File

@@ -20,7 +20,13 @@ export class DashboardService {
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
await this.dashboardOverviewUseCase.execute({ driverId });
const result = await this.dashboardOverviewUseCase.execute({ driverId });
if (result.isErr()) {
const error = result.error;
const message = error?.details?.message || 'Unknown error';
throw new Error(`Failed to get dashboard overview: ${message}`);
}
return this.presenter.getResponseModel();
}

View File

@@ -144,7 +144,7 @@ describe('DashboardOverviewPresenter', () => {
it('maps DashboardOverviewResult to DashboardOverviewDTO correctly', () => {
const output = createOutput();
presenter.present(Result.ok(output));
presenter.present(output);
const dto = presenter.getResponseModel();
expect(dto.activeLeaguesCount).toBe(2);

View File

@@ -1,4 +1,4 @@
import { Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type {
DashboardOverviewResult,
} from '@core/racing/application/use-cases/DashboardOverviewUseCase';
@@ -13,12 +13,10 @@ import {
DashboardFriendSummaryDTO,
} from '../dtos/DashboardOverviewDTO';
export class DashboardOverviewPresenter {
export class DashboardOverviewPresenter implements UseCaseOutputPort<DashboardOverviewResult> {
private responseModel: DashboardOverviewDTO | null = null;
present(result: Result<DashboardOverviewResult, unknown>): void {
const data = result.unwrap();
present(data: DashboardOverviewResult): void {
const currentDriver: DashboardDriverSummaryDTO | null = data.currentDriver
? {
id: data.currentDriver.driver.id,

View File

@@ -15,6 +15,7 @@ import { Result as RaceResult } from '@core/racing/domain/entities/result/Result
import type { FeedItem } from '@core/social/domain/types/FeedItem';
import { Result as UseCaseResult } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
describe('DashboardOverviewUseCase', () => {
it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => {
@@ -226,6 +227,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
};
const feedRepository = {
@@ -253,6 +255,14 @@ describe('DashboardOverviewUseCase', () => {
}
: null;
// Mock output port to capture presented data
let _presentedData: DashboardOverviewResult | null = null;
const outputPort: UseCaseOutputPort<DashboardOverviewResult> = {
present: (data: DashboardOverviewResult) => {
_presentedData = data;
},
};
const useCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
@@ -265,21 +275,23 @@ describe('DashboardOverviewUseCase', () => {
socialRepository,
getDriverAvatar,
getDriverStats,
outputPort,
);
const input: DashboardOverviewInput = { driverId };
const result: UseCaseResult<
DashboardOverviewResult,
void,
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
> = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const vm = result.unwrap();
expect(_presentedData).not.toBeNull();
const vm = _presentedData!;
expect(vm.myUpcomingRaces.map(r => r.race.id)).toEqual(['race-1', 'race-3']);
expect(vm.myUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-1', 'race-3']);
expect(vm.otherUpcomingRaces.map(r => r.race.id)).toEqual(['race-2', 'race-4']);
expect(vm.otherUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-2', 'race-4']);
expect(vm.nextRace).not.toBeNull();
expect(vm.nextRace!.race.id).toBe('race-1');
@@ -465,7 +477,7 @@ describe('DashboardOverviewUseCase', () => {
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
return (
memberships.find(
m => m.leagueId === leagueId && m.driverId === driverIdParam,
m => m.leagueId.toString() === leagueId && m.driverId.toString() === driverIdParam,
) ?? null
);
},
@@ -499,6 +511,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
};
const feedRepository = {
@@ -526,6 +539,14 @@ describe('DashboardOverviewUseCase', () => {
}
: null;
// Mock output port to capture presented data
let _presentedData: DashboardOverviewResult | null = null;
const outputPort: UseCaseOutputPort<DashboardOverviewResult> = {
present: (data: DashboardOverviewResult) => {
_presentedData = data;
},
};
const useCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
@@ -538,37 +559,39 @@ describe('DashboardOverviewUseCase', () => {
socialRepository,
getDriverAvatar,
getDriverStats,
outputPort,
);
const input: DashboardOverviewInput = { driverId };
const result: UseCaseResult<
DashboardOverviewResult,
void,
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
> = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const vm = result.unwrap();
expect(_presentedData).not.toBeNull();
const vm = _presentedData!;
expect(vm.recentResults.length).toBe(2);
expect(vm.recentResults[0]!.race.id).toBe('race-new');
expect(vm.recentResults[1]!.race.id).toBe('race-old');
const summariesByLeague = new Map(
vm.leagueStandingsSummaries.map(s => [s.league.id, s]),
vm.leagueStandingsSummaries.map((s: any) => [s.league.id.toString(), s]),
);
const summaryA = summariesByLeague.get('league-A');
const summaryB = summariesByLeague.get('league-B');
expect(summaryA).toBeDefined();
expect(summaryA!.standing?.position).toBe(3);
expect(summaryA!.standing?.points).toBe(50);
expect(summaryA!.standing?.position.toNumber()).toBe(3);
expect(summaryA!.standing?.points.toNumber()).toBe(50);
expect(summaryA!.totalDrivers).toBe(2);
expect(summaryB).toBeDefined();
expect(summaryB!.standing?.position).toBe(1);
expect(summaryB!.standing?.points).toBe(100);
expect(summaryB!.standing?.position.toNumber()).toBe(1);
expect(summaryB!.standing?.points.toNumber()).toBe(100);
expect(summaryB!.totalDrivers).toBe(2);
});
@@ -708,6 +731,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
};
const feedRepository = {
@@ -725,6 +749,14 @@ describe('DashboardOverviewUseCase', () => {
const getDriverStats = () => null;
// Mock output port to capture presented data
let _presentedData: DashboardOverviewResult | null = null;
const outputPort: UseCaseOutputPort<DashboardOverviewResult> = {
present: (data: DashboardOverviewResult) => {
_presentedData = data;
},
};
const useCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
@@ -737,17 +769,19 @@ describe('DashboardOverviewUseCase', () => {
socialRepository,
getDriverAvatar,
getDriverStats,
outputPort,
);
const input: DashboardOverviewInput = { driverId };
const result: UseCaseResult<
DashboardOverviewResult,
void,
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
> = await useCase.execute(input);
expect(result.isOk()).toBe(true);
const vm = result.unwrap();
expect(_presentedData).not.toBeNull();
const vm = _presentedData!;
expect(vm.myUpcomingRaces).toEqual([]);
expect(vm.otherUpcomingRaces).toEqual([]);
@@ -893,6 +927,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
};
const feedRepository = {
@@ -910,6 +945,13 @@ describe('DashboardOverviewUseCase', () => {
const getDriverStats = () => null;
// Mock output port to capture presented data
const outputPort: UseCaseOutputPort<DashboardOverviewResult> = {
present: (_data: DashboardOverviewResult) => {
// No-op
},
};
const useCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
@@ -922,12 +964,13 @@ describe('DashboardOverviewUseCase', () => {
socialRepository,
getDriverAvatar,
getDriverStats,
outputPort,
);
const input: DashboardOverviewInput = { driverId };
const result: UseCaseResult<
DashboardOverviewResult,
void,
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
> = await useCase.execute(input);
@@ -1075,6 +1118,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
};
const feedRepository = {
@@ -1092,6 +1136,13 @@ describe('DashboardOverviewUseCase', () => {
const getDriverStats = () => null;
// Mock output port to capture presented data
const outputPort: UseCaseOutputPort<DashboardOverviewResult> = {
present: (_data: DashboardOverviewResult) => {
// No-op
},
};
const useCase = new DashboardOverviewUseCase(
driverRepository,
raceRepository,
@@ -1104,12 +1155,13 @@ describe('DashboardOverviewUseCase', () => {
socialRepository,
getDriverAvatar,
getDriverStats,
outputPort,
);
const input: DashboardOverviewInput = { driverId };
const result: UseCaseResult<
DashboardOverviewResult,
void,
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
> = await useCase.execute(input);

View File

@@ -1,5 +1,6 @@
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
@@ -14,6 +15,7 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { Result as RaceResult } from '../../domain/entities/result/Result';
export interface DashboardOverviewInput {
driverId: string;
@@ -48,7 +50,7 @@ export interface DashboardRaceSummary {
export interface DashboardRecentRaceResultSummary {
race: Race;
league: League | null;
result: Result;
result: RaceResult;
}
export interface DashboardLeagueStandingSummary {
@@ -95,13 +97,14 @@ export class DashboardOverviewUseCase {
private readonly getDriverStats: (
driverId: string,
) => DashboardDriverStatsAdapter | null,
private readonly output: UseCaseOutputPort<DashboardOverviewResult>,
) {}
async execute(
input: DashboardOverviewInput,
): Promise<
Result<
DashboardOverviewResult,
void,
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
>
> {
@@ -206,7 +209,9 @@ export class DashboardOverviewUseCase {
friends: friendsSummary,
};
return Result.ok(result);
this.output.present(result);
return Result.ok(undefined);
} catch (error) {
return Result.err({
code: 'REPOSITORY_ERROR',

View File

@@ -146,7 +146,7 @@ describe('FileProtestUseCase', () => {
);
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as unknown as Mock).mock.calls[0][0] as FileProtestResult;
const presented = (output.present as unknown as Mock).mock.calls[0]?.[0] as FileProtestResult;
expect(presented.protest.raceId).toBe('race1');
expect(presented.protest.protestingDriverId).toBe('driver1');
expect(presented.protest.accusedDriverId).toBe('driver2');

View File

@@ -1,16 +1,15 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import {
GetAllLeaguesWithCapacityAndScoringUseCase,
type GetAllLeaguesWithCapacityAndScoringInput,
type GetAllLeaguesWithCapacityAndScoringResult,
type LeagueCapacityAndScoringSummary,
} from './GetAllLeaguesWithCapacityAndScoringUseCase';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
describe('GetAllLeaguesWithCapacityAndScoringUseCase', () => {
let mockLeagueRepo: { findAll: Mock };
@@ -63,18 +62,18 @@ describe('GetAllLeaguesWithCapacityAndScoringUseCase', () => {
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0][0] as GetAllLeaguesWithCapacityAndScoringResult;
output.present.mock.calls[0]?.[0] as GetAllLeaguesWithCapacityAndScoringResult;
expect(presented.leagues).toHaveLength(1);
expect(presented?.leagues).toHaveLength(1);
const [summary] = presented.leagues as LeagueCapacityAndScoringSummary[];
const [summary] = presented?.leagues ?? [];
expect(summary.league).toEqual(league);
expect(summary.currentDrivers).toBe(2);
expect(summary.maxDrivers).toBe(30);
expect(summary.season).toEqual(season);
expect(summary.scoringConfig).toEqual(scoringConfig);
expect(summary.game).toEqual(game);
expect(summary.preset).toEqual({ id: 'preset1', name: 'Default' });
expect(summary?.league).toEqual(league);
expect(summary?.currentDrivers).toBe(2);
expect(summary?.maxDrivers).toBe(30);
expect(summary?.season).toEqual(season);
expect(summary?.scoringConfig).toEqual(scoringConfig);
expect(summary?.game).toEqual(game);
expect(summary?.preset).toEqual({ id: 'preset1', name: 'Default' });
});
});

View File

@@ -7,7 +7,7 @@ import type { League } from '../../domain/entities/League';
import type { Season } from '../../domain/entities/season/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Game } from '../../domain/entities/Game';
import type { LeagueScoringPreset } from '../../../bootstrap/LeagueScoringPresets';
import type { LeagueScoringPreset } from '../../domain/types/LeagueScoringPreset';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
@@ -62,18 +62,18 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase {
const enrichedLeagues: LeagueCapacityAndScoringSummary[] = [];
for (const league of leagues) {
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id.toString());
const currentDrivers = members.filter(
(m) =>
m.status === 'active' &&
(m.role === 'owner' ||
m.role === 'admin' ||
m.role === 'steward' ||
m.role === 'member'),
m.status.toString() === 'active' &&
(m.role.toString() === 'owner' ||
m.role.toString() === 'admin' ||
m.role.toString() === 'steward' ||
m.role.toString() === 'member'),
).length;
const seasons = await this.seasonRepository.findByLeagueId(league.id);
const seasons = await this.seasonRepository.findByLeagueId(league.id.toString());
const activeSeason =
seasons && seasons.length > 0
? seasons.find((s) => s.status === 'active') ?? seasons[0]
@@ -85,14 +85,14 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase {
if (activeSeason) {
const scoringConfigResult =
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id.toString());
scoringConfig = scoringConfigResult ?? undefined;
if (scoringConfig) {
const gameResult = await this.gameRepository.findById(activeSeason.gameId);
const gameResult = await this.gameRepository.findById(activeSeason.gameId.toString());
game = gameResult ?? undefined;
const presetId = scoringConfig.scoringPresetId;
if (presetId) {
preset = this.presetProvider.getPresetById(presetId);
preset = this.presetProvider.getPresetById(presetId.toString());
}
}
}

View File

@@ -1,13 +1,12 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import {
GetAllLeaguesWithCapacityUseCase,
type GetAllLeaguesWithCapacityInput,
type GetAllLeaguesWithCapacityResult,
type LeagueCapacitySummary,
} from './GetAllLeaguesWithCapacityUseCase';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
describe('GetAllLeaguesWithCapacityUseCase', () => {
let mockLeagueRepo: { findAll: Mock };
@@ -24,7 +23,6 @@ describe('GetAllLeaguesWithCapacityUseCase', () => {
const useCase = new GetAllLeaguesWithCapacityUseCase(
mockLeagueRepo as unknown as ILeagueRepository,
mockMembershipRepo as unknown as ILeagueMembershipRepository,
output,
);
const league1 = { id: 'league1', name: 'Test League 1', settings: { maxDrivers: 10 } };
@@ -48,27 +46,26 @@ describe('GetAllLeaguesWithCapacityUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const resultValue = result.unwrap();
expect(resultValue).toBeDefined();
const presented = output.present.mock.calls[0][0] as GetAllLeaguesWithCapacityResult;
expect(presented.leagues).toHaveLength(2);
expect(resultValue?.leagues).toHaveLength(2);
const [first, second] = presented.leagues as LeagueCapacitySummary[];
const [first, second] = resultValue?.leagues ?? [];
expect(first.league).toEqual(league1);
expect(first.currentDrivers).toBe(2);
expect(first.maxDrivers).toBe(10);
expect(first?.league).toEqual(league1);
expect(first?.currentDrivers).toBe(2);
expect(first?.maxDrivers).toBe(10);
expect(second.league).toEqual(league2);
expect(second.currentDrivers).toBe(1);
expect(second.maxDrivers).toBe(20);
expect(second?.league).toEqual(league2);
expect(second?.currentDrivers).toBe(1);
expect(second?.maxDrivers).toBe(20);
});
it('should return empty result when no leagues', async () => {
const useCase = new GetAllLeaguesWithCapacityUseCase(
mockLeagueRepo as unknown as ILeagueRepository,
mockMembershipRepo as unknown as ILeagueMembershipRepository,
output,
);
mockLeagueRepo.findAll.mockResolvedValue([]);
@@ -78,8 +75,8 @@ describe('GetAllLeaguesWithCapacityUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllLeaguesWithCapacityResult;
expect(presented.leagues).toEqual([]);
const resultValue = result.unwrap();
expect(resultValue).toBeDefined();
expect(resultValue?.leagues).toEqual([]);
});
});

View File

@@ -3,7 +3,6 @@ import type { ILeagueMembershipRepository } from '../../domain/repositories/ILea
import type { League } from '../../domain/entities/League';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
export type GetAllLeaguesWithCapacityInput = {};
@@ -27,7 +26,6 @@ export class GetAllLeaguesWithCapacityUseCase {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly outputPort: UseCaseOutputPort<GetAllLeaguesWithCapacityResult, GetAllLeaguesWithCapacityErrorCode>,
) {}
async execute(

View File

@@ -93,7 +93,7 @@ describe('GetAllRacesPageDataUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllRacesPageDataResult;
const presented = output.present.mock.calls[0]?.[0] as GetAllRacesPageDataResult;
expect(presented.races).toEqual([
{
@@ -150,7 +150,7 @@ describe('GetAllRacesPageDataUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllRacesPageDataResult;
const presented = output.present.mock.calls[0]?.[0] as GetAllRacesPageDataResult;
expect(presented.races).toEqual([]);
expect(presented.filters).toEqual({

View File

@@ -89,7 +89,7 @@ describe('GetAllRacesUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllRacesResult;
const presented = output.present.mock.calls[0]?.[0] as GetAllRacesResult;
expect(presented.totalCount).toBe(2);
expect(presented.races).toEqual([race1, race2]);
expect(presented.leagues).toEqual([league1, league2]);
@@ -112,7 +112,7 @@ describe('GetAllRacesUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllRacesResult;
const presented = output.present.mock.calls[0]?.[0] as GetAllRacesResult;
expect(presented.totalCount).toBe(0);
expect(presented.races).toEqual([]);
expect(presented.leagues).toEqual([]);
@@ -123,7 +123,6 @@ describe('GetAllRacesUseCase', () => {
mockRaceRepo,
mockLeagueRepo,
mockLogger,
output,
);
const error = new Error('Repository error');

View File

@@ -82,7 +82,7 @@ describe('GetAllTeamsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllTeamsResult;
const presented = output.present.mock.calls[0]?.[0] as GetAllTeamsResult;
expect(presented).toEqual({
teams: [
@@ -127,7 +127,7 @@ describe('GetAllTeamsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetAllTeamsResult;
const presented = output.present.mock.calls[0]?.[0] as GetAllTeamsResult;
expect(presented).toEqual({
teams: [],

View File

@@ -3,19 +3,19 @@ import {
GetDriversLeaderboardUseCase,
type GetDriversLeaderboardResult,
type GetDriversLeaderboardInput,
type GetDriversLeaderboardErrorCode,
} from './GetDriversLeaderboardUseCase';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IRankingService } from '../../domain/services/IRankingService';
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('GetDriversLeaderboardUseCase', () => {
const mockDriverFindAll = vi.fn();
const mockDriverRepo: IDriverRepository = {
findById: vi.fn(),
findByIRacingId: vi.fn(),
existsByIRacingId: vi.fn(),
findAll: mockDriverFindAll,
create: vi.fn(),
update: vi.fn(),
@@ -57,7 +57,6 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverStatsService,
mockGetDriverAvatar,
mockLogger,
output,
);
const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } };
@@ -99,7 +98,7 @@ describe('GetDriversLeaderboardUseCase', () => {
rating: 2500,
skillLevel: 'advanced',
racesCompleted: 10,
wins: 5,
wins:5,
podiums: 7,
isActive: true,
rank: 1,
@@ -130,7 +129,6 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverStatsService,
mockGetDriverAvatar,
mockLogger,
output,
);
mockDriverFindAll.mockResolvedValue([]);
@@ -161,7 +159,6 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverStatsService,
mockGetDriverAvatar,
mockLogger,
output,
);
const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } };
@@ -209,7 +206,6 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverStatsService,
mockGetDriverAvatar,
mockLogger,
output,
);
const error = new Error('Repository error');

View File

@@ -5,16 +5,12 @@ import {
type GetEntitySponsorshipPricingResult,
} from './GetEntitySponsorshipPricingUseCase';
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('GetEntitySponsorshipPricingUseCase', () => {
let mockSponsorshipPricingRepo: ISponsorshipPricingRepository;
let mockSponsorshipRequestRepo: ISponsorshipRequestRepository;
let mockSeasonSponsorshipRepo: ISeasonSponsorshipRepository;
let mockLogger: Logger;
let mockFindByEntity: Mock;
let mockFindPendingByEntity: Mock;
@@ -37,37 +33,6 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
exists: vi.fn(),
findAcceptingApplications: vi.fn(),
} as ISponsorshipPricingRepository;
mockSponsorshipRequestRepo = {
findPendingByEntity: mockFindPendingByEntity,
findByEntity: vi.fn(),
findById: vi.fn(),
save: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
findBySponsorId: vi.fn(),
findByStatus: vi.fn(),
findBySponsorIdAndStatus: vi.fn(),
hasPendingRequest: vi.fn(),
findPendingBySponsor: vi.fn(),
findApprovedByEntity: vi.fn(),
findRejectedByEntity: vi.fn(),
countPendingByEntity: vi.fn(),
create: vi.fn(),
exists: vi.fn(),
} as ISponsorshipRequestRepository;
mockSeasonSponsorshipRepo = {
findBySeasonId: mockFindBySeasonId,
findById: vi.fn(),
save: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
findAll: vi.fn(),
findByLeagueId: vi.fn(),
findBySponsorId: vi.fn(),
findBySeasonAndTier: vi.fn(),
create: vi.fn(),
exists: vi.fn(),
} as ISeasonSponsorshipRepository;
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
@@ -80,8 +45,6 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
it('should return PRICING_NOT_CONFIGURED when no pricing found', async () => {
const useCase = new GetEntitySponsorshipPricingUseCase(
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
mockSeasonSponsorshipRepo as unknown as ISeasonSponsorshipRepository,
mockLogger,
output,
);
@@ -108,8 +71,6 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
it('should return pricing data when found', async () => {
const useCase = new GetEntitySponsorshipPricingUseCase(
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
mockSeasonSponsorshipRepo as unknown as ISeasonSponsorshipRepository,
mockLogger,
output,
);
@@ -118,6 +79,7 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
entityType: 'season',
entityId: 'season1',
};
const pricing = {
acceptingApplications: true,
customRequirements: 'Some requirements',
@@ -145,7 +107,7 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as Mock).mock.calls[0][0] as GetEntitySponsorshipPricingResult;
const presented = (output.present as Mock).mock.calls[0]?.[0] as GetEntitySponsorshipPricingResult;
expect(presented.entityType).toBe('season');
expect(presented.entityId).toBe('season1');
@@ -170,8 +132,6 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
it('should return error when repository throws', async () => {
const useCase = new GetEntitySponsorshipPricingUseCase(
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
mockSeasonSponsorshipRepo as unknown as ISeasonSponsorshipRepository,
mockLogger,
output,
);
@@ -180,6 +140,7 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
entityType: 'season',
entityId: 'season1',
};
const error = new Error('Repository error');
mockFindByEntity.mockRejectedValue(error);
@@ -195,4 +156,4 @@ describe('GetEntitySponsorshipPricingUseCase', () => {
expect(err.details.message).toBe('Repository error');
expect(output.present).not.toHaveBeenCalled();
});
});
});

View File

@@ -6,9 +6,6 @@
*/
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { SponsorshipPricing, SponsorshipSlotConfig } from '../../domain/value-objects/SponsorshipPricing';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { Result } from '@core/shared/application/Result';
@@ -47,8 +44,6 @@ export type GetEntitySponsorshipPricingErrorCode =
export class GetEntitySponsorshipPricingUseCase {
constructor(
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetEntitySponsorshipPricingResult>,
) {}

View File

@@ -133,7 +133,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueAdminPermissionsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueAdminPermissionsResult;
expect(presented.league).toBe(league);
expect(presented.permissions).toEqual({
canManageSchedule: true,
@@ -155,7 +155,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueAdminPermissionsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueAdminPermissionsResult;
expect(presented.league).toBe(league);
expect(presented.permissions).toEqual({
canManageSchedule: true,

View File

@@ -52,7 +52,7 @@ export class GetLeagueAdminPermissionsUseCase {
}
const membership = await this.leagueMembershipRepository.getMembership(leagueId, performerDriverId);
if (!membership || membership.status !== 'active' || (membership.role !== 'owner' && membership.role !== 'admin')) {
if (!membership || membership.status.toString() !== 'active' || (membership.role.toString() !== 'owner' && membership.role.toString() !== 'admin')) {
this.logger.warn('User is not a member or not authorized for league admin permissions', {
leagueId,
performerDriverId,

View File

@@ -64,7 +64,7 @@ describe('GetLeagueAdminUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueAdminResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueAdminResult;
expect(presented.league.id).toBe('league1');
expect(presented.league.ownerId).toBe('owner1');
});

View File

@@ -55,10 +55,29 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
recalculate: vi.fn(),
};
resultRepository = {
findById: vi.fn(),
findAll: vi.fn(),
findByRaceId: vi.fn(),
findByDriverId: vi.fn(),
findByDriverIdAndLeagueId: mockResultFindByDriverIdAndLeagueId,
create: vi.fn(),
createMany: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
deleteByRaceId: vi.fn(),
exists: vi.fn(),
existsByRaceId: vi.fn(),
};
penaltyRepository = {
findById: vi.fn(),
findByDriverId: vi.fn(),
findByProtestId: vi.fn(),
findPending: vi.fn(),
findByRaceId: mockPenaltyFindByRaceId,
findIssuedBy: vi.fn(),
create: vi.fn(),
update: vi.fn(),
exists: vi.fn(),
};
raceRepository = {
findById: vi.fn(),
@@ -75,12 +94,27 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
};
driverRepository = {
findById: mockDriverFindById,
findByIRacingId: vi.fn(),
findAll: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
exists: vi.fn(),
existsByIRacingId: vi.fn(),
};
teamRepository = {
findById: mockTeamFindById,
findAll: vi.fn(),
findByLeagueId: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
exists: vi.fn(),
};
driverRatingPort = {
getRating: mockDriverRatingGetRating,
getDriverRating: mockDriverRatingGetRating,
calculateRatingChange: vi.fn(),
updateDriverRating: vi.fn(),
};
output = {
@@ -138,7 +172,7 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueDriverSeasonStatsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueDriverSeasonStatsResult;
expect(presented.leagueId).toBe('league-1');
expect(presented.stats).toHaveLength(2);
expect(presented.stats[0]).toEqual({
@@ -188,8 +222,8 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueDriverSeasonStatsResult;
expect(presented.stats[0].penaltyPoints).toBe(0);
const presented = output.present.mock.calls[0]?.[0] as GetLeagueDriverSeasonStatsResult;
expect(presented?.stats[0]?.penaltyPoints).toBe(0);
});
it('should return LEAGUE_NOT_FOUND when no standings are found', async () => {

View File

@@ -72,7 +72,7 @@ describe('GetLeagueJoinRequestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueJoinRequestsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueJoinRequestsResult;
expect(presented.joinRequests).toHaveLength(1);
expect(presented.joinRequests[0]).toMatchObject({
@@ -81,7 +81,7 @@ describe('GetLeagueJoinRequestsUseCase', () => {
driverId: 'driver-1',
message: 'msg',
});
expect(presented.joinRequests[0].driver).toBe(driver);
expect(presented?.joinRequests[0]?.driver).toBe(driver);
});
it('should return LEAGUE_NOT_FOUND error when league does not exist', async () => {

View File

@@ -1,10 +1,10 @@
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { Driver } from '../../domain/entities/Driver';
import type { UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Driver } from '../../domain/entities/Driver';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
export interface GetLeagueJoinRequestsInput {
leagueId: string;
@@ -47,7 +47,7 @@ export class GetLeagueJoinRequestsUseCase {
}
const joinRequests = await this.leagueMembershipRepository.getJoinRequests(input.leagueId);
const driverIds = [...new Set(joinRequests.map(request => request.driverId))];
const driverIds = [...new Set(joinRequests.map(request => request.driverId.toString()))];
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
const driverMap = new Map(
@@ -55,10 +55,14 @@ export class GetLeagueJoinRequestsUseCase {
);
const enrichedJoinRequests: LeagueJoinRequestWithDriver[] = joinRequests
.filter(request => driverMap.has(request.driverId))
.filter(request => driverMap.has(request.driverId.toString()))
.map(request => ({
...request,
driver: driverMap.get(request.driverId)!,
id: request.id,
leagueId: request.leagueId.toString(),
driverId: request.driverId.toString(),
requestedAt: request.requestedAt.toDate(),
...(request.message !== undefined && { message: request.message }),
driver: driverMap.get(request.driverId.toString())!,
}));
const result: GetLeagueJoinRequestsResult = {
@@ -71,7 +75,7 @@ export class GetLeagueJoinRequestsUseCase {
} catch (error: unknown) {
const message =
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
? (error as any).message
? (error as Error).message
: 'Failed to load league join requests';
return Result.err({

View File

@@ -99,14 +99,14 @@ describe('GetLeagueMembershipsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueMembershipsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueMembershipsResult;
expect(presented.league).toEqual(league);
expect(presented.memberships).toHaveLength(2);
expect(presented.memberships[0].membership).toEqual(memberships[0]);
expect(presented.memberships[0].driver).toEqual(driver1);
expect(presented.memberships[1].membership).toEqual(memberships[1]);
expect(presented.memberships[1].driver).toEqual(driver2);
expect(presented?.league).toEqual(league);
expect(presented?.memberships).toHaveLength(2);
expect(presented?.memberships[0]?.membership).toEqual(memberships[0]);
expect(presented?.memberships[0]?.driver).toEqual(driver1);
expect(presented?.memberships[1]?.membership).toEqual(memberships[1]);
expect(presented?.memberships[1]?.driver).toEqual(driver2);
});
it('should handle drivers not found', async () => {
@@ -137,12 +137,12 @@ describe('GetLeagueMembershipsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueMembershipsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueMembershipsResult;
expect(presented.league).toEqual(league);
expect(presented.memberships).toHaveLength(1);
expect(presented.memberships[0].membership).toEqual(memberships[0]);
expect(presented.memberships[0].driver).toBeNull();
expect(presented?.league).toEqual(league);
expect(presented?.memberships).toHaveLength(1);
expect(presented?.memberships[0]?.membership).toEqual(memberships[0]);
expect(presented?.memberships[0]?.driver).toBeNull();
});
it('should return error when league not found', async () => {

View File

@@ -66,12 +66,12 @@ describe('GetLeagueOwnerSummaryUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueOwnerSummaryResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueOwnerSummaryResult;
expect(presented.league).toBe(league);
expect(presented.owner).toBe(driver);
expect(presented.rating).toBe(0);
expect(presented.rank).toBe(0);
expect(presented?.league).toBe(league);
expect(presented?.owner).toBe(driver);
expect(presented?.rating).toBe(0);
expect(presented?.rank).toBe(0);
});
it('should return error when league does not exist', async () => {

View File

@@ -111,15 +111,15 @@ describe('GetLeagueProtestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueProtestsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueProtestsResult;
expect(presented.league).toEqual(league);
expect(presented.protests).toHaveLength(1);
const presentedProtest = presented.protests[0];
expect(presentedProtest.protest).toEqual(protest);
expect(presentedProtest.race).toEqual(race);
expect(presentedProtest.protestingDriver).toEqual(driver1);
expect(presentedProtest.accusedDriver).toEqual(driver2);
expect(presented?.league).toEqual(league);
expect(presented?.protests).toHaveLength(1);
const presentedProtest = presented?.protests[0];
expect(presentedProtest?.protest).toEqual(protest);
expect(presentedProtest?.race).toEqual(race);
expect(presentedProtest?.protestingDriver).toEqual(driver1);
expect(presentedProtest?.accusedDriver).toEqual(driver2);
});
it('should return empty protests when no races', async () => {
@@ -141,10 +141,10 @@ describe('GetLeagueProtestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueProtestsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueProtestsResult;
expect(presented.league).toEqual(league);
expect(presented.protests).toEqual([]);
expect(presented?.league).toEqual(league);
expect(presented?.protests).toEqual([]);
});
it('should return LEAGUE_NOT_FOUND when league does not exist', async () => {

View File

@@ -70,11 +70,11 @@ describe('GetLeagueScheduleUseCase', () => {
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0]![0] as GetLeagueScheduleResult;
output.present.mock.calls[0]?.[0] as GetLeagueScheduleResult;
expect(presented.league).toBe(league);
expect(presented.races).toHaveLength(1);
expect(presented.races[0].race).toBe(race);
expect(presented.races[0]?.race).toBe(race);
});
it('should present empty schedule when no races exist', async () => {
@@ -92,7 +92,7 @@ describe('GetLeagueScheduleUseCase', () => {
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0]![0] as GetLeagueScheduleResult;
output.present.mock.calls[0]?.[0] as GetLeagueScheduleResult;
expect(presented.league).toBe(league);
expect(presented.races).toHaveLength(0);
@@ -119,7 +119,7 @@ describe('GetLeagueScheduleUseCase', () => {
it('should return REPOSITORY_ERROR when repository throws', async () => {
const leagueId = 'league-1';
const league = { id: leagueId } as League;
const league = { id: leagueId } as unknown as League;
const repositoryError = new Error('DB down');
leagueRepository.findById.mockResolvedValue(league);

View File

@@ -60,12 +60,12 @@ describe('GetLeagueScoringConfigUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0][0] as GetLeagueScoringConfigResult;
expect(presented.league).toEqual(league);
expect(presented.season).toEqual(season);
expect(presented.scoringConfig).toEqual(scoringConfig);
expect(presented.game).toEqual(game);
expect(presented.preset).toEqual(preset);
output.present.mock.calls[0]?.[0] as GetLeagueScoringConfigResult;
expect(presented?.league).toEqual(league);
expect(presented?.season).toEqual(season);
expect(presented?.scoringConfig).toEqual(scoringConfig);
expect(presented?.game).toEqual(game);
expect(presented?.preset).toEqual(preset);
});
it('should return scoring config for first season if no active', async () => {
@@ -86,12 +86,12 @@ describe('GetLeagueScoringConfigUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0][0] as GetLeagueScoringConfigResult;
expect(presented.league).toEqual(league);
expect(presented.season).toEqual(season);
expect(presented.scoringConfig).toEqual(scoringConfig);
expect(presented.game).toEqual(game);
expect(presented.preset).toBeUndefined();
output.present.mock.calls[0]?.[0] as GetLeagueScoringConfigResult;
expect(presented?.league).toEqual(league);
expect(presented?.season).toEqual(season);
expect(presented?.scoringConfig).toEqual(scoringConfig);
expect(presented?.game).toEqual(game);
expect(presented?.preset).toBeUndefined();
});
it('should return error if league not found', async () => {

View File

@@ -1,15 +1,15 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { LeagueScoringPreset } from '../../../bootstrap/LeagueScoringPresets';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { League } from '../../domain/entities/League';
import type { Season } from '../../domain/entities/season/Season';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Game } from '../../domain/entities/Game';
import type { League } from '../../domain/entities/League';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { Season } from '../../domain/entities/season/Season';
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { LeagueScoringPreset } from '../../domain/types/LeagueScoringPreset';
export type GetLeagueScoringConfigInput = {
leagueId: string;
@@ -101,7 +101,7 @@ export class GetLeagueScoringConfigUseCase {
const presetId = scoringConfig.scoringPresetId;
const preset = presetId
? this.presetProvider.getPresetById(presetId)
? this.presetProvider.getPresetById(presetId.toString())
: undefined;
const result: GetLeagueScoringConfigResult = {

View File

@@ -9,7 +9,7 @@ import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import { Season } from '../../domain/entities/Season';
import { Season } from '../../domain/entities/season';
import { League } from '../../domain/entities/League';
describe('GetLeagueSeasonsUseCase', () => {
@@ -83,18 +83,18 @@ describe('GetLeagueSeasonsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueSeasonsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueSeasonsResult;
expect(presented.league).toBe(league);
expect(presented.seasons).toHaveLength(2);
expect(presented?.league).toBe(league);
expect(presented?.seasons).toHaveLength(2);
expect(presented.seasons[0]!.season).toBe(seasons[0]);
expect(presented.seasons[0]!.isPrimary).toBe(false);
expect(presented.seasons[0]!.isParallelActive).toBe(false);
expect(presented?.seasons[0]?.season).toBe(seasons[0]);
expect(presented?.seasons[0]?.isPrimary).toBe(false);
expect(presented?.seasons[0]?.isParallelActive).toBe(false);
expect(presented.seasons[1]!.season).toBe(seasons[1]);
expect(presented.seasons[1]!.isPrimary).toBe(false);
expect(presented.seasons[1]!.isParallelActive).toBe(false);
expect(presented?.seasons[1]?.season).toBe(seasons[1]);
expect(presented?.seasons[1]?.isPrimary).toBe(false);
expect(presented?.seasons[1]?.isParallelActive).toBe(false);
});
it('should set isParallelActive true for active seasons when multiple active', async () => {
@@ -132,11 +132,11 @@ describe('GetLeagueSeasonsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetLeagueSeasonsResult;
const presented = output.present.mock.calls[0]?.[0] as GetLeagueSeasonsResult;
expect(presented.seasons).toHaveLength(2);
expect(presented.seasons[0]!.isParallelActive).toBe(true);
expect(presented.seasons[1]!.isParallelActive).toBe(true);
expect(presented?.seasons).toHaveLength(2);
expect(presented?.seasons[0]?.isParallelActive).toBe(true);
expect(presented?.seasons[1]?.isParallelActive).toBe(true);
});
it('should return LEAGUE_NOT_FOUND error when league does not exist', async () => {

View File

@@ -74,6 +74,7 @@ describe('GetLeagueWalletUseCase', () => {
amount: Money.create(1200, 'USD'),
description: 'Main Sponsor - TechCorp',
metadata: {},
completedAt: undefined,
}).complete();
const membershipTx = Transaction.create({
@@ -83,6 +84,7 @@ describe('GetLeagueWalletUseCase', () => {
amount: Money.create(1600, 'USD'),
description: 'Season Fee - 32 drivers',
metadata: {},
completedAt: undefined,
}).complete();
const withdrawalTx = Transaction.create({
@@ -92,6 +94,7 @@ describe('GetLeagueWalletUseCase', () => {
amount: Money.create(430, 'USD'),
description: 'Bank Transfer - Season 1 Payout',
metadata: {},
completedAt: undefined,
}).complete();
const pendingPrizeTx = Transaction.create({
@@ -101,6 +104,7 @@ describe('GetLeagueWalletUseCase', () => {
amount: Money.create(150, 'USD'),
description: 'Championship Prize Pool (reserved)',
metadata: {},
completedAt: undefined,
});
const refundTx = Transaction.create({
@@ -110,6 +114,7 @@ describe('GetLeagueWalletUseCase', () => {
amount: Money.create(100, 'USD'),
description: 'Refund for cancelled sponsorship',
metadata: {},
completedAt: undefined,
});
const transactions = [

View File

@@ -8,7 +8,7 @@ import {
import { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
import { Sponsor } from '../../domain/entities/Sponsor';
import { Sponsor } from '../../domain/entities/sponsor/Sponsor';
import { Money } from '../../domain/value-objects/Money';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -72,16 +72,18 @@ describe('GetPendingSponsorshipRequestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as Mock).mock.calls[0][0] as GetPendingSponsorshipRequestsResult;
const presented = (output.present as Mock).mock.calls[0]?.[0] as GetPendingSponsorshipRequestsResult;
expect(presented).toBeDefined();
expect(presented.entityType).toBe('season');
expect(presented.entityId).toBe('entity-1');
expect(presented.totalCount).toBe(1);
expect(presented.requests).toHaveLength(1);
const summary = presented.requests[0];
expect(summary.sponsor?.name).toBe('Test Sponsor');
expect(summary.financials.offeredAmount.amount).toBe(10000);
expect(summary.financials.offeredAmount.currency).toBe('USD');
expect(summary).toBeDefined();
expect(summary!.sponsor?.name).toBe('Test Sponsor');
expect(summary!.financials.offeredAmount.amount).toBe(10000);
expect(summary!.financials.offeredAmount.currency).toBe('USD');
});
it('should return error when repository fails', async () => {

View File

@@ -11,7 +11,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
import { Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
import type { Sponsor } from '../../domain/entities/Sponsor';
import type { Sponsor } from '../../domain/entities/sponsor/Sponsor';
import { Money } from '../../domain/value-objects/Money';
export interface GetPendingSponsorshipRequestsInput {

View File

@@ -1,16 +1,15 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result as DomainResult, Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { League } from '../../domain/entities/League';
import type { Race } from '../../domain/entities/Race';
import type { RaceRegistration } from '../../domain/entities/RaceRegistration';
import type { Result as DomainResult } from '../../domain/entities/Result';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
export type GetRaceDetailInput = {
raceId: string;
@@ -33,7 +32,7 @@ export type GetRaceDetailResult = {
};
export class GetRaceDetailUseCase {
private output: UseCaseOutputPort<GetRaceDetailResult> | null = null;
private output: UseCaseOutputPort<GetRaceDetailResult> | null = null; // TODO wtf this must be injected via constructor
constructor(
private readonly raceRepository: IRaceRepository,
@@ -44,7 +43,7 @@ export class GetRaceDetailUseCase {
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
) {}
setOutput(output: UseCaseOutputPort<GetRaceDetailResult>) {
setOutput(output: UseCaseOutputPort<GetRaceDetailResult>) { // TODO must be removed
this.output = output;
}

View File

@@ -1,10 +1,10 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Race } from '../../domain/entities/Race';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
export type GetRacesPageDataInput = {
leagueId: string;

View File

@@ -45,7 +45,7 @@ export class GetSeasonDetailsUseCase {
}
const season = await this.seasonRepository.findById(input.seasonId);
if (!season || season.leagueId !== league.id) {
if (!season || season.leagueId.toString() !== league.id.toString()) {
return Result.err({
code: 'SEASON_NOT_FOUND',
details: {

View File

@@ -11,7 +11,7 @@ import { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import { Sponsor } from '../../domain/entities/Sponsor';
import { Sponsor } from '../../domain/entities/sponsor/Sponsor';
import { SeasonSponsorship } from '../../domain/entities/season/SeasonSponsorship';
import { Season } from '../../domain/entities/season/Season';
import { League } from '../../domain/entities/League';
@@ -121,6 +121,7 @@ describe('GetSponsorDashboardUseCase', () => {
expect(output.present).toHaveBeenCalledTimes(1);
const dashboard = (output.present as Mock).mock.calls[0][0] as GetSponsorDashboardResult;
expect(dashboard).toBeDefined();
expect(dashboard.sponsorId).toBe(sponsorId);
expect(dashboard.metrics.impressions).toBe(100); // 1 completed race * 1 driver * 100
expect(dashboard.investment.totalInvestment.amount).toBe(10000);

View File

@@ -170,7 +170,7 @@ export class GetSponsorDashboardUseCase {
const result: GetSponsorDashboardResult = {
sponsorId,
sponsorName: sponsor.name,
sponsorName: sponsor.name.toString(),
metrics: {
impressions: totalImpressions,
impressionsChange: 0,

View File

@@ -3,7 +3,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import { Result as RaceResult } from '../../domain/entities/Result';
import { Result as RaceResult } from '../../domain/entities/result/Result';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -154,15 +154,15 @@ export class ImportRaceResultsUseCase {
this.logger.info('ImportRaceResultsUseCase:race results created', { raceId });
await this.standingRepository.recalculate(league.id);
await this.standingRepository.recalculate(league.id.toString());
this.logger.info('ImportRaceResultsUseCase:standings recalculated', {
leagueId: league.id,
leagueId: league.id.toString(),
});
const result: ImportRaceResultsResult = {
raceId,
leagueId: league.id,
leagueId: league.id.toString(),
driversProcessed: rows.length,
resultsRecorded: validEntities.length,
errors: [],

View File

@@ -1,19 +1,11 @@
import type { LeagueScoringPreset as BootstrapLeagueScoringPreset } from '../../../bootstrap/LeagueScoringPresets';
import type { LeagueScoringPreset } from '../../domain/types/LeagueScoringPreset';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application';
export type ListLeagueScoringPresetsInput = {};
export type LeagueScoringPreset = {
id: string;
name: string;
description: string;
primaryChampionshipType: string;
sessionSummary: string;
bonusSummary: string;
dropPolicySummary: string;
};
export type { LeagueScoringPreset } from '../../domain/types/LeagueScoringPreset';
export interface ListLeagueScoringPresetsResult {
presets: LeagueScoringPreset[];
@@ -27,7 +19,7 @@ export type ListLeagueScoringPresetsErrorCode = 'REPOSITORY_ERROR';
*/
export class ListLeagueScoringPresetsUseCase {
constructor(
private readonly presets: BootstrapLeagueScoringPreset[],
private readonly presets: LeagueScoringPreset[],
private readonly output: UseCaseOutputPort<ListLeagueScoringPresetsResult>,
) {}

View File

@@ -63,7 +63,7 @@ export class RegisterForRaceUseCase {
}
const membership = await this.membershipRepository.getMembership(leagueId, driverId);
if (!membership || membership.status !== 'active') {
if (!membership || membership.status.toString() !== 'active') {
this.logger.error(`RegisterForRaceUseCase: driver ${driverId} not an active member of league ${leagueId}`);
return Result.err<void, ApplicationErrorCode<RegisterForRaceErrorCode, { message: string }>>({
code: 'NOT_ACTIVE_MEMBER',

View File

@@ -58,10 +58,10 @@ export class ReviewProtestUseCase {
// Validate steward has authority (owner or admin of the league)
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
const stewardMembership = memberships.find(
m => m.driverId === input.stewardId && m.status === 'active'
m => m.driverId.toString() === input.stewardId && m.status.toString() === 'active'
);
if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) {
if (!stewardMembership || (stewardMembership.role.toString() !== 'owner' && stewardMembership.role.toString() !== 'admin')) {
this.logger.warn('Unauthorized steward attempting to review protest', { stewardId: input.stewardId, leagueId: race.leagueId });
return Result.err({ code: 'NOT_LEAGUE_ADMIN', details: { message: 'Only league owners and admins can review protests' } });
}
@@ -90,7 +90,7 @@ export class ReviewProtestUseCase {
return Result.ok(undefined);
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to review protest';
this.logger.error('Failed to review protest', { error: message });
this.logger.error('Failed to review protest', new Error(message));
return Result.err({ code: 'REPOSITORY_ERROR', details: { message } });
}
}

View File

@@ -4,7 +4,7 @@ import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { NotificationService } from '../../../notifications/application/ports/NotificationService';
import type { NotificationType } from '../../../notifications/domain/types/NotificationTypes';
import type { RaceEvent } from '../../domain/entities/RaceEvent';
import type { Result as RaceResult } from '../../domain/entities/Result';
import type { Result as RaceResult } from '../../domain/entities/result/Result';
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
@@ -61,7 +61,7 @@ export class SendFinalResultsUseCase {
return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race event not found' } });
}
const membership = await this.membershipRepository.getMembership(league.id, input.triggeredById);
const membership = await this.membershipRepository.getMembership(league.id.toString(), input.triggeredById);
if (!membership || !isLeagueStewardOrHigherRole(membership.role)) {
return Result.err({
code: 'INSUFFICIENT_PERMISSIONS',
@@ -90,17 +90,17 @@ export class SendFinalResultsUseCase {
for (const driverResult of results) {
await this.sendFinalResultsNotification(
driverResult.driverId,
driverResult.driverId.toString(),
raceEvent,
driverResult,
league.id,
league.id.toString(),
false,
);
notificationsSent += 1;
}
const result: SendFinalResultsResult = {
leagueId: league.id,
leagueId: league.id.toString(),
raceId: raceEvent.id,
notificationsSent,
};
@@ -127,8 +127,8 @@ export class SendFinalResultsUseCase {
const incidents = driverResult?.incidents ?? 0;
const finalRatingChange = this.calculateFinalRatingChange(
driverResult?.position,
driverResult?.incidents,
driverResult?.position?.toNumber(),
driverResult?.incidents?.toNumber(),
hadPenaltiesApplied,
);

View File

@@ -4,7 +4,9 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
import type { NotificationService } from '../../../notifications/application/ports/NotificationService';
import type { NotificationType } from '../../../notifications/domain/types/NotificationTypes';
import type { RaceEvent } from '../../domain/entities/RaceEvent';
import type { Result as RaceResult } from '../../domain/entities/Result';
import type { Result as RaceResult } from '../../domain/entities/result/Result';
import { Position } from '../../domain/entities/result/Position';
import { IncidentCount } from '../../domain/entities/result/IncidentCount';
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
@@ -72,7 +74,7 @@ export class SendPerformanceSummaryUseCase {
}
if (input.triggeredById !== input.driverId) {
const membership = await this.membershipRepository.getMembership(league.id, input.triggeredById);
const membership = await this.membershipRepository.getMembership(league.id.toString(), input.triggeredById);
if (!membership || !isLeagueStewardOrHigherRole(membership.role)) {
return Result.err({
code: 'INSUFFICIENT_PERMISSIONS',
@@ -90,7 +92,7 @@ export class SendPerformanceSummaryUseCase {
}
const results = await this.resultRepository.findByRaceId(mainRaceSession.id);
const driverResult = results.find(r => r.driverId === input.driverId);
const driverResult = results.find(r => r.driverId.toString() === input.driverId);
if (!driverResult) {
return Result.err({
@@ -101,11 +103,11 @@ export class SendPerformanceSummaryUseCase {
let notificationsSent = 0;
await this.sendPerformanceSummaryNotification(input.driverId, raceEvent, driverResult, league.id);
await this.sendPerformanceSummaryNotification(input.driverId, raceEvent, driverResult, league.id.toString());
notificationsSent += 1;
const result: SendPerformanceSummaryResult = {
leagueId: league.id,
leagueId: league.id.toString(),
raceId: raceEvent.id,
driverId: input.driverId,
notificationsSent,
@@ -131,10 +133,12 @@ export class SendPerformanceSummaryUseCase {
const positionChange = driverResult?.getPositionChange() ?? 0;
const incidents = driverResult?.incidents ?? 0;
const provisionalRatingChange = this.calculateProvisionalRatingChange(driverResult?.position, driverResult?.incidents);
const provisionalRatingChange = this.calculateProvisionalRatingChange(driverResult?.position?.toNumber(), driverResult?.incidents?.toNumber());
const title = `Race Complete: ${raceEvent.name}`;
const body = this.buildPerformanceSummaryBody(position, positionChange, incidents, provisionalRatingChange);
const positionValue = position instanceof Position ? position.toNumber() : position;
const incidentValue = incidents instanceof IncidentCount ? incidents.toNumber() : incidents;
const body = this.buildPerformanceSummaryBody(positionValue, positionChange, incidentValue, provisionalRatingChange);
await this.notificationService.sendNotification({
recipientId: driverId,
@@ -147,9 +151,9 @@ export class SendPerformanceSummaryUseCase {
raceEventId: raceEvent.id,
sessionId: raceEvent.getMainRaceSession()?.id ?? '',
leagueId,
position,
position: positionValue,
positionChange,
incidents,
incidents: incidentValue,
provisionalRatingChange,
},
actions: [

View File

@@ -1,4 +1,4 @@
import { Result } from '../../domain/entities/Result';
import { Result } from '../../domain/entities/result/Result';
/**
* Enhanced race result generator with detailed incident types

View File

@@ -2,13 +2,13 @@
* Enhanced Result entity with detailed incident tracking
*/
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import type { IEntity } from '@core/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import { RaceIncidents, type IncidentRecord } from '../value-objects/RaceIncidents';
import { RaceId } from './RaceId';
import { DriverId } from './DriverId';
import { Position } from './result/Position';
import { RaceId } from './RaceId';
import { LapTime } from './result/LapTime';
import { Position } from './result/Position';
export class ResultWithIncidents implements IEntity<string> {
readonly id: string;
@@ -66,6 +66,7 @@ export class ResultWithIncidents implements IEntity<string> {
});
}
// TODO WE DONT NEED ANY LEGACY CODE WE ARE NOT EVEN LIVE
/**
* Create from legacy Result data (with incidents as number)
*/

View File

@@ -8,9 +8,9 @@ describe('SponsorshipRequest', () => {
const validProps = {
id: 'request-123',
sponsorId: 'sponsor-456',
entityType: 'driver',
entityType: 'driver' as SponsorableEntityType,
entityId: 'driver-789',
tier: 'main',
tier: 'main' as SponsorshipTier,
offeredAmount: validMoney,
};

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { LeagueWallet } from './LeagueWallet';
import { Money } from '../value-objects/Money';
import { Money } from '../../value-objects/Money';
describe('LeagueWallet', () => {
it('should create a league wallet', () => {

View File

@@ -11,7 +11,7 @@ import type { Money } from '../../value-objects/Money';
import { Position } from '../championship/Position';
import { PrizeId } from './PrizeId';
import { PrizeStatus } from './PrizeStatus';
import { SeasonId } from '../SeasonId';
import { SeasonId } from '../season/SeasonId';
import { DriverId } from '../DriverId';
export interface PrizeProps {

View File

@@ -78,9 +78,9 @@ export class Sponsor implements IEntity<SponsorId> {
id: this.id,
name,
contactEmail,
logoUrl,
websiteUrl,
createdAt: this.createdAt,
...(logoUrl !== undefined ? { logoUrl } : {}),
...(websiteUrl !== undefined ? { websiteUrl } : {}),
});
}
}

View File

@@ -4,13 +4,14 @@
* Defines operations for Prize entity persistence
*/
import type { Prize, PrizeStatus } from '../entities/Prize';
import type { Prize } from '../entities/prize/Prize';
import type { PrizeStatusValue } from '../entities/prize/PrizeStatus';
export interface IPrizeRepository {
findById(id: string): Promise<Prize | null>;
findBySeasonId(seasonId: string): Promise<Prize[]>;
findByDriverId(driverId: string): Promise<Prize[]>;
findByStatus(status: PrizeStatus): Promise<Prize[]>;
findByStatus(status: PrizeStatusValue): Promise<Prize[]>;
findBySeasonAndPosition(seasonId: string, position: number): Promise<Prize | null>;
create(prize: Prize): Promise<Prize>;
update(prize: Prize): Promise<Prize>;

View File

@@ -4,7 +4,7 @@
* Defines operations for Sponsor aggregate persistence
*/
import type { Sponsor } from '../entities/Sponsor';
import type { Sponsor } from '../entities/sponsor/Sponsor';
export interface ISponsorRepository {
findById(id: string): Promise<Sponsor | null>;

View File

@@ -6,12 +6,12 @@ import type { SessionType } from '@core/racing/domain/types/SessionType';
import { PointsTable } from '@core/racing/domain/value-objects/PointsTable';
import type { BonusRule } from '@core/racing/domain/types/BonusRule';
import type { ChampionshipConfig } from '@core/racing/domain/types/ChampionshipConfig';
import { Result } from '@core/racing/domain/entities/Result';
import { Result } from '@core/racing/domain/entities/result/Result';
import type { Penalty } from '@core/racing/domain/entities/Penalty';
import type { ChampionshipType } from '@core/racing/domain/types/ChampionshipType';
import { makeDriverRef } from '../../testing/factories/racing/DriverRefFactory';
import { makePointsTable } from '../../testing/factories/racing/PointsTableFactory';
import { makeChampionshipConfig } from '../../testing/factories/racing/ChampionshipConfigFactory';
import { makeDriverRef } from '../../../testing/factories/racing/DriverRefFactory';
import { makePointsTable } from '../../../testing/factories/racing/PointsTableFactory';
import { makeChampionshipConfig } from '../../../testing/factories/racing/ChampionshipConfigFactory';
describe('EventScoringService', () => {

View File

@@ -6,6 +6,7 @@ import { RaceTimeOfDay } from '../value-objects/RaceTimeOfDay';
import type { Weekday } from '../types/Weekday';
import { weekdayToIndex } from '../types/Weekday';
import type { IDomainCalculationService } from '@core/shared/domain';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
function cloneDate(date: Date): Date {
return new Date(date.getTime());
@@ -63,15 +64,15 @@ function generateWeeklyOrEveryNWeeksSlots(
const result: ScheduledRaceSlot[] = [];
const recurrence = schedule.recurrence;
const weekdays =
recurrence.kind === 'weekly' || recurrence.kind === 'everyNWeeks'
? recurrence.weekdays.getAll()
recurrence.props.kind === 'weekly' || recurrence.props.kind === 'everyNWeeks'
? recurrence.props.weekdays.getAll()
: [];
if (weekdays.length === 0) {
throw new RacingDomainValidationError('RecurrenceStrategy has no weekdays');
}
const intervalWeeks = recurrence.kind === 'everyNWeeks' ? recurrence.intervalWeeks : 1;
const intervalWeeks = recurrence.props.kind === 'everyNWeeks' ? recurrence.props.intervalWeeks : 1;
let anchorWeekStart = cloneDate(schedule.startDate);
let roundNumber = 1;
@@ -123,11 +124,11 @@ function findNthWeekdayOfMonth(base: Date, ordinal: 1 | 2 | 3 | 4, weekday: Week
function generateMonthlySlots(schedule: SeasonSchedule, maxRounds: number): ScheduledRaceSlot[] {
const result: ScheduledRaceSlot[] = [];
const recurrence = schedule.recurrence;
if (recurrence.kind !== 'monthlyNthWeekday') {
if (recurrence.props.kind !== 'monthlyNthWeekday') {
return result;
}
const { ordinal, weekday } = recurrence.monthlyPattern;
const { ordinal, weekday } = recurrence.props.monthlyPattern;
let currentMonthDate = new Date(
schedule.startDate.getFullYear(),
schedule.startDate.getMonth(),
@@ -168,7 +169,7 @@ export class SeasonScheduleGenerator {
const recurrence: RecurrenceStrategy = schedule.recurrence;
if (recurrence.kind === 'monthlyNthWeekday') {
if (recurrence.props.kind === 'monthlyNthWeekday') {
return generateMonthlySlots(schedule, maxRounds);
}

View File

@@ -4,12 +4,12 @@
* Utility functions for working with league membership roles.
*/
import type { MembershipRole } from '../entities/LeagueMembership';
import type { MembershipRoleValue } from '../entities/MembershipRole';
/**
* Role hierarchy (higher number = more authority)
*/
const ROLE_HIERARCHY: Record<MembershipRole, number> = {
const ROLE_HIERARCHY: Record<MembershipRoleValue, number> = {
member: 0,
steward: 1,
admin: 2,
@@ -19,21 +19,21 @@ const ROLE_HIERARCHY: Record<MembershipRole, number> = {
/**
* Check if a role is at least steward level
*/
export function isLeagueStewardOrHigherRole(role: MembershipRole): boolean {
export function isLeagueStewardOrHigherRole(role: MembershipRoleValue): boolean {
return ROLE_HIERARCHY[role] >= ROLE_HIERARCHY.steward;
}
/**
* Check if a role is at least admin level
*/
export function isLeagueAdminOrHigherRole(role: MembershipRole): boolean {
export function isLeagueAdminOrHigherRole(role: MembershipRoleValue): boolean {
return ROLE_HIERARCHY[role] >= ROLE_HIERARCHY.admin;
}
/**
* Check if a role is owner
*/
export function isLeagueOwnerRole(role: MembershipRole): boolean {
export function isLeagueOwnerRole(role: MembershipRoleValue): boolean {
return role === 'owner';
}
@@ -41,33 +41,33 @@ export function isLeagueOwnerRole(role: MembershipRole): boolean {
* Compare two roles
* Returns positive if role1 > role2, negative if role1 < role2, 0 if equal
*/
export function compareRoles(role1: MembershipRole, role2: MembershipRole): number {
export function compareRoles(role1: MembershipRoleValue, role2: MembershipRoleValue): number {
return ROLE_HIERARCHY[role1] - ROLE_HIERARCHY[role2];
}
/**
* Get role display name
*/
export function getRoleDisplayName(role: MembershipRole): string {
const names: Record<MembershipRole, string> = {
export function getRoleDisplayName(role: MembershipRoleValue): string {
const names: Record<MembershipRoleValue, string> = {
member: 'Member',
steward: 'Steward',
admin: 'Admin',
owner: 'Owner',
};
return names[role];
return names[role] || role;
}
/**
* Get all roles in order of hierarchy
*/
export function getAllRolesOrdered(): MembershipRole[] {
export function getAllRolesOrdered(): MembershipRoleValue[] {
return ['member', 'steward', 'admin', 'owner'];
}
/**
* Get roles that can be assigned (excludes owner as it's transferred, not assigned)
*/
export function getAssignableRoles(): MembershipRole[] {
export function getAssignableRoles(): MembershipRoleValue[] {
return ['member', 'steward', 'admin'];
}

View File

@@ -0,0 +1,21 @@
/**
* Domain Types: LeagueScoringPreset
*
* Local type definition for league scoring presets to avoid cross-module dependencies
*/
export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver'
| 'team'
| 'nations'
| 'trophy';
export interface LeagueScoringPreset {
id: string;
name: string;
description: string;
primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType;
dropPolicySummary: string;
sessionSummary: string;
bonusSummary: string;
}

View File

@@ -104,10 +104,10 @@ export class RaceIncidents implements IValueObject<IncidentRecord[]> {
return sortedThis.every((incident, index) => {
const otherIncident = sortedOther[index];
return incident.type === otherIncident.type &&
incident.lap === otherIncident.lap &&
incident.description === otherIncident.description &&
incident.penaltyPoints === otherIncident.penaltyPoints;
return incident.type === otherIncident?.type &&
incident.lap === otherIncident?.lap &&
incident.description === otherIncident?.description &&
incident.penaltyPoints === otherIncident?.penaltyPoints;
});
}

View File

@@ -1,4 +1,4 @@
import type { Logger , UseCaseOutputPort } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
@@ -59,9 +59,11 @@ export class GetCurrentUserSocialUseCase {
);
}
// TODO looks like this must still be implemented?
const friends: FriendDTO[] = friendsDomain.map(friend => ({
driverId: friend.id,
displayName: friend.name,
displayName: friend.name.toString(),
avatarUrl: '',
isOnline: false,
lastSeen: new Date(),

View File

@@ -1,8 +1,8 @@
import type { Driver } from '@core/racing/domain/entities/Driver';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
import type { Logger } from '@core/shared/application';
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { Logger } from '@core/shared/application';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
export type Friendship = {
driverId: string;
@@ -18,7 +18,6 @@ export type RacingSeedData = {
export class InMemoryFeedRepository implements IFeedRepository {
private readonly feedEvents: FeedItem[];
private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>;
private readonly logger: Logger;
constructor(logger: Logger, seed: RacingSeedData) {
@@ -26,7 +25,6 @@ export class InMemoryFeedRepository implements IFeedRepository {
this.logger.info('InMemoryFeedRepository initialized.');
this.feedEvents = seed.feedEvents;
this.friendships = seed.friendships;
this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
}
async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> {
@@ -52,7 +50,7 @@ export class InMemoryFeedRepository implements IFeedRepository {
this.logger.info(`Found ${sorted.length} feed items for driver: ${driverId}.`);
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
} catch (error) {
this.logger.error(`Error getting feed for driver ${driverId}:`, error);
this.logger.error(`Error getting feed for driver ${driverId}:`, error as Error);
throw error;
}
}
@@ -67,7 +65,7 @@ export class InMemoryFeedRepository implements IFeedRepository {
this.logger.info(`Found ${sorted.length} global feed items.`);
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
} catch (error) {
this.logger.error(`Error getting global feed:`, error);
this.logger.error(`Error getting global feed:`, error as Error);
throw error;
}
}
@@ -94,7 +92,7 @@ export class InMemorySocialGraphRepository implements ISocialGraphRepository {
this.logger.info(`Found ${friendIds.length} friend IDs for driver: ${driverId}.`);
return friendIds;
} catch (error) {
this.logger.error(`Error getting friend IDs for driver ${driverId}:`, error);
this.logger.error(`Error getting friend IDs for driver ${driverId}:`, error as Error);
throw error;
}
}
@@ -109,7 +107,7 @@ export class InMemorySocialGraphRepository implements ISocialGraphRepository {
this.logger.info(`Found ${friends.length} friends for driver: ${driverId}.`);
return friends;
} catch (error) {
this.logger.error(`Error getting friends for driver ${driverId}:`, error);
this.logger.error(`Error getting friends for driver ${driverId}:`, error as Error);
throw error;
}
}
@@ -141,7 +139,7 @@ export class InMemorySocialGraphRepository implements ISocialGraphRepository {
this.logger.info(`Found ${result.length} suggested friends for driver: ${driverId}.`);
return result;
} catch (error) {
this.logger.error(`Error getting suggested friends for driver ${driverId}:`, error);
this.logger.error(`Error getting suggested friends for driver ${driverId}:`, error as Error);
throw error;
}
}

View File

@@ -0,0 +1,58 @@
import type { ChampionshipConfig } from '../../../racing/domain/types/ChampionshipConfig';
import type { ChampionshipType } from '../../../racing/domain/types/ChampionshipType';
import type { SessionType } from '../../../racing/domain/types/SessionType';
import type { BonusRule } from '../../../racing/domain/types/BonusRule';
import type { DropScorePolicy, DropScoreStrategy } from '../../../racing/domain/types/DropScorePolicy';
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
interface ChampionshipConfigInput {
id: string;
name: string;
sessionTypes: SessionType[];
mainPoints?: number[];
mainBonusRules?: BonusRule[];
type?: ChampionshipType;
dropScorePolicy?: DropScorePolicy;
strategy?: DropScoreStrategy;
}
export function makeChampionshipConfig(input: ChampionshipConfigInput): ChampionshipConfig {
const {
id,
name,
sessionTypes,
mainPoints = [25, 18, 15, 12, 10, 8, 6, 4, 2, 1],
mainBonusRules = [],
type = 'driver',
dropScorePolicy = { strategy: 'none' },
} = input;
const pointsTableBySessionType: Record<SessionType, PointsTable> = {} as Record<SessionType, PointsTable>;
// Convert array format to PointsTable for each session type
sessionTypes.forEach(sessionType => {
const pointsArray = mainPoints;
const pointsMap: Record<number, number> = {};
pointsArray.forEach((points, index) => {
pointsMap[index + 1] = points;
});
pointsTableBySessionType[sessionType] = new PointsTable(pointsMap);
});
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {} as Record<SessionType, BonusRule[]>;
// Add bonus rules for each session type
sessionTypes.forEach(sessionType => {
bonusRulesBySessionType[sessionType] = mainBonusRules;
});
return {
id,
name,
type,
sessionTypes,
pointsTableBySessionType,
bonusRulesBySessionType,
dropScorePolicy,
};
}

View File

@@ -0,0 +1,9 @@
import type { DriverId } from '../../../racing/domain/entities/DriverId';
import type { ParticipantRef } from '../../../racing/domain/types/ParticipantRef';
export function makeDriverRef(driverId: string): ParticipantRef {
return {
id: driverId,
type: 'driver',
};
}

View File

@@ -0,0 +1,5 @@
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
export function makePointsTable(points: number[]): PointsTable {
return new PointsTable(points);
}