fix issues in core
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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' } });
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,7 +55,7 @@ export class GetSeasonDetailsUseCase {
|
||||
}
|
||||
|
||||
const result: GetSeasonDetailsResult = {
|
||||
leagueId: league.id,
|
||||
leagueId: league.id.toString(),
|
||||
season,
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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()}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]!;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user