refactor core presenters
This commit is contained in:
@@ -49,10 +49,10 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
expect(driverRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'user-1',
|
||||
iracingId: 'user-1',
|
||||
name: 'John Doe',
|
||||
country: 'US',
|
||||
bio: 'Test bio',
|
||||
iracingId: expect.objectContaining({ value: 'user-1' }),
|
||||
name: expect.objectContaining({ value: 'John Doe' }),
|
||||
country: expect.objectContaining({ value: 'US' }),
|
||||
bio: expect.objectContaining({ value: 'Test bio' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -123,9 +123,9 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
expect(driverRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'user-1',
|
||||
iracingId: 'user-1',
|
||||
name: 'John Doe',
|
||||
country: 'US',
|
||||
iracingId: expect.objectContaining({ value: 'user-1' }),
|
||||
name: expect.objectContaining({ value: 'John Doe' }),
|
||||
country: expect.objectContaining({ value: 'US' }),
|
||||
bio: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,16 +4,17 @@ import { Driver } from '../../domain/entities/Driver';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { CompleteDriverOnboardingCommand } from '../dto/CompleteDriverOnboardingCommand';
|
||||
import type { CompleteDriverOnboardingOutputPort } from '../ports/output/CompleteDriverOnboardingOutputPort';
|
||||
|
||||
/**
|
||||
* Use Case for completing driver onboarding.
|
||||
*/
|
||||
export class CompleteDriverOnboardingUseCase
|
||||
implements AsyncUseCase<CompleteDriverOnboardingCommand, { driverId: string }, string>
|
||||
implements AsyncUseCase<CompleteDriverOnboardingCommand, CompleteDriverOnboardingOutputPort, string>
|
||||
{
|
||||
constructor(private readonly driverRepository: IDriverRepository) {}
|
||||
|
||||
async execute(command: CompleteDriverOnboardingCommand): Promise<Result<{ driverId: string }, ApplicationErrorCode<string>>> {
|
||||
async execute(command: CompleteDriverOnboardingCommand): Promise<Result<CompleteDriverOnboardingOutputPort, ApplicationErrorCode<string>>> {
|
||||
try {
|
||||
// Check if driver already exists
|
||||
const existing = await this.driverRepository.findById(command.userId);
|
||||
|
||||
@@ -17,15 +17,15 @@ import { Driver } from '../../domain/entities/Driver';
|
||||
import { Standing } from '../../domain/entities/Standing';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type {
|
||||
DashboardOverviewViewModel,
|
||||
DashboardDriverSummaryViewModel,
|
||||
DashboardRaceSummaryViewModel,
|
||||
DashboardRecentResultViewModel,
|
||||
DashboardLeagueStandingSummaryViewModel,
|
||||
DashboardFeedItemSummaryViewModel,
|
||||
DashboardFeedSummaryViewModel,
|
||||
DashboardFriendSummaryViewModel,
|
||||
} from '../presenters/IDashboardOverviewPresenter';
|
||||
DashboardOverviewOutputPort,
|
||||
DashboardDriverSummaryOutputPort,
|
||||
DashboardRaceSummaryOutputPort,
|
||||
DashboardRecentResultOutputPort,
|
||||
DashboardLeagueStandingSummaryOutputPort,
|
||||
DashboardFeedItemSummaryOutputPort,
|
||||
DashboardFeedSummaryOutputPort,
|
||||
DashboardFriendSummaryOutputPort,
|
||||
} from '../ports/output/DashboardOverviewOutputPort';
|
||||
|
||||
interface DashboardOverviewParams {
|
||||
driverId: string;
|
||||
@@ -55,7 +55,7 @@ export class DashboardOverviewUseCase {
|
||||
private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null,
|
||||
) {}
|
||||
|
||||
async execute(params: DashboardOverviewParams): Promise<Result<DashboardOverviewViewModel>> {
|
||||
async execute(params: DashboardOverviewParams): Promise<Result<DashboardOverviewOutputPort>> {
|
||||
const { driverId } = params;
|
||||
|
||||
const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([
|
||||
@@ -71,7 +71,7 @@ export class DashboardOverviewUseCase {
|
||||
|
||||
const driverStats = this.getDriverStats(driverId);
|
||||
|
||||
const currentDriver: DashboardDriverSummaryViewModel | null = driver
|
||||
const currentDriver: DashboardDriverSummaryOutputPort | null = driver
|
||||
? {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
@@ -101,10 +101,10 @@ export class DashboardOverviewUseCase {
|
||||
const { myUpcomingRaces, otherUpcomingRaces } =
|
||||
await this.partitionUpcomingRacesByRegistration(upcomingRacesInDriverLeagues, driverId, leagueMap);
|
||||
|
||||
const nextRace: DashboardRaceSummaryViewModel | null =
|
||||
const nextRace: DashboardRaceSummaryOutputPort | null =
|
||||
myUpcomingRaces.length > 0 ? myUpcomingRaces[0]! : null;
|
||||
|
||||
const upcomingRacesSummaries: DashboardRaceSummaryViewModel[] = [
|
||||
const upcomingRacesSummaries: DashboardRaceSummaryOutputPort[] = [
|
||||
...myUpcomingRaces,
|
||||
...otherUpcomingRaces,
|
||||
].slice().sort(
|
||||
@@ -128,7 +128,7 @@ export class DashboardOverviewUseCase {
|
||||
|
||||
const friendsSummary = await this.buildFriendsSummary(friends);
|
||||
|
||||
const viewModel: DashboardOverviewViewModel = {
|
||||
const viewModel: DashboardOverviewOutputPort = {
|
||||
currentDriver,
|
||||
myUpcomingRaces,
|
||||
otherUpcomingRaces,
|
||||
@@ -162,11 +162,11 @@ export class DashboardOverviewUseCase {
|
||||
driverId: string,
|
||||
leagueMap: Map<string, string>,
|
||||
): Promise<{
|
||||
myUpcomingRaces: DashboardRaceSummaryViewModel[];
|
||||
otherUpcomingRaces: DashboardRaceSummaryViewModel[];
|
||||
myUpcomingRaces: DashboardRaceSummaryOutputPort[];
|
||||
otherUpcomingRaces: DashboardRaceSummaryOutputPort[];
|
||||
}> {
|
||||
const myUpcomingRaces: DashboardRaceSummaryViewModel[] = [];
|
||||
const otherUpcomingRaces: DashboardRaceSummaryViewModel[] = [];
|
||||
const myUpcomingRaces: DashboardRaceSummaryOutputPort[] = [];
|
||||
const otherUpcomingRaces: DashboardRaceSummaryOutputPort[] = [];
|
||||
|
||||
for (const race of upcomingRaces) {
|
||||
const isRegistered = await this.raceRegistrationRepository.isRegistered(race.id, driverId);
|
||||
@@ -186,7 +186,7 @@ export class DashboardOverviewUseCase {
|
||||
race: Race,
|
||||
leagueMap: Map<string, string>,
|
||||
isMyLeague: boolean,
|
||||
): DashboardRaceSummaryViewModel {
|
||||
): DashboardRaceSummaryOutputPort {
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
@@ -204,7 +204,7 @@ export class DashboardOverviewUseCase {
|
||||
allRaces: Race[],
|
||||
allLeagues: League[],
|
||||
driverId: string,
|
||||
): DashboardRecentResultViewModel[] {
|
||||
): DashboardRecentResultOutputPort[] {
|
||||
const raceById = new Map(allRaces.map(race => [race.id, race]));
|
||||
const leagueById = new Map(allLeagues.map(league => [league.id, league]));
|
||||
|
||||
@@ -219,7 +219,7 @@ export class DashboardOverviewUseCase {
|
||||
|
||||
const finishedAt = race.scheduledAt.toISOString();
|
||||
|
||||
const item: DashboardRecentResultViewModel = {
|
||||
const item: DashboardRecentResultOutputPort = {
|
||||
raceId: race.id,
|
||||
raceName: race.track,
|
||||
leagueId: race.leagueId,
|
||||
@@ -231,7 +231,7 @@ export class DashboardOverviewUseCase {
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is DashboardRecentResultViewModel => !!item)
|
||||
.filter((item): item is DashboardRecentResultOutputPort => !!item)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.finishedAt).getTime() - new Date(a.finishedAt).getTime(),
|
||||
@@ -245,8 +245,8 @@ export class DashboardOverviewUseCase {
|
||||
private async buildLeagueStandingsSummaries(
|
||||
driverLeagues: League[],
|
||||
driverId: string,
|
||||
): Promise<DashboardLeagueStandingSummaryViewModel[]> {
|
||||
const summaries: DashboardLeagueStandingSummaryViewModel[] = [];
|
||||
): Promise<DashboardLeagueStandingSummaryOutputPort[]> {
|
||||
const summaries: DashboardLeagueStandingSummaryOutputPort[] = [];
|
||||
|
||||
for (const league of driverLeagues.slice(0, 3)) {
|
||||
const standings = await this.standingRepository.findByLeagueId(league.id);
|
||||
@@ -267,8 +267,8 @@ export class DashboardOverviewUseCase {
|
||||
}
|
||||
|
||||
private computeActiveLeaguesCount(
|
||||
upcomingRaces: DashboardRaceSummaryViewModel[],
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummaryViewModel[],
|
||||
upcomingRaces: DashboardRaceSummaryOutputPort[],
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummaryOutputPort[],
|
||||
): number {
|
||||
const activeLeagueIds = new Set<string>();
|
||||
|
||||
@@ -283,8 +283,8 @@ export class DashboardOverviewUseCase {
|
||||
return activeLeagueIds.size;
|
||||
}
|
||||
|
||||
private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummaryViewModel {
|
||||
const items: DashboardFeedItemSummaryViewModel[] = feedItems.map(item => ({
|
||||
private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummaryOutputPort {
|
||||
const items: DashboardFeedItemSummaryOutputPort[] = feedItems.map(item => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
headline: item.headline,
|
||||
@@ -303,8 +303,8 @@ export class DashboardOverviewUseCase {
|
||||
};
|
||||
}
|
||||
|
||||
private async buildFriendsSummary(friends: Driver[]): Promise<DashboardFriendSummaryViewModel[]> {
|
||||
const friendSummaries: DashboardFriendSummaryViewModel[] = [];
|
||||
private async buildFriendsSummary(friends: Driver[]): Promise<DashboardFriendSummaryOutputPort[]> {
|
||||
const friendSummaries: DashboardFriendSummaryOutputPort[] = [];
|
||||
|
||||
for (const friend of friends) {
|
||||
const avatarResult = await this.getDriverAvatar({ driverId: friend.id });
|
||||
|
||||
@@ -54,15 +54,17 @@ describe('GetAllLeaguesWithCapacityAndScoringUseCase', () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual([
|
||||
{
|
||||
league,
|
||||
usedDriverSlots: 2,
|
||||
season,
|
||||
scoringConfig,
|
||||
game,
|
||||
preset,
|
||||
},
|
||||
]);
|
||||
expect(result.value).toEqual({
|
||||
leagues: [
|
||||
{
|
||||
league,
|
||||
usedDriverSlots: 2,
|
||||
season,
|
||||
scoringConfig,
|
||||
game,
|
||||
preset,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import type { LeagueEnrichedData, AllLeaguesWithCapacityAndScoringOutputPort } from '../ports/output/AllLeaguesWithCapacityAndScoringOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<LeagueEnrichedData[]>> {
|
||||
async execute(): Promise<Result<AllLeaguesWithCapacityAndScoringOutputPort>> {
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
|
||||
const enrichedLeagues: LeagueEnrichedData[] = [];
|
||||
@@ -72,7 +72,7 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
...(preset ? { preset } : {}),
|
||||
});
|
||||
}
|
||||
return Result.ok(enrichedLeagues);
|
||||
return Result.ok({ leagues: enrichedLeagues });
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { AllLeaguesWithCapacityResultDTO } from '../presenters/IAllLeaguesWithCapacityPresenter';
|
||||
import type { AllLeaguesWithCapacityOutputPort } from '../ports/output/AllLeaguesWithCapacityOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -10,17 +10,17 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
* Orchestrates domain logic and returns result.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityUseCase
|
||||
implements AsyncUseCase<void, AllLeaguesWithCapacityResultDTO, string>
|
||||
implements AsyncUseCase<void, AllLeaguesWithCapacityOutputPort, string>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<AllLeaguesWithCapacityResultDTO, ApplicationErrorCode<string>>> {
|
||||
async execute(): Promise<Result<AllLeaguesWithCapacityOutputPort, ApplicationErrorCode<string>>> {
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
|
||||
const memberCounts = new Map<string, number>();
|
||||
const memberCounts: Record<string, number> = {};
|
||||
|
||||
for (const league of leagues) {
|
||||
const members = await this.leagueMembershipRepository.getLeagueMembers(league.id);
|
||||
@@ -34,14 +34,14 @@ export class GetAllLeaguesWithCapacityUseCase
|
||||
m.role === 'member'),
|
||||
).length;
|
||||
|
||||
memberCounts.set(league.id, usedSlots);
|
||||
memberCounts[league.id] = usedSlots;
|
||||
}
|
||||
|
||||
const dto: AllLeaguesWithCapacityResultDTO = {
|
||||
const output: AllLeaguesWithCapacityOutputPort = {
|
||||
leagues,
|
||||
memberCounts,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type {
|
||||
AllRacesPageResultDTO,
|
||||
AllRacesPageViewModel,
|
||||
AllRacesListItemViewModel,
|
||||
AllRacesFilterOptionsViewModel,
|
||||
} from '../presenters/IAllRacesPagePresenter';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger , AsyncUseCase } from '@core/shared/application';
|
||||
import type { AllRacesPageOutputPort, AllRacesListItem, AllRacesFilterOptions } from '../ports/output/AllRacesPageOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export class GetAllRacesPageDataUseCase
|
||||
implements AsyncUseCase<void, AllRacesPageResultDTO, 'REPOSITORY_ERROR'> {
|
||||
implements AsyncUseCase<void, AllRacesPageOutputPort, 'REPOSITORY_ERROR'> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -30,7 +24,7 @@ export class GetAllRacesPageDataUseCase
|
||||
|
||||
const leagueMap = new Map(allLeagues.map((league) => [league.id.toString(), league.name.toString()]));
|
||||
|
||||
const races: AllRacesListItemViewModel[] = allRaces
|
||||
const races: AllRacesListItem[] = allRaces
|
||||
.slice()
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
|
||||
.map((race) => ({
|
||||
@@ -49,7 +43,7 @@ export class GetAllRacesPageDataUseCase
|
||||
uniqueLeagues.set(league.id.toString(), { id: league.id.toString(), name: league.name.toString() });
|
||||
}
|
||||
|
||||
const filters: AllRacesFilterOptionsViewModel = {
|
||||
const filters: AllRacesFilterOptions = {
|
||||
statuses: [
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'scheduled', label: 'Scheduled' },
|
||||
@@ -60,7 +54,7 @@ export class GetAllRacesPageDataUseCase
|
||||
leagues: Array.from(uniqueLeagues.values()),
|
||||
};
|
||||
|
||||
const viewModel: AllRacesPageViewModel = {
|
||||
const viewModel: AllRacesPageOutputPort = {
|
||||
races,
|
||||
filters,
|
||||
};
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { GetAllRacesResultDTO } from '../presenters/IGetAllRacesPresenter';
|
||||
import type { GetAllRacesOutputPort } from '../ports/output/GetAllRacesOutputPort';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export class GetAllRacesUseCase implements AsyncUseCase<void, GetAllRacesResultDTO, 'REPOSITORY_ERROR'> {
|
||||
export class GetAllRacesUseCase implements AsyncUseCase<void, GetAllRacesOutputPort, 'REPOSITORY_ERROR'> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<GetAllRacesResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
async execute(): Promise<Result<GetAllRacesOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug('Executing GetAllRacesUseCase');
|
||||
try {
|
||||
const races = await this.raceRepository.findAll();
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
const leagueMap = new Map(leagues.map(league => [league.id, league.name]));
|
||||
|
||||
const raceViewModels = races.map(race => ({
|
||||
id: race.id,
|
||||
name: `${race.track} - ${race.car}`,
|
||||
date: race.scheduledAt.toISOString(),
|
||||
leagueName: leagueMap.get(race.leagueId) || 'Unknown League',
|
||||
}));
|
||||
|
||||
const dto: GetAllRacesResultDTO = {
|
||||
races: raceViewModels,
|
||||
const output: GetAllRacesOutputPort = {
|
||||
races: races.map(race => ({
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
strengthOfField: race.strengthOfField || null,
|
||||
leagueName: (leagueMap.get(race.leagueId) || 'Unknown League').toString(),
|
||||
})),
|
||||
totalCount: races.length,
|
||||
};
|
||||
|
||||
this.logger.debug('Successfully retrieved all races.');
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetAllRacesUseCase', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { AllTeamsResultDTO } from '../presenters/IAllTeamsPresenter';
|
||||
import type { GetAllTeamsOutputPort } from '../ports/output/GetAllTeamsOutputPort';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -8,7 +8,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
*/
|
||||
export class GetAllTeamsUseCase implements AsyncUseCase<void, AllTeamsResultDTO, 'REPOSITORY_ERROR'> {
|
||||
export class GetAllTeamsUseCase implements AsyncUseCase<void, GetAllTeamsOutputPort, 'REPOSITORY_ERROR'> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
@@ -37,8 +37,9 @@ export class GetAllTeamsUseCase implements AsyncUseCase<void, AllTeamsResultDTO,
|
||||
}),
|
||||
);
|
||||
|
||||
const dto: AllTeamsResultDTO = {
|
||||
const dto: GetAllTeamsOutputPort = {
|
||||
teams: enrichedTeams,
|
||||
totalCount: enrichedTeams.length,
|
||||
};
|
||||
|
||||
this.logger.debug('Successfully retrieved all teams.');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { DriverTeamResultDTO } from '../presenters/IDriverTeamPresenter';
|
||||
import type { DriverTeamOutputPort } from '../ports/output/DriverTeamOutputPort';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -10,7 +10,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
* Orchestrates domain logic and returns result.
|
||||
*/
|
||||
export class GetDriverTeamUseCase
|
||||
implements AsyncUseCase<{ driverId: string }, Result<DriverTeamResultDTO, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND' | 'TEAM_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>>>
|
||||
implements AsyncUseCase<{ driverId: string }, Result<DriverTeamOutputPort, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND' | 'TEAM_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>>>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
@@ -18,7 +18,7 @@ export class GetDriverTeamUseCase
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: { driverId: string }): Promise<Result<DriverTeamResultDTO, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND' | 'TEAM_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>>> {
|
||||
async execute(input: { driverId: string }): Promise<Result<DriverTeamOutputPort, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND' | 'TEAM_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug(`Executing GetDriverTeamUseCase for driverId: ${input.driverId}`);
|
||||
try {
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId);
|
||||
@@ -35,14 +35,22 @@ export class GetDriverTeamUseCase
|
||||
}
|
||||
this.logger.debug(`Found team for teamId: ${team.id}, name: ${team.name}`);
|
||||
|
||||
const dto: DriverTeamResultDTO = {
|
||||
team,
|
||||
membership,
|
||||
const output: DriverTeamOutputPort = {
|
||||
driverId: input.driverId,
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name.value,
|
||||
tag: team.tag.value,
|
||||
description: team.description.value,
|
||||
ownerId: team.ownerId.value,
|
||||
leagues: team.leagues.map(l => l.value),
|
||||
createdAt: team.createdAt.value,
|
||||
},
|
||||
membership,
|
||||
};
|
||||
|
||||
this.logger.info(`Successfully retrieved driver team for driverId: ${input.driverId}`);
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetDriverTeamUseCase', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error occurred' } });
|
||||
|
||||
@@ -47,11 +47,14 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const driver1 = { id: 'driver1', name: 'Driver One' };
|
||||
const driver2 = { id: 'driver2', name: 'Driver Two' };
|
||||
const rankings = { driver1: 1, driver2: 2 };
|
||||
const stats1 = { wins: 5, losses: 2 };
|
||||
const stats2 = { wins: 3, losses: 1 };
|
||||
const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } };
|
||||
const driver2 = { id: 'driver2', name: { value: 'Driver Two' }, country: { value: 'US' } };
|
||||
const rankings = [
|
||||
{ driverId: 'driver1', rating: 2500, overallRank: 1 },
|
||||
{ driverId: 'driver2', rating: 2400, overallRank: 2 },
|
||||
];
|
||||
const stats1 = { totalRaces: 10, wins: 5, podiums: 7 };
|
||||
const stats2 = { totalRaces: 8, wins: 3, podiums: 4 };
|
||||
|
||||
mockDriverFindAll.mockResolvedValue([driver1, driver2]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
|
||||
@@ -70,10 +73,37 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [driver1, driver2],
|
||||
rankings,
|
||||
stats: { driver1: stats1, driver2: stats2 },
|
||||
avatarUrls: { driver1: 'avatar-driver1', driver2: 'avatar-driver2' },
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 10,
|
||||
wins: 5,
|
||||
podiums: 7,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'avatar-driver1',
|
||||
},
|
||||
{
|
||||
id: 'driver2',
|
||||
name: 'Driver Two',
|
||||
rating: 2400,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 8,
|
||||
wins: 3,
|
||||
podiums: 4,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'avatar-driver2',
|
||||
},
|
||||
],
|
||||
totalRaces: 18,
|
||||
totalWins: 8,
|
||||
activeCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,16 +117,16 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
);
|
||||
|
||||
mockDriverFindAll.mockResolvedValue([]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue({});
|
||||
mockRankingGetAllDriverRankings.mockReturnValue([]);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [],
|
||||
rankings: {},
|
||||
stats: {},
|
||||
avatarUrls: {},
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,8 +139,8 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const driver1 = { id: 'driver1', name: 'Driver One' };
|
||||
const rankings = { driver1: 1 };
|
||||
const driver1 = { id: 'driver1', name: { value: 'Driver One' }, country: { value: 'US' } };
|
||||
const rankings = [{ driverId: 'driver1', rating: 2500, overallRank: 1 }];
|
||||
|
||||
mockDriverFindAll.mockResolvedValue([driver1]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
|
||||
@@ -121,10 +151,24 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [driver1],
|
||||
rankings,
|
||||
stats: {},
|
||||
avatarUrls: { driver1: 'avatar-driver1' },
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'avatar-driver1',
|
||||
},
|
||||
],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { IRankingService } from '../../domain/services/IRankingService';
|
||||
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
|
||||
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
|
||||
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
|
||||
import type { DriversLeaderboardResultDTO } from '../presenters/IDriversLeaderboardPresenter';
|
||||
import type { DriversLeaderboardOutputPort, DriverLeaderboardItemOutputPort } from '../ports/output/DriversLeaderboardOutputPort';
|
||||
import type { SkillLevel } from '../../domain/services/SkillLevelService';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -13,7 +14,7 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
* Orchestrates domain logic and returns result.
|
||||
*/
|
||||
export class GetDriversLeaderboardUseCase
|
||||
implements AsyncUseCase<void, DriversLeaderboardResultDTO, 'REPOSITORY_ERROR'>
|
||||
implements AsyncUseCase<void, DriversLeaderboardOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
@@ -23,34 +24,52 @@ export class GetDriversLeaderboardUseCase
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<DriversLeaderboardResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
async execute(): Promise<Result<DriversLeaderboardOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
this.logger.debug('Executing GetDriversLeaderboardUseCase');
|
||||
try {
|
||||
const drivers = await this.driverRepository.findAll();
|
||||
const rankings = this.rankingService.getAllDriverRankings();
|
||||
|
||||
const stats: DriversLeaderboardResultDTO['stats'] = {};
|
||||
const avatarUrls: DriversLeaderboardResultDTO['avatarUrls'] = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
for (const driver of drivers) {
|
||||
const driverStats = this.driverStatsService.getDriverStats(driver.id);
|
||||
if (driverStats) {
|
||||
stats[driver.id] = driverStats;
|
||||
}
|
||||
|
||||
const avatarResult = await this.getDriverAvatar({ driverId: driver.id });
|
||||
avatarUrls[driver.id] = avatarResult.avatarUrl;
|
||||
}
|
||||
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers,
|
||||
rankings,
|
||||
stats,
|
||||
avatarUrls,
|
||||
const driverItems: DriverLeaderboardItemOutputPort[] = drivers.map(driver => {
|
||||
const ranking = rankings.find(r => r.driverId === driver.id);
|
||||
const stats = this.driverStatsService.getDriverStats(driver.id);
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name.value,
|
||||
rating: ranking?.rating ?? 0,
|
||||
skillLevel: 'Pro' as SkillLevel, // TODO: map from domain
|
||||
nationality: driver.country.value,
|
||||
racesCompleted: stats?.totalRaces ?? 0,
|
||||
wins: stats?.wins ?? 0,
|
||||
podiums: stats?.podiums ?? 0,
|
||||
isActive: true, // TODO: determine from domain
|
||||
rank: ranking?.overallRank ?? 0,
|
||||
avatarUrl: avatarUrls[driver.id],
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate totals
|
||||
const totalRaces = driverItems.reduce((sum, d) => sum + d.racesCompleted, 0);
|
||||
const totalWins = driverItems.reduce((sum, d) => sum + d.wins, 0);
|
||||
const activeCount = driverItems.filter(d => d.isActive).length;
|
||||
|
||||
const result: DriversLeaderboardOutputPort = {
|
||||
drivers: driverItems.sort((a, b) => b.rating - a.rating),
|
||||
totalRaces,
|
||||
totalWins,
|
||||
activeCount,
|
||||
};
|
||||
|
||||
this.logger.debug('Successfully retrieved drivers leaderboard.');
|
||||
return Result.ok(dto);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetDriversLeaderboardUseCase', error instanceof Error ? error : new Error(String(error)));
|
||||
return Result.err({
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
|
||||
describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
@@ -12,12 +14,16 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
const mockPenaltyFindByRaceId = vi.fn();
|
||||
const mockRaceFindByLeagueId = vi.fn();
|
||||
const mockDriverRatingGetRating = vi.fn();
|
||||
const mockDriverFindById = vi.fn();
|
||||
const mockTeamFindById = vi.fn();
|
||||
|
||||
let useCase: GetLeagueDriverSeasonStatsUseCase;
|
||||
let standingRepository: IStandingRepository;
|
||||
let resultRepository: IResultRepository;
|
||||
let penaltyRepository: IPenaltyRepository;
|
||||
let raceRepository: IRaceRepository;
|
||||
let driverRepository: IDriverRepository;
|
||||
let teamRepository: ITeamRepository;
|
||||
let driverRatingPort: DriverRatingPort;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -51,6 +57,12 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
driverRepository = {
|
||||
findById: mockDriverFindById,
|
||||
};
|
||||
teamRepository = {
|
||||
findById: mockTeamFindById,
|
||||
};
|
||||
driverRatingPort = {
|
||||
getRating: mockDriverRatingGetRating,
|
||||
};
|
||||
@@ -60,6 +72,8 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
resultRepository,
|
||||
penaltyRepository,
|
||||
raceRepository,
|
||||
driverRepository,
|
||||
teamRepository,
|
||||
driverRatingPort,
|
||||
);
|
||||
});
|
||||
@@ -80,6 +94,8 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
];
|
||||
const mockResults = [{ position: 1 }];
|
||||
const mockRating = { rating: 1500, ratingChange: 50 };
|
||||
const mockDriver = { id: 'driver-1', name: 'Driver One', teamId: 'team-1' };
|
||||
const mockTeam = { id: 'team-1', name: 'Team One' };
|
||||
|
||||
standingRepository.findByLeagueId.mockResolvedValue(mockStandings);
|
||||
raceRepository.findByLeagueId.mockResolvedValue(mockRaces);
|
||||
@@ -89,19 +105,39 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
});
|
||||
driverRatingPort.getRating.mockReturnValue(mockRating);
|
||||
resultRepository.findByDriverIdAndLeagueId.mockResolvedValue(mockResults);
|
||||
driverRepository.findById.mockImplementation((id) => {
|
||||
if (id === 'driver-1') return Promise.resolve(mockDriver);
|
||||
if (id === 'driver-2') return Promise.resolve({ id: 'driver-2', name: 'Driver Two' });
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
teamRepository.findById.mockResolvedValue(mockTeam);
|
||||
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.value!;
|
||||
expect(dto.leagueId).toBe('league-1');
|
||||
expect(dto.standings).toEqual([
|
||||
{ driverId: 'driver-1', position: 1, points: 100, racesCompleted: 5 },
|
||||
{ driverId: 'driver-2', position: 2, points: 80, racesCompleted: 5 },
|
||||
]);
|
||||
expect(dto.penalties.get('driver-1')).toEqual({ baseDelta: -10, bonusDelta: 0 });
|
||||
expect(dto.driverRatings.get('driver-1')).toEqual(mockRating);
|
||||
expect(dto.driverResults.get('driver-1')).toEqual(mockResults);
|
||||
const output = result.value!;
|
||||
expect(output.leagueId).toBe('league-1');
|
||||
expect(output.stats).toHaveLength(2);
|
||||
expect(output.stats[0]).toEqual({
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
position: 1,
|
||||
driverName: 'Driver One',
|
||||
teamId: 'team-1',
|
||||
teamName: 'Team One',
|
||||
totalPoints: 100,
|
||||
basePoints: 90,
|
||||
penaltyPoints: -10,
|
||||
bonusPoints: 0,
|
||||
pointsPerRace: 20,
|
||||
racesStarted: 1,
|
||||
racesFinished: 1,
|
||||
dnfs: 0,
|
||||
noShows: 1,
|
||||
avgFinish: 1,
|
||||
rating: 1500,
|
||||
ratingChange: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle no penalties', async () => {
|
||||
@@ -111,17 +147,20 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
const mockRaces = [{ id: 'race-1' }];
|
||||
const mockResults = [{ position: 1 }];
|
||||
const mockRating = { rating: null, ratingChange: null };
|
||||
const mockDriver = { id: 'driver-1', name: 'Driver One' };
|
||||
|
||||
standingRepository.findByLeagueId.mockResolvedValue(mockStandings);
|
||||
raceRepository.findByLeagueId.mockResolvedValue(mockRaces);
|
||||
penaltyRepository.findByRaceId.mockResolvedValue([]);
|
||||
driverRatingPort.getRating.mockReturnValue(mockRating);
|
||||
resultRepository.findByDriverIdAndLeagueId.mockResolvedValue(mockResults);
|
||||
driverRepository.findById.mockResolvedValue(mockDriver);
|
||||
teamRepository.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.value!;
|
||||
expect(dto.penalties.size).toBe(0);
|
||||
const output = result.value!;
|
||||
expect(output.stats[0].penaltyPoints).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,9 @@ import type { IStandingRepository } from '../../domain/repositories/IStandingRep
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { LeagueDriverSeasonStatsResultDTO } from '../presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { LeagueDriverSeasonStatsOutputPort } from '../ports/output/LeagueDriverSeasonStatsOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
@@ -11,16 +13,18 @@ import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
* Use Case for retrieving league driver season statistics.
|
||||
* Orchestrates domain logic and returns the result.
|
||||
*/
|
||||
export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueId: string }, LeagueDriverSeasonStatsResultDTO, 'NO_ERROR'> {
|
||||
export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueId: string }, LeagueDriverSeasonStatsOutputPort, 'NO_ERROR'> {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly driverRatingPort: DriverRatingPort,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueDriverSeasonStatsResultDTO, never>> {
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueDriverSeasonStatsOutputPort, never>> {
|
||||
const { leagueId } = params;
|
||||
|
||||
// Get standings and races for the league
|
||||
@@ -69,19 +73,54 @@ export class GetLeagueDriverSeasonStatsUseCase implements AsyncUseCase<{ leagueI
|
||||
driverResults.set(standing.driverId, results);
|
||||
}
|
||||
|
||||
const dto: LeagueDriverSeasonStatsResultDTO = {
|
||||
leagueId,
|
||||
standings: standings.map(standing => ({
|
||||
// Fetch drivers and teams
|
||||
const driverIds = standings.map(s => s.driverId);
|
||||
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
|
||||
const driversMap = new Map(drivers.filter(d => d).map(d => [d!.id, d!]));
|
||||
const teamIds = Array.from(new Set(drivers.filter(d => d?.teamId).map(d => d!.teamId!)));
|
||||
const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id)));
|
||||
const teamsMap = new Map(teams.filter(t => t).map(t => [t!.id, t!]));
|
||||
|
||||
// Compute stats
|
||||
const stats = standings.map(standing => {
|
||||
const driver = driversMap.get(standing.driverId);
|
||||
const team = driver?.teamId ? teamsMap.get(driver.teamId) : undefined;
|
||||
const penalties = penaltiesByDriver.get(standing.driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const results = driverResults.get(standing.driverId) ?? [];
|
||||
const rating = driverRatings.get(standing.driverId);
|
||||
|
||||
const racesStarted = results.length;
|
||||
const racesFinished = results.filter(r => r.position > 0).length;
|
||||
const dnfs = results.filter(r => r.position === 0).length;
|
||||
const noShows = races.length - racesStarted;
|
||||
const avgFinish = results.length > 0 ? results.reduce((sum, r) => sum + r.position, 0) / results.length : null;
|
||||
const pointsPerRace = racesStarted > 0 ? standing.points / racesStarted : 0;
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
driverId: standing.driverId,
|
||||
position: standing.position,
|
||||
points: standing.points,
|
||||
racesCompleted: standing.racesCompleted,
|
||||
})),
|
||||
penalties: penaltiesByDriver,
|
||||
driverResults,
|
||||
driverRatings,
|
||||
};
|
||||
driverName: driver?.name ?? '',
|
||||
teamId: driver?.teamId ?? undefined,
|
||||
teamName: team?.name ?? undefined,
|
||||
totalPoints: standing.points,
|
||||
basePoints: standing.points - penalties.baseDelta,
|
||||
penaltyPoints: penalties.baseDelta,
|
||||
bonusPoints: penalties.bonusDelta,
|
||||
pointsPerRace,
|
||||
racesStarted,
|
||||
racesFinished,
|
||||
dnfs,
|
||||
noShows,
|
||||
avgFinish,
|
||||
rating: rating?.rating ?? null,
|
||||
ratingChange: rating?.ratingChange ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok({
|
||||
leagueId,
|
||||
stats,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILeagueFullConfigPresenter, LeagueConfigFormViewModel } from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { LeagueFullConfigOutputPort } from '../ports/output/LeagueFullConfigOutputPort';
|
||||
|
||||
describe('GetLeagueFullConfigUseCase', () => {
|
||||
let useCase: GetLeagueFullConfigUseCase;
|
||||
@@ -12,7 +12,6 @@ describe('GetLeagueFullConfigUseCase', () => {
|
||||
let seasonRepository: ISeasonRepository;
|
||||
let leagueScoringConfigRepository: ILeagueScoringConfigRepository;
|
||||
let gameRepository: IGameRepository;
|
||||
let presenter: ILeagueFullConfigPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
leagueRepository = {
|
||||
@@ -31,19 +30,12 @@ describe('GetLeagueFullConfigUseCase', () => {
|
||||
findById: vi.fn(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
presenter = {
|
||||
reset: vi.fn(),
|
||||
present: vi.fn(),
|
||||
getViewModel: vi.fn(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
|
||||
useCase = new GetLeagueFullConfigUseCase(
|
||||
leagueRepository,
|
||||
seasonRepository,
|
||||
leagueScoringConfigRepository,
|
||||
gameRepository,
|
||||
presenter,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,63 +63,17 @@ describe('GetLeagueFullConfigUseCase', () => {
|
||||
const mockSeasons = [{ id: 'season-1', status: 'active', gameId: 'game-1' }];
|
||||
const mockScoringConfig = { id: 'config-1' };
|
||||
const mockGame = { id: 'game-1' };
|
||||
const mockViewModel: LeagueConfigFormViewModel = {
|
||||
leagueId: 'league-1',
|
||||
basics: {
|
||||
name: 'Test League',
|
||||
description: 'A test league',
|
||||
visibility: 'public',
|
||||
gameId: 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: 'solo',
|
||||
maxDrivers: 32,
|
||||
multiClassEnabled: false,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: true,
|
||||
enableTeamChampionship: false,
|
||||
enableNationsChampionship: false,
|
||||
enableTrophyChampionship: false,
|
||||
},
|
||||
scoring: {
|
||||
customScoringEnabled: false,
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: 'none',
|
||||
},
|
||||
timings: {
|
||||
practiceMinutes: 30,
|
||||
qualifyingMinutes: 15,
|
||||
mainRaceMinutes: 60,
|
||||
sessionCount: 1,
|
||||
roundsPlanned: 10,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: 'admin_only',
|
||||
requireDefense: false,
|
||||
defenseTimeLimit: 48,
|
||||
voteTimeLimit: 72,
|
||||
protestDeadlineHours: 48,
|
||||
stewardingClosesHours: 168,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
|
||||
leagueRepository.findById.mockResolvedValue(mockLeague);
|
||||
seasonRepository.findByLeagueId.mockResolvedValue(mockSeasons);
|
||||
leagueScoringConfigRepository.findBySeasonId.mockResolvedValue(mockScoringConfig);
|
||||
gameRepository.findById.mockResolvedValue(mockGame);
|
||||
presenter.getViewModel.mockReturnValue(mockViewModel);
|
||||
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const viewModel = result.value!;
|
||||
expect(viewModel).toEqual(mockViewModel);
|
||||
expect(presenter.reset).toHaveBeenCalled();
|
||||
expect(presenter.present).toHaveBeenCalledWith({
|
||||
const output = result.value!;
|
||||
expect(output).toEqual({
|
||||
league: mockLeague,
|
||||
activeSeason: mockSeasons[0],
|
||||
scoringConfig: mockScoringConfig,
|
||||
@@ -157,25 +103,15 @@ describe('GetLeagueFullConfigUseCase', () => {
|
||||
description: 'A test league',
|
||||
settings: { maxDrivers: 32 },
|
||||
};
|
||||
const mockViewModel: LeagueConfigFormViewModel = {
|
||||
leagueId: 'league-1',
|
||||
basics: { name: 'Test League', description: 'A test league', visibility: 'public', gameId: 'iracing' },
|
||||
structure: { mode: 'solo', maxDrivers: 32, multiClassEnabled: false },
|
||||
championships: { enableDriverChampionship: true, enableTeamChampionship: false, enableNationsChampionship: false, enableTrophyChampionship: false },
|
||||
scoring: { customScoringEnabled: false },
|
||||
dropPolicy: { strategy: 'none' },
|
||||
timings: { practiceMinutes: 30, qualifyingMinutes: 15, mainRaceMinutes: 60, sessionCount: 1, roundsPlanned: 10 },
|
||||
stewarding: { decisionMode: 'admin_only', requireDefense: false, defenseTimeLimit: 48, voteTimeLimit: 72, protestDeadlineHours: 48, stewardingClosesHours: 168, notifyAccusedOnProtest: true, notifyOnVoteRequired: true },
|
||||
};
|
||||
|
||||
leagueRepository.findById.mockResolvedValue(mockLeague);
|
||||
seasonRepository.findByLeagueId.mockResolvedValue([]);
|
||||
presenter.getViewModel.mockReturnValue(mockViewModel);
|
||||
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(presenter.present).toHaveBeenCalledWith({
|
||||
const output = result.value!;
|
||||
expect(output).toEqual({
|
||||
league: mockLeague,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,29 +2,24 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type {
|
||||
ILeagueFullConfigPresenter,
|
||||
LeagueFullConfigData,
|
||||
LeagueConfigFormViewModel,
|
||||
} from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { LeagueFullConfigOutputPort } from '../ports/output/LeagueFullConfigOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a league's full configuration.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
* Orchestrates domain logic and returns the configuration data.
|
||||
*/
|
||||
export class GetLeagueFullConfigUseCase implements AsyncUseCase<{ leagueId: string }, LeagueConfigFormViewModel, 'LEAGUE_NOT_FOUND' | 'PRESENTATION_FAILED'> {
|
||||
export class GetLeagueFullConfigUseCase implements AsyncUseCase<{ leagueId: string }, LeagueFullConfigOutputPort, 'LEAGUE_NOT_FOUND'> {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
private readonly presenter: ILeagueFullConfigPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueConfigFormViewModel, ApplicationErrorCode<'LEAGUE_NOT_FOUND' | 'PRESENTATION_FAILED', { message: string }>>> {
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueFullConfigOutputPort, ApplicationErrorCode<'LEAGUE_NOT_FOUND', { message: string }>>> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
@@ -47,20 +42,13 @@ export class GetLeagueFullConfigUseCase implements AsyncUseCase<{ leagueId: stri
|
||||
return this.gameRepository.findById(activeSeason.gameId);
|
||||
})();
|
||||
|
||||
const data: LeagueFullConfigData = {
|
||||
const output: LeagueFullConfigOutputPort = {
|
||||
league,
|
||||
...(activeSeason ? { activeSeason } : {}),
|
||||
...(scoringConfig ? { scoringConfig } : {}),
|
||||
...(game ? { game } : {}),
|
||||
};
|
||||
|
||||
this.presenter.reset();
|
||||
this.presenter.present(data);
|
||||
const viewModel = this.presenter.getViewModel();
|
||||
if (!viewModel) {
|
||||
return Result.err({ code: 'PRESENTATION_FAILED', details: { message: 'Failed to present league config' } });
|
||||
}
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(output);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class GetLeagueOwnerSummaryUseCase implements AsyncUseCase<GetLeagueOwner
|
||||
|
||||
async execute(params: GetLeagueOwnerSummaryUseCaseParams): Promise<Result<GetLeagueOwnerSummaryOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const driver = await this.driverRepository.findById(params.ownerId);
|
||||
const summary = driver ? { driver: { id: driver.id, name: driver.name }, rating: 0, rank: 0 } : null;
|
||||
const summary = driver ? { driver: { id: driver.id, iracingId: driver.iracingId.toString(), name: driver.name.toString(), country: driver.country.toString(), bio: driver.bio?.toString(), joinedAt: driver.joinedAt.toDate().toISOString() }, rating: 0, rank: 0 } : null;
|
||||
return Result.ok({ summary });
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,41 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { GetLeagueProtestsResultDTO, ProtestDTO } from '../dto/GetLeagueProtestsResultDTO';
|
||||
import type { GetLeagueProtestsOutputPort, ProtestOutputPort, RaceOutputPort, DriverOutputPort } from '../ports/output/GetLeagueProtestsOutputPort';
|
||||
|
||||
export interface GetLeagueProtestsUseCaseParams {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueProtestsUseCase implements AsyncUseCase<GetLeagueProtestsUseCaseParams, GetLeagueProtestsResultDTO, 'NO_ERROR'> {
|
||||
export class GetLeagueProtestsUseCase implements AsyncUseCase<GetLeagueProtestsUseCaseParams, GetLeagueProtestsOutputPort, 'NO_ERROR'> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetLeagueProtestsUseCaseParams): Promise<Result<GetLeagueProtestsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(params: GetLeagueProtestsUseCaseParams): Promise<Result<GetLeagueProtestsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const races = await this.raceRepository.findByLeagueId(params.leagueId);
|
||||
const protests: ProtestDTO[] = [];
|
||||
const raceMap = new Map();
|
||||
const protests: ProtestOutputPort[] = [];
|
||||
const racesById: Record<string, RaceOutputPort> = {};
|
||||
const driversById: Record<string, DriverOutputPort> = {};
|
||||
const driverIds = new Set<string>();
|
||||
|
||||
for (const race of races) {
|
||||
raceMap.set(race.id, { id: race.id, name: race.track, date: race.scheduledAt.toISOString() });
|
||||
racesById[race.id] = {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
track: race.track,
|
||||
trackId: race.trackId,
|
||||
car: race.car,
|
||||
carId: race.carId,
|
||||
sessionType: race.sessionType.toString(),
|
||||
status: race.status,
|
||||
strengthOfField: race.strengthOfField,
|
||||
registeredCount: race.registeredCount,
|
||||
maxParticipants: race.maxParticipants,
|
||||
};
|
||||
const raceProtests = await this.protestRepository.findByRaceId(race.id);
|
||||
for (const protest of raceProtests) {
|
||||
protests.push({
|
||||
@@ -32,26 +46,48 @@ export class GetLeagueProtestsUseCase implements AsyncUseCase<GetLeagueProtestsU
|
||||
raceId: protest.raceId,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
submittedAt: protest.filedAt,
|
||||
description: protest.comment || '',
|
||||
status: protest.status,
|
||||
incident: {
|
||||
lap: protest.incident.lap,
|
||||
description: protest.incident.description,
|
||||
timeInRace: protest.incident.timeInRace,
|
||||
},
|
||||
comment: protest.comment,
|
||||
proofVideoUrl: protest.proofVideoUrl,
|
||||
status: protest.status.toString(),
|
||||
reviewedBy: protest.reviewedBy,
|
||||
decisionNotes: protest.decisionNotes,
|
||||
filedAt: protest.filedAt.toISOString(),
|
||||
reviewedAt: protest.reviewedAt?.toISOString(),
|
||||
defense: protest.defense ? {
|
||||
statement: protest.defense.statement.toString(),
|
||||
videoUrl: protest.defense.videoUrl?.toString(),
|
||||
submittedAt: protest.defense.submittedAt.toDate().toISOString(),
|
||||
} : undefined,
|
||||
defenseRequestedAt: protest.defenseRequestedAt?.toISOString(),
|
||||
defenseRequestedBy: protest.defenseRequestedBy,
|
||||
});
|
||||
driverIds.add(protest.protestingDriverId);
|
||||
driverIds.add(protest.accusedDriverId);
|
||||
}
|
||||
}
|
||||
|
||||
const drivers: { id: string; name: string }[] = [];
|
||||
for (const driverId of driverIds) {
|
||||
const driver = await this.driverRepository.findById(driverId);
|
||||
if (driver) {
|
||||
drivers.push({ id: driver.id, name: driver.name });
|
||||
driversById[driver.id] = {
|
||||
id: driver.id,
|
||||
iracingId: driver.iracingId.toString(),
|
||||
name: driver.name.toString(),
|
||||
country: driver.country.toString(),
|
||||
bio: driver.bio?.toString(),
|
||||
joinedAt: driver.joinedAt.toDate().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
return Result.ok({
|
||||
protests,
|
||||
races: Array.from(raceMap.values()),
|
||||
drivers,
|
||||
racesById,
|
||||
driversById,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { GetLeagueScheduleResultDTO } from '../dto/GetLeagueScheduleResultDTO';
|
||||
import type { GetLeagueScheduleOutputPort } from '../ports/output/GetLeagueScheduleOutputPort';
|
||||
|
||||
export interface GetLeagueScheduleUseCaseParams {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueScheduleUseCase implements AsyncUseCase<GetLeagueScheduleUseCaseParams, GetLeagueScheduleResultDTO, 'NO_ERROR'> {
|
||||
export class GetLeagueScheduleUseCase implements AsyncUseCase<GetLeagueScheduleUseCaseParams, GetLeagueScheduleOutputPort, 'NO_ERROR'> {
|
||||
constructor(private readonly raceRepository: IRaceRepository) {}
|
||||
|
||||
async execute(params: GetLeagueScheduleUseCaseParams): Promise<Result<GetLeagueScheduleResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(params: GetLeagueScheduleUseCaseParams): Promise<Result<GetLeagueScheduleOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const races = await this.raceRepository.findByLeagueId(params.leagueId);
|
||||
return Result.ok({
|
||||
races: races.map(race => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ILeagueScoringConfigRepository } from '../../domain/repositories/I
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { GetLeagueScoringPresetByIdInputPort } from '../ports/input/GetLeagueScoringPresetByIdInputPort';
|
||||
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
|
||||
import type { LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
|
||||
import type { LeagueScoringConfigOutputPort } from '../ports/output/LeagueScoringConfigOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -20,7 +20,7 @@ type GetLeagueScoringConfigErrorCode =
|
||||
* Use Case for retrieving a league's scoring configuration for its active season.
|
||||
*/
|
||||
export class GetLeagueScoringConfigUseCase
|
||||
implements AsyncUseCase<{ leagueId: string }, LeagueScoringConfigData, GetLeagueScoringConfigErrorCode>
|
||||
implements AsyncUseCase<{ leagueId: string }, LeagueScoringConfigOutputPort, GetLeagueScoringConfigErrorCode>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -30,7 +30,7 @@ export class GetLeagueScoringConfigUseCase
|
||||
private readonly getLeagueScoringPresetById: (input: GetLeagueScoringPresetByIdInputPort) => Promise<LeagueScoringPresetOutputPort | undefined>,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueScoringConfigData, ApplicationErrorCode<GetLeagueScoringConfigErrorCode>>> {
|
||||
async execute(params: { leagueId: string }): Promise<Result<LeagueScoringConfigOutputPort, ApplicationErrorCode<GetLeagueScoringConfigErrorCode>>> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
@@ -64,7 +64,7 @@ export class GetLeagueScoringConfigUseCase
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
const preset = presetId ? await this.getLeagueScoringPresetById({ presetId }) : undefined;
|
||||
|
||||
const data: LeagueScoringConfigData = {
|
||||
const output: LeagueScoringConfigOutputPort = {
|
||||
leagueId: league.id,
|
||||
seasonId: activeSeason.id,
|
||||
gameId: game.id,
|
||||
@@ -74,6 +74,6 @@ export class GetLeagueScoringConfigUseCase
|
||||
championships: scoringConfig.championships,
|
||||
};
|
||||
|
||||
return Result.ok(data);
|
||||
return Result.ok(output);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { GetLeagueSeasonsViewModel } from '../presenters/IGetLeagueSeasonsPresenter';
|
||||
import type { GetLeagueSeasonsOutputPort } from '../ports/output/GetLeagueSeasonsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -10,11 +10,11 @@ export interface GetLeagueSeasonsUseCaseParams {
|
||||
export class GetLeagueSeasonsUseCase {
|
||||
constructor(private readonly seasonRepository: ISeasonRepository) {}
|
||||
|
||||
async execute(params: GetLeagueSeasonsUseCaseParams): Promise<Result<GetLeagueSeasonsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
async execute(params: GetLeagueSeasonsUseCaseParams): Promise<Result<GetLeagueSeasonsOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
|
||||
try {
|
||||
const seasons = await this.seasonRepository.findByLeagueId(params.leagueId);
|
||||
const activeCount = seasons.filter(s => s.status === 'active').length;
|
||||
const viewModel: GetLeagueSeasonsViewModel = {
|
||||
const output: GetLeagueSeasonsOutputPort = {
|
||||
seasons: seasons.map(s => ({
|
||||
seasonId: s.id,
|
||||
name: s.name,
|
||||
@@ -25,7 +25,7 @@ export class GetLeagueSeasonsUseCase {
|
||||
isParallelActive: s.status === 'active' && activeCount > 1
|
||||
}))
|
||||
};
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(output);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to fetch seasons' } });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { LeagueStandingsViewModel } from '../presenters/ILeagueStandingsPresenter';
|
||||
import type { LeagueStandingsOutputPort } from '../ports/output/LeagueStandingsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -19,7 +19,7 @@ export class GetLeagueStandingsUseCase {
|
||||
|
||||
async execute(
|
||||
params: GetLeagueStandingsUseCaseParams,
|
||||
): Promise<Result<LeagueStandingsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
): Promise<Result<LeagueStandingsOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const standings = await this.standingRepository.findByLeagueId(params.leagueId);
|
||||
const driverIds = [...new Set(standings.map(s => s.driverId))];
|
||||
@@ -27,7 +27,7 @@ export class GetLeagueStandingsUseCase {
|
||||
const driverResults = await Promise.all(driverPromises);
|
||||
const drivers = driverResults.filter((d): d is NonNullable<typeof d> => d !== null);
|
||||
const driverMap = new Map(drivers.map(d => [d.id, { id: d.id, name: d.name }]));
|
||||
const viewModel: LeagueStandingsViewModel = {
|
||||
const viewModel: LeagueStandingsOutputPort = {
|
||||
standings: standings.map(s => ({
|
||||
driverId: s.driverId,
|
||||
driver: driverMap.get(s.driverId)!,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { LeagueStatsViewModel } from '../presenters/ILeagueStatsPresenter';
|
||||
import type { LeagueStatsOutputPort } from '../ports/output/LeagueStatsOutputPort';
|
||||
import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort';
|
||||
import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
@@ -17,7 +17,7 @@ export class GetLeagueStatsUseCase {
|
||||
private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise<GetDriverRatingOutputPort>,
|
||||
) {}
|
||||
|
||||
async execute(params: GetLeagueStatsUseCaseParams): Promise<Result<LeagueStatsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(params: GetLeagueStatsUseCaseParams): Promise<Result<LeagueStatsOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
|
||||
const races = await this.raceRepository.findByLeagueId(params.leagueId);
|
||||
@@ -35,7 +35,7 @@ export class GetLeagueStatsUseCase {
|
||||
|
||||
const averageRating = validRatings.length > 0 ? Math.round(validRatings.reduce((sum, r) => sum + r, 0) / validRatings.length) : 0;
|
||||
|
||||
const viewModel: LeagueStatsViewModel = {
|
||||
const viewModel: LeagueStatsOutputPort = {
|
||||
totalMembers: memberships.length,
|
||||
totalRaces: races.length,
|
||||
averageRating,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { PendingSponsorshipRequestsViewModel } from '../presenters/IPendingSponsorshipRequestsPresenter';
|
||||
import type { PendingSponsorshipRequestsOutputPort } from '../ports/output/PendingSponsorshipRequestsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -47,7 +47,7 @@ export class GetPendingSponsorshipRequestsUseCase {
|
||||
|
||||
async execute(
|
||||
dto: GetPendingSponsorshipRequestsDTO,
|
||||
): Promise<Result<PendingSponsorshipRequestsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
): Promise<Result<PendingSponsorshipRequestsOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
@@ -78,13 +78,13 @@ export class GetPendingSponsorshipRequestsUseCase {
|
||||
// Sort by creation date (newest first)
|
||||
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
const viewModel: PendingSponsorshipRequestsViewModel = {
|
||||
const outputPort: PendingSponsorshipRequestsOutputPort = {
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
requests: requestDTOs,
|
||||
totalCount: requestDTOs.length,
|
||||
};
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch pending sponsorship requests' });
|
||||
}
|
||||
|
||||
@@ -5,14 +5,7 @@ import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { Driver } from '../../domain/entities/Driver';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type {
|
||||
ProfileOverviewViewModel,
|
||||
ProfileOverviewDriverSummaryViewModel,
|
||||
ProfileOverviewStatsViewModel,
|
||||
ProfileOverviewFinishDistributionViewModel,
|
||||
ProfileOverviewTeamMembershipViewModel,
|
||||
ProfileOverviewSocialSummaryViewModel,
|
||||
} from '../presenters/IProfileOverviewPresenter';
|
||||
import type { ProfileOverviewOutputPort } from '../ports/output/ProfileOverviewOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -51,7 +44,7 @@ export class GetProfileOverviewUseCase {
|
||||
private readonly getAllDriverRankings: () => DriverRankingEntry[],
|
||||
) {}
|
||||
|
||||
async execute(params: GetProfileOverviewParams): Promise<Result<ProfileOverviewViewModel, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR'>>> {
|
||||
async execute(params: GetProfileOverviewParams): Promise<Result<ProfileOverviewOutputPort, ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const { driverId } = params;
|
||||
|
||||
@@ -73,8 +66,8 @@ export class GetProfileOverviewUseCase {
|
||||
const teamMemberships = await this.buildTeamMemberships(driver.id, teams as Team[]);
|
||||
const socialSummary = this.buildSocialSummary(friends as Driver[]);
|
||||
|
||||
const viewModel: ProfileOverviewViewModel = {
|
||||
currentDriver: driverSummary,
|
||||
const outputPort: ProfileOverviewOutputPort = {
|
||||
driver: driverSummary,
|
||||
stats,
|
||||
finishDistribution,
|
||||
teamMemberships,
|
||||
@@ -82,7 +75,7 @@ export class GetProfileOverviewUseCase {
|
||||
extendedProfile: null,
|
||||
};
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch profile overview' });
|
||||
}
|
||||
@@ -91,25 +84,25 @@ export class GetProfileOverviewUseCase {
|
||||
private buildDriverSummary(
|
||||
driver: Driver,
|
||||
stats: ProfileDriverStatsAdapter | null,
|
||||
): ProfileOverviewDriverSummaryViewModel {
|
||||
): ProfileOverviewOutputPort['driver'] {
|
||||
const rankings = this.getAllDriverRankings();
|
||||
const fallbackRank = this.computeFallbackRank(driver.id, rankings);
|
||||
const totalDrivers = rankings.length;
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
country: driver.country,
|
||||
name: driver.name.value,
|
||||
country: driver.country.value,
|
||||
avatarUrl: this.imageService.getDriverAvatar(driver.id),
|
||||
iracingId: driver.iracingId ?? null,
|
||||
iracingId: driver.iracingId?.value ?? null,
|
||||
joinedAt:
|
||||
driver.joinedAt instanceof Date
|
||||
? driver.joinedAt.toISOString()
|
||||
: new Date(driver.joinedAt).toISOString(),
|
||||
? driver.joinedAt
|
||||
: new Date(driver.joinedAt.value),
|
||||
rating: stats?.rating ?? null,
|
||||
globalRank: stats?.overallRank ?? fallbackRank,
|
||||
consistency: stats?.consistency ?? null,
|
||||
bio: driver.bio ?? null,
|
||||
bio: driver.bio?.value ?? null,
|
||||
totalDrivers,
|
||||
};
|
||||
}
|
||||
@@ -193,8 +186,8 @@ export class GetProfileOverviewUseCase {
|
||||
private async buildTeamMemberships(
|
||||
driverId: string,
|
||||
teams: Team[],
|
||||
): Promise<ProfileOverviewTeamMembershipViewModel[]> {
|
||||
const memberships: ProfileOverviewTeamMembershipViewModel[] = [];
|
||||
): Promise<ProfileOverviewOutputPort['teamMemberships']> {
|
||||
const memberships: ProfileOverviewOutputPort['teamMemberships'] = [];
|
||||
|
||||
for (const team of teams) {
|
||||
const membership = await this.teamMembershipRepository.getMembership(
|
||||
@@ -205,29 +198,29 @@ export class GetProfileOverviewUseCase {
|
||||
|
||||
memberships.push({
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
teamTag: team.tag ?? null,
|
||||
teamName: team.name.value,
|
||||
teamTag: team.tag?.value ?? null,
|
||||
role: membership.role,
|
||||
joinedAt:
|
||||
membership.joinedAt instanceof Date
|
||||
? membership.joinedAt.toISOString()
|
||||
: new Date(membership.joinedAt).toISOString(),
|
||||
? membership.joinedAt
|
||||
: new Date(membership.joinedAt),
|
||||
isCurrent: membership.status === 'active',
|
||||
});
|
||||
}
|
||||
|
||||
memberships.sort((a, b) => a.joinedAt.localeCompare(b.joinedAt));
|
||||
memberships.sort((a, b) => a.joinedAt.getTime() - b.joinedAt.getTime());
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
private buildSocialSummary(friends: Driver[]): ProfileOverviewSocialSummaryViewModel {
|
||||
private buildSocialSummary(friends: Driver[]): ProfileOverviewOutputPort['socialSummary'] {
|
||||
return {
|
||||
friendsCount: friends.length,
|
||||
friends: friends.map(friend => ({
|
||||
id: friend.id,
|
||||
name: friend.name,
|
||||
country: friend.country,
|
||||
name: friend.name.value,
|
||||
country: friend.country.value,
|
||||
avatarUrl: this.imageService.getDriverAvatar(friend.id),
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -4,17 +4,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort';
|
||||
import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort';
|
||||
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
|
||||
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
|
||||
import type {
|
||||
RaceDetailViewModel,
|
||||
RaceDetailRaceViewModel,
|
||||
RaceDetailLeagueViewModel,
|
||||
RaceDetailEntryViewModel,
|
||||
RaceDetailUserResultViewModel,
|
||||
} from '../presenters/IRaceDetailPresenter';
|
||||
import type { RaceDetailOutputPort } from '../ports/output/RaceDetailOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -37,7 +27,7 @@ export interface GetRaceDetailQueryParams {
|
||||
type GetRaceDetailErrorCode = 'RACE_NOT_FOUND';
|
||||
|
||||
export class GetRaceDetailUseCase
|
||||
implements AsyncUseCase<GetRaceDetailQueryParams, RaceDetailViewModel, GetRaceDetailErrorCode>
|
||||
implements AsyncUseCase<GetRaceDetailQueryParams, RaceDetailOutputPort, GetRaceDetailErrorCode>
|
||||
{
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
@@ -46,11 +36,9 @@ export class GetRaceDetailUseCase
|
||||
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise<GetDriverRatingOutputPort>,
|
||||
private readonly getDriverAvatar: (input: GetDriverAvatarInputPort) => Promise<GetDriverAvatarOutputPort>,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceDetailQueryParams): Promise<Result<RaceDetailViewModel, ApplicationErrorCode<GetRaceDetailErrorCode>>> {
|
||||
async execute(params: GetRaceDetailQueryParams): Promise<Result<RaceDetailOutputPort, ApplicationErrorCode<GetRaceDetailErrorCode>>> {
|
||||
const { raceId, driverId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
@@ -58,105 +46,40 @@ export class GetRaceDetailUseCase
|
||||
return Result.err({ code: 'RACE_NOT_FOUND' });
|
||||
}
|
||||
|
||||
const [league, registeredDriverIds, membership] = await Promise.all([
|
||||
const [league, registrations, membership] = await Promise.all([
|
||||
this.leagueRepository.findById(race.leagueId),
|
||||
this.raceRegistrationRepository.getRegisteredDrivers(race.id),
|
||||
this.raceRegistrationRepository.findByRaceId(race.id),
|
||||
this.leagueMembershipRepository.getMembership(race.leagueId, driverId),
|
||||
]);
|
||||
|
||||
const drivers = await Promise.all(
|
||||
registeredDriverIds.map(id => this.driverRepository.findById(id)),
|
||||
registrations.map(registration => this.driverRepository.findById(registration.driverId.toString())),
|
||||
);
|
||||
|
||||
const entryList: RaceDetailEntryViewModel[] = [];
|
||||
for (const driver of drivers) {
|
||||
if (driver) {
|
||||
const ratingResult = await this.getDriverRating({ driverId: driver.id });
|
||||
const avatarResult = await this.getDriverAvatar({ driverId: driver.id });
|
||||
|
||||
entryList.push({
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
country: driver.country,
|
||||
avatarUrl: avatarResult.avatarUrl,
|
||||
rating: ratingResult.rating,
|
||||
isCurrentUser: driver.id === driverId,
|
||||
});
|
||||
}
|
||||
}
|
||||
const validDrivers = drivers.filter((driver): driver is NonNullable<typeof driver> => driver !== null);
|
||||
|
||||
const isUserRegistered = registeredDriverIds.includes(driverId);
|
||||
const isUserRegistered = registrations.some(reg => reg.driverId.toString() === driverId);
|
||||
const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date();
|
||||
const canRegister = !!membership && membership.status === 'active' && isUpcoming;
|
||||
|
||||
let userResultView: RaceDetailUserResultViewModel | null = null;
|
||||
let userResult: Result | null = null;
|
||||
|
||||
if (race.status === 'completed') {
|
||||
const results = await this.resultRepository.findByRaceId(race.id);
|
||||
const userResult = results.find(r => r.driverId === driverId) ?? null;
|
||||
|
||||
if (userResult) {
|
||||
const ratingChange = this.calculateRatingChange(userResult.position);
|
||||
|
||||
userResultView = {
|
||||
position: userResult.position,
|
||||
startPosition: userResult.startPosition,
|
||||
incidents: userResult.incidents,
|
||||
fastestLap: userResult.fastestLap,
|
||||
positionChange: userResult.getPositionChange(),
|
||||
isPodium: userResult.isPodium(),
|
||||
isClean: userResult.isClean(),
|
||||
ratingChange,
|
||||
};
|
||||
}
|
||||
userResult = results.find(r => r.driverId.toString() === driverId) ?? null;
|
||||
}
|
||||
|
||||
const raceView: RaceDetailRaceViewModel = {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
sessionType: race.sessionType,
|
||||
status: race.status,
|
||||
strengthOfField: race.strengthOfField ?? null,
|
||||
...(race.registeredCount !== undefined ? { registeredCount: race.registeredCount } : {}),
|
||||
...(race.maxParticipants !== undefined ? { maxParticipants: race.maxParticipants } : {}),
|
||||
const outputPort: RaceDetailOutputPort = {
|
||||
race,
|
||||
league,
|
||||
registrations,
|
||||
drivers: validDrivers,
|
||||
userResult,
|
||||
isUserRegistered,
|
||||
canRegister,
|
||||
};
|
||||
|
||||
const leagueView: RaceDetailLeagueViewModel | null = league
|
||||
? {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
settings: {
|
||||
...(league.settings.maxDrivers !== undefined
|
||||
? { maxDrivers: league.settings.maxDrivers }
|
||||
: {}),
|
||||
...(league.settings.qualifyingFormat !== undefined
|
||||
? { qualifyingFormat: league.settings.qualifyingFormat }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
const viewModel: RaceDetailViewModel = {
|
||||
race: raceView,
|
||||
league: leagueView,
|
||||
entryList,
|
||||
registration: {
|
||||
isUserRegistered,
|
||||
canRegister,
|
||||
},
|
||||
userResult: userResultView,
|
||||
};
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
|
||||
private calculateRatingChange(position: number): number {
|
||||
const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5;
|
||||
const positionBonus = Math.max(0, (20 - position) * 2);
|
||||
return baseChange + positionBonus;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { RacePenaltiesResultDTO } from '../presenters/IRacePenaltiesPresenter';
|
||||
import type { RacePenaltiesOutputPort } from '../ports/output/RacePenaltiesOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -16,13 +16,13 @@ export interface GetRacePenaltiesInput {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRacePenaltiesUseCase implements AsyncUseCase<GetRacePenaltiesInput, RacePenaltiesResultDTO, 'NO_ERROR'> {
|
||||
export class GetRacePenaltiesUseCase implements AsyncUseCase<GetRacePenaltiesInput, RacePenaltiesOutputPort, 'NO_ERROR'> {
|
||||
constructor(
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
) {}
|
||||
|
||||
async execute(input: GetRacePenaltiesInput): Promise<Result<RacePenaltiesResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(input: GetRacePenaltiesInput): Promise<Result<RacePenaltiesOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
@@ -35,17 +35,12 @@ export class GetRacePenaltiesUseCase implements AsyncUseCase<GetRacePenaltiesInp
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
);
|
||||
|
||||
const driverMap = new Map<string, string>();
|
||||
drivers.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap.set(driver.id, driver.name);
|
||||
}
|
||||
});
|
||||
const validDrivers = drivers.filter((driver): driver is NonNullable<typeof driver> => driver !== null);
|
||||
|
||||
const dto: RacePenaltiesResultDTO = {
|
||||
const outputPort: RacePenaltiesOutputPort = {
|
||||
penalties,
|
||||
driverMap,
|
||||
drivers: validDrivers,
|
||||
};
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { RaceProtestsResultDTO } from '../presenters/IRaceProtestsPresenter';
|
||||
import type { RaceProtestsOutputPort } from '../ports/output/RaceProtestsOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -16,13 +16,13 @@ export interface GetRaceProtestsInput {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRaceProtestsUseCase implements AsyncUseCase<GetRaceProtestsInput, RaceProtestsResultDTO, 'NO_ERROR'> {
|
||||
export class GetRaceProtestsUseCase implements AsyncUseCase<GetRaceProtestsInput, RaceProtestsOutputPort, 'NO_ERROR'> {
|
||||
constructor(
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
) {}
|
||||
|
||||
async execute(input: GetRaceProtestsInput): Promise<Result<RaceProtestsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(input: GetRaceProtestsInput): Promise<Result<RaceProtestsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const protests = await this.protestRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
@@ -38,17 +38,12 @@ export class GetRaceProtestsUseCase implements AsyncUseCase<GetRaceProtestsInput
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
);
|
||||
|
||||
const driverMap = new Map<string, string>();
|
||||
drivers.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap.set(driver.id, driver.name);
|
||||
}
|
||||
});
|
||||
const validDrivers = drivers.filter((driver): driver is NonNullable<typeof driver> => driver !== null);
|
||||
|
||||
const dto: RaceProtestsResultDTO = {
|
||||
const outputPort: RaceProtestsOutputPort = {
|
||||
protests,
|
||||
driverMap,
|
||||
drivers: validDrivers,
|
||||
};
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,42 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { GetRaceRegistrationsUseCase } from './GetRaceRegistrationsUseCase';
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
|
||||
describe('GetRaceRegistrationsUseCase', () => {
|
||||
let useCase: GetRaceRegistrationsUseCase;
|
||||
let registrationRepository: { getRegisteredDrivers: Mock };
|
||||
let registrationRepository: { findByRaceId: Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
registrationRepository = { getRegisteredDrivers: vi.fn() };
|
||||
registrationRepository = { findByRaceId: vi.fn() };
|
||||
useCase = new GetRaceRegistrationsUseCase(
|
||||
registrationRepository as unknown as IRaceRegistrationRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return registered driver ids', async () => {
|
||||
it('should return registrations', async () => {
|
||||
const raceId = 'race-1';
|
||||
const registeredDriverIds = ['driver-1', 'driver-2'];
|
||||
const registrations = [
|
||||
RaceRegistration.create({ raceId, driverId: 'driver-1' }),
|
||||
RaceRegistration.create({ raceId, driverId: 'driver-2' }),
|
||||
];
|
||||
|
||||
registrationRepository.getRegisteredDrivers.mockResolvedValue(registeredDriverIds);
|
||||
registrationRepository.findByRaceId.mockResolvedValue(registrations);
|
||||
|
||||
const result = await useCase.execute({ raceId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.registeredDriverIds).toEqual(registeredDriverIds);
|
||||
const outputPort = result.unwrap();
|
||||
expect(outputPort.registrations).toEqual(registrations);
|
||||
});
|
||||
|
||||
it('should return empty array when no registrations', async () => {
|
||||
registrationRepository.getRegisteredDrivers.mockResolvedValue([]);
|
||||
registrationRepository.findByRaceId.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute({ raceId: 'race-1' });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const dto = result.unwrap();
|
||||
expect(dto.registeredDriverIds).toEqual([]);
|
||||
const outputPort = result.unwrap();
|
||||
expect(outputPort.registrations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { GetRaceRegistrationsQueryParamsDTO } from '../dto/RaceRegistrationQueryDTO';
|
||||
import type { RaceRegistrationsResultDTO } from '../presenters/IRaceRegistrationsPresenter';
|
||||
import type { RaceRegistrationsOutputPort } from '../ports/output/RaceRegistrationsOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -11,19 +11,19 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
* Returns registered driver IDs for a race.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetRaceRegistrationsUseCase implements AsyncUseCase<GetRaceRegistrationsQueryParamsDTO, RaceRegistrationsResultDTO, 'NO_ERROR'> {
|
||||
export class GetRaceRegistrationsUseCase implements AsyncUseCase<GetRaceRegistrationsQueryParamsDTO, RaceRegistrationsOutputPort, 'NO_ERROR'> {
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<Result<RaceRegistrationsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(params: GetRaceRegistrationsQueryParamsDTO): Promise<Result<RaceRegistrationsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const { raceId } = params;
|
||||
const registeredDriverIds = await this.registrationRepository.getRegisteredDrivers(raceId);
|
||||
const registrations = await this.registrationRepository.findByRaceId(raceId);
|
||||
|
||||
const dto: RaceRegistrationsResultDTO = {
|
||||
registeredDriverIds,
|
||||
const outputPort: RaceRegistrationsOutputPort = {
|
||||
registrations,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
}
|
||||
@@ -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 { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { RaceResultsDetailViewModel, RaceResultsPenaltySummaryViewModel } from '../presenters/IRaceResultsDetailPresenter';
|
||||
import type { RaceResultsDetailOutputPort } from '../ports/output/RaceResultsDetailOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -19,7 +19,7 @@ export interface GetRaceResultsDetailParams {
|
||||
|
||||
type GetRaceResultsDetailErrorCode = 'RACE_NOT_FOUND';
|
||||
|
||||
export class GetRaceResultsDetailUseCase implements AsyncUseCase<GetRaceResultsDetailParams, RaceResultsDetailViewModel, GetRaceResultsDetailErrorCode> {
|
||||
export class GetRaceResultsDetailUseCase implements AsyncUseCase<GetRaceResultsDetailParams, RaceResultsDetailOutputPort, GetRaceResultsDetailErrorCode> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -28,7 +28,7 @@ export class GetRaceResultsDetailUseCase implements AsyncUseCase<GetRaceResultsD
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetRaceResultsDetailParams): Promise<Result<RaceResultsDetailViewModel, ApplicationErrorCode<GetRaceResultsDetailErrorCode>>> {
|
||||
async execute(params: GetRaceResultsDetailParams): Promise<Result<RaceResultsDetailOutputPort, ApplicationErrorCode<GetRaceResultsDetailErrorCode>>> {
|
||||
const { raceId, driverId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
@@ -49,31 +49,19 @@ export class GetRaceResultsDetailUseCase implements AsyncUseCase<GetRaceResultsD
|
||||
|
||||
const pointsSystem = this.buildPointsSystem(league);
|
||||
const fastestLapTime = this.getFastestLapTime(results);
|
||||
const penaltySummary = this.mapPenaltySummary(penalties);
|
||||
|
||||
const viewModel: RaceResultsDetailViewModel = {
|
||||
race: {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
track: race.track,
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status,
|
||||
},
|
||||
league: league
|
||||
? {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
}
|
||||
: null,
|
||||
const outputPort: RaceResultsDetailOutputPort = {
|
||||
race,
|
||||
league,
|
||||
results,
|
||||
drivers,
|
||||
penalties: penaltySummary,
|
||||
penalties,
|
||||
...(pointsSystem ? { pointsSystem } : {}),
|
||||
...(fastestLapTime !== undefined ? { fastestLapTime } : {}),
|
||||
...(effectiveCurrentDriverId ? { currentDriverId: effectiveCurrentDriverId } : {}),
|
||||
};
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
|
||||
private buildPointsSystem(league: League | null): Record<number, number> | undefined {
|
||||
@@ -129,11 +117,4 @@ export class GetRaceResultsDetailUseCase implements AsyncUseCase<GetRaceResultsD
|
||||
return Math.min(...results.map((r) => r.fastestLap));
|
||||
}
|
||||
|
||||
private mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewModel[] {
|
||||
return penalties.map((p) => ({
|
||||
driverId: p.driverId,
|
||||
type: p.type,
|
||||
...(p.value !== undefined ? { value: p.value } : {}),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort';
|
||||
import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort';
|
||||
import type { RaceWithSOFOutputPort } from '../ports/output/RaceWithSOFOutputPort';
|
||||
import {
|
||||
AverageStrengthOfFieldCalculator,
|
||||
type StrengthOfFieldCalculator,
|
||||
@@ -22,25 +23,9 @@ export interface GetRaceWithSOFQueryParams {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export interface RaceWithSOFResultDTO {
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
scheduledAt: Date;
|
||||
track: string;
|
||||
trackId: string;
|
||||
car: string;
|
||||
carId: string;
|
||||
sessionType: string;
|
||||
status: string;
|
||||
strengthOfField: number | null;
|
||||
registeredCount: number;
|
||||
maxParticipants: number;
|
||||
participantCount: number;
|
||||
}
|
||||
|
||||
type GetRaceWithSOFErrorCode = 'RACE_NOT_FOUND';
|
||||
|
||||
export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryParams, RaceWithSOFResultDTO, GetRaceWithSOFErrorCode> {
|
||||
export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryParams, RaceWithSOFOutputPort, GetRaceWithSOFErrorCode> {
|
||||
private readonly sofCalculator: StrengthOfFieldCalculator;
|
||||
|
||||
constructor(
|
||||
@@ -53,7 +38,7 @@ export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryPa
|
||||
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
|
||||
}
|
||||
|
||||
async execute(params: GetRaceWithSOFQueryParams): Promise<Result<RaceWithSOFResultDTO, ApplicationErrorCode<GetRaceWithSOFErrorCode>>> {
|
||||
async execute(params: GetRaceWithSOFQueryParams): Promise<Result<RaceWithSOFOutputPort, ApplicationErrorCode<GetRaceWithSOFErrorCode>>> {
|
||||
const { raceId } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
@@ -93,15 +78,12 @@ export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryPa
|
||||
strengthOfField = this.sofCalculator.calculate(driverRatings);
|
||||
}
|
||||
|
||||
const dto: RaceWithSOFResultDTO = {
|
||||
raceId: race.id,
|
||||
const outputPort: RaceWithSOFOutputPort = {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
scheduledAt: race.scheduledAt,
|
||||
track: race.track ?? '',
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car ?? '',
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType.props,
|
||||
status: race.status,
|
||||
strengthOfField,
|
||||
registeredCount: race.registeredCount ?? participantIds.length,
|
||||
@@ -109,6 +91,6 @@ export class GetRaceWithSOFUseCase implements AsyncUseCase<GetRaceWithSOFQueryPa
|
||||
participantCount: participantIds.length,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { RacesPageResultDTO } from '@core/racing/application/presenters/IRacesPagePresenter';
|
||||
import type { RacesPageOutputPort } from '../ports/output/RacesPageOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export class GetRacesPageDataUseCase implements AsyncUseCase<void, RacesPageResultDTO, 'NO_ERROR'> {
|
||||
export class GetRacesPageDataUseCase implements AsyncUseCase<void, RacesPageOutputPort, 'NO_ERROR'> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<RacesPageResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(): Promise<Result<RacesPageOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const [allRaces, allLeagues] = await Promise.all([
|
||||
this.raceRepository.findAll(),
|
||||
this.leagueRepository.findAll(),
|
||||
@@ -25,20 +25,19 @@ export class GetRacesPageDataUseCase implements AsyncUseCase<void, RacesPageResu
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
scheduledAt: race.scheduledAt,
|
||||
status: race.status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
|
||||
strengthOfField: race.strengthOfField,
|
||||
isUpcoming: race.isUpcoming(),
|
||||
isLive: race.isLive(),
|
||||
isPast: race.isPast(),
|
||||
}));
|
||||
|
||||
const dto: RacesPageResultDTO = {
|
||||
const outputPort: RacesPageOutputPort = {
|
||||
page: 1,
|
||||
pageSize: races.length,
|
||||
totalCount: races.length,
|
||||
races,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { SponsorDashboardViewModel } from '../presenters/ISponsorDashboardPresenter';
|
||||
import type { SponsorDashboardOutputPort } from '../ports/output/SponsorDashboardOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -61,7 +61,7 @@ export class GetSponsorDashboardUseCase {
|
||||
|
||||
async execute(
|
||||
params: GetSponsorDashboardQueryParams,
|
||||
): Promise<Result<SponsorDashboardViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
): Promise<Result<SponsorDashboardOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const { sponsorId } = params;
|
||||
|
||||
@@ -146,7 +146,7 @@ export class GetSponsorDashboardUseCase {
|
||||
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
|
||||
: 0;
|
||||
|
||||
const dto: SponsorDashboardDTO = {
|
||||
const outputPort: SponsorDashboardOutputPort = {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics: {
|
||||
@@ -167,7 +167,7 @@ export class GetSponsorDashboardUseCase {
|
||||
},
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsor dashboard' });
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { SponsorSponsorshipsViewModel } from '../presenters/ISponsorSponsorshipsPresenter';
|
||||
import type { SponsorSponsorshipsOutputPort } from '../ports/output/SponsorSponsorshipsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -76,7 +76,7 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
|
||||
async execute(
|
||||
params: GetSponsorSponsorshipsQueryParams,
|
||||
): Promise<Result<SponsorSponsorshipsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
): Promise<Result<SponsorSponsorshipsOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const { sponsorId } = params;
|
||||
|
||||
@@ -153,7 +153,7 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
|
||||
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
||||
|
||||
const dto: SponsorSponsorshipsDTO = {
|
||||
const outputPort: SponsorSponsorshipsOutputPort = {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
@@ -166,7 +166,7 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
},
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsor sponsorships' });
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { GetSponsorsViewModel } from '../presenters/IGetSponsorsPresenter';
|
||||
import type { GetSponsorsOutputPort } from '../ports/output/GetSponsorsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -14,11 +14,11 @@ export class GetSponsorsUseCase {
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<GetSponsorsViewModel, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(): Promise<Result<GetSponsorsOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const sponsors = await this.sponsorRepository.findAll();
|
||||
|
||||
const viewModel: GetSponsorsViewModel = {
|
||||
const outputPort: GetSponsorsOutputPort = {
|
||||
sponsors: sponsors.map(sponsor => ({
|
||||
id: sponsor.id,
|
||||
name: sponsor.name,
|
||||
@@ -29,7 +29,7 @@ export class GetSponsorsUseCase {
|
||||
})),
|
||||
};
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsors' });
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
* Retrieves general sponsorship pricing tiers.
|
||||
*/
|
||||
|
||||
import type { GetSponsorshipPricingViewModel } from '../presenters/IGetSponsorshipPricingPresenter';
|
||||
import type { GetSponsorshipPricingOutputPort } from '../ports/output/GetSponsorshipPricingOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
export class GetSponsorshipPricingUseCase {
|
||||
constructor() {}
|
||||
|
||||
async execute(): Promise<Result<GetSponsorshipPricingViewModel, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const viewModel: GetSponsorshipPricingViewModel = {
|
||||
async execute(): Promise<Result<GetSponsorshipPricingOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const outputPort: GetSponsorshipPricingOutputPort = {
|
||||
entityType: 'season',
|
||||
entityId: '',
|
||||
pricing: [
|
||||
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
|
||||
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
|
||||
@@ -20,6 +22,6 @@ export class GetSponsorshipPricingUseCase {
|
||||
],
|
||||
};
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { TeamDetailsViewModel } from '../presenters/ITeamDetailsPresenter';
|
||||
import type { GetTeamDetailsOutputPort } from '../ports/output/GetTeamDetailsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -15,7 +15,7 @@ export class GetTeamDetailsUseCase {
|
||||
|
||||
async execute(
|
||||
params: { teamId: string; driverId: string },
|
||||
): Promise<Result<TeamDetailsViewModel, ApplicationErrorCode<'TEAM_NOT_FOUND' | 'REPOSITORY_ERROR'>>> {
|
||||
): Promise<Result<GetTeamDetailsOutputPort, ApplicationErrorCode<'TEAM_NOT_FOUND' | 'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const { teamId, driverId } = params;
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
@@ -25,25 +25,25 @@ export class GetTeamDetailsUseCase {
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
|
||||
const viewModel: TeamDetailsViewModel = {
|
||||
const outputPort: GetTeamDetailsOutputPort = {
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
name: team.name.value,
|
||||
tag: team.tag?.value ?? '',
|
||||
description: team.description?.value ?? '',
|
||||
ownerId: team.ownerId,
|
||||
leagues: team.leagues,
|
||||
createdAt: team.createdAt.toISOString(),
|
||||
leagues: team.leagues.map(l => l), // assuming leagues are strings
|
||||
createdAt: team.createdAt instanceof Date ? team.createdAt : new Date(team.createdAt.value),
|
||||
},
|
||||
membership: membership ? {
|
||||
role: membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
joinedAt: membership.joinedAt instanceof Date ? membership.joinedAt : new Date(membership.joinedAt),
|
||||
isActive: membership.status === 'active',
|
||||
} : null,
|
||||
canManage: membership ? membership.role === 'owner' || membership.role === 'manager' : false,
|
||||
};
|
||||
|
||||
return Result.ok(viewModel);
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch team details' });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
|
||||
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
|
||||
import type { TeamJoinRequestsResultDTO } from '../presenters/ITeamJoinRequestsPresenter';
|
||||
import type { TeamJoinRequestsOutputPort } from '../ports/output/TeamJoinRequestsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
@@ -11,7 +11,7 @@ import type { Logger } from '@core/shared/application';
|
||||
/**
|
||||
* Use Case for retrieving team join requests.
|
||||
*/
|
||||
export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string }, TeamJoinRequestsResultDTO, 'REPOSITORY_ERROR'>
|
||||
export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string }, TeamJoinRequestsOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
@@ -20,7 +20,7 @@ export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: { teamId: string }): Promise<Result<TeamJoinRequestsResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(input: { teamId: string }): Promise<Result<TeamJoinRequestsOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
this.logger.debug('Executing GetTeamJoinRequestsUseCase', { teamId: input.teamId });
|
||||
|
||||
try {
|
||||
@@ -33,7 +33,7 @@ export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string
|
||||
for (const request of requests) {
|
||||
const driver = await this.driverRepository.findById(request.driverId);
|
||||
if (driver) {
|
||||
driverNames[request.driverId] = driver.name;
|
||||
driverNames[request.driverId] = driver.name.value;
|
||||
} else {
|
||||
this.logger.warn(`Driver not found for ID: ${request.driverId} during join request processing.`);
|
||||
}
|
||||
@@ -43,13 +43,23 @@ export class GetTeamJoinRequestsUseCase implements AsyncUseCase<{ teamId: string
|
||||
this.logger.debug('Processed driver details for join request', { driverId: request.driverId });
|
||||
}
|
||||
|
||||
const dto: TeamJoinRequestsResultDTO = {
|
||||
requests,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
const requestsViewModel = requests.map(request => ({
|
||||
requestId: request.id,
|
||||
driverId: request.driverId,
|
||||
driverName: driverNames[request.driverId] || 'Unknown',
|
||||
teamId: request.teamId,
|
||||
status: request.status as 'pending' | 'approved' | 'rejected',
|
||||
requestedAt: request.requestedAt instanceof Date ? request.requestedAt : new Date(request.requestedAt),
|
||||
avatarUrl: avatarUrls[request.driverId] || '',
|
||||
}));
|
||||
|
||||
const outputPort: TeamJoinRequestsOutputPort = {
|
||||
requests: requestsViewModel,
|
||||
pendingCount: requests.filter(r => r.status === 'pending').length,
|
||||
totalCount: requests.length,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving team join requests', { teamId: input.teamId, err: error });
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve team join requests' } });
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
|
||||
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
|
||||
import type { TeamMembersResultDTO } from '../presenters/ITeamMembersPresenter';
|
||||
import type { TeamMembersOutputPort } from '../ports/output/TeamMembersOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
@@ -11,7 +11,7 @@ import type { Logger } from '@core/shared/application';
|
||||
/**
|
||||
* Use Case for retrieving team members.
|
||||
*/
|
||||
export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, TeamMembersResultDTO, 'REPOSITORY_ERROR'>
|
||||
export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, TeamMembersOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
@@ -20,7 +20,7 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: { teamId: string }): Promise<Result<TeamMembersResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(input: { teamId: string }): Promise<Result<TeamMembersOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
this.logger.debug(`Executing GetTeamMembersUseCase for teamId: ${input.teamId}`);
|
||||
|
||||
try {
|
||||
@@ -34,7 +34,7 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T
|
||||
this.logger.debug(`Processing membership for driverId: ${membership.driverId}`);
|
||||
const driver = await this.driverRepository.findById(membership.driverId);
|
||||
if (driver) {
|
||||
driverNames[membership.driverId] = driver.name;
|
||||
driverNames[membership.driverId] = driver.name.value;
|
||||
} else {
|
||||
this.logger.warn(`Driver with ID ${membership.driverId} not found while fetching team members for team ${input.teamId}.`);
|
||||
}
|
||||
@@ -43,13 +43,24 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T
|
||||
avatarUrls[membership.driverId] = avatarResult.avatarUrl;
|
||||
}
|
||||
|
||||
const dto: TeamMembersResultDTO = {
|
||||
memberships,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
const members = memberships.map(membership => ({
|
||||
driverId: membership.driverId,
|
||||
driverName: driverNames[membership.driverId] || 'Unknown',
|
||||
role: membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: membership.joinedAt instanceof Date ? membership.joinedAt : new Date(membership.joinedAt),
|
||||
isActive: membership.status === 'active',
|
||||
avatarUrl: avatarUrls[membership.driverId] || '',
|
||||
}));
|
||||
|
||||
const outputPort: TeamMembersOutputPort = {
|
||||
members,
|
||||
totalCount: memberships.length,
|
||||
ownerCount: memberships.filter(m => m.role === 'owner').length,
|
||||
managerCount: memberships.filter(m => m.role === 'manager').length,
|
||||
memberCount: memberships.filter(m => m.role === 'member').length,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(outputPort);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in GetTeamMembersUseCase for teamId: ${input.teamId}`, error as Error, { teamId: input.teamId });
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve team members' } });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { TeamsLeaderboardResultDTO } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import type { TeamsLeaderboardOutputPort, SkillLevel } from '../ports/output/TeamsLeaderboardOutputPort';
|
||||
import { SkillLevelService } from '@core/racing/domain/services/SkillLevelService';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -30,7 +30,7 @@ interface TeamLeaderboardItem {
|
||||
/**
|
||||
* Use case: GetTeamsLeaderboardUseCase
|
||||
*/
|
||||
export class GetTeamsLeaderboardUseCase implements AsyncUseCase<void, TeamsLeaderboardResultDTO, 'REPOSITORY_ERROR'>
|
||||
export class GetTeamsLeaderboardUseCase implements AsyncUseCase<void, TeamsLeaderboardOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
@@ -40,7 +40,7 @@ export class GetTeamsLeaderboardUseCase implements AsyncUseCase<void, TeamsLeade
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<TeamsLeaderboardResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(): Promise<Result<TeamsLeaderboardOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const allTeams = await this.teamRepository.findAll();
|
||||
const teams: TeamLeaderboardItem[] = [];
|
||||
@@ -88,12 +88,28 @@ export class GetTeamsLeaderboardUseCase implements AsyncUseCase<void, TeamsLeade
|
||||
|
||||
const recruitingCount = teams.filter((t) => t.isRecruiting).length;
|
||||
|
||||
const result: TeamsLeaderboardResultDTO = {
|
||||
teams,
|
||||
recruitingCount,
|
||||
const groupsBySkillLevel: Record<SkillLevel, typeof teams> = {
|
||||
beginner: [],
|
||||
intermediate: [],
|
||||
advanced: [],
|
||||
pro: [],
|
||||
};
|
||||
|
||||
return Result.ok(result);
|
||||
teams.forEach(team => {
|
||||
const level = team.performanceLevel as SkillLevel;
|
||||
groupsBySkillLevel[level].push(team);
|
||||
});
|
||||
|
||||
const topTeams = teams.slice(0, 10); // Assuming top 10
|
||||
|
||||
const outputPort: TeamsLeaderboardOutputPort = {
|
||||
teams,
|
||||
recruitingCount,
|
||||
groupsBySkillLevel,
|
||||
topTeams,
|
||||
};
|
||||
|
||||
return Result.ok(outputPort);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving teams leaderboard', error as Error);
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve teams leaderboard' } });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { TotalDriversResultDTO } from '../presenters/ITotalDriversPresenter';
|
||||
import type { TotalDriversOutputPort } from '../ports/output/TotalDriversOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
@@ -8,21 +8,21 @@ import type { Logger } from '@core/shared/application';
|
||||
/**
|
||||
* Use Case for retrieving total number of drivers.
|
||||
*/
|
||||
export class GetTotalDriversUseCase implements AsyncUseCase<void, TotalDriversResultDTO, 'REPOSITORY_ERROR'>
|
||||
export class GetTotalDriversUseCase implements AsyncUseCase<void, TotalDriversOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<TotalDriversResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(): Promise<Result<TotalDriversOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const drivers = await this.driverRepository.findAll();
|
||||
const dto: TotalDriversResultDTO = {
|
||||
const output: TotalDriversOutputPort = {
|
||||
totalDrivers: drivers.length,
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving total drivers', error as Error);
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve total drivers' } });
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { GetTotalLeaguesResultDTO } from '../presenters/IGetTotalLeaguesPresenter';
|
||||
import type { GetTotalLeaguesOutputPort } from '../ports/output/GetTotalLeaguesOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
export class GetTotalLeaguesUseCase implements AsyncUseCase<void, GetTotalLeaguesResultDTO, 'REPOSITORY_ERROR'>
|
||||
export class GetTotalLeaguesUseCase implements AsyncUseCase<void, GetTotalLeaguesOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<GetTotalLeaguesResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(): Promise<Result<GetTotalLeaguesOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
const dto: GetTotalLeaguesResultDTO = { totalLeagues: leagues.length };
|
||||
const output: GetTotalLeaguesOutputPort = { totalLeagues: leagues.length };
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving total leagues', error as Error);
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve total leagues' } });
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { GetTotalRacesResultDTO } from '../presenters/IGetTotalRacesPresenter';
|
||||
import type { GetTotalRacesOutputPort } from '../ports/output/GetTotalRacesOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
export class GetTotalRacesUseCase implements AsyncUseCase<void, GetTotalRacesResultDTO, 'REPOSITORY_ERROR'>
|
||||
export class GetTotalRacesUseCase implements AsyncUseCase<void, GetTotalRacesOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<GetTotalRacesResultDTO, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
async execute(): Promise<Result<GetTotalRacesOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
const races = await this.raceRepository.findAll();
|
||||
const dto: GetTotalRacesResultDTO = { totalRaces: races.length };
|
||||
const output: GetTotalRacesOutputPort = { totalRaces: races.length };
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving total races', error as Error);
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'Failed to retrieve total races' } });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Result } from '../../domain/entities/Result';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result as SharedResult } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { ImportRaceResultsApiResultDTO } from '../presenters/IImportRaceResultsApiPresenter';
|
||||
import type { ImportRaceResultsApiOutputPort } from '../ports/output/ImportRaceResultsApiOutputPort';
|
||||
|
||||
export interface ImportRaceResultDTO {
|
||||
id: string;
|
||||
@@ -29,7 +29,7 @@ type ImportRaceResultsApiErrorCode =
|
||||
|
||||
type ImportRaceResultsApiApplicationError = ApplicationErrorCode<ImportRaceResultsApiErrorCode, { message: string }>;
|
||||
|
||||
export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: string; resultsFileContent: string }, ImportRaceResultsApiResultDTO, ImportRaceResultsApiErrorCode> {
|
||||
export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: string; resultsFileContent: string }, ImportRaceResultsApiOutputPort, ImportRaceResultsApiErrorCode> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -39,7 +39,7 @@ export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: strin
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(params: { raceId: string; resultsFileContent: string }): Promise<SharedResult<ImportRaceResultsApiResultDTO, ApplicationErrorCode<ImportRaceResultsApiErrorCode>>> {
|
||||
async execute(params: { raceId: string; resultsFileContent: string }): Promise<SharedResult<ImportRaceResultsApiOutputPort, ApplicationErrorCode<ImportRaceResultsApiErrorCode>>> {
|
||||
this.logger.debug('ImportRaceResultsApiUseCase:execute', { raceId: params.raceId });
|
||||
const { raceId, resultsFileContent } = params;
|
||||
|
||||
@@ -107,9 +107,10 @@ export class ImportRaceResultsApiUseCase implements AsyncUseCase<{ raceId: strin
|
||||
await this.standingRepository.recalculate(league.id);
|
||||
this.logger.info('ImportRaceResultsApiUseCase:standings recalculated', { leagueId: league.id });
|
||||
|
||||
const dto: ImportRaceResultsApiResultDTO = {
|
||||
const output: ImportRaceResultsApiOutputPort = {
|
||||
success: true,
|
||||
raceId,
|
||||
leagueId: league.id,
|
||||
driversProcessed: results.length,
|
||||
resultsRecorded: validEntities.length,
|
||||
errors: [],
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IsDriverRegisteredForRaceQueryParamsDTO } from '../dto/RaceRegistr
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result as SharedResult } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { DriverRegistrationStatusOutputPort } from '../ports/output/DriverRegistrationStatusOutputPort';
|
||||
|
||||
type IsDriverRegisteredForRaceErrorCode = 'REPOSITORY_ERROR';
|
||||
|
||||
@@ -14,20 +15,20 @@ type IsDriverRegisteredForRaceApplicationError = ApplicationErrorCode<IsDriverRe
|
||||
* Checks if a driver is registered for a specific race.
|
||||
*/
|
||||
export class IsDriverRegisteredForRaceUseCase
|
||||
implements AsyncUseCase<IsDriverRegisteredForRaceQueryParamsDTO, boolean, IsDriverRegisteredForRaceErrorCode>
|
||||
implements AsyncUseCase<IsDriverRegisteredForRaceQueryParamsDTO, DriverRegistrationStatusOutputPort, IsDriverRegisteredForRaceErrorCode>
|
||||
{
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(params: IsDriverRegisteredForRaceQueryParamsDTO): Promise<SharedResult<boolean, IsDriverRegisteredForRaceApplicationError>> {
|
||||
async execute(params: IsDriverRegisteredForRaceQueryParamsDTO): Promise<SharedResult<DriverRegistrationStatusOutputPort, IsDriverRegisteredForRaceApplicationError>> {
|
||||
this.logger.debug('IsDriverRegisteredForRaceUseCase:execute', { params });
|
||||
const { raceId, driverId } = params;
|
||||
|
||||
try {
|
||||
const isRegistered = await this.registrationRepository.isRegistered(raceId, driverId);
|
||||
return SharedResult.ok(isRegistered);
|
||||
return SharedResult.ok({ isRegistered, raceId, driverId });
|
||||
} catch (error) {
|
||||
this.logger.error('IsDriverRegisteredForRaceUseCase:execution error', error instanceof Error ? error : new Error('Unknown error'));
|
||||
return SharedResult.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } });
|
||||
|
||||
@@ -50,12 +50,9 @@ describe('JoinLeagueUseCase', () => {
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
id: 'membership-1',
|
||||
membershipId: 'membership-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,7 +73,6 @@ describe('JoinLeagueUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'ALREADY_MEMBER',
|
||||
details: { message: 'Already a member or have a pending request' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,7 +87,6 @@ describe('JoinLeagueUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: 'Repository error' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,14 +4,13 @@ import { LeagueMembership, type MembershipRole, type MembershipStatus } from '..
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result as SharedResult } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { JoinLeagueOutputPort } from '../ports/output/JoinLeagueOutputPort';
|
||||
|
||||
import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO';
|
||||
|
||||
type JoinLeagueErrorCode = 'ALREADY_MEMBER' | 'REPOSITORY_ERROR';
|
||||
|
||||
type JoinLeagueApplicationError = ApplicationErrorCode<JoinLeagueErrorCode, { message: string }>;
|
||||
|
||||
export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, LeagueMembership, JoinLeagueErrorCode> {
|
||||
export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, JoinLeagueOutputPort, JoinLeagueErrorCode> {
|
||||
constructor(
|
||||
private readonly membershipRepository: ILeagueMembershipRepository,
|
||||
private readonly logger: Logger,
|
||||
@@ -24,7 +23,7 @@ export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, Lea
|
||||
* - Returns error when membership already exists for this league/driver.
|
||||
* - Creates a new active membership with role "member" and current timestamp.
|
||||
*/
|
||||
async execute(command: JoinLeagueCommandDTO): Promise<SharedResult<LeagueMembership, JoinLeagueApplicationError>> {
|
||||
async execute(command: JoinLeagueCommandDTO): Promise<SharedResult<JoinLeagueOutputPort, ApplicationErrorCode<JoinLeagueErrorCode>>> {
|
||||
this.logger.debug('Attempting to join league', { command });
|
||||
const { leagueId, driverId } = command;
|
||||
|
||||
@@ -32,7 +31,7 @@ export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, Lea
|
||||
const existing = await this.membershipRepository.getMembership(leagueId, driverId);
|
||||
if (existing) {
|
||||
this.logger.warn('Driver already a member or has pending request', { leagueId, driverId });
|
||||
return SharedResult.err({ code: 'ALREADY_MEMBER', details: { message: 'Already a member or have a pending request' } });
|
||||
return SharedResult.err({ code: 'ALREADY_MEMBER' });
|
||||
}
|
||||
|
||||
const membership = LeagueMembership.create({
|
||||
@@ -44,10 +43,14 @@ export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, Lea
|
||||
|
||||
const savedMembership = await this.membershipRepository.saveMembership(membership);
|
||||
this.logger.info('Successfully joined league', { membershipId: savedMembership.id });
|
||||
return SharedResult.ok(savedMembership);
|
||||
return SharedResult.ok({
|
||||
membershipId: savedMembership.id,
|
||||
leagueId: savedMembership.leagueId.toString(),
|
||||
status: savedMembership.status.toString(),
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to join league due to an unexpected error', error instanceof Error ? error : new Error('Unknown error'));
|
||||
return SharedResult.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } });
|
||||
return SharedResult.err({ code: 'REPOSITORY_ERROR' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,60 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { ListLeagueScoringPresetsUseCase } from './ListLeagueScoringPresetsUseCase';
|
||||
import { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
|
||||
describe('ListLeagueScoringPresetsUseCase', () => {
|
||||
let useCase: ListLeagueScoringPresetsUseCase;
|
||||
let presetProvider: {
|
||||
listPresets: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
presetProvider = {
|
||||
listPresets: vi.fn(),
|
||||
};
|
||||
useCase = new ListLeagueScoringPresetsUseCase(
|
||||
presetProvider as unknown as LeagueScoringPresetProvider,
|
||||
);
|
||||
const mockPresets = [
|
||||
{
|
||||
id: 'preset-1',
|
||||
name: 'Preset 1',
|
||||
description: 'Desc 1',
|
||||
primaryChampionshipType: 'driver' as const,
|
||||
dropPolicySummary: 'Drop 1',
|
||||
sessionSummary: 'Session 1',
|
||||
bonusSummary: 'Bonus 1',
|
||||
createConfig: vi.fn(),
|
||||
},
|
||||
{
|
||||
id: 'preset-2',
|
||||
name: 'Preset 2',
|
||||
description: 'Desc 2',
|
||||
primaryChampionshipType: 'team' as const,
|
||||
dropPolicySummary: 'Drop 2',
|
||||
sessionSummary: 'Session 2',
|
||||
bonusSummary: 'Bonus 2',
|
||||
createConfig: vi.fn(),
|
||||
},
|
||||
];
|
||||
useCase = new ListLeagueScoringPresetsUseCase(mockPresets);
|
||||
});
|
||||
|
||||
it('should list presets successfully', async () => {
|
||||
const mockPresets = [
|
||||
{ id: 'preset-1', name: 'Preset 1' },
|
||||
{ id: 'preset-2', name: 'Preset 2' },
|
||||
];
|
||||
|
||||
presetProvider.listPresets.mockResolvedValue(mockPresets);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({
|
||||
presets: mockPresets,
|
||||
presets: [
|
||||
{
|
||||
id: 'preset-1',
|
||||
name: 'Preset 1',
|
||||
description: 'Desc 1',
|
||||
primaryChampionshipType: 'driver',
|
||||
sessionSummary: 'Session 1',
|
||||
bonusSummary: 'Bonus 1',
|
||||
dropPolicySummary: 'Drop 1',
|
||||
},
|
||||
{
|
||||
id: 'preset-2',
|
||||
name: 'Preset 2',
|
||||
description: 'Desc 2',
|
||||
primaryChampionshipType: 'team',
|
||||
sessionSummary: 'Session 2',
|
||||
bonusSummary: 'Bonus 2',
|
||||
dropPolicySummary: 'Drop 2',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
|
||||
import type { LeagueScoringPresetsResultDTO } from '../presenters/ILeagueScoringPresetsPresenter';
|
||||
import type { LeagueScoringPresetsOutputPort } from '../ports/output/LeagueScoringPresetsOutputPort';
|
||||
import type { LeagueScoringPreset } from '../../../bootstrap/LeagueScoringPresets';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -9,15 +10,23 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
* Returns preset data without business logic.
|
||||
*/
|
||||
export class ListLeagueScoringPresetsUseCase
|
||||
implements AsyncUseCase<void, LeagueScoringPresetsResultDTO, 'NO_ERROR'>
|
||||
implements AsyncUseCase<void, LeagueScoringPresetsOutputPort, 'NO_ERROR'>
|
||||
{
|
||||
constructor(private readonly presets: LeagueScoringPresetOutputPort[]) {}
|
||||
constructor(private readonly presets: LeagueScoringPreset[]) {}
|
||||
|
||||
async execute(): Promise<Result<LeagueScoringPresetsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const dto: LeagueScoringPresetsResultDTO = {
|
||||
presets: this.presets,
|
||||
async execute(): Promise<Result<LeagueScoringPresetsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
const output: LeagueScoringPresetsOutputPort = {
|
||||
presets: this.presets.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
primaryChampionshipType: p.primaryChampionshipType,
|
||||
sessionSummary: p.sessionSummary,
|
||||
bonusSummary: p.bonusSummary,
|
||||
dropPolicySummary: p.dropPolicySummary,
|
||||
} as LeagueScoringPresetOutputPort)),
|
||||
};
|
||||
|
||||
return Result.ok(dto);
|
||||
return Result.ok(output);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator';
|
||||
import type { LeagueSchedulePreviewDTO, LeagueScheduleDTO } from '../dto/LeagueScheduleDTO';
|
||||
import type { LeagueScheduleDTO } from '../dto/LeagueScheduleDTO';
|
||||
import { scheduleDTOToSeasonSchedule } from '../dto/LeagueScheduleDTO';
|
||||
import type { LeagueSchedulePreviewOutputPort } from '../ports/output/LeagueSchedulePreviewOutputPort';
|
||||
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -16,13 +17,13 @@ type PreviewLeagueScheduleErrorCode = 'INVALID_SCHEDULE';
|
||||
|
||||
type PreviewLeagueScheduleApplicationError = ApplicationErrorCode<PreviewLeagueScheduleErrorCode, { message: string }>;
|
||||
|
||||
export class PreviewLeagueScheduleUseCase implements AsyncUseCase<PreviewLeagueScheduleQueryParams, LeagueSchedulePreviewDTO, PreviewLeagueScheduleErrorCode> {
|
||||
export class PreviewLeagueScheduleUseCase implements AsyncUseCase<PreviewLeagueScheduleQueryParams, LeagueSchedulePreviewOutputPort, PreviewLeagueScheduleErrorCode> {
|
||||
constructor(
|
||||
private readonly scheduleGenerator: typeof SeasonScheduleGenerator = SeasonScheduleGenerator,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(params: PreviewLeagueScheduleQueryParams): Promise<Result<LeagueSchedulePreviewDTO, PreviewLeagueScheduleApplicationError>> {
|
||||
async execute(params: PreviewLeagueScheduleQueryParams): Promise<Result<LeagueSchedulePreviewOutputPort, PreviewLeagueScheduleApplicationError>> {
|
||||
this.logger.debug('Previewing league schedule', { params });
|
||||
|
||||
let seasonSchedule: SeasonSchedule;
|
||||
@@ -48,7 +49,7 @@ export class PreviewLeagueScheduleUseCase implements AsyncUseCase<PreviewLeagueS
|
||||
|
||||
const summary = this.buildSummary(params.schedule, rounds);
|
||||
|
||||
const result: LeagueSchedulePreviewDTO = {
|
||||
const result: LeagueSchedulePreviewOutputPort = {
|
||||
rounds,
|
||||
summary,
|
||||
};
|
||||
|
||||
@@ -2,22 +2,18 @@ import type { ILeagueMembershipRepository } from '../../domain/repositories/ILea
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { RejectLeagueJoinRequestOutputPort } from '../ports/output/RejectLeagueJoinRequestOutputPort';
|
||||
|
||||
export interface RejectLeagueJoinRequestUseCaseParams {
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface RejectLeagueJoinRequestResultDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class RejectLeagueJoinRequestUseCase implements AsyncUseCase<RejectLeagueJoinRequestUseCaseParams, RejectLeagueJoinRequestResultDTO, 'NO_ERROR'> {
|
||||
export class RejectLeagueJoinRequestUseCase implements AsyncUseCase<RejectLeagueJoinRequestUseCaseParams, RejectLeagueJoinRequestOutputPort, 'NO_ERROR'> {
|
||||
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
|
||||
|
||||
async execute(params: RejectLeagueJoinRequestUseCaseParams): Promise<Result<RejectLeagueJoinRequestResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
async execute(params: RejectLeagueJoinRequestUseCaseParams): Promise<Result<RejectLeagueJoinRequestOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
|
||||
await this.leagueMembershipRepository.removeJoinRequest(params.requestId);
|
||||
const dto: RejectLeagueJoinRequestResultDTO = { success: true, message: 'Join request rejected.' };
|
||||
return Result.ok(dto);
|
||||
const port: RejectLeagueJoinRequestOutputPort = { success: true, message: 'Join request rejected.' };
|
||||
return Result.ok(port);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { RemoveLeagueMemberOutputPort } from '../ports/output/RemoveLeagueMemberOutputPort';
|
||||
|
||||
export interface RemoveLeagueMemberUseCaseParams {
|
||||
leagueId: string;
|
||||
targetDriverId: string;
|
||||
}
|
||||
|
||||
export interface RemoveLeagueMemberResultDTO {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberUseCase {
|
||||
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
|
||||
|
||||
async execute(params: RemoveLeagueMemberUseCaseParams): Promise<Result<RemoveLeagueMemberResultDTO, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND'>>> {
|
||||
async execute(params: RemoveLeagueMemberUseCaseParams): Promise<Result<RemoveLeagueMemberOutputPort, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND'>>> {
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
|
||||
const membership = memberships.find(m => m.driverId === params.targetDriverId);
|
||||
if (!membership) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeague
|
||||
import type {
|
||||
MembershipRole,
|
||||
} from '@core/racing/domain/entities/LeagueMembership';
|
||||
import type { TransferLeagueOwnershipResultDTO } from '../presenters/ITransferLeagueOwnershipPresenter';
|
||||
import type { TransferLeagueOwnershipOutputPort } from '../ports/output/TransferLeagueOwnershipOutputPort';
|
||||
|
||||
export interface TransferLeagueOwnershipCommandDTO {
|
||||
leagueId: string;
|
||||
@@ -23,7 +23,7 @@ export class TransferLeagueOwnershipUseCase {
|
||||
private readonly membershipRepository: ILeagueMembershipRepository
|
||||
) {}
|
||||
|
||||
async execute(command: TransferLeagueOwnershipCommandDTO): Promise<Result<void, ApplicationErrorCode<TransferLeagueOwnershipErrorCode>>> {
|
||||
async execute(command: TransferLeagueOwnershipCommandDTO): Promise<Result<TransferLeagueOwnershipOutputPort, ApplicationErrorCode<TransferLeagueOwnershipErrorCode>>> {
|
||||
const { leagueId, currentOwnerId, newOwnerId } = command;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
@@ -57,6 +57,6 @@ export class TransferLeagueOwnershipUseCase {
|
||||
const updatedLeague = league.update({ ownerId: newOwnerId });
|
||||
await this.leagueRepository.update(updatedLeague);
|
||||
|
||||
return Result.ok(undefined);
|
||||
return Result.ok({ success: true });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { UpdateLeagueMemberRoleResultDTO } from '../presenters/IUpdateLeagueMemberRolePresenter';
|
||||
import type { UpdateLeagueMemberRoleOutputPort } from '../ports/output/UpdateLeagueMemberRoleOutputPort';
|
||||
|
||||
export interface UpdateLeagueMemberRoleUseCaseParams {
|
||||
leagueId: string;
|
||||
@@ -12,7 +12,7 @@ export interface UpdateLeagueMemberRoleUseCaseParams {
|
||||
export class UpdateLeagueMemberRoleUseCase {
|
||||
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
|
||||
|
||||
async execute(params: UpdateLeagueMemberRoleUseCaseParams): Promise<Result<UpdateLeagueMemberRoleResultDTO, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND'>>> {
|
||||
async execute(params: UpdateLeagueMemberRoleUseCaseParams): Promise<Result<UpdateLeagueMemberRoleOutputPort, ApplicationErrorCode<'MEMBERSHIP_NOT_FOUND'>>> {
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
|
||||
const membership = memberships.find(m => m.driverId === params.targetDriverId);
|
||||
if (!membership) {
|
||||
|
||||
Reference in New Issue
Block a user