fix issues in core

This commit is contained in:
2025-12-23 15:38:50 +01:00
parent df5c20c5cc
commit 120d3bb1a1
125 changed files with 1005 additions and 793 deletions

View File

@@ -18,4 +18,66 @@ export interface LeagueSchedulePreviewDTO {
scheduledTime: Date;
trackId: string;
}>;
}
export type SeasonScheduleConfigDTO = {
seasonStartDate: string;
recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
weekdays?: string[];
raceStartTime: string;
timezoneId: string;
plannedRounds: number;
intervalWeeks?: number;
monthlyOrdinal?: 1 | 2 | 3 | 4;
monthlyWeekday?: string;
};
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
import { ALL_WEEKDAYS, type Weekday } from '../../domain/types/Weekday';
function toWeekdaySet(values: string[] | undefined): WeekdaySet {
const weekdays = (values ?? []).filter((v): v is Weekday =>
ALL_WEEKDAYS.includes(v as Weekday),
);
return WeekdaySet.fromArray(weekdays.length > 0 ? weekdays : ['Mon']);
}
export function scheduleDTOToSeasonSchedule(dto: SeasonScheduleConfigDTO): SeasonSchedule {
const startDate = new Date(dto.seasonStartDate);
const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime);
const timezone = LeagueTimezone.create(dto.timezoneId);
const recurrence = (() => {
switch (dto.recurrenceStrategy) {
case 'everyNWeeks':
return RecurrenceStrategyFactory.everyNWeeks(
dto.intervalWeeks ?? 2,
toWeekdaySet(dto.weekdays),
);
case 'monthlyNthWeekday': {
const pattern = MonthlyRecurrencePattern.create(
dto.monthlyOrdinal ?? 1,
((dto.monthlyWeekday ?? 'Mon') as Weekday),
);
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
}
case 'weekly':
default:
return RecurrenceStrategyFactory.weekly(toWeekdaySet(dto.weekdays));
}
})();
return new SeasonSchedule({
startDate,
timeOfDay,
timezone,
recurrence,
plannedRounds: dto.plannedRounds,
});
}

View File

@@ -1,7 +1,9 @@
export * from './LeagueConfigFormDTO';
export * from './LeagueDTO';
export * from './LeagueDriverSeasonStatsDTO';
export * from './LeagueDTO';
export * from './LeagueScheduleDTO';
export * from './RaceDTO';
export * from './ResultDTO';
export * from './StandingDTO';
export * from './StandingDTO';
// TODO DTOs dont belong into core. We use Results in UseCases and DTOs in apps/api.

View File

@@ -12,4 +12,6 @@ export interface AllRacesPageOutputPort {
total: number;
page: number;
limit: number;
}
}
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort

View File

@@ -9,4 +9,6 @@ export interface ChampionshipStandingsOutputPort {
teamId?: string;
teamName?: string;
}>;
}
}
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort

View File

@@ -5,4 +5,6 @@ export interface ChampionshipStandingsRowOutputPort {
driverName: string;
teamId?: string;
teamName?: string;
}
}
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort

View File

@@ -4,4 +4,6 @@ export interface DriverRegistrationStatusOutputPort {
leagueId: string;
registered: boolean;
status: 'registered' | 'withdrawn' | 'pending' | 'not_registered';
}
}
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort

View File

@@ -7,12 +7,13 @@ import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy';
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
import type { StewardingDecisionMode } from '../../domain/entities/League';
// TODO The whole file mixes a lot of concerns...should be resolved into use cases or is it obsolet?
@@ -280,6 +281,27 @@ export class SeasonApplicationService {
};
}
private parseDropStrategy(value: unknown): SeasonDropStrategy {
if (value === 'none' || value === 'bestNResults' || value === 'dropWorstN') {
return value;
}
return 'none';
}
private parseDecisionMode(value: unknown): StewardingDecisionMode {
if (
value === 'admin_only' ||
value === 'steward_decides' ||
value === 'steward_vote' ||
value === 'member_vote' ||
value === 'steward_veto' ||
value === 'member_veto'
) {
return value;
}
return 'admin_only';
}
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
schedule?: SeasonSchedule;
scoringConfig?: SeasonScoringConfig;
@@ -298,14 +320,14 @@ export class SeasonApplicationService {
const dropPolicy = config.dropPolicy
? new SeasonDropPolicy({
strategy: config.dropPolicy.strategy as any,
strategy: this.parseDropStrategy(config.dropPolicy.strategy),
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
})
: undefined;
const stewardingConfig = config.stewarding
? new SeasonStewardingConfig({
decisionMode: config.stewarding.decisionMode as any,
decisionMode: this.parseDecisionMode(config.stewarding.decisionMode),
...(config.stewarding.requiredVotes !== undefined
? { requiredVotes: config.stewarding.requiredVotes }
: {}),

View File

@@ -52,7 +52,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue(null);
@@ -80,7 +80,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
@@ -109,7 +109,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
@@ -142,7 +142,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
@@ -175,7 +175,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
@@ -209,7 +209,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
@@ -242,7 +242,7 @@ describe('ApplyForSponsorshipUseCase', () => {
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
mockSponsorRepo as unknown as ISponsorRepository,
mockLogger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });

View File

@@ -9,7 +9,7 @@ import { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import { Money } from '../../domain/value-objects/Money';
import { Money, isCurrency } from '../../domain/value-objects/Money';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -123,7 +123,9 @@ export class ApplyForSponsorshipUseCase {
// Create the sponsorship request
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const offeredAmount = Money.create(input.offeredAmount, (input.currency as any) || 'USD');
const currency =
input.currency !== undefined && isCurrency(input.currency) ? input.currency : 'USD';
const offeredAmount = Money.create(input.offeredAmount, currency);
const request = SponsorshipRequest.create({
id: requestId,

View File

@@ -1,10 +1,11 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { ApplyPenaltyUseCase } from './ApplyPenaltyUseCase';
import { ApplyPenaltyUseCase, type ApplyPenaltyResult } from './ApplyPenaltyUseCase';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
describe('ApplyPenaltyUseCase', () => {
let mockPenaltyRepo: {
@@ -48,7 +49,7 @@ describe('ApplyPenaltyUseCase', () => {
});
it('should return error when race does not exist', async () => {
const output = {
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
present: vi.fn(),
};
@@ -58,7 +59,7 @@ describe('ApplyPenaltyUseCase', () => {
mockRaceRepo as unknown as IRaceRepository,
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
mockLogger as unknown as Logger,
output as any,
output,
);
mockRaceRepo.findById.mockResolvedValue(null);
@@ -77,7 +78,7 @@ describe('ApplyPenaltyUseCase', () => {
});
it('should return error when steward does not have authority', async () => {
const output = {
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
present: vi.fn(),
};
@@ -87,13 +88,18 @@ describe('ApplyPenaltyUseCase', () => {
mockRaceRepo as unknown as IRaceRepository,
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
mockLogger as unknown as Logger,
output as any,
output,
);
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
{ driverId: 'steward1', role: 'member', status: 'active' },
]);
const membership = {
driverId: { toString: () => 'steward1' },
role: { toString: () => 'member' },
status: { toString: () => 'active' },
};
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
const result = await useCase.execute({
raceId: 'race1',
@@ -109,7 +115,7 @@ describe('ApplyPenaltyUseCase', () => {
});
it('should return error when protest does not exist', async () => {
const output = {
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
present: vi.fn(),
};
@@ -119,13 +125,18 @@ describe('ApplyPenaltyUseCase', () => {
mockRaceRepo as unknown as IRaceRepository,
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
mockLogger as unknown as Logger,
output as any,
output,
);
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
{ driverId: 'steward1', role: 'owner', status: 'active' },
]);
const membership = {
driverId: { toString: () => 'steward1' },
role: { toString: () => 'owner' },
status: { toString: () => 'active' },
};
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
mockProtestRepo.findById.mockResolvedValue(null);
const result = await useCase.execute({
@@ -143,7 +154,7 @@ describe('ApplyPenaltyUseCase', () => {
});
it('should return error when protest is not upheld', async () => {
const output = {
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
present: vi.fn(),
};
@@ -153,13 +164,18 @@ describe('ApplyPenaltyUseCase', () => {
mockRaceRepo as unknown as IRaceRepository,
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
mockLogger as unknown as Logger,
output as any,
output,
);
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
{ driverId: 'steward1', role: 'owner', status: 'active' },
]);
const membership = {
driverId: { toString: () => 'steward1' },
role: { toString: () => 'owner' },
status: { toString: () => 'active' },
};
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
mockProtestRepo.findById.mockResolvedValue({ id: 'protest1', status: 'pending', raceId: 'race1' });
const result = await useCase.execute({
@@ -177,7 +193,7 @@ describe('ApplyPenaltyUseCase', () => {
});
it('should return error when protest is not for this race', async () => {
const output = {
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
present: vi.fn(),
};
@@ -187,13 +203,18 @@ describe('ApplyPenaltyUseCase', () => {
mockRaceRepo as unknown as IRaceRepository,
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
mockLogger as unknown as Logger,
output as any,
output,
);
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
{ driverId: 'steward1', role: 'owner', status: 'active' },
]);
const membership = {
driverId: { toString: () => 'steward1' },
role: { toString: () => 'owner' },
status: { toString: () => 'active' },
};
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
mockProtestRepo.findById.mockResolvedValue({ id: 'protest1', status: 'upheld', raceId: 'race2' });
const result = await useCase.execute({
@@ -211,7 +232,7 @@ describe('ApplyPenaltyUseCase', () => {
});
it('should create penalty and return result on success', async () => {
const output = {
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
present: vi.fn(),
};
@@ -221,13 +242,18 @@ describe('ApplyPenaltyUseCase', () => {
mockRaceRepo as unknown as IRaceRepository,
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
mockLogger as unknown as Logger,
output as any,
output,
);
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
{ driverId: 'steward1', role: 'admin', status: 'active' },
]);
const membership = {
driverId: { toString: () => 'steward1' },
role: { toString: () => 'admin' },
status: { toString: () => 'active' },
};
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
mockPenaltyRepo.create.mockResolvedValue(undefined);
const result = await useCase.execute({

View File

@@ -1,5 +1,8 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { ApproveLeagueJoinRequestUseCase } from './ApproveLeagueJoinRequestUseCase';
import {
ApproveLeagueJoinRequestUseCase,
type ApproveLeagueJoinRequestResult,
} from './ApproveLeagueJoinRequestUseCase';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
@@ -33,7 +36,10 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue(joinRequests);
const result = await useCase.execute({ leagueId, requestId }, output as unknown as UseCaseOutputPort<any>);
const result = await useCase.execute(
{ leagueId, requestId },
output as unknown as UseCaseOutputPort<ApproveLeagueJoinRequestResult>,
);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
@@ -62,7 +68,10 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue([]);
const result = await useCase.execute({ leagueId: 'league-1', requestId: 'req-1' }, output as unknown as UseCaseOutputPort<any>);
const result = await useCase.execute(
{ leagueId: 'league-1', requestId: 'req-1' },
output as unknown as UseCaseOutputPort<ApproveLeagueJoinRequestResult>,
);
expect(result.isOk()).toBe(false);
expect(result.error!.code).toBe('JOIN_REQUEST_NOT_FOUND');

View File

@@ -91,8 +91,11 @@ describe('CancelRaceUseCase', () => {
const result = await useCase.execute({ raceId, cancelledById: 'admin-1' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED');
expect((result.unwrapErr() as any).details.message).toContain('already cancelled');
const err = result.unwrapErr();
expect(err.code).toBe('NOT_AUTHORIZED');
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
expect(err.details.message).toContain('already cancelled');
}
expect(output.present).not.toHaveBeenCalled();
});
@@ -113,8 +116,11 @@ describe('CancelRaceUseCase', () => {
const result = await useCase.execute({ raceId, cancelledById: 'admin-1' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED');
expect((result.unwrapErr() as any).details.message).toContain('completed race');
const err = result.unwrapErr();
expect(err.code).toBe('NOT_AUTHORIZED');
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
expect(err.details.message).toContain('completed race');
}
expect(output.present).not.toHaveBeenCalled();
});
});

View File

@@ -117,8 +117,11 @@ describe('CloseRaceEventStewardingUseCase', () => {
const result = await useCase.execute({ raceId: 'event-1', closedById: 'admin-1' });
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
expect((result.unwrapErr() as any).details.message).toContain('DB error');
const err = result.unwrapErr();
expect(err.code).toBe('REPOSITORY_ERROR');
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
expect(err.details.message).toContain('DB error');
}
expect(output.present).not.toHaveBeenCalled();
});
});

View File

@@ -162,8 +162,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
const result = await useCase.execute(command);
expect(result.isErr()).toBe(true);
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
expect((result.unwrapErr() as any).details.message).toBe('gameId is required');
const err = result.unwrapErr();
expect(err.code).toBe('VALIDATION_ERROR');
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
expect(err.details.message).toBe('gameId is required');
}
expect(output.present).not.toHaveBeenCalled();
});

View File

@@ -5,8 +5,9 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO';
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy';
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
import type { StewardingDecisionMode } from '../../domain/entities/League';
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
@@ -120,6 +121,27 @@ export class CreateSeasonForLeagueUseCase {
}
}
private parseDropStrategy(value: unknown): SeasonDropStrategy {
if (value === 'none' || value === 'bestNResults' || value === 'dropWorstN') {
return value;
}
return 'none';
}
private parseDecisionMode(value: unknown): StewardingDecisionMode {
if (
value === 'admin_only' ||
value === 'steward_decides' ||
value === 'steward_vote' ||
value === 'member_vote' ||
value === 'steward_veto' ||
value === 'member_veto'
) {
return value;
}
return 'admin_only';
}
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
schedule?: SeasonSchedule;
scoringConfig?: SeasonScoringConfig;
@@ -133,11 +155,11 @@ export class CreateSeasonForLeagueUseCase {
customScoringEnabled: config.scoring?.customScoringEnabled ?? false,
});
const dropPolicy = new SeasonDropPolicy({
strategy: (config.dropPolicy?.strategy as any) ?? 'none',
strategy: this.parseDropStrategy(config.dropPolicy?.strategy),
...(config.dropPolicy?.n !== undefined ? { n: config.dropPolicy.n } : {}),
});
const stewardingConfig = new SeasonStewardingConfig({
decisionMode: (config.stewarding?.decisionMode as any) ?? 'auto',
decisionMode: this.parseDecisionMode(config.stewarding?.decisionMode),
...(config.stewarding?.requiredVotes !== undefined
? { requiredVotes: config.stewarding.requiredVotes }
: {}),

View File

@@ -35,7 +35,7 @@ describe('CreateSponsorUseCase', () => {
useCase = new CreateSponsorUseCase(
sponsorRepository as unknown as ISponsorRepository,
logger as unknown as Logger,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
});

View File

@@ -16,6 +16,8 @@ import type { FeedItem } from '@core/social/domain/types/FeedItem';
import { Result as UseCaseResult } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
describe('DashboardOverviewUseCase', () => {
it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => {
@@ -195,14 +197,14 @@ describe('DashboardOverviewUseCase', () => {
);
},
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
getJoinRequests: async (): Promise<JoinRequest[]> => [],
saveMembership: async (): Promise<LeagueMembership> => {
throw new Error('Not implemented');
},
removeMembership: async (): Promise<void> => {
throw new Error('Not implemented');
},
saveJoinRequest: async (): Promise<any> => {
saveJoinRequest: async (): Promise<JoinRequest> => {
throw new Error('Not implemented');
},
removeJoinRequest: async (): Promise<void> => {
@@ -227,7 +229,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
findByRaceId: async (): Promise<RaceRegistration[]> => [],
};
const feedRepository = {
@@ -289,9 +291,9 @@ describe('DashboardOverviewUseCase', () => {
expect(_presentedData).not.toBeNull();
const vm = _presentedData!;
expect(vm.myUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-1', 'race-3']);
expect(vm.myUpcomingRaces.map(r => r.race.id)).toEqual(['race-1', 'race-3']);
expect(vm.otherUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-2', 'race-4']);
expect(vm.otherUpcomingRaces.map(r => r.race.id)).toEqual(['race-2', 'race-4']);
expect(vm.nextRace).not.toBeNull();
expect(vm.nextRace!.race.id).toBe('race-1');
@@ -482,14 +484,14 @@ describe('DashboardOverviewUseCase', () => {
);
},
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
getJoinRequests: async (): Promise<JoinRequest[]> => [],
saveMembership: async (): Promise<LeagueMembership> => {
throw new Error('Not implemented');
},
removeMembership: async (): Promise<void> => {
throw new Error('Not implemented');
},
saveJoinRequest: async (): Promise<any> => {
saveJoinRequest: async (): Promise<JoinRequest> => {
throw new Error('Not implemented');
},
removeJoinRequest: async (): Promise<void> => {
@@ -511,7 +513,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
findByRaceId: async (): Promise<RaceRegistration[]> => [],
};
const feedRepository = {
@@ -578,7 +580,7 @@ describe('DashboardOverviewUseCase', () => {
expect(vm.recentResults[1]!.race.id).toBe('race-old');
const summariesByLeague = new Map(
vm.leagueStandingsSummaries.map((s: any) => [s.league.id.toString(), s]),
vm.leagueStandingsSummaries.map(s => [s.league.id.toString(), s] as const),
);
const summaryA = summariesByLeague.get('league-A');
@@ -702,14 +704,14 @@ describe('DashboardOverviewUseCase', () => {
const leagueMembershipRepository = {
getMembership: async (): Promise<LeagueMembership | null> => null,
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
getJoinRequests: async (): Promise<JoinRequest[]> => [],
saveMembership: async (): Promise<LeagueMembership> => {
throw new Error('Not implemented');
},
removeMembership: async (): Promise<void> => {
throw new Error('Not implemented');
},
saveJoinRequest: async (): Promise<any> => {
saveJoinRequest: async (): Promise<JoinRequest> => {
throw new Error('Not implemented');
},
removeJoinRequest: async (): Promise<void> => {
@@ -731,7 +733,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
findByRaceId: async (): Promise<RaceRegistration[]> => [],
};
const feedRepository = {
@@ -898,14 +900,14 @@ describe('DashboardOverviewUseCase', () => {
const leagueMembershipRepository = {
getMembership: async (): Promise<LeagueMembership | null> => null,
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
getJoinRequests: async (): Promise<JoinRequest[]> => [],
saveMembership: async (): Promise<LeagueMembership> => {
throw new Error('Not implemented');
},
removeMembership: async (): Promise<void> => {
throw new Error('Not implemented');
},
saveJoinRequest: async (): Promise<any> => {
saveJoinRequest: async (): Promise<JoinRequest> => {
throw new Error('Not implemented');
},
removeJoinRequest: async (): Promise<void> => {
@@ -927,7 +929,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
findByRaceId: async (): Promise<RaceRegistration[]> => [],
};
const feedRepository = {
@@ -1089,14 +1091,14 @@ describe('DashboardOverviewUseCase', () => {
const leagueMembershipRepository = {
getMembership: async (): Promise<LeagueMembership | null> => null,
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
getJoinRequests: async (): Promise<JoinRequest[]> => [],
saveMembership: async (): Promise<LeagueMembership> => {
throw new Error('Not implemented');
},
removeMembership: async (): Promise<void> => {
throw new Error('Not implemented');
},
saveJoinRequest: async (): Promise<any> => {
saveJoinRequest: async (): Promise<JoinRequest> => {
throw new Error('Not implemented');
},
removeJoinRequest: async (): Promise<void> => {
@@ -1118,7 +1120,7 @@ describe('DashboardOverviewUseCase', () => {
clearRaceRegistrations: async (): Promise<void> => {
throw new Error('Not implemented');
},
findByRaceId: async (): Promise<any[]> => [],
findByRaceId: async (): Promise<RaceRegistration[]> => [],
};
const feedRepository = {

View File

@@ -55,11 +55,11 @@ export class FileProtestUseCase {
// Validate protesting driver is a member of the league
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
const protestingDriverMembership = memberships.find(m => {
const driverId = (m as any).driverId;
const status = (m as any).status;
return driverId === command.protestingDriverId && status === 'active';
});
const protestingDriverMembership = memberships.find(
m =>
m.driverId.toString() === command.protestingDriverId &&
m.status.toString() === 'active',
);
if (!protestingDriverMembership) {
return Result.err({ code: 'NOT_MEMBER', details: { message: 'Protesting driver is not an active member of this league' } });

View File

@@ -8,6 +8,8 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { Race } from '../../domain/entities/Race';
import { League } from '../../domain/entities/League';
describe('GetAllRacesPageDataUseCase', () => {
const mockRaceFindAll = vi.fn();
@@ -61,26 +63,38 @@ describe('GetAllRacesPageDataUseCase', () => {
output,
);
const race1 = {
const race1 = Race.create({
id: 'race1',
leagueId: 'league1',
track: 'Track A',
car: 'Car A',
scheduledAt: new Date('2023-01-01T10:00:00Z'),
status: 'scheduled' as const,
leagueId: 'league1',
status: 'scheduled',
strengthOfField: 5,
} as any;
const race2 = {
});
const race2 = Race.create({
id: 'race2',
leagueId: 'league2',
track: 'Track B',
car: 'Car B',
scheduledAt: new Date('2023-01-02T10:00:00Z'),
status: 'completed' as const,
leagueId: 'league2',
strengthOfField: null,
} as any;
const league1 = { id: 'league1', name: 'League One' } as any;
const league2 = { id: 'league2', name: 'League Two' } as any;
status: 'completed',
});
const league1 = League.create({
id: 'league1',
name: 'League One',
description: 'League One',
ownerId: 'owner-1',
});
const league2 = League.create({
id: 'league2',
name: 'League Two',
description: 'League Two',
ownerId: 'owner-2',
});
mockRaceFindAll.mockResolvedValue([race1, race2]);
mockLeagueFindAll.mockResolvedValue([league1, league2]);

View File

@@ -8,6 +8,8 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { Race } from '../../domain/entities/Race';
import { League } from '../../domain/entities/League';
describe('GetAllRacesUseCase', () => {
const mockRaceFindAll = vi.fn();
@@ -61,22 +63,37 @@ describe('GetAllRacesUseCase', () => {
);
useCase.setOutput(output);
const race1 = {
const race1 = Race.create({
id: 'race1',
leagueId: 'league1',
track: 'Track A',
car: 'Car A',
scheduledAt: new Date('2023-01-01T10:00:00Z'),
leagueId: 'league1',
} as any;
const race2 = {
status: 'scheduled',
});
const race2 = Race.create({
id: 'race2',
leagueId: 'league2',
track: 'Track B',
car: 'Car B',
scheduledAt: new Date('2023-01-02T10:00:00Z'),
leagueId: 'league2',
} as any;
const league1 = { id: 'league1' } as any;
const league2 = { id: 'league2' } as any;
status: 'scheduled',
});
const league1 = League.create({
id: 'league1',
name: 'League One',
description: 'League One',
ownerId: 'owner-1',
});
const league2 = League.create({
id: 'league2',
name: 'League Two',
description: 'League Two',
ownerId: 'owner-2',
});
mockRaceFindAll.mockResolvedValue([race1, race2]);
mockLeagueFindAll.mockResolvedValue([league1, league2]);

View File

@@ -70,7 +70,7 @@ describe('GetDriversLeaderboardUseCase', () => {
mockDriverFindAll.mockResolvedValue([driver1, driver2]);
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
mockDriverStatsGetDriverStats.mockImplementation((id) => {
mockDriverStatsGetDriverStats.mockImplementation((id: string) => {
if (id === 'driver1') return stats1;
if (id === 'driver2') return stats2;
return null;
@@ -89,7 +89,7 @@ describe('GetDriversLeaderboardUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult;
const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult;
expect(presented).toEqual({
items: [
@@ -142,7 +142,7 @@ describe('GetDriversLeaderboardUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult;
const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult;
expect(presented).toEqual({
items: [],
@@ -177,7 +177,7 @@ describe('GetDriversLeaderboardUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult;
const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult;
expect(presented).toEqual({
items: [
@@ -218,7 +218,9 @@ describe('GetDriversLeaderboardUseCase', () => {
expect(result.isErr()).toBe(true);
const err = result.unwrapErr();
expect(err.code).toBe('REPOSITORY_ERROR');
expect((err as any).details?.message).toBe('Repository error');
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
expect(err.details.message).toBe('Repository error');
}
expect(output.present).not.toHaveBeenCalled();
});
});

View File

@@ -9,6 +9,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Logger } from '@core/shared/application';
describe('GetLeagueAdminPermissionsUseCase', () => {
let mockLeagueRepo: ILeagueRepository;
@@ -16,12 +17,12 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
let mockFindById: Mock;
let mockGetMembership: Mock;
let output: UseCaseOutputPort<GetLeagueAdminPermissionsResult> & { present: Mock };
const logger = {
const logger: Logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as any;
};
beforeEach(() => {
mockFindById = vi.fn();
@@ -122,7 +123,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
});
it('returns admin permissions for admin role and calls output once', async () => {
const league = { id: 'league1' } as any;
const league = { id: 'league1' } as unknown as { id: string };
mockFindById.mockResolvedValue(league);
mockGetMembership.mockResolvedValue({ status: 'active', role: 'admin' });
@@ -144,7 +145,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
});
it('returns admin permissions for owner role and calls output once', async () => {
const league = { id: 'league1' } as any;
const league = { id: 'league1' } as unknown as { id: string };
mockFindById.mockResolvedValue(league);
mockGetMembership.mockResolvedValue({ status: 'active', role: 'owner' });

View File

@@ -1,19 +1,18 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
GetLeagueDriverSeasonStatsUseCase,
type GetLeagueDriverSeasonStatsResult,
type GetLeagueDriverSeasonStatsInput,
type GetLeagueDriverSeasonStatsErrorCode,
} from './GetLeagueDriverSeasonStatsUseCase';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
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';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { DriverRatingPort } from '../ports/DriverRatingPort';
import {
GetLeagueDriverSeasonStatsUseCase,
type GetLeagueDriverSeasonStatsErrorCode,
type GetLeagueDriverSeasonStatsInput,
type GetLeagueDriverSeasonStatsResult,
} from './GetLeagueDriverSeasonStatsUseCase';
describe('GetLeagueDriverSeasonStatsUseCase', () => {
const mockStandingFindByLeagueId = vi.fn();
@@ -30,7 +29,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
let penaltyRepository: IPenaltyRepository;
let raceRepository: IRaceRepository;
let driverRepository: IDriverRepository;
let teamRepository: ITeamRepository;
let driverRatingPort: DriverRatingPort;
let output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult> & { present: ReturnType<typeof vi.fn> };
@@ -102,15 +100,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
exists: vi.fn(),
existsByIRacingId: vi.fn(),
};
teamRepository = {
findById: mockTeamFindById,
findAll: vi.fn(),
findByLeagueId: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
exists: vi.fn(),
};
driverRatingPort = {
getDriverRating: mockDriverRatingGetRating,
calculateRatingChange: vi.fn(),
@@ -129,7 +118,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
penaltyRepository,
raceRepository,
driverRepository,
teamRepository,
driverRatingPort,
output,
);

View File

@@ -1,12 +1,11 @@
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import { Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
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 { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { DriverRatingPort } from '../ports/DriverRatingPort';
export type DriverSeasonStats = {
@@ -56,7 +55,6 @@ export class GetLeagueDriverSeasonStatsUseCase {
private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository,
private readonly driverRepository: IDriverRepository,
private readonly teamRepository: ITeamRepository,
private readonly driverRatingPort: DriverRatingPort,
private readonly output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult>,
) {}
@@ -101,39 +99,32 @@ export class GetLeagueDriverSeasonStatsUseCase {
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
for (const standing of standings) {
const driverId = String(standing.driverId);
const driverId = standing.driverId.toString();
const rating = await this.driverRatingPort.getDriverRating(driverId);
driverRatings.set(driverId, { rating, ratingChange: null });
}
const driverResults = new Map<string, Array<{ position: number }>>();
for (const standing of standings) {
const driverId = String(standing.driverId);
const driverId = standing.driverId.toString();
const results = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId);
driverResults.set(
driverId,
results.map(result => ({ position: Number((result as any).position) })),
results.map(result => ({ position: result.position.toNumber() })),
);
}
const driverIds = standings.map(s => String(s.driverId));
const driverIds = standings.map(s => s.driverId.toString());
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
const driversMap = new Map(drivers.filter(d => d).map(d => [String(d!.id), d!]));
const teamIds = Array.from(
new Set(
drivers
.filter(d => (d as any)?.teamId)
.map(d => (d as any).teamId as string),
),
const driversMap = new Map(
drivers
.filter((driver): driver is NonNullable<typeof driver> => driver !== null)
.map(driver => [driver.id, driver]),
);
const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id)));
const teamsMap = new Map(teams.filter(t => t).map(t => [String(t!.id), t!]));
const stats: DriverSeasonStats[] = standings.map(standing => {
const driverId = String(standing.driverId);
const driver = driversMap.get(driverId) as any;
const teamId = driver?.teamId as string | undefined;
const team = teamId ? teamsMap.get(String(teamId)) : undefined;
const driverId = standing.driverId.toString();
const driver = driversMap.get(driverId);
const penalties = penaltiesByDriver.get(driverId) ?? { baseDelta: 0, bonusDelta: 0 };
const results = driverResults.get(driverId) ?? [];
const rating = driverRatings.get(driverId);
@@ -146,16 +137,16 @@ export class GetLeagueDriverSeasonStatsUseCase {
results.length > 0
? results.reduce((sum, r) => sum + r.position, 0) / results.length
: null;
const totalPoints = Number(standing.points);
const totalPoints = standing.points.toNumber();
const pointsPerRace = racesStarted > 0 ? totalPoints / racesStarted : 0;
return {
leagueId,
driverId,
position: Number(standing.position),
driverName: String(driver?.name ?? ''),
teamId,
teamName: (team as any)?.name as string | undefined,
position: standing.position.toNumber(),
driverName: driver ? driver.name.toString() : '',
teamId: undefined,
teamName: undefined,
totalPoints,
basePoints: totalPoints - penalties.baseDelta,
penaltyPoints: penalties.baseDelta,

View File

@@ -73,10 +73,7 @@ export class GetLeagueJoinRequestsUseCase {
return Result.ok(undefined);
} catch (error: unknown) {
const message =
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
? (error as Error).message
: 'Failed to load league join requests';
const message = error instanceof Error ? error.message : 'Failed to load league join requests';
return Result.err({
code: 'REPOSITORY_ERROR',

View File

@@ -67,10 +67,7 @@ export class GetLeagueMembershipsUseCase {
return Result.ok(undefined);
} catch (error: unknown) {
const message =
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
? (error as any).message
: 'Failed to load league memberships';
const message = error instanceof Error ? error.message : 'Failed to load league memberships';
return Result.err({
code: 'REPOSITORY_ERROR',

View File

@@ -94,8 +94,8 @@ export class GetLeagueProtestsUseCase {
return Result.ok(undefined);
} catch (error: unknown) {
const message =
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
? (error as any).message
error instanceof Error && error.message
? error.message
: 'Failed to load league protests';
return Result.err({

View File

@@ -27,11 +27,11 @@ describe('GetLeagueSeasonsUseCase', () => {
beforeEach(() => {
seasonRepository = {
findByLeagueId: vi.fn(),
} as unknown as ISeasonRepository as any;
};
leagueRepository = {
findById: vi.fn(),
} as unknown as ILeagueRepository as any;
};
output = {
present: vi.fn(),

View File

@@ -6,11 +6,10 @@
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorableEntityType, SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
import type { Sponsor } from '../../domain/entities/sponsor/Sponsor';
import { Money } from '../../domain/value-objects/Money';

View File

@@ -9,7 +9,6 @@ import { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { IImageServicePort } from '../ports/IImageServicePort';
import { Driver } from '../../domain/entities/Driver';
import { Team } from '../../domain/entities/Team';
import type { UseCaseOutputPort } from '@core/shared/application';
@@ -29,9 +28,6 @@ describe('GetProfileOverviewUseCase', () => {
let socialRepository: {
getFriends: Mock;
};
let imageService: {
getDriverAvatar: Mock;
};
let getDriverStats: Mock;
let getAllDriverRankings: Mock;
let driverExtendedProfileProvider: {
@@ -52,9 +48,6 @@ describe('GetProfileOverviewUseCase', () => {
socialRepository = {
getFriends: vi.fn(),
};
imageService = {
getDriverAvatar: vi.fn(),
};
getDriverStats = vi.fn();
getAllDriverRankings = vi.fn();
driverExtendedProfileProvider = {
@@ -69,7 +62,6 @@ describe('GetProfileOverviewUseCase', () => {
teamRepository as unknown as ITeamRepository,
teamMembershipRepository as unknown as ITeamMembershipRepository,
socialRepository as unknown as ISocialGraphRepository,
imageService as unknown as IImageServicePort,
driverExtendedProfileProvider,
getDriverStats,
getAllDriverRankings,
@@ -117,7 +109,6 @@ describe('GetProfileOverviewUseCase', () => {
teamRepository.findAll.mockResolvedValue(teams);
teamMembershipRepository.getMembership.mockResolvedValue(null);
socialRepository.getFriends.mockResolvedValue(friends);
imageService.getDriverAvatar.mockReturnValue('avatar-url');
getDriverStats.mockReturnValue(statsAdapter);
getAllDriverRankings.mockReturnValue(rankings);
driverExtendedProfileProvider.getExtendedProfile.mockReturnValue(null);
@@ -127,8 +118,7 @@ describe('GetProfileOverviewUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as unknown as Mock).mock
.calls[0][0] as GetProfileOverviewResult;
const presented = (output.present as unknown as Mock).mock.calls[0]?.[0] as GetProfileOverviewResult;
expect(presented.driverInfo.driver.id).toBe(driverId);
expect(presented.extendedProfile).toBeNull();
});

View File

@@ -1,7 +1,6 @@
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
import type { DriverExtendedProfileProvider } from '../ports/DriverExtendedProfileProvider';
import type { Driver } from '../../domain/entities/Driver';
@@ -9,7 +8,7 @@ import type { Team } from '../../domain/entities/Team';
import type { TeamMembership } from '../../domain/types/TeamMembership';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, UseCase } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
interface ProfileDriverStatsAdapter {
rating: number | null;
@@ -85,29 +84,29 @@ export type GetProfileOverviewResult = {
finishDistribution: ProfileOverviewFinishDistribution | null;
teamMemberships: ProfileOverviewTeamMembership[];
socialSummary: ProfileOverviewSocialSummary;
extendedProfile: unknown;
extendedProfile: ReturnType<DriverExtendedProfileProvider['getExtendedProfile']>;
};
export type GetProfileOverviewErrorCode =
| 'DRIVER_NOT_FOUND'
| 'REPOSITORY_ERROR';
export class GetProfileOverviewUseCase implements UseCase<GetProfileOverviewInput, GetProfileOverviewResult, GetProfileOverviewErrorCode> {
export class GetProfileOverviewUseCase {
constructor(
private readonly driverRepository: IDriverRepository,
private readonly teamRepository: ITeamRepository,
private readonly teamMembershipRepository: ITeamMembershipRepository,
private readonly socialRepository: ISocialGraphRepository,
private readonly imageService: IImageServicePort,
private readonly driverExtendedProfileProvider: DriverExtendedProfileProvider,
private readonly getDriverStats: (driverId: string) => ProfileDriverStatsAdapter | null,
private readonly getAllDriverRankings: () => DriverRankingEntry[],
private readonly output: UseCaseOutputPort<GetProfileOverviewResult>,
) {}
async execute(
input: GetProfileOverviewInput,
): Promise<
Result<GetProfileOverviewResult, ApplicationErrorCode<GetProfileOverviewErrorCode, { message: string }>>
Result<void, ApplicationErrorCode<GetProfileOverviewErrorCode, { message: string }>>
> {
try {
const { driverId } = input;
@@ -130,10 +129,11 @@ export class GetProfileOverviewUseCase implements UseCase<GetProfileOverviewInpu
const driverInfo = this.buildDriverInfo(driver, statsAdapter);
const stats = this.buildStats(statsAdapter);
const finishDistribution = this.buildFinishDistribution(statsAdapter);
const teamMemberships = await this.buildTeamMemberships(driver.id, teams as Team[]);
const socialSummary = this.buildSocialSummary(friends as Driver[]);
const extendedProfile = this.driverExtendedProfileProvider.getExtendedProfile(driverId);
const teamMemberships = await this.buildTeamMemberships(driver.id, teams);
const socialSummary = this.buildSocialSummary(friends);
const extendedProfile =
this.driverExtendedProfileProvider.getExtendedProfile(driverId);
const result: GetProfileOverviewResult = {
driverInfo,
stats,
@@ -143,7 +143,9 @@ export class GetProfileOverviewUseCase implements UseCase<GetProfileOverviewInpu
extendedProfile,
};
return Result.ok(result);
this.output.present(result);
return Result.ok(undefined);
} catch (error) {
return Result.err({
code: 'REPOSITORY_ERROR',

View File

@@ -91,7 +91,7 @@ describe('GetRaceDetailUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceDetailResult;
const presented = output.present.mock.calls[0]?.[0] as GetRaceDetailResult;
expect(presented.race).toEqual(race);
expect(presented.league).toEqual(league);
expect(presented.registrations).toEqual(registrations);
@@ -145,7 +145,7 @@ describe('GetRaceDetailUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceDetailResult;
const presented = output.present.mock.calls[0]?.[0] as GetRaceDetailResult;
expect(presented.userResult).toBe(userDomainResult);
expect(presented.race).toEqual(race);
expect(presented.league).toBeNull();

View File

@@ -1,9 +1,10 @@
import { Result as DomainResult, Result } from '@core/shared/application/Result';
import { Result } from '@core/shared/application/Result';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { League } from '../../domain/entities/League';
import type { Race } from '../../domain/entities/Race';
import type { RaceRegistration } from '../../domain/entities/RaceRegistration';
import type { Result as RaceResult } from '../../domain/entities/result/Result';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
@@ -26,14 +27,12 @@ export type GetRaceDetailResult = {
league: League | null;
registrations: RaceRegistration[];
drivers: NonNullable<Awaited<ReturnType<IDriverRepository['findById']>>>[];
userResult: DomainResult | null;
userResult: RaceResult | null;
isUserRegistered: boolean;
canRegister: boolean;
};
export class GetRaceDetailUseCase {
private output: UseCaseOutputPort<GetRaceDetailResult> | null = null; // TODO wtf this must be injected via constructor
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
@@ -41,12 +40,9 @@ export class GetRaceDetailUseCase {
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly output: UseCaseOutputPort<GetRaceDetailResult>,
) {}
setOutput(output: UseCaseOutputPort<GetRaceDetailResult>) { // TODO must be removed
this.output = output;
}
async execute(
input: GetRaceDetailInput,
): Promise<Result<void, ApplicationErrorCode<GetRaceDetailErrorCode, { message: string }>>> {
@@ -75,9 +71,10 @@ export class GetRaceDetailUseCase {
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;
const canRegister =
!!membership && membership.status.toString() === 'active' && isUpcoming;
let userResult: DomainResult | null = null;
let userResult: RaceResult | null = null;
if (race.status === 'completed') {
const results = await this.resultRepository.findByRaceId(race.id);
@@ -94,9 +91,6 @@ export class GetRaceDetailUseCase {
canRegister,
};
if (!this.output) {
throw new Error('Output not set');
}
this.output.present(result);
return Result.ok(undefined);

View File

@@ -38,10 +38,10 @@ export class GetRacePenaltiesUseCase {
const penalties = await this.penaltyRepository.findByRaceId(input.raceId);
const driverIds = new Set<string>();
penalties.forEach((penalty: any) => {
for (const penalty of penalties) {
driverIds.add(penalty.driverId);
driverIds.add(penalty.issuedBy);
});
}
const drivers = await Promise.all(
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
@@ -52,16 +52,16 @@ export class GetRacePenaltiesUseCase {
this.output.present({ penalties, drivers: validDrivers });
return Result.ok(undefined);
} catch (error) {
} catch (error: unknown) {
const message =
error instanceof Error && error.message ? error.message : 'Failed to load race penalties';
error instanceof Error && error.message
? error.message
: 'Failed to load race penalties';
return Result.err({
code: 'REPOSITORY_ERROR',
details: {
message,
},
} as ApplicationErrorCode<GetRacePenaltiesErrorCode, { message: string }>);
details: { message },
});
}
}
}

View File

@@ -77,7 +77,9 @@ describe('GetRaceProtestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceProtestsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetRaceProtestsResult;
expect(presented.protests).toHaveLength(1);
expect(presented.protests[0]).toEqual(protest);
@@ -96,7 +98,9 @@ describe('GetRaceProtestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceProtestsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetRaceProtestsResult;
expect(presented.protests).toEqual([]);
expect(presented.drivers).toEqual([]);

View File

@@ -62,8 +62,8 @@ export class GetRaceProtestsUseCase {
return Result.ok(undefined);
} catch (error: unknown) {
const message =
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
? (error as any).message
error instanceof Error && error.message
? error.message
: 'Failed to load race protests';
return Result.err({

View File

@@ -57,12 +57,14 @@ describe('GetRaceRegistrationsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetRaceRegistrationsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetRaceRegistrationsResult;
expect(presented.race).toEqual(race);
expect(presented.registrations).toHaveLength(2);
expect(presented.registrations[0].registration).toEqual(registrations[0]);
expect(presented.registrations[1].registration).toEqual(registrations[1]);
expect(presented.registrations[0]!.registration).toEqual(registrations[0]);
expect(presented.registrations[1]!.registration).toEqual(registrations[1]);
});
it('should return RACE_NOT_FOUND error when race does not exist', async () => {

View File

@@ -62,13 +62,28 @@ describe('GetRacesPageDataUseCase', () => {
});
it('should present races page data for a league', async () => {
const races = [
type RaceRow = {
id: string;
track: string;
car: string;
scheduledAt: Date;
status: 'scheduled' | 'completed';
leagueId: string;
strengthOfField: number;
isUpcoming: () => boolean;
isLive: () => boolean;
isPast: () => boolean;
};
type LeagueRow = { id: string; name: string };
const races: RaceRow[] = [
{
id: 'race-1',
track: 'Track 1',
car: 'Car 1',
scheduledAt: new Date('2023-01-01T10:00:00Z'),
status: 'scheduled' as const,
status: 'scheduled',
leagueId: 'league-1',
strengthOfField: 1500,
isUpcoming: () => true,
@@ -80,16 +95,16 @@ describe('GetRacesPageDataUseCase', () => {
track: 'Track 2',
car: 'Car 2',
scheduledAt: new Date('2023-01-02T10:00:00Z'),
status: 'completed' as const,
status: 'completed',
leagueId: 'league-1',
strengthOfField: 1600,
isUpcoming: () => false,
isLive: () => false,
isPast: () => true,
},
] as any[];
];
const leagues = [{ id: 'league-1', name: 'League 1' }] as any[];
const leagues: LeagueRow[] = [{ id: 'league-1', name: 'League 1' }];
(raceRepository.findAll as Mock).mockResolvedValue(races);
(leagueRepository.findAll as Mock).mockResolvedValue(leagues);
@@ -103,14 +118,16 @@ describe('GetRacesPageDataUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0]! as GetRacesPageDataResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetRacesPageDataResult;
expect(presented.leagueId).toBe('league-1');
expect(presented.races).toHaveLength(2);
expect(presented.races[0].race.id).toBe('race-1');
expect(presented.races[0].leagueName).toBe('League 1');
expect(presented.races[1].race.id).toBe('race-2');
expect(presented.races[0]!.race.id).toBe('race-1');
expect(presented.races[0]!.leagueName).toBe('League 1');
expect(presented.races[1]!.race.id).toBe('race-2');
});
it('should return repository error when repositories throw and not present data', async () => {

View File

@@ -41,7 +41,9 @@ export class GetRacesPageDataUseCase {
this.leagueRepository.findAll(),
]);
const leagueMap = new Map(allLeagues.map(league => [league.id, league.name]));
const leagueMap = new Map(
allLeagues.map(league => [league.id.toString(), league.name.toString()]),
);
const filteredRaces = allRaces
.filter(race => race.leagueId === input.leagueId)

View File

@@ -55,7 +55,7 @@ export class GetSeasonDetailsUseCase {
}
const result: GetSeasonDetailsResult = {
leagueId: league.id,
leagueId: league.id.toString(),
season,
};

View File

@@ -23,13 +23,10 @@ export type SeasonSponsorshipFinancials = {
currency: string;
};
import type { LeagueId } from '../../domain/entities/LeagueId';
import type { LeagueName } from '../../domain/entities/LeagueName';
export type SeasonSponsorshipDetail = {
id: string;
leagueId: LeagueId;
leagueName: LeagueName;
leagueId: string;
leagueName: string;
seasonId: string;
seasonName: string;
seasonStartDate?: Date;
@@ -101,20 +98,18 @@ export class GetSeasonSponsorshipsUseCase {
const completedRaces = races.filter(r => r.status === 'completed').length;
const impressions = completedRaces * driverCount * 100;
const sponsorshipDetails: SeasonSponsorshipDetail[] = sponsorships.map(sponsorship => {
const sponsorshipDetails: SeasonSponsorshipDetail[] = sponsorships.map((sponsorship) => {
const platformFee = sponsorship.getPlatformFee();
const netAmount = sponsorship.getNetAmount();
return {
const detail: SeasonSponsorshipDetail = {
id: sponsorship.id,
leagueId: league.id,
leagueName: league.name,
leagueId: league.id.toString(),
leagueName: league.name.toString(),
seasonId: season.id,
seasonName: season.name,
seasonStartDate: season.startDate,
seasonEndDate: season.endDate,
tier: sponsorship.tier,
status: sponsorship.status,
tier: sponsorship.tier.toString(),
status: sponsorship.status.toString(),
pricing: {
amount: sponsorship.pricing.amount,
currency: sponsorship.pricing.currency,
@@ -134,8 +129,12 @@ export class GetSeasonSponsorshipsUseCase {
impressions,
},
createdAt: sponsorship.createdAt,
activatedAt: sponsorship.activatedAt,
...(season.startDate ? { seasonStartDate: season.startDate } : {}),
...(season.endDate ? { seasonEndDate: season.endDate } : {}),
...(sponsorship.activatedAt ? { activatedAt: sponsorship.activatedAt } : {}),
};
return detail;
});
this.output.present({

View File

@@ -119,7 +119,9 @@ describe('GetSponsorDashboardUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const dashboard = (output.present as Mock).mock.calls[0][0] as GetSponsorDashboardResult;
const dashboardRaw = (output.present as Mock).mock.calls[0]?.[0];
expect(dashboardRaw).toBeDefined();
const dashboard = dashboardRaw as GetSponsorDashboardResult;
expect(dashboard).toBeDefined();
expect(dashboard.sponsorId).toBe(sponsorId);

View File

@@ -119,7 +119,9 @@ describe('GetSponsorSponsorshipsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as Mock).mock.calls[0][0] as GetSponsorSponsorshipsResult;
const presentedRaw = (output.present as Mock).mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetSponsorSponsorshipsResult;
expect(presented.sponsor).toBe(sponsor);
expect(presented.sponsorships).toHaveLength(1);

View File

@@ -22,7 +22,7 @@ describe('GetSponsorsUseCase', () => {
};
useCase = new GetSponsorsUseCase(
sponsorRepository as unknown as ISponsorRepository,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
});

View File

@@ -68,7 +68,9 @@ describe('GetTeamDetailsUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetTeamDetailsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetTeamDetailsResult;
expect(presented.team).toBe(team);
expect(presented.membership).toEqual(membership);
expect(presented.canManage).toBe(false);
@@ -103,7 +105,9 @@ describe('GetTeamDetailsUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetTeamDetailsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetTeamDetailsResult;
expect(presented.canManage).toBe(true);
});

View File

@@ -82,17 +82,19 @@ describe('GetTeamJoinRequestsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as GetTeamJoinRequestsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetTeamJoinRequestsResult;
expect(presented.team).toBe(team);
expect(presented.joinRequests).toHaveLength(1);
expect(presented.joinRequests[0]).toMatchObject({
expect(presented.joinRequests[0]!).toMatchObject({
id: 'req-1',
teamId,
driverId: 'driver-1',
message: 'msg',
});
expect(presented.joinRequests[0].driver).toBe(driver);
expect(presented.joinRequests[0]!.driver).toBe(driver);
});
it('should return TEAM_NOT_FOUND error when team does not exist', async () => {

View File

@@ -69,8 +69,8 @@ export class GetTeamJoinRequestsUseCase {
return Result.ok(undefined);
} catch (error: unknown) {
const message =
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
? (error as any).message
error instanceof Error && error.message
? error.message
: 'Failed to load team join requests';
return Result.err({

View File

@@ -7,7 +7,6 @@ import {
} from './GetTeamsLeaderboardUseCase';
import { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import { Team } from '../../domain/entities/Team';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
@@ -21,9 +20,6 @@ describe('GetTeamsLeaderboardUseCase', () => {
let teamMembershipRepository: {
getTeamMembers: Mock;
};
let driverRepository: {
findById: Mock;
};
let getDriverStats: Mock;
let logger: {
debug: Mock;
@@ -40,9 +36,6 @@ describe('GetTeamsLeaderboardUseCase', () => {
teamMembershipRepository = {
getTeamMembers: vi.fn(),
};
driverRepository = {
findById: vi.fn(),
};
getDriverStats = vi.fn();
logger = {
debug: vi.fn(),
@@ -52,12 +45,12 @@ describe('GetTeamsLeaderboardUseCase', () => {
};
output = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<GetTeamsLeaderboardResult> & { present: Mock };
useCase = new GetTeamsLeaderboardUseCase(
teamRepository as unknown as ITeamRepository,
teamMembershipRepository as unknown as ITeamMembershipRepository,
driverRepository as unknown as IDriverRepository,
getDriverStats,
getDriverStats as unknown as (driverId: string) => { rating: number | null; wins: number; totalRaces: number } | null,
logger as unknown as Logger,
output,
);
@@ -109,7 +102,9 @@ describe('GetTeamsLeaderboardUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = (output.present as unknown as Mock).mock.calls[0][0] as GetTeamsLeaderboardResult;
const presentedRaw = (output.present as unknown as Mock).mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as GetTeamsLeaderboardResult;
expect(presented.recruitingCount).toBe(2); // both teams are recruiting
expect(presented.items).toHaveLength(2);

View File

@@ -1,6 +1,5 @@
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 { SkillLevelService, type SkillLevel } from '@core/racing/domain/services/SkillLevelService';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -46,7 +45,6 @@ export class GetTeamsLeaderboardUseCase {
constructor(
private readonly teamRepository: ITeamRepository,
private readonly teamMembershipRepository: ITeamMembershipRepository,
private readonly driverRepository: IDriverRepository,
private readonly getDriverStats: (driverId: string) => DriverStatsAdapter | null,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<GetTeamsLeaderboardResult>,

View File

@@ -2,11 +2,9 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import {
GetTotalDriversUseCase,
GetTotalDriversInput,
GetTotalDriversResult,
GetTotalDriversErrorCode,
} from './GetTotalDriversUseCase';
import { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('GetTotalDriversUseCase', () => {
@@ -14,21 +12,12 @@ describe('GetTotalDriversUseCase', () => {
let driverRepository: {
findAll: Mock;
};
let output: UseCaseOutputPort<GetTotalDriversResult> & { present: Mock };
beforeEach(() => {
driverRepository = {
findAll: vi.fn(),
};
output = {
present: vi.fn(),
} as unknown as UseCaseOutputPort<GetTotalDriversResult> & { present: Mock };
useCase = new GetTotalDriversUseCase(
driverRepository as unknown as IDriverRepository,
output,
);
useCase = new GetTotalDriversUseCase(driverRepository as unknown as IDriverRepository);
});
it('should return total number of drivers', async () => {
@@ -41,11 +30,7 @@ describe('GetTotalDriversUseCase', () => {
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
expect(output.present).toHaveBeenCalledWith<[{ totalDrivers: number }]>(
expect.objectContaining({ totalDrivers: 2 }),
);
expect(result.unwrap()).toEqual({ totalDrivers: 2 });
});
it('should return error on repository failure', async () => {
@@ -66,6 +51,5 @@ describe('GetTotalDriversUseCase', () => {
expect(unwrappedError.code).toBe('REPOSITORY_ERROR');
expect(unwrappedError.details.message).toBe(error.message);
expect(output.present).not.toHaveBeenCalled();
});
});

View File

@@ -59,7 +59,9 @@ describe('GetTotalRacesUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const payload = output.present.mock.calls[0][0] as GetTotalRacesResult;
const payloadRaw = output.present.mock.calls[0]?.[0];
expect(payloadRaw).toBeDefined();
const payload = payloadRaw as GetTotalRacesResult;
expect(payload.totalRaces).toBe(2);
});

View File

@@ -188,8 +188,9 @@ describe('ImportRaceResultsApiUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0][0] as ImportRaceResultsApiResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as ImportRaceResultsApiResult;
expect(presented.success).toBe(true);
expect(presented.raceId).toBe('race-1');

View File

@@ -172,16 +172,16 @@ export class ImportRaceResultsApiUseCase {
this.logger.info('ImportRaceResultsApiUseCase:race results created', { raceId });
await this.standingRepository.recalculate(league.id);
await this.standingRepository.recalculate(league.id.toString());
this.logger.info('ImportRaceResultsApiUseCase:standings recalculated', {
leagueId: league.id,
leagueId: league.id.toString(),
});
const result: ImportRaceResultsApiResult = {
success: true,
raceId,
leagueId: league.id,
leagueId: league.id.toString(),
driversProcessed: results.length,
resultsRecorded: validEntities.length,
errors: [],

View File

@@ -2,11 +2,10 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import {
IsDriverRegisteredForRaceUseCase,
type IsDriverRegisteredForRaceInput,
type IsDriverRegisteredForRaceResult,
type IsDriverRegisteredForRaceErrorCode,
} from './IsDriverRegisteredForRaceUseCase';
import { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('IsDriverRegisteredForRaceUseCase', () => {
@@ -20,10 +19,6 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
warn: Mock;
error: Mock;
};
let output: UseCaseOutputPort<IsDriverRegisteredForRaceResult> & {
present: Mock;
};
beforeEach(() => {
registrationRepository = {
isRegistered: vi.fn(),
@@ -34,13 +29,9 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
warn: vi.fn(),
error: vi.fn(),
};
output = {
present: vi.fn(),
} as unknown as UseCaseOutputPort<IsDriverRegisteredForRaceResult> & { present: Mock };
useCase = new IsDriverRegisteredForRaceUseCase(
registrationRepository as unknown as IRaceRegistrationRepository,
logger as unknown as Logger,
output as UseCaseOutputPort<IsDriverRegisteredForRaceResult>,
);
});
@@ -52,10 +43,7 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
const result = await useCase.execute(params);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const [[presented]] = (output.present as Mock).mock.calls as [[IsDriverRegisteredForRaceResult]];
expect(presented).toEqual({
expect(result.unwrap()).toEqual({
raceId: params.raceId,
driverId: params.driverId,
isRegistered: true,
@@ -70,10 +58,7 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
const result = await useCase.execute(params);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const [[presented]] = (output.present as Mock).mock.calls as [[IsDriverRegisteredForRaceResult]];
expect(presented).toEqual({
expect(result.unwrap()).toEqual({
raceId: params.raceId,
driverId: params.driverId,
isRegistered: false,
@@ -95,6 +80,5 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
>;
expect(errorResult.code).toBe('REPOSITORY_ERROR');
expect(errorResult.details?.message).toBe('Repository error');
expect(output.present).not.toHaveBeenCalled();
});
});

View File

@@ -1,8 +1,7 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { JoinLeagueUseCase, type JoinLeagueResult, type JoinLeagueInput, type JoinLeagueErrorCode } from './JoinLeagueUseCase';
import { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
describe('JoinLeagueUseCase', () => {

View File

@@ -1,9 +1,8 @@
import type { Logger } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { LeagueMembership } from '../../domain/entities/LeagueMembership';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application';
export type JoinLeagueErrorCode = 'ALREADY_MEMBER' | 'REPOSITORY_ERROR';

View File

@@ -10,7 +10,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import { Season } from '../../domain/entities/season/Season';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result';
describe('ListSeasonsForLeagueUseCase', () => {

View File

@@ -10,7 +10,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import { Season } from '../../domain/entities/season/Season';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result';
describe('ManageSeasonLifecycleUseCase', () => {
let useCase: ManageSeasonLifecycleUseCase;
@@ -107,7 +106,11 @@ describe('ManageSeasonLifecycleUseCase', () => {
expect(archived.isOk()).toBe(true);
expect(archived.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
presented = output.present.mock.calls[0][0] as ManageSeasonLifecycleResult;
{
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
presented = presentedRaw as ManageSeasonLifecycleResult;
}
expect(presented.season.status).toBe('archived');
});

View File

@@ -58,10 +58,12 @@ export class ManageSeasonLifecycleUseCase {
}
const season = await this.seasonRepository.findById(input.seasonId);
if (!season || season.leagueId !== league.id) {
if (!season || season.leagueId !== league.id.toString()) {
return Result.err({
code: 'SEASON_NOT_FOUND',
details: { message: `Season ${input.seasonId} does not belong to league ${league.id}` },
details: {
message: `Season ${input.seasonId} does not belong to league ${league.id.toString()}`,
},
});
}

View File

@@ -55,8 +55,9 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented =
output.present.mock.calls[0][0] as PreviewLeagueScheduleResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as PreviewLeagueScheduleResult;
expect(presented.rounds.length).toBeGreaterThan(0);
expect(presented.summary).toContain('Every Mon');
});

View File

@@ -1,22 +1,15 @@
import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator';
import { scheduleDTOToSeasonSchedule } from '../dto/LeagueScheduleDTO';
import {
scheduleDTOToSeasonSchedule,
type SeasonScheduleConfigDTO,
} from '../dto/LeagueScheduleDTO';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Logger } from '@core/shared/application';
import type { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
export type PreviewLeagueScheduleSeasonConfig = {
seasonStartDate: string;
recurrenceStrategy: string;
weekdays?: string[];
raceStartTime: string;
timezoneId: string;
plannedRounds: number;
intervalWeeks?: number;
monthlyOrdinal?: 1 | 2 | 3 | 4;
monthlyWeekday?: string;
};
export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfigDTO;
export type PreviewLeagueScheduleInput = {
schedule: PreviewLeagueScheduleSeasonConfig;
@@ -61,7 +54,7 @@ export class PreviewLeagueScheduleUseCase {
try {
let seasonSchedule: SeasonSchedule;
try {
seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule as any);
seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule);
} catch (error) {
this.logger.warn('Invalid schedule data provided', {
schedule: params.schedule,
@@ -83,11 +76,11 @@ export class PreviewLeagueScheduleUseCase {
maxRounds,
);
const rounds: PreviewLeagueScheduleRound[] = slots.map((slot) => ({
roundNumber: slot.roundNumber,
scheduledAt: slot.scheduledAt.toISOString(),
timezoneId: slot.timezone.id,
}));
const rounds: PreviewLeagueScheduleRound[] = slots.map(slot => ({
roundNumber: slot.roundNumber,
scheduledAt: slot.scheduledAt.toISOString(),
timezoneId: slot.timezone.id,
}));
const summary = this.buildSummary(params.schedule, rounds);

View File

@@ -4,7 +4,6 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';

View File

@@ -5,7 +5,7 @@
* Designed for fast, common penalty scenarios like track limits, warnings, etc.
*/
import { Penalty } from '../../domain/entities/Penalty';
import { Penalty } from '../../domain/entities/penalty/Penalty';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
@@ -74,7 +74,11 @@ export class QuickPenaltyUseCase {
);
if (!penaltyMapping) {
this.logger.error('Unknown infraction type', { infractionType: input.infractionType, severity: input.severity });
this.logger.error(
'Unknown infraction type',
undefined,
{ infractionType: input.infractionType, severity: input.severity },
);
return Result.err({ code: 'UNKNOWN_INFRACTION', details: { message: 'Unknown infraction type' } });
}
@@ -111,9 +115,16 @@ export class QuickPenaltyUseCase {
this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: input.raceId, driverId: input.driverId });
return Result.ok(undefined);
} catch (error) {
this.logger.error('Failed to apply quick penalty', { error: error instanceof Error ? error.message : 'Unknown error' });
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } });
} catch (error: unknown) {
const err =
error instanceof Error ? error : new Error('Failed to apply quick penalty');
this.logger.error('Failed to apply quick penalty', err);
return Result.err({
code: 'REPOSITORY_ERROR',
details: { message: err.message },
});
}
}

View File

@@ -13,6 +13,7 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
import type { IChampionshipStandingRepository } from '../../domain/repositories/IChampionshipStandingRepository';
import type { Penalty } from '../../domain/entities/Penalty';
import { EventScoringService } from '../../domain/services/EventScoringService';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import { ChampionshipAggregator } from '../../domain/services/ChampionshipAggregator';
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -53,7 +54,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
output = { present: vi.fn() } as unknown as typeof output;
useCase = new RecalculateChampionshipStandingsUseCase(
leagueRepository as unknown as ISeasonRepository,
leagueRepository as unknown as ILeagueRepository,
seasonRepository as unknown as ISeasonRepository,
leagueScoringConfigRepository as unknown as ILeagueScoringConfigRepository,
raceRepository as unknown as IRaceRepository,
@@ -172,7 +173,9 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as RecalculateChampionshipStandingsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as RecalculateChampionshipStandingsResult;
expect(presented.leagueId).toBe('league-1');
expect(presented.seasonId).toBe('season-1');
expect(presented.entries).toHaveLength(1);

View File

@@ -97,7 +97,7 @@ export class RecalculateChampionshipStandingsUseCase {
{};
for (const race of races) {
const sessionType = this.mapRaceSessionType(race.sessionType);
const sessionType = this.mapRaceSessionType(String(race.sessionType));
if (!championship.sessionTypes.includes(sessionType)) {
continue;
}

View File

@@ -100,7 +100,9 @@ describe('RegisterForRaceUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as RegisterForRaceResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as RegisterForRaceResult;
expect(presented).toEqual<RegisterForRaceResult>({
raceId: 'race-1',
driverId: 'driver-1',

View File

@@ -9,7 +9,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result';
interface LeagueRepositoryMock {
findById: Mock;
@@ -31,13 +30,13 @@ describe('RejectLeagueJoinRequestUseCase', () => {
beforeEach(() => {
leagueRepository = {
findById: vi.fn(),
} as unknown as ILeagueRepository as any;
};
leagueMembershipRepository = {
getMembership: vi.fn(),
getJoinRequests: vi.fn(),
removeJoinRequest: vi.fn(),
} as unknown as ILeagueMembershipRepository as any;
};
logger = {
debug: vi.fn(),
@@ -87,7 +86,9 @@ describe('RejectLeagueJoinRequestUseCase', () => {
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as RejectLeagueJoinRequestResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as RejectLeagueJoinRequestResult;
expect(presented.leagueId).toBe('league-1');
expect(presented.requestId).toBe('req-1');
expect(presented.status).toBe('rejected');

View File

@@ -74,7 +74,12 @@ export class RejectLeagueJoinRequestUseCase {
});
}
const currentStatus = (joinRequest as any).status ?? 'pending';
const currentStatus = (() => {
const rawStatus = (joinRequest as unknown as { status?: unknown }).status;
return rawStatus === 'pending' || rawStatus === 'approved' || rawStatus === 'rejected'
? rawStatus
: 'pending';
})();
if (currentStatus !== 'pending') {
this.logger.warn('Join request is in invalid state for rejection', {
leagueId,

View File

@@ -9,7 +9,6 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result';
interface TeamRepositoryMock {
findById: Mock;
@@ -31,13 +30,13 @@ describe('RejectTeamJoinRequestUseCase', () => {
beforeEach(() => {
teamRepository = {
findById: vi.fn(),
} as unknown as ITeamRepository as any;
};
membershipRepository = {
getMembership: vi.fn(),
getJoinRequests: vi.fn(),
removeJoinRequest: vi.fn(),
} as unknown as ITeamMembershipRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -70,7 +70,12 @@ export class RejectTeamJoinRequestUseCase {
});
}
const currentStatus = (joinRequest as any).status ?? 'pending';
const currentStatus = (() => {
const rawStatus = (joinRequest as unknown as { status?: unknown }).status;
return rawStatus === 'pending' || rawStatus === 'approved' || rawStatus === 'rejected'
? rawStatus
: 'pending';
})();
if (currentStatus !== 'pending') {
this.logger.warn('Join request is in invalid state for rejection', {
teamId,

View File

@@ -50,8 +50,9 @@ describe('RemoveLeagueMemberUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(leagueMembershipRepository.saveMembership).toHaveBeenCalledTimes(1);
const savedMembership = leagueMembershipRepository.saveMembership.mock.calls[0][0];
expect(savedMembership.status.toString()).toBe('inactive');
const savedMembership = leagueMembershipRepository.saveMembership.mock.calls[0]?.[0];
expect(savedMembership).toBeDefined();
expect(savedMembership!.status.toString()).toBe('inactive');
expect(output.present).toHaveBeenCalledTimes(1);
expect(output.present).toHaveBeenCalledWith({

View File

@@ -149,7 +149,9 @@ describe('RequestProtestDefenseUseCase', () => {
expect(protestRepository.update).toHaveBeenCalledWith(updatedProtest);
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as RequestProtestDefenseResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as RequestProtestDefenseResult;
expect(presented).toEqual({
leagueId: 'league-1',
protestId: 'protest-1',

View File

@@ -55,8 +55,14 @@ export class RequestProtestDefenseUseCase {
return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race not found' } });
}
const membership = await this.membershipRepository.getMembership(race.leagueId, input.stewardId);
if (!membership || !isLeagueStewardOrHigherRole(membership.role)) {
const membership = await this.membershipRepository.getMembership(
race.leagueId,
input.stewardId,
);
if (
!membership ||
!isLeagueStewardOrHigherRole(membership.role.toString())
) {
return Result.err({
code: 'INSUFFICIENT_PERMISSIONS',
details: { message: 'Insufficient permissions to request defense' },

View File

@@ -5,7 +5,6 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
describe('ReviewProtestUseCase', () => {
let useCase: ReviewProtestUseCase;

View File

@@ -73,9 +73,23 @@ export class ReviewProtestUseCase {
await this.protestRepository.update(updatedProtest);
const protestId = (() => {
const unknownId = (protest as unknown as { id: unknown }).id;
if (typeof unknownId === 'string') return unknownId;
if (
unknownId &&
typeof unknownId === 'object' &&
'toString' in unknownId &&
typeof (unknownId as { toString: unknown }).toString === 'function'
) {
return (unknownId as { toString: () => string }).toString();
}
return String(unknownId);
})();
const result: ReviewProtestResult = {
leagueId: race.leagueId,
protestId: typeof protest.id === 'string' ? protest.id : (protest as any).id,
protestId,
status: input.decision === 'uphold' ? 'upheld' : 'dismissed',
};

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { Season } from '@core/racing/domain/entities/season/Season';
import { League } from '@core/racing/domain/entities/League';
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import {
@@ -23,13 +24,49 @@ import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueC
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
function createFakeLeagueRepository(seed: Array<{ id: string }>): ILeagueRepository {
type MockOutputPort<T> = UseCaseOutputPort<T> & { present: ReturnType<typeof vi.fn> };
function createOutputPort<T>(): MockOutputPort<T> {
return {
findById: async (id: string) => seed.find((l) => l.id === id) ?? null,
findAll: async () => seed,
create: async (league: any) => league,
update: async (league: any) => league,
} as unknown as ILeagueRepository;
present: vi.fn<(data: T) => void>(),
};
}
function getUnknownString(value: unknown): string | null {
if (typeof value === 'string') return value;
if (
value &&
typeof value === 'object' &&
'toString' in value &&
typeof (value as { toString: unknown }).toString === 'function'
) {
return (value as { toString: () => string }).toString();
}
return null;
}
function createFakeLeagueRepository(seed: Array<{ id: string }>): ILeagueRepository {
const leagues: League[] = seed.map(({ id }) =>
League.create({
id,
name: `League ${id}`,
description: 'Test league',
ownerId: 'owner-1',
}),
);
return {
findById: async (id: string) =>
leagues.find((league) => league.id.toString() === id) ?? null,
findAll: async () => leagues,
findByOwnerId: async (ownerId: string) =>
leagues.filter((league) => getUnknownString((league as unknown as { ownerId: unknown }).ownerId) === ownerId),
create: async (league: League) => league,
update: async (league: League) => league,
delete: async () => undefined,
exists: async (id: string) => leagues.some((league) => league.id.toString() === id),
searchByName: async () => [],
};
}
function createLeagueConfigFormModel(overrides?: Partial<LeagueConfigFormModel>): LeagueConfigFormModel {
@@ -100,9 +137,7 @@ describe('CreateSeasonForLeagueUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<CreateSeasonForLeagueResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<CreateSeasonForLeagueResult>();
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo, output);
@@ -150,7 +185,9 @@ describe('CreateSeasonForLeagueUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const payload = (output.present as ReturnType<typeof vi.fn>).mock.calls[0][0] as CreateSeasonForLeagueResult;
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(payloadRaw).toBeDefined();
const payload = payloadRaw as CreateSeasonForLeagueResult;
const season = payload.season;
expect(season.leagueId).toBe('league-1');
@@ -199,7 +236,9 @@ describe('CreateSeasonForLeagueUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const payload = (output.present as ReturnType<typeof vi.fn>).mock.calls[0][0] as CreateSeasonForLeagueResult;
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(payloadRaw).toBeDefined();
const payload = payloadRaw as CreateSeasonForLeagueResult;
const season = payload.season;
expect(season.id).not.toBe(sourceSeason.id);
@@ -223,9 +262,7 @@ describe('CreateSeasonForLeagueUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<CreateSeasonForLeagueResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<CreateSeasonForLeagueResult>();
const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo, output);
@@ -257,9 +294,7 @@ describe('ListSeasonsForLeagueUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<ListSeasonsForLeagueResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<ListSeasonsForLeagueResult>();
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo, output);
@@ -303,7 +338,9 @@ describe('ListSeasonsForLeagueUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const payload = (output.present as ReturnType<typeof vi.fn>).mock.calls[0][0] as ListSeasonsForLeagueResult;
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(payloadRaw).toBeDefined();
const payload = payloadRaw as ListSeasonsForLeagueResult;
const league1Seasons = payload.seasons.filter((s) => s.leagueId === 'league-1');
expect(league1Seasons.map((s) => s.id).sort()).toEqual(['season-1', 'season-2']);
@@ -319,9 +356,7 @@ describe('ListSeasonsForLeagueUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<ListSeasonsForLeagueResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<ListSeasonsForLeagueResult>();
const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo, output);
@@ -347,9 +382,7 @@ describe('GetSeasonDetailsUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<GetSeasonDetailsResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<GetSeasonDetailsResult>();
const useCase = new GetSeasonDetailsUseCase(leagueRepo, seasonRepo, output);
@@ -378,7 +411,9 @@ describe('GetSeasonDetailsUseCase', () => {
expect(result.unwrap()).toBeUndefined();
expect(output.present).toHaveBeenCalledTimes(1);
const payload = (output.present as ReturnType<typeof vi.fn>).mock.calls[0][0] as GetSeasonDetailsResult;
const payloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(payloadRaw).toBeDefined();
const payload = payloadRaw as GetSeasonDetailsResult;
expect(payload.season.id).toBe('season-1');
expect(payload.season.leagueId).toBe('league-1');
@@ -441,9 +476,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<ManageSeasonLifecycleResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<ManageSeasonLifecycleResult>();
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output);
@@ -476,7 +509,9 @@ describe('ManageSeasonLifecycleUseCase', () => {
const activated = await useCase.execute(activateCommand);
expect(activated.isOk()).toBe(true);
const activatePayload = (output.present as ReturnType<typeof vi.fn>).mock.calls[0][0] as ManageSeasonLifecycleResult;
const activatePayloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(activatePayloadRaw).toBeDefined();
const activatePayload = activatePayloadRaw as ManageSeasonLifecycleResult;
expect(activatePayload.season.status).toBe('active');
const completeCommand: ManageSeasonLifecycleCommand = {
@@ -488,7 +523,9 @@ describe('ManageSeasonLifecycleUseCase', () => {
const completed = await useCase.execute(completeCommand);
expect(completed.isOk()).toBe(true);
const completePayload = (output.present as ReturnType<typeof vi.fn>).mock.calls[1][0] as ManageSeasonLifecycleResult;
const completePayloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[1]?.[0];
expect(completePayloadRaw).toBeDefined();
const completePayload = completePayloadRaw as ManageSeasonLifecycleResult;
expect(completePayload.season.status).toBe('completed');
const archiveCommand: ManageSeasonLifecycleCommand = {
@@ -500,7 +537,9 @@ describe('ManageSeasonLifecycleUseCase', () => {
const archived = await useCase.execute(archiveCommand);
expect(archived.isOk()).toBe(true);
const archivePayload = (output.present as ReturnType<typeof vi.fn>).mock.calls[2][0] as ManageSeasonLifecycleResult;
const archivePayloadRaw = (output.present as ReturnType<typeof vi.fn>).mock.calls[2]?.[0];
expect(archivePayloadRaw).toBeDefined();
const archivePayload = archivePayloadRaw as ManageSeasonLifecycleResult;
expect(archivePayload.season.status).toBe('archived');
expect(currentSeason.status).toBe('archived');
@@ -544,9 +583,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<ManageSeasonLifecycleResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<ManageSeasonLifecycleResult>();
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output);
@@ -575,9 +612,7 @@ describe('ManageSeasonLifecycleUseCase', () => {
listActiveByLeague: vi.fn(),
} as unknown as ISeasonRepository;
const output: UseCaseOutputPort<ManageSeasonLifecycleResult> & { present: ReturnType<typeof vi.fn> } = {
present: vi.fn(),
} as any;
const output = createOutputPort<ManageSeasonLifecycleResult>();
const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo, output);

View File

@@ -190,7 +190,7 @@ export class CreateSeasonForLeagueUseCase {
const season = Season.create({
id: seasonId,
leagueId: league.id,
leagueId: league.id.toString(),
gameId: command.gameId,
name: command.name,
year: new Date().getFullYear(),
@@ -235,31 +235,52 @@ export class CreateSeasonForLeagueUseCase {
maxDrivers?: number;
} {
const schedule = this.buildScheduleFromTimings(config);
const scoring = config.scoring ?? {};
const scoringConfig = new SeasonScoringConfig({
scoringPresetId: config.scoring.patternId ?? 'custom',
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
scoringPresetId: scoring.patternId ?? 'custom',
customScoringEnabled: scoring.customScoringEnabled ?? false,
});
const dropPolicyInput = config.dropPolicy ?? {};
const dropStrategy = dropPolicyInput.strategy;
const dropPolicy = new SeasonDropPolicy({
strategy: config.dropPolicy.strategy,
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
strategy:
dropStrategy === 'none' ||
dropStrategy === 'bestNResults' ||
dropStrategy === 'dropWorstN'
? dropStrategy
: 'none',
...(dropPolicyInput.n !== undefined ? { n: dropPolicyInput.n } : {}),
});
const stewardingInput = config.stewarding ?? {};
const decisionMode = stewardingInput.decisionMode;
const stewardingConfig = new SeasonStewardingConfig({
decisionMode: config.stewarding.decisionMode,
...(config.stewarding.requiredVotes !== undefined
? { requiredVotes: config.stewarding.requiredVotes }
decisionMode:
decisionMode === 'admin_only' ||
decisionMode === 'steward_decides' ||
decisionMode === 'steward_vote' ||
decisionMode === 'member_vote' ||
decisionMode === 'steward_veto' ||
decisionMode === 'member_veto'
? decisionMode
: 'admin_only',
...(stewardingInput.requiredVotes !== undefined
? { requiredVotes: stewardingInput.requiredVotes }
: {}),
requireDefense: config.stewarding.requireDefense,
defenseTimeLimit: config.stewarding.defenseTimeLimit,
voteTimeLimit: config.stewarding.voteTimeLimit,
protestDeadlineHours: config.stewarding.protestDeadlineHours,
stewardingClosesHours: config.stewarding.stewardingClosesHours,
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
requireDefense: stewardingInput.requireDefense ?? false,
defenseTimeLimit: stewardingInput.defenseTimeLimit ?? 48,
voteTimeLimit: stewardingInput.voteTimeLimit ?? 72,
protestDeadlineHours: stewardingInput.protestDeadlineHours ?? 48,
stewardingClosesHours: stewardingInput.stewardingClosesHours ?? 168,
notifyAccusedOnProtest: stewardingInput.notifyAccusedOnProtest ?? true,
notifyOnVoteRequired: stewardingInput.notifyOnVoteRequired ?? true,
});
const structure = config.structure;
const maxDrivers =
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
typeof structure?.maxDrivers === 'number' && structure.maxDrivers > 0
? structure.maxDrivers
: undefined;
@@ -275,44 +296,50 @@ export class CreateSeasonForLeagueUseCase {
private buildScheduleFromTimings(
config: LeagueConfigFormModel,
): SeasonSchedule | undefined {
const { timings } = config;
if (!timings.seasonStartDate || !timings.raceStartTime) {
const timings = config.timings;
if (!timings?.seasonStartDate || !timings.raceStartTime) {
return undefined;
}
const startDate = new Date(timings.seasonStartDate);
const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime);
const timezoneId = timings.timezoneId ?? 'UTC';
const timezone = new LeagueTimezone(timezoneId);
const timezone = LeagueTimezone.create(timings.timezoneId ?? 'UTC');
const plannedRounds =
typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0
? timings.roundsPlanned
: timings.sessionCount;
: timings.sessionCount ?? 0;
if (!Number.isInteger(plannedRounds) || plannedRounds <= 0) {
return undefined;
}
const weekdaysRaw = timings.weekdays ?? [];
const weekdays = WeekdaySet.fromArray(
weekdaysRaw
.filter((d): d is Weekday => (['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const).includes(d as Weekday))
.slice(0),
);
const safeWeekdays = weekdays.getAll().length > 0 ? weekdays : WeekdaySet.fromArray(['Mon']);
const recurrence = (() => {
const weekdays: WeekdaySet =
timings.weekdays && timings.weekdays.length > 0
? WeekdaySet.fromArray(
timings.weekdays as unknown as Weekday[],
)
: WeekdaySet.fromArray(['Mon']);
switch (timings.recurrenceStrategy) {
case 'everyNWeeks':
return RecurrenceStrategyFactory.everyNWeeks(
timings.intervalWeeks ?? 2,
weekdays,
safeWeekdays,
);
case 'monthlyNthWeekday': {
const pattern = new MonthlyRecurrencePattern({
ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday,
});
const pattern = MonthlyRecurrencePattern.create(
(timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
(timings.monthlyWeekday ?? 'Mon') as Weekday,
);
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
}
case 'weekly':
default:
return RecurrenceStrategyFactory.weekly(weekdays);
return RecurrenceStrategyFactory.weekly(safeWeekdays);
}
})();
@@ -350,7 +377,7 @@ export class ListSeasonsForLeagueUseCase {
});
}
const seasons = await this.seasonRepository.listByLeague(league.id);
const seasons = await this.seasonRepository.listByLeague(league.id.toString());
this.output.present({ seasons });
@@ -391,7 +418,7 @@ export class GetSeasonDetailsUseCase {
}
const season = await this.seasonRepository.findById(query.seasonId);
if (!season || season.leagueId !== league.id) {
if (!season || season.leagueId !== league.id.toString()) {
return Result.err({
code: 'SEASON_NOT_FOUND',
details: {
@@ -439,7 +466,7 @@ export class ManageSeasonLifecycleUseCase {
}
const season = await this.seasonRepository.findById(command.seasonId);
if (!season || season.leagueId !== league.id) {
if (!season || season.leagueId !== league.id.toString()) {
return Result.err({
code: 'SEASON_NOT_FOUND',
details: {

View File

@@ -110,7 +110,9 @@ describe('SendFinalResultsUseCase', () => {
expect(notificationService.sendNotification).toHaveBeenCalledTimes(2);
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as SendFinalResultsResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as SendFinalResultsResult;
expect(presented).toEqual({
leagueId: 'league-1',
raceId: 'race-1',

View File

@@ -10,6 +10,8 @@ import type { IResultRepository } from '../../domain/repositories/IResultReposit
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import { isLeagueStewardOrHigherRole } from '../../domain/types/LeagueRoles';
import { Position } from '../../domain/entities/result/Position';
import { IncidentCount } from '../../domain/entities/result/IncidentCount';
export type SendFinalResultsInput = {
leagueId: string;
@@ -61,8 +63,11 @@ export class SendFinalResultsUseCase {
return Result.err({ code: 'RACE_NOT_FOUND', details: { message: 'Race event not found' } });
}
const membership = await this.membershipRepository.getMembership(league.id.toString(), input.triggeredById);
if (!membership || !isLeagueStewardOrHigherRole(membership.role)) {
const membership = await this.membershipRepository.getMembership(
league.id.toString(),
input.triggeredById,
);
if (!membership || !isLeagueStewardOrHigherRole(membership.role.toString())) {
return Result.err({
code: 'INSUFFICIENT_PERMISSIONS',
details: { message: 'Insufficient permissions to send final results' },
@@ -133,10 +138,12 @@ export class SendFinalResultsUseCase {
);
const title = `Final Results: ${raceEvent.name}`;
const positionValue = position instanceof Position ? position.toNumber() : position;
const incidentValue = incidents instanceof IncidentCount ? incidents.toNumber() : incidents;
const body = this.buildFinalResultsBody(
position,
positionValue,
positionChange,
incidents,
incidentValue,
finalRatingChange,
hadPenaltiesApplied,
);
@@ -152,9 +159,9 @@ export class SendFinalResultsUseCase {
raceEventId: raceEvent.id,
sessionId: raceEvent.getMainRaceSession()?.id ?? '',
leagueId,
position,
position: positionValue,
positionChange,
incidents,
incidents: incidentValue,
finalRatingChange,
hadPenaltiesApplied,
},

View File

@@ -117,7 +117,9 @@ describe('SendPerformanceSummaryUseCase', () => {
);
expect(output.present).toHaveBeenCalledTimes(1);
const presented = output.present.mock.calls[0][0] as SendPerformanceSummaryResult;
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
const presented = presentedRaw as SendPerformanceSummaryResult;
expect(presented).toEqual({
leagueId: 'league-1',
raceId: 'race-1',

View File

@@ -74,8 +74,11 @@ export class SendPerformanceSummaryUseCase {
}
if (input.triggeredById !== input.driverId) {
const membership = await this.membershipRepository.getMembership(league.id.toString(), input.triggeredById);
if (!membership || !isLeagueStewardOrHigherRole(membership.role)) {
const membership = await this.membershipRepository.getMembership(
league.id.toString(),
input.triggeredById,
);
if (!membership || !isLeagueStewardOrHigherRole(membership.role.toString())) {
return Result.err({
code: 'INSUFFICIENT_PERMISSIONS',
details: { message: 'Insufficient permissions to send performance summary' },

View File

@@ -35,7 +35,7 @@ describe('UpdateDriverProfileUseCase', () => {
error: vi.fn(),
} as unknown as Logger & { error: ReturnType<typeof vi.fn> };
useCase = new UpdateDriverProfileUseCase(driverRepository, output, logger);
useCase = new UpdateDriverProfileUseCase(driverRepository, logger, output);
});
it('updates driver profile successfully', async () => {
@@ -66,7 +66,9 @@ describe('UpdateDriverProfileUseCase', () => {
expect(driverRepository.update).toHaveBeenCalled();
expect(output.present).toHaveBeenCalledTimes(1);
expect(output.present).toHaveBeenCalledWith({ driverId: 'driver-1' });
const presentedRaw = output.present.mock.calls[0]?.[0];
expect(presentedRaw).toBeDefined();
expect(presentedRaw).toEqual({ id: 'driver-1' });
});
it('returns error when driver not found', async () => {

View File

@@ -1,3 +1,4 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { Result } from '@core/shared/application/Result';
import type { UseCase } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -23,15 +24,20 @@ export type UpdateDriverProfileErrorCode =
* Encapsulates domain entity mutation. Mapping to DTOs is handled by presenters
* in the presentation layer through the output port.
*/
export class UpdateDriverProfileUseCase implements UseCase<UpdateDriverProfileInput, UpdateDriverProfileResult, UpdateDriverProfileErrorCode> {
export class UpdateDriverProfileUseCase
implements UseCase<UpdateDriverProfileInput, void, UpdateDriverProfileErrorCode>
{
constructor(
private readonly driverRepository: IDriverRepository,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<UpdateDriverProfileResult>,
) {}
async execute(
input: UpdateDriverProfileInput,
): Promise<Result<UpdateDriverProfileResult, ApplicationErrorCode<UpdateDriverProfileErrorCode, { message: string }>>> {
): Promise<
Result<void, ApplicationErrorCode<UpdateDriverProfileErrorCode, { message: string }>>
> {
const { driverId, bio, country } = input;
if ((bio !== undefined && bio.trim().length === 0) || (country !== undefined && country.trim().length === 0)) {
@@ -61,7 +67,9 @@ export class UpdateDriverProfileUseCase implements UseCase<UpdateDriverProfileIn
await this.driverRepository.update(updated);
return Result.ok(updated);
this.output.present(updated);
return Result.ok(undefined);
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to update driver profile';

View File

@@ -10,23 +10,22 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
import type { Logger } from '@core/shared/application/Logger';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Result } from '@core/shared/application/Result';
describe('WithdrawFromRaceUseCase', () => {
let raceRepository: IRaceRepository;
let registrationRepository: IRaceRegistrationRepository;
let raceRepository: { findById: ReturnType<typeof vi.fn> };
let registrationRepository: { isRegistered: ReturnType<typeof vi.fn>; withdraw: ReturnType<typeof vi.fn> };
let logger: Logger;
let output: UseCaseOutputPort<WithdrawFromRaceResult> & { present: ReturnType<typeof vi.fn> };
beforeEach(() => {
raceRepository = {
findById: vi.fn(),
} as unknown as IRaceRepository;
};
registrationRepository = {
isRegistered: vi.fn(),
withdraw: vi.fn(),
} as unknown as IRaceRegistrationRepository;
};
logger = {
debug: vi.fn(),
@@ -35,21 +34,30 @@ describe('WithdrawFromRaceUseCase', () => {
error: vi.fn(),
};
output = { present: vi.fn() } as any;
output = { present: vi.fn() } as unknown as UseCaseOutputPort<WithdrawFromRaceResult> & {
present: ReturnType<typeof vi.fn>;
};
});
const createUseCase = () =>
new WithdrawFromRaceUseCase(raceRepository, registrationRepository, logger, output);
new WithdrawFromRaceUseCase(
raceRepository as unknown as IRaceRepository,
registrationRepository as unknown as IRaceRegistrationRepository,
logger,
output,
);
it('withdraws from race successfully', async () => {
const race = {
id: 'race-1',
isUpcoming: vi.fn().mockReturnValue(true),
} as any;
};
(raceRepository.findById as any).mockResolvedValue(race);
(registrationRepository.isRegistered as any).mockResolvedValue(true);
(registrationRepository.withdraw as any).mockResolvedValue(undefined);
raceRepository.findById.mockResolvedValue(
race as unknown as Awaited<ReturnType<IRaceRepository['findById']>>,
);
registrationRepository.isRegistered.mockResolvedValue(true);
registrationRepository.withdraw.mockResolvedValue(undefined);
const useCase = createUseCase();
@@ -72,7 +80,7 @@ describe('WithdrawFromRaceUseCase', () => {
});
it('returns error when race is not found', async () => {
(raceRepository.findById as any).mockResolvedValue(null);
raceRepository.findById.mockResolvedValue(null);
const useCase = createUseCase();
@@ -94,10 +102,12 @@ describe('WithdrawFromRaceUseCase', () => {
const race = {
id: 'race-1',
isUpcoming: vi.fn().mockReturnValue(true),
} as any;
};
(raceRepository.findById as any).mockResolvedValue(race);
(registrationRepository.isRegistered as any).mockResolvedValue(false);
raceRepository.findById.mockResolvedValue(
race as unknown as Awaited<ReturnType<IRaceRepository['findById']>>,
);
registrationRepository.isRegistered.mockResolvedValue(false);
const useCase = createUseCase();
@@ -119,10 +129,12 @@ describe('WithdrawFromRaceUseCase', () => {
const race = {
id: 'race-1',
isUpcoming: vi.fn().mockReturnValue(false),
} as any;
};
(raceRepository.findById as any).mockResolvedValue(race);
(registrationRepository.isRegistered as any).mockResolvedValue(true);
raceRepository.findById.mockResolvedValue(
race as unknown as Awaited<ReturnType<IRaceRepository['findById']>>,
);
registrationRepository.isRegistered.mockResolvedValue(true);
const useCase = createUseCase();
@@ -144,11 +156,13 @@ describe('WithdrawFromRaceUseCase', () => {
const race = {
id: 'race-1',
isUpcoming: vi.fn().mockReturnValue(true),
} as any;
};
(raceRepository.findById as any).mockResolvedValue(race);
(registrationRepository.isRegistered as any).mockResolvedValue(true);
(registrationRepository.withdraw as any).mockRejectedValue(new Error('DB failure'));
raceRepository.findById.mockResolvedValue(
race as unknown as Awaited<ReturnType<IRaceRepository['findById']>>,
);
registrationRepository.isRegistered.mockResolvedValue(true);
registrationRepository.withdraw.mockRejectedValue(new Error('DB failure'));
const useCase = createUseCase();

View File

@@ -39,10 +39,11 @@ export class RaceResultGenerator {
// Generate results
const results: Result[] = [];
for (let i = 0; i < driverPerformances.length; i++) {
const { driverId } = driverPerformances[i];
const position = i + 1;
const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1;
for (const [index, performance] of driverPerformances.entries()) {
const driverId = performance.driverId;
const position = index + 1;
const startPosition =
qualiPerformances.findIndex(p => p.driverId === driverId) + 1;
// Generate realistic lap times (90-120 seconds for a lap)
const baseLapTime = 90000 + Math.random() * 30000;

View File

@@ -40,10 +40,11 @@ export class RaceResultGeneratorWithIncidents {
// Generate results
const results: ResultWithIncidents[] = [];
for (let i = 0; i < driverPerformances.length; i++) {
const { driverId } = driverPerformances[i];
const position = i + 1;
const startPosition = qualiPerformances.findIndex(p => p.driverId === driverId) + 1;
for (const [index, performance] of driverPerformances.entries()) {
const driverId = performance.driverId;
const position = index + 1;
const startPosition =
qualiPerformances.findIndex(p => p.driverId === driverId) + 1;
// Generate realistic lap times (90-120 seconds for a lap)
const baseLapTime = 90000 + Math.random() * 30000;
@@ -118,7 +119,7 @@ export class RaceResultGeneratorWithIncidents {
/**
* Select appropriate incident type based on context
*/
private static selectIncidentType(position: number, totalDrivers: number, incidentIndex: number): IncidentType {
private static selectIncidentType(position: number, totalDrivers: number, _incidentIndex: number): IncidentType {
// Different incident types have different probabilities
const incidentProbabilities: Array<{ type: IncidentType; weight: number }> = [
{ type: 'track_limits', weight: 40 }, // Most common
@@ -153,9 +154,8 @@ export class RaceResultGeneratorWithIncidents {
/**
* Select appropriate lap for incident
*/
private static selectIncidentLap(incidentNumber: number, totalIncidents: number): number {
private static selectIncidentLap(incidentNumber: number, _totalIncidents: number): number {
// Spread incidents throughout the race
const raceLaps = 20; // Assume 20 lap race
const lapRanges = [
{ min: 1, max: 5 }, // Early race
{ min: 6, max: 12 }, // Mid race
@@ -164,8 +164,7 @@ export class RaceResultGeneratorWithIncidents {
// Distribute incidents across race phases
const phaseIndex = Math.min(incidentNumber - 1, lapRanges.length - 1);
const range = lapRanges[phaseIndex];
const range = lapRanges[phaseIndex] ?? lapRanges[0]!;
return Math.floor(Math.random() * (range.max - range.min + 1)) + range.min;
}
@@ -228,8 +227,9 @@ export class RaceResultGeneratorWithIncidents {
],
};
const options = descriptions[type] || descriptions.other;
return options[Math.floor(Math.random() * options.length)];
const options = descriptions[type] ?? descriptions.other;
const selected = options[Math.floor(Math.random() * options.length)];
return selected ?? descriptions.other[0]!;
}
/**