fix issues in core
This commit is contained in:
@@ -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,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>,
|
||||
) {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -170,7 +170,7 @@ export class GetSponsorDashboardUseCase {
|
||||
|
||||
const result: GetSponsorDashboardResult = {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorName: sponsor.name.toString(),
|
||||
metrics: {
|
||||
impressions: totalImpressions,
|
||||
impressionsChange: 0,
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
21
core/racing/domain/types/LeagueScoringPreset.ts
Normal file
21
core/racing/domain/types/LeagueScoringPreset.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
58
core/testing/factories/racing/ChampionshipConfigFactory.ts
Normal file
58
core/testing/factories/racing/ChampionshipConfigFactory.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
9
core/testing/factories/racing/DriverRefFactory.ts
Normal file
9
core/testing/factories/racing/DriverRefFactory.ts
Normal 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',
|
||||
};
|
||||
}
|
||||
5
core/testing/factories/racing/PointsTableFactory.ts
Normal file
5
core/testing/factories/racing/PointsTableFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PointsTable } from '../../../racing/domain/value-objects/PointsTable';
|
||||
|
||||
export function makePointsTable(points: number[]): PointsTable {
|
||||
return new PointsTable(points);
|
||||
}
|
||||
Reference in New Issue
Block a user