fix issues in core

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

View File

@@ -20,11 +20,11 @@ describe('GetEntityAnalyticsQuery', () => {
pageViewRepository = {
countByEntityId: vi.fn(),
countUniqueVisitors: vi.fn(),
} as unknown as IPageViewRepository as any;
};
engagementRepository = {
getSponsorClicksForEntity: vi.fn(),
} as unknown as IEngagementRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -16,7 +16,7 @@ describe('RecordEngagementUseCase', () => {
beforeEach(() => {
engagementRepository = {
save: vi.fn(),
} as unknown as IEngagementRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -16,7 +16,7 @@ describe('RecordPageViewUseCase', () => {
beforeEach(() => {
pageViewRepository = {
save: vi.fn(),
} as unknown as IPageViewRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -31,26 +31,19 @@ export class RecordPageViewUseCase implements UseCase<RecordPageViewInput, void,
async execute(input: RecordPageViewInput): Promise<Result<void, ApplicationErrorCode<RecordPageViewErrorCode, { message: string }>>> {
try {
const props = {
type PageViewCreateProps = Parameters<(typeof PageView)['create']>[0];
const props: PageViewCreateProps = {
id: crypto.randomUUID(),
entityType: input.entityType,
entityId: input.entityId,
visitorType: input.visitorType,
sessionId: input.sessionId,
} as any;
if (input.visitorId !== undefined) {
props.visitorId = input.visitorId;
}
if (input.referrer !== undefined) {
props.referrer = input.referrer;
}
if (input.userAgent !== undefined) {
props.userAgent = input.userAgent;
}
if (input.country !== undefined) {
props.country = input.country;
}
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
...(input.country !== undefined ? { country: input.country } : {}),
};
const pageView = PageView.create(props);

View File

@@ -4,6 +4,10 @@ import { User } from '../../domain/entities/User';
import { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
type GetCurrentSessionOutput = {
user: User;
};
describe('GetCurrentSessionUseCase', () => {
let useCase: GetCurrentSessionUseCase;
let mockUserRepo: {
@@ -14,7 +18,7 @@ describe('GetCurrentSessionUseCase', () => {
emailExists: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<GetCurrentSessionOutput> & { present: Mock };
beforeEach(() => {
mockUserRepo = {

View File

@@ -11,7 +11,7 @@ describe('GetCurrentUserSessionUseCase', () => {
clearSession: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<AuthSessionDTO | null> & { present: Mock };
let useCase: GetCurrentUserSessionUseCase;
beforeEach(() => {

View File

@@ -3,14 +3,16 @@ import { GetUserUseCase } from './GetUserUseCase';
import { User } from '../../domain/entities/User';
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import type { Result } from '@core/shared/application/Result';
import { Result } from '@core/shared/application/Result';
type GetUserOutput = Result<{ user: User }, unknown>;
describe('GetUserUseCase', () => {
let userRepository: {
findById: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<GetUserOutput> & { present: Mock };
let useCase: GetUserUseCase;
beforeEach(() => {
@@ -54,8 +56,9 @@ describe('GetUserUseCase', () => {
expect(userRepository.findById).toHaveBeenCalledWith('user-1');
expect(result.isOk()).toBe(true);
expect(output.present).toHaveBeenCalled();
const callArgs = output.present.mock.calls?.[0]?.[0] as Result<any, any>;
const user = callArgs.unwrap().user;
const callArgs = output.present.mock.calls?.[0]?.[0];
expect(callArgs).toBeInstanceOf(Result);
const user = (callArgs as GetUserOutput).unwrap().user;
expect(user).toBeInstanceOf(User);
expect(user.getId().value).toBe('user-1');
expect(user.getDisplayName()).toBe('Test User');

View File

@@ -17,7 +17,7 @@ describe('HandleAuthCallbackUseCase', () => {
clearSession: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<AuthSessionDTO> & { present: Mock };
let useCase: HandleAuthCallbackUseCase;
beforeEach(() => {

View File

@@ -6,6 +6,8 @@ import {
type LoginErrorCode,
} from './LoginUseCase';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { UserId } from '../../domain/value-objects/UserId';
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
import { User } from '../../domain/entities/User';
@@ -56,13 +58,12 @@ describe('LoginUseCase', () => {
const emailVO = EmailAddress.create(input.email);
const user = User.create({
id: { value: 'user-1' } as any,
id: UserId.fromString('user-1'),
displayName: 'Test User',
email: emailVO.value,
passwordHash: PasswordHash.fromHash('stored-hash'),
});
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
authRepo.findByEmail.mockResolvedValue(user);
passwordService.verify.mockResolvedValue(true);
@@ -107,13 +108,12 @@ describe('LoginUseCase', () => {
const emailVO = EmailAddress.create(input.email);
const user = User.create({
id: { value: 'user-1' } as any,
id: UserId.fromString('user-1'),
displayName: 'Test User',
email: emailVO.value,
passwordHash: PasswordHash.fromHash('stored-hash'),
});
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
authRepo.findByEmail.mockResolvedValue(user);
passwordService.verify.mockResolvedValue(false);

View File

@@ -70,21 +70,29 @@ export class LoginWithEmailUseCase {
} as LoginWithEmailApplicationError);
}
const session = await this.sessionPort.createSession({
type CreateSessionInput = Parameters<IdentitySessionPort['createSession']>[0];
const createSessionInput = {
id: user.id,
displayName: user.displayName,
email: user.email,
primaryDriverId: user.primaryDriverId,
} as any);
...(user.email !== undefined ? { email: user.email } : {}),
...(user.primaryDriverId !== undefined
? { primaryDriverId: user.primaryDriverId }
: {}),
} satisfies CreateSessionInput;
const session = await this.sessionPort.createSession(createSessionInput);
const result: LoginWithEmailResult = {
sessionToken: (session as any).token,
userId: (session as any).user.id,
displayName: (session as any).user.displayName,
email: (session as any).user.email,
primaryDriverId: (session as any).user.primaryDriverId,
issuedAt: (session as any).issuedAt,
expiresAt: (session as any).expiresAt,
sessionToken: session.token,
userId: session.user.id,
displayName: session.user.displayName,
...(session.user.email !== undefined ? { email: session.user.email } : {}),
...(session.user.primaryDriverId !== undefined
? { primaryDriverId: session.user.primaryDriverId }
: {}),
issuedAt: session.issuedAt,
expiresAt: session.expiresAt,
};
this.output.present(result);

View File

@@ -13,6 +13,8 @@ vi.mock('../../domain/value-objects/PasswordHash', () => ({
},
}));
type SignupOutput = unknown;
describe('SignupUseCase', () => {
let authRepo: {
findByEmail: Mock;
@@ -22,7 +24,7 @@ describe('SignupUseCase', () => {
hash: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<SignupOutput> & { present: Mock };
let useCase: SignupUseCase;
beforeEach(() => {

View File

@@ -6,6 +6,8 @@ import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
type SignupWithEmailOutput = unknown;
describe('SignupWithEmailUseCase', () => {
let userRepository: {
findByEmail: Mock;
@@ -17,7 +19,7 @@ describe('SignupWithEmailUseCase', () => {
clearSession: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<SignupWithEmailOutput> & { present: Mock };
let useCase: SignupWithEmailUseCase;
beforeEach(() => {

View File

@@ -43,7 +43,7 @@ describe('StartAuthUseCase', () => {
it('returns ok and presents redirect when provider call succeeds', async () => {
const input: StartAuthInput = {
provider: 'IRACING_DEMO' as any,
provider: 'IRACING_DEMO',
returnTo: 'https://app/callback',
};
@@ -69,7 +69,7 @@ describe('StartAuthUseCase', () => {
it('wraps unexpected errors as REPOSITORY_ERROR and logs them', async () => {
const input: StartAuthInput = {
provider: 'IRACING_DEMO' as any,
provider: 'IRACING_DEMO',
returnTo: 'https://app/callback',
};

View File

@@ -3,13 +3,17 @@ import { CreateAchievementUseCase, type IAchievementRepository } from './CreateA
import { Achievement } from '@core/identity/domain/entities/Achievement';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
type CreateAchievementOutput = {
achievement: Achievement;
};
describe('CreateAchievementUseCase', () => {
let achievementRepository: {
save: Mock;
findById: Mock;
};
let logger: Logger;
let output: UseCaseOutputPort<any> & { present: Mock };
let output: UseCaseOutputPort<CreateAchievementOutput> & { present: Mock };
let useCase: CreateAchievementUseCase;
beforeEach(() => {

View File

@@ -31,6 +31,7 @@ export class User {
this.id = props.id;
this.displayName = props.displayName.trim();
this.email = props.email;
this.passwordHash = props.passwordHash;
this.iracingCustomerId = props.iracingCustomerId;
this.primaryDriverId = props.primaryDriverId;
this.avatarUrl = props.avatarUrl;
@@ -52,18 +53,20 @@ export class User {
}
public static fromStored(stored: StoredUser): User {
const passwordHash = stored.passwordHash ? PasswordHash.fromHash(stored.passwordHash) : undefined;
const userProps: any = {
const passwordHash = stored.passwordHash
? PasswordHash.fromHash(stored.passwordHash)
: undefined;
const userProps: UserProps = {
id: UserId.fromString(stored.id),
displayName: stored.displayName,
email: stored.email,
...(stored.email !== undefined ? { email: stored.email } : {}),
...(passwordHash !== undefined ? { passwordHash } : {}),
...(stored.primaryDriverId !== undefined
? { primaryDriverId: stored.primaryDriverId }
: {}),
};
if (passwordHash) {
userProps.passwordHash = passwordHash;
}
if (stored.primaryDriverId) {
userProps.primaryDriverId = stored.primaryDriverId;
}
return new User(userProps);
}

View File

@@ -7,7 +7,7 @@
export interface UploadOptions {
filename: string;
mimeType: string;
metadata?: Record<string, any>;
metadata?: Record<string, unknown>;
}
export interface UploadResult {

View File

@@ -33,11 +33,11 @@ describe('DeleteMediaUseCase', () => {
mediaRepo = {
findById: vi.fn(),
delete: vi.fn(),
} as unknown as IMediaRepository as any;
};
mediaStorage = {
deleteMedia: vi.fn(),
} as unknown as MediaStoragePort as any;
};
logger = {
debug: vi.fn(),

View File

@@ -29,7 +29,7 @@ describe('GetAvatarUseCase', () => {
avatarRepo = {
findActiveByDriverId: vi.fn(),
save: vi.fn(),
} as unknown as IAvatarRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -27,7 +27,7 @@ describe('GetMediaUseCase', () => {
beforeEach(() => {
mediaRepo = {
findById: vi.fn(),
} as unknown as IMediaRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -24,7 +24,7 @@ export interface GetMediaResult {
type: string;
uploadedBy: string;
uploadedAt: Date;
metadata?: Record<string, any>;
metadata?: Record<string, unknown>;
};
}

View File

@@ -29,7 +29,7 @@ export interface MulterFile {
export interface UploadMediaInput {
file: MulterFile;
uploadedBy: string;
metadata?: Record<string, any>;
metadata?: Record<string, unknown>;
}
export interface UploadMediaResult {
@@ -60,7 +60,11 @@ export class UploadMediaUseCase {
try {
// Upload file to storage service
const uploadOptions: { filename: string; mimeType: string; metadata?: Record<string, any> } = {
const uploadOptions: {
filename: string;
mimeType: string;
metadata?: Record<string, unknown>;
} = {
filename: input.file.originalname,
mimeType: input.file.mimetype,
};

View File

@@ -19,7 +19,7 @@ export interface MediaProps {
type: MediaType;
uploadedBy: string;
uploadedAt: Date;
metadata?: Record<string, any> | undefined;
metadata?: Record<string, unknown> | undefined;
}
export class Media implements IEntity<string> {
@@ -32,7 +32,7 @@ export class Media implements IEntity<string> {
readonly type: MediaType;
readonly uploadedBy: string;
readonly uploadedAt: Date;
readonly metadata?: Record<string, any> | undefined;
readonly metadata?: Record<string, unknown> | undefined;
private constructor(props: MediaProps) {
this.id = props.id;
@@ -56,7 +56,7 @@ export class Media implements IEntity<string> {
url: string;
type: MediaType;
uploadedBy: string;
metadata?: Record<string, any>;
metadata?: Record<string, unknown>;
}): Media {
if (!props.filename) {
throw new Error('Filename is required');

View File

@@ -27,7 +27,7 @@ describe('GetUnreadNotificationsUseCase', () => {
beforeEach(() => {
notificationRepository = {
findUnreadByRecipientId: vi.fn(),
} as unknown as INotificationRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -31,7 +31,7 @@ describe('MarkNotificationReadUseCase', () => {
findById: vi.fn(),
update: vi.fn(),
markAllAsReadByRecipientId: vi.fn(),
} as unknown as INotificationRepository as any;
};
logger = {
debug: vi.fn(),

View File

@@ -35,7 +35,7 @@ describe('NotificationPreferencesUseCases', () => {
preferenceRepository = {
getOrCreateDefault: vi.fn(),
save: vi.fn(),
} as unknown as INotificationPreferenceRepository as any;
};
logger = {
debug: vi.fn(),
@@ -54,7 +54,7 @@ describe('NotificationPreferencesUseCases', () => {
const output: UseCaseOutputPort<GetNotificationPreferencesResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<GetNotificationPreferencesResult> & { present: Mock };
const useCase = new GetNotificationPreferencesQuery(
preferenceRepository as unknown as INotificationPreferenceRepository,
@@ -79,7 +79,7 @@ describe('NotificationPreferencesUseCases', () => {
const output: UseCaseOutputPort<UpdateChannelPreferenceResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<UpdateChannelPreferenceResult> & { present: Mock };
const useCase = new UpdateChannelPreferenceUseCase(
preferenceRepository as unknown as INotificationPreferenceRepository,
@@ -110,7 +110,7 @@ describe('NotificationPreferencesUseCases', () => {
const output: UseCaseOutputPort<UpdateTypePreferenceResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<UpdateTypePreferenceResult> & { present: Mock };
const useCase = new UpdateTypePreferenceUseCase(
preferenceRepository as unknown as INotificationPreferenceRepository,
@@ -141,7 +141,7 @@ describe('NotificationPreferencesUseCases', () => {
const output: UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock };
const useCase = new UpdateQuietHoursUseCase(
preferenceRepository as unknown as INotificationPreferenceRepository,
@@ -170,7 +170,7 @@ describe('NotificationPreferencesUseCases', () => {
it('UpdateQuietHoursUseCase returns error on invalid hours', async () => {
const output: UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<UpdateQuietHoursResult> & { present: Mock };
const useCase = new UpdateQuietHoursUseCase(
preferenceRepository as unknown as INotificationPreferenceRepository,
@@ -200,7 +200,7 @@ describe('NotificationPreferencesUseCases', () => {
const output: UseCaseOutputPort<SetDigestModeResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<SetDigestModeResult> & { present: Mock };
const useCase = new SetDigestModeUseCase(
preferenceRepository as unknown as INotificationPreferenceRepository,
@@ -228,7 +228,7 @@ describe('NotificationPreferencesUseCases', () => {
it('SetDigestModeUseCase returns error on invalid frequency', async () => {
const output: UseCaseOutputPort<SetDigestModeResult> & { present: Mock } = {
present: vi.fn(),
} as any;
} as unknown as UseCaseOutputPort<SetDigestModeResult> & { present: Mock };
const useCase = new SetDigestModeUseCase(
preferenceRepository as unknown as INotificationPreferenceRepository,

View File

@@ -16,7 +16,7 @@ describe('CreatePaymentUseCase', () => {
beforeEach(() => {
paymentRepository = {
create: vi.fn(),
} as unknown as IPaymentRepository as any;
};
output = {
present: vi.fn(),
@@ -24,7 +24,7 @@ describe('CreatePaymentUseCase', () => {
useCase = new CreatePaymentUseCase(
paymentRepository as unknown as IPaymentRepository,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
});

View File

@@ -18,11 +18,11 @@ describe('GetMembershipFeesUseCase', () => {
beforeEach(() => {
membershipFeeRepository = {
findByLeagueId: vi.fn(),
} as unknown as IMembershipFeeRepository as any;
};
memberPaymentRepository = {
findByLeagueIdAndDriverId: vi.fn(),
} as unknown as IMemberPaymentRepository as any;
};
output = {
present: vi.fn(),
@@ -31,7 +31,7 @@ describe('GetMembershipFeesUseCase', () => {
useCase = new GetMembershipFeesUseCase(
membershipFeeRepository as unknown as IMembershipFeeRepository,
memberPaymentRepository as unknown as IMemberPaymentRepository,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
});

View File

@@ -16,7 +16,7 @@ describe('GetPaymentsUseCase', () => {
beforeEach(() => {
paymentRepository = {
findByFilters: vi.fn(),
} as unknown as IPaymentRepository as any;
};
output = {
present: vi.fn(),
@@ -24,7 +24,7 @@ describe('GetPaymentsUseCase', () => {
useCase = new GetPaymentsUseCase(
paymentRepository as unknown as IPaymentRepository,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
});

View File

@@ -23,11 +23,11 @@ describe('ProcessWalletTransactionUseCase', () => {
findByLeagueId: vi.fn(),
create: vi.fn(),
update: vi.fn(),
} as unknown as IWalletRepository as any;
};
transactionRepository = {
create: vi.fn(),
} as unknown as ITransactionRepository as any;
};
output = {
present: vi.fn(),
@@ -36,7 +36,7 @@ describe('ProcessWalletTransactionUseCase', () => {
useCase = new ProcessWalletTransactionUseCase(
walletRepository as unknown as IWalletRepository,
transactionRepository as unknown as ITransactionRepository,
output as unknown as UseCaseOutputPort<any>,
output as unknown as UseCaseOutputPort<unknown>,
);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More