fix issues in core
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('RecordEngagementUseCase', () => {
|
||||
beforeEach(() => {
|
||||
engagementRepository = {
|
||||
save: vi.fn(),
|
||||
} as unknown as IEngagementRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('RecordPageViewUseCase', () => {
|
||||
beforeEach(() => {
|
||||
pageViewRepository = {
|
||||
save: vi.fn(),
|
||||
} as unknown as IPageViewRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export interface UploadOptions {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('GetAvatarUseCase', () => {
|
||||
avatarRepo = {
|
||||
findActiveByDriverId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
} as unknown as IAvatarRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('GetMediaUseCase', () => {
|
||||
beforeEach(() => {
|
||||
mediaRepo = {
|
||||
findById: vi.fn(),
|
||||
} as unknown as IMediaRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GetMediaResult {
|
||||
type: string;
|
||||
uploadedBy: string;
|
||||
uploadedAt: Date;
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('GetUnreadNotificationsUseCase', () => {
|
||||
beforeEach(() => {
|
||||
notificationRepository = {
|
||||
findUnreadByRecipientId: vi.fn(),
|
||||
} as unknown as INotificationRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,4 +18,66 @@ export interface LeagueSchedulePreviewDTO {
|
||||
scheduledTime: Date;
|
||||
trackId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SeasonScheduleConfigDTO = {
|
||||
seasonStartDate: string;
|
||||
recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
|
||||
weekdays?: string[];
|
||||
raceStartTime: string;
|
||||
timezoneId: string;
|
||||
plannedRounds: number;
|
||||
intervalWeeks?: number;
|
||||
monthlyOrdinal?: 1 | 2 | 3 | 4;
|
||||
monthlyWeekday?: string;
|
||||
};
|
||||
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
|
||||
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
|
||||
import { ALL_WEEKDAYS, type Weekday } from '../../domain/types/Weekday';
|
||||
|
||||
function toWeekdaySet(values: string[] | undefined): WeekdaySet {
|
||||
const weekdays = (values ?? []).filter((v): v is Weekday =>
|
||||
ALL_WEEKDAYS.includes(v as Weekday),
|
||||
);
|
||||
|
||||
return WeekdaySet.fromArray(weekdays.length > 0 ? weekdays : ['Mon']);
|
||||
}
|
||||
|
||||
export function scheduleDTOToSeasonSchedule(dto: SeasonScheduleConfigDTO): SeasonSchedule {
|
||||
const startDate = new Date(dto.seasonStartDate);
|
||||
const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime);
|
||||
const timezone = LeagueTimezone.create(dto.timezoneId);
|
||||
|
||||
const recurrence = (() => {
|
||||
switch (dto.recurrenceStrategy) {
|
||||
case 'everyNWeeks':
|
||||
return RecurrenceStrategyFactory.everyNWeeks(
|
||||
dto.intervalWeeks ?? 2,
|
||||
toWeekdaySet(dto.weekdays),
|
||||
);
|
||||
case 'monthlyNthWeekday': {
|
||||
const pattern = MonthlyRecurrencePattern.create(
|
||||
dto.monthlyOrdinal ?? 1,
|
||||
((dto.monthlyWeekday ?? 'Mon') as Weekday),
|
||||
);
|
||||
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||
}
|
||||
case 'weekly':
|
||||
default:
|
||||
return RecurrenceStrategyFactory.weekly(toWeekdaySet(dto.weekdays));
|
||||
}
|
||||
})();
|
||||
|
||||
return new SeasonSchedule({
|
||||
startDate,
|
||||
timeOfDay,
|
||||
timezone,
|
||||
recurrence,
|
||||
plannedRounds: dto.plannedRounds,
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from './LeagueConfigFormDTO';
|
||||
export * from './LeagueDTO';
|
||||
export * from './LeagueDriverSeasonStatsDTO';
|
||||
export * from './LeagueDTO';
|
||||
export * from './LeagueScheduleDTO';
|
||||
export * from './RaceDTO';
|
||||
export * from './ResultDTO';
|
||||
export * from './StandingDTO';
|
||||
export * from './StandingDTO';
|
||||
|
||||
// TODO DTOs dont belong into core. We use Results in UseCases and DTOs in apps/api.
|
||||
@@ -12,4 +12,6 @@ export interface AllRacesPageOutputPort {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort
|
||||
@@ -9,4 +9,6 @@ export interface ChampionshipStandingsOutputPort {
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort
|
||||
@@ -5,4 +5,6 @@ export interface ChampionshipStandingsRowOutputPort {
|
||||
driverName: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort
|
||||
@@ -4,4 +4,6 @@ export interface DriverRegistrationStatusOutputPort {
|
||||
leagueId: string;
|
||||
registered: boolean;
|
||||
status: 'registered' | 'withdrawn' | 'pending' | 'not_registered';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this code must be resolved into a Result within UseCase, thats not an OutputPort
|
||||
@@ -7,12 +7,13 @@ import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
|
||||
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
|
||||
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
|
||||
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
|
||||
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
|
||||
import type { StewardingDecisionMode } from '../../domain/entities/League';
|
||||
|
||||
// TODO The whole file mixes a lot of concerns...should be resolved into use cases or is it obsolet?
|
||||
|
||||
@@ -280,6 +281,27 @@ export class SeasonApplicationService {
|
||||
};
|
||||
}
|
||||
|
||||
private parseDropStrategy(value: unknown): SeasonDropStrategy {
|
||||
if (value === 'none' || value === 'bestNResults' || value === 'dropWorstN') {
|
||||
return value;
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
private parseDecisionMode(value: unknown): StewardingDecisionMode {
|
||||
if (
|
||||
value === 'admin_only' ||
|
||||
value === 'steward_decides' ||
|
||||
value === 'steward_vote' ||
|
||||
value === 'member_vote' ||
|
||||
value === 'steward_veto' ||
|
||||
value === 'member_veto'
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
return 'admin_only';
|
||||
}
|
||||
|
||||
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
@@ -298,14 +320,14 @@ export class SeasonApplicationService {
|
||||
|
||||
const dropPolicy = config.dropPolicy
|
||||
? new SeasonDropPolicy({
|
||||
strategy: config.dropPolicy.strategy as any,
|
||||
strategy: this.parseDropStrategy(config.dropPolicy.strategy),
|
||||
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const stewardingConfig = config.stewarding
|
||||
? new SeasonStewardingConfig({
|
||||
decisionMode: config.stewarding.decisionMode as any,
|
||||
decisionMode: this.parseDecisionMode(config.stewarding.decisionMode),
|
||||
...(config.stewarding.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue(null);
|
||||
@@ -80,7 +80,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
@@ -109,7 +109,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
@@ -142,7 +142,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
@@ -175,7 +175,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
@@ -209,7 +209,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
@@ -242,7 +242,7 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import { Money } from '../../domain/value-objects/Money';
|
||||
import { Money, isCurrency } from '../../domain/value-objects/Money';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -123,7 +123,9 @@ export class ApplyForSponsorshipUseCase {
|
||||
|
||||
// Create the sponsorship request
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const offeredAmount = Money.create(input.offeredAmount, (input.currency as any) || 'USD');
|
||||
const currency =
|
||||
input.currency !== undefined && isCurrency(input.currency) ? input.currency : 'USD';
|
||||
const offeredAmount = Money.create(input.offeredAmount, currency);
|
||||
|
||||
const request = SponsorshipRequest.create({
|
||||
id: requestId,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { ApplyPenaltyUseCase } from './ApplyPenaltyUseCase';
|
||||
import { ApplyPenaltyUseCase, type ApplyPenaltyResult } from './ApplyPenaltyUseCase';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
describe('ApplyPenaltyUseCase', () => {
|
||||
let mockPenaltyRepo: {
|
||||
@@ -48,7 +49,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when race does not exist', async () => {
|
||||
const output = {
|
||||
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -58,7 +59,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
output,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue(null);
|
||||
@@ -77,7 +78,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when steward does not have authority', async () => {
|
||||
const output = {
|
||||
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -87,13 +88,18 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
output,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
|
||||
{ driverId: 'steward1', role: 'member', status: 'active' },
|
||||
]);
|
||||
|
||||
const membership = {
|
||||
driverId: { toString: () => 'steward1' },
|
||||
role: { toString: () => 'member' },
|
||||
status: { toString: () => 'active' },
|
||||
};
|
||||
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
|
||||
|
||||
const result = await useCase.execute({
|
||||
raceId: 'race1',
|
||||
@@ -109,7 +115,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when protest does not exist', async () => {
|
||||
const output = {
|
||||
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -119,13 +125,18 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
output,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
|
||||
{ driverId: 'steward1', role: 'owner', status: 'active' },
|
||||
]);
|
||||
|
||||
const membership = {
|
||||
driverId: { toString: () => 'steward1' },
|
||||
role: { toString: () => 'owner' },
|
||||
status: { toString: () => 'active' },
|
||||
};
|
||||
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
|
||||
mockProtestRepo.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute({
|
||||
@@ -143,7 +154,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when protest is not upheld', async () => {
|
||||
const output = {
|
||||
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -153,13 +164,18 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
output,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
|
||||
{ driverId: 'steward1', role: 'owner', status: 'active' },
|
||||
]);
|
||||
|
||||
const membership = {
|
||||
driverId: { toString: () => 'steward1' },
|
||||
role: { toString: () => 'owner' },
|
||||
status: { toString: () => 'active' },
|
||||
};
|
||||
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
|
||||
mockProtestRepo.findById.mockResolvedValue({ id: 'protest1', status: 'pending', raceId: 'race1' });
|
||||
|
||||
const result = await useCase.execute({
|
||||
@@ -177,7 +193,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when protest is not for this race', async () => {
|
||||
const output = {
|
||||
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -187,13 +203,18 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
output,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
|
||||
{ driverId: 'steward1', role: 'owner', status: 'active' },
|
||||
]);
|
||||
|
||||
const membership = {
|
||||
driverId: { toString: () => 'steward1' },
|
||||
role: { toString: () => 'owner' },
|
||||
status: { toString: () => 'active' },
|
||||
};
|
||||
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
|
||||
mockProtestRepo.findById.mockResolvedValue({ id: 'protest1', status: 'upheld', raceId: 'race2' });
|
||||
|
||||
const result = await useCase.execute({
|
||||
@@ -211,7 +232,7 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should create penalty and return result on success', async () => {
|
||||
const output = {
|
||||
const output: UseCaseOutputPort<ApplyPenaltyResult> & { present: Mock } = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -221,13 +242,18 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
output,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([
|
||||
{ driverId: 'steward1', role: 'admin', status: 'active' },
|
||||
]);
|
||||
|
||||
const membership = {
|
||||
driverId: { toString: () => 'steward1' },
|
||||
role: { toString: () => 'admin' },
|
||||
status: { toString: () => 'active' },
|
||||
};
|
||||
|
||||
mockLeagueMembershipRepo.getLeagueMembers.mockResolvedValue([membership]);
|
||||
mockPenaltyRepo.create.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCase.execute({
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { ApproveLeagueJoinRequestUseCase } from './ApproveLeagueJoinRequestUseCase';
|
||||
import {
|
||||
ApproveLeagueJoinRequestUseCase,
|
||||
type ApproveLeagueJoinRequestResult,
|
||||
} from './ApproveLeagueJoinRequestUseCase';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
@@ -33,7 +36,10 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
|
||||
|
||||
mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue(joinRequests);
|
||||
|
||||
const result = await useCase.execute({ leagueId, requestId }, output as unknown as UseCaseOutputPort<any>);
|
||||
const result = await useCase.execute(
|
||||
{ leagueId, requestId },
|
||||
output as unknown as UseCaseOutputPort<ApproveLeagueJoinRequestResult>,
|
||||
);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
@@ -62,7 +68,10 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
|
||||
|
||||
mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute({ leagueId: 'league-1', requestId: 'req-1' }, output as unknown as UseCaseOutputPort<any>);
|
||||
const result = await useCase.execute(
|
||||
{ leagueId: 'league-1', requestId: 'req-1' },
|
||||
output as unknown as UseCaseOutputPort<ApproveLeagueJoinRequestResult>,
|
||||
);
|
||||
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.error!.code).toBe('JOIN_REQUEST_NOT_FOUND');
|
||||
|
||||
@@ -91,8 +91,11 @@ describe('CancelRaceUseCase', () => {
|
||||
const result = await useCase.execute({ raceId, cancelledById: 'admin-1' });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED');
|
||||
expect((result.unwrapErr() as any).details.message).toContain('already cancelled');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('NOT_AUTHORIZED');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toContain('already cancelled');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -113,8 +116,11 @@ describe('CancelRaceUseCase', () => {
|
||||
const result = await useCase.execute({ raceId, cancelledById: 'admin-1' });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED');
|
||||
expect((result.unwrapErr() as any).details.message).toContain('completed race');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('NOT_AUTHORIZED');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toContain('completed race');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -117,8 +117,11 @@ describe('CloseRaceEventStewardingUseCase', () => {
|
||||
const result = await useCase.execute({ raceId: 'event-1', closedById: 'admin-1' });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect((result.unwrapErr() as any).details.message).toContain('DB error');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toContain('DB error');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -162,8 +162,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect((result.unwrapErr() as any).details.message).toBe('gameId is required');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('VALIDATION_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('gameId is required');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO';
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
|
||||
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonDropPolicy, type SeasonDropStrategy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
|
||||
import type { StewardingDecisionMode } from '../../domain/entities/League';
|
||||
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
@@ -120,6 +121,27 @@ export class CreateSeasonForLeagueUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
private parseDropStrategy(value: unknown): SeasonDropStrategy {
|
||||
if (value === 'none' || value === 'bestNResults' || value === 'dropWorstN') {
|
||||
return value;
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
private parseDecisionMode(value: unknown): StewardingDecisionMode {
|
||||
if (
|
||||
value === 'admin_only' ||
|
||||
value === 'steward_decides' ||
|
||||
value === 'steward_vote' ||
|
||||
value === 'member_vote' ||
|
||||
value === 'steward_veto' ||
|
||||
value === 'member_veto'
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
return 'admin_only';
|
||||
}
|
||||
|
||||
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
@@ -133,11 +155,11 @@ export class CreateSeasonForLeagueUseCase {
|
||||
customScoringEnabled: config.scoring?.customScoringEnabled ?? false,
|
||||
});
|
||||
const dropPolicy = new SeasonDropPolicy({
|
||||
strategy: (config.dropPolicy?.strategy as any) ?? 'none',
|
||||
strategy: this.parseDropStrategy(config.dropPolicy?.strategy),
|
||||
...(config.dropPolicy?.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
});
|
||||
const stewardingConfig = new SeasonStewardingConfig({
|
||||
decisionMode: (config.stewarding?.decisionMode as any) ?? 'auto',
|
||||
decisionMode: this.parseDecisionMode(config.stewarding?.decisionMode),
|
||||
...(config.stewarding?.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('CreateSponsorUseCase', () => {
|
||||
useCase = new CreateSponsorUseCase(
|
||||
sponsorRepository as unknown as ISponsorRepository,
|
||||
logger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import { Result as UseCaseResult } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
|
||||
import { RaceRegistration } from '@core/racing/domain/entities/RaceRegistration';
|
||||
|
||||
describe('DashboardOverviewUseCase', () => {
|
||||
it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => {
|
||||
@@ -195,14 +197,14 @@ describe('DashboardOverviewUseCase', () => {
|
||||
);
|
||||
},
|
||||
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
|
||||
getJoinRequests: async (): Promise<any[]> => [],
|
||||
getJoinRequests: async (): Promise<JoinRequest[]> => [],
|
||||
saveMembership: async (): Promise<LeagueMembership> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeMembership: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
saveJoinRequest: async (): Promise<any> => {
|
||||
saveJoinRequest: async (): Promise<JoinRequest> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeJoinRequest: async (): Promise<void> => {
|
||||
@@ -227,7 +229,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
clearRaceRegistrations: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
findByRaceId: async (): Promise<any[]> => [],
|
||||
findByRaceId: async (): Promise<RaceRegistration[]> => [],
|
||||
};
|
||||
|
||||
const feedRepository = {
|
||||
@@ -289,9 +291,9 @@ describe('DashboardOverviewUseCase', () => {
|
||||
expect(_presentedData).not.toBeNull();
|
||||
const vm = _presentedData!;
|
||||
|
||||
expect(vm.myUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-1', 'race-3']);
|
||||
expect(vm.myUpcomingRaces.map(r => r.race.id)).toEqual(['race-1', 'race-3']);
|
||||
|
||||
expect(vm.otherUpcomingRaces.map((r: any) => r.race.id)).toEqual(['race-2', 'race-4']);
|
||||
expect(vm.otherUpcomingRaces.map(r => r.race.id)).toEqual(['race-2', 'race-4']);
|
||||
|
||||
expect(vm.nextRace).not.toBeNull();
|
||||
expect(vm.nextRace!.race.id).toBe('race-1');
|
||||
@@ -482,14 +484,14 @@ describe('DashboardOverviewUseCase', () => {
|
||||
);
|
||||
},
|
||||
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
|
||||
getJoinRequests: async (): Promise<any[]> => [],
|
||||
getJoinRequests: async (): Promise<JoinRequest[]> => [],
|
||||
saveMembership: async (): Promise<LeagueMembership> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeMembership: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
saveJoinRequest: async (): Promise<any> => {
|
||||
saveJoinRequest: async (): Promise<JoinRequest> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeJoinRequest: async (): Promise<void> => {
|
||||
@@ -511,7 +513,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
clearRaceRegistrations: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
findByRaceId: async (): Promise<any[]> => [],
|
||||
findByRaceId: async (): Promise<RaceRegistration[]> => [],
|
||||
};
|
||||
|
||||
const feedRepository = {
|
||||
@@ -578,7 +580,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
expect(vm.recentResults[1]!.race.id).toBe('race-old');
|
||||
|
||||
const summariesByLeague = new Map(
|
||||
vm.leagueStandingsSummaries.map((s: any) => [s.league.id.toString(), s]),
|
||||
vm.leagueStandingsSummaries.map(s => [s.league.id.toString(), s] as const),
|
||||
);
|
||||
|
||||
const summaryA = summariesByLeague.get('league-A');
|
||||
@@ -702,14 +704,14 @@ describe('DashboardOverviewUseCase', () => {
|
||||
const leagueMembershipRepository = {
|
||||
getMembership: async (): Promise<LeagueMembership | null> => null,
|
||||
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
|
||||
getJoinRequests: async (): Promise<any[]> => [],
|
||||
getJoinRequests: async (): Promise<JoinRequest[]> => [],
|
||||
saveMembership: async (): Promise<LeagueMembership> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeMembership: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
saveJoinRequest: async (): Promise<any> => {
|
||||
saveJoinRequest: async (): Promise<JoinRequest> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeJoinRequest: async (): Promise<void> => {
|
||||
@@ -731,7 +733,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
clearRaceRegistrations: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
findByRaceId: async (): Promise<any[]> => [],
|
||||
findByRaceId: async (): Promise<RaceRegistration[]> => [],
|
||||
};
|
||||
|
||||
const feedRepository = {
|
||||
@@ -898,14 +900,14 @@ describe('DashboardOverviewUseCase', () => {
|
||||
const leagueMembershipRepository = {
|
||||
getMembership: async (): Promise<LeagueMembership | null> => null,
|
||||
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
|
||||
getJoinRequests: async (): Promise<any[]> => [],
|
||||
getJoinRequests: async (): Promise<JoinRequest[]> => [],
|
||||
saveMembership: async (): Promise<LeagueMembership> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeMembership: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
saveJoinRequest: async (): Promise<any> => {
|
||||
saveJoinRequest: async (): Promise<JoinRequest> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeJoinRequest: async (): Promise<void> => {
|
||||
@@ -927,7 +929,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
clearRaceRegistrations: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
findByRaceId: async (): Promise<any[]> => [],
|
||||
findByRaceId: async (): Promise<RaceRegistration[]> => [],
|
||||
};
|
||||
|
||||
const feedRepository = {
|
||||
@@ -1089,14 +1091,14 @@ describe('DashboardOverviewUseCase', () => {
|
||||
const leagueMembershipRepository = {
|
||||
getMembership: async (): Promise<LeagueMembership | null> => null,
|
||||
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
|
||||
getJoinRequests: async (): Promise<any[]> => [],
|
||||
getJoinRequests: async (): Promise<JoinRequest[]> => [],
|
||||
saveMembership: async (): Promise<LeagueMembership> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeMembership: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
saveJoinRequest: async (): Promise<any> => {
|
||||
saveJoinRequest: async (): Promise<JoinRequest> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
removeJoinRequest: async (): Promise<void> => {
|
||||
@@ -1118,7 +1120,7 @@ describe('DashboardOverviewUseCase', () => {
|
||||
clearRaceRegistrations: async (): Promise<void> => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
findByRaceId: async (): Promise<any[]> => [],
|
||||
findByRaceId: async (): Promise<RaceRegistration[]> => [],
|
||||
};
|
||||
|
||||
const feedRepository = {
|
||||
|
||||
@@ -55,11 +55,11 @@ export class FileProtestUseCase {
|
||||
|
||||
// Validate protesting driver is a member of the league
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
|
||||
const protestingDriverMembership = memberships.find(m => {
|
||||
const driverId = (m as any).driverId;
|
||||
const status = (m as any).status;
|
||||
return driverId === command.protestingDriverId && status === 'active';
|
||||
});
|
||||
const protestingDriverMembership = memberships.find(
|
||||
m =>
|
||||
m.driverId.toString() === command.protestingDriverId &&
|
||||
m.status.toString() === 'active',
|
||||
);
|
||||
|
||||
if (!protestingDriverMembership) {
|
||||
return Result.err({ code: 'NOT_MEMBER', details: { message: 'Protesting driver is not an active member of this league' } });
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { Race } from '../../domain/entities/Race';
|
||||
import { League } from '../../domain/entities/League';
|
||||
|
||||
describe('GetAllRacesPageDataUseCase', () => {
|
||||
const mockRaceFindAll = vi.fn();
|
||||
@@ -61,26 +63,38 @@ describe('GetAllRacesPageDataUseCase', () => {
|
||||
output,
|
||||
);
|
||||
|
||||
const race1 = {
|
||||
const race1 = Race.create({
|
||||
id: 'race1',
|
||||
leagueId: 'league1',
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
scheduledAt: new Date('2023-01-01T10:00:00Z'),
|
||||
status: 'scheduled' as const,
|
||||
leagueId: 'league1',
|
||||
status: 'scheduled',
|
||||
strengthOfField: 5,
|
||||
} as any;
|
||||
const race2 = {
|
||||
});
|
||||
|
||||
const race2 = Race.create({
|
||||
id: 'race2',
|
||||
leagueId: 'league2',
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
scheduledAt: new Date('2023-01-02T10:00:00Z'),
|
||||
status: 'completed' as const,
|
||||
leagueId: 'league2',
|
||||
strengthOfField: null,
|
||||
} as any;
|
||||
const league1 = { id: 'league1', name: 'League One' } as any;
|
||||
const league2 = { id: 'league2', name: 'League Two' } as any;
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
const league1 = League.create({
|
||||
id: 'league1',
|
||||
name: 'League One',
|
||||
description: 'League One',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league2',
|
||||
name: 'League Two',
|
||||
description: 'League Two',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
|
||||
mockRaceFindAll.mockResolvedValue([race1, race2]);
|
||||
mockLeagueFindAll.mockResolvedValue([league1, league2]);
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { Race } from '../../domain/entities/Race';
|
||||
import { League } from '../../domain/entities/League';
|
||||
|
||||
describe('GetAllRacesUseCase', () => {
|
||||
const mockRaceFindAll = vi.fn();
|
||||
@@ -61,22 +63,37 @@ describe('GetAllRacesUseCase', () => {
|
||||
);
|
||||
useCase.setOutput(output);
|
||||
|
||||
const race1 = {
|
||||
const race1 = Race.create({
|
||||
id: 'race1',
|
||||
leagueId: 'league1',
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
scheduledAt: new Date('2023-01-01T10:00:00Z'),
|
||||
leagueId: 'league1',
|
||||
} as any;
|
||||
const race2 = {
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
const race2 = Race.create({
|
||||
id: 'race2',
|
||||
leagueId: 'league2',
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
scheduledAt: new Date('2023-01-02T10:00:00Z'),
|
||||
leagueId: 'league2',
|
||||
} as any;
|
||||
const league1 = { id: 'league1' } as any;
|
||||
const league2 = { id: 'league2' } as any;
|
||||
status: 'scheduled',
|
||||
});
|
||||
|
||||
const league1 = League.create({
|
||||
id: 'league1',
|
||||
name: 'League One',
|
||||
description: 'League One',
|
||||
ownerId: 'owner-1',
|
||||
});
|
||||
|
||||
const league2 = League.create({
|
||||
id: 'league2',
|
||||
name: 'League Two',
|
||||
description: 'League Two',
|
||||
ownerId: 'owner-2',
|
||||
});
|
||||
|
||||
mockRaceFindAll.mockResolvedValue([race1, race2]);
|
||||
mockLeagueFindAll.mockResolvedValue([league1, league2]);
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
|
||||
mockDriverFindAll.mockResolvedValue([driver1, driver2]);
|
||||
mockRankingGetAllDriverRankings.mockReturnValue(rankings);
|
||||
mockDriverStatsGetDriverStats.mockImplementation((id) => {
|
||||
mockDriverStatsGetDriverStats.mockImplementation((id: string) => {
|
||||
if (id === 'driver1') return stats1;
|
||||
if (id === 'driver2') return stats2;
|
||||
return null;
|
||||
@@ -89,7 +89,7 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult;
|
||||
const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult;
|
||||
|
||||
expect(presented).toEqual({
|
||||
items: [
|
||||
@@ -142,7 +142,7 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult;
|
||||
const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult;
|
||||
|
||||
expect(presented).toEqual({
|
||||
items: [],
|
||||
@@ -177,7 +177,7 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = (output.present as any).mock.calls[0][0] as GetDriversLeaderboardResult;
|
||||
const presented = output.present.mock.calls[0]![0] as GetDriversLeaderboardResult;
|
||||
|
||||
expect(presented).toEqual({
|
||||
items: [
|
||||
@@ -218,7 +218,9 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
expect((err as any).details?.message).toBe('Repository error');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('Repository error');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetLeagueAdminPermissionsUseCase', () => {
|
||||
let mockLeagueRepo: ILeagueRepository;
|
||||
@@ -16,12 +17,12 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
|
||||
let mockFindById: Mock;
|
||||
let mockGetMembership: Mock;
|
||||
let output: UseCaseOutputPort<GetLeagueAdminPermissionsResult> & { present: Mock };
|
||||
const logger = {
|
||||
const logger: Logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as any;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockFindById = vi.fn();
|
||||
@@ -122,7 +123,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
|
||||
});
|
||||
|
||||
it('returns admin permissions for admin role and calls output once', async () => {
|
||||
const league = { id: 'league1' } as any;
|
||||
const league = { id: 'league1' } as unknown as { id: string };
|
||||
mockFindById.mockResolvedValue(league);
|
||||
mockGetMembership.mockResolvedValue({ status: 'active', role: 'admin' });
|
||||
|
||||
@@ -144,7 +145,7 @@ describe('GetLeagueAdminPermissionsUseCase', () => {
|
||||
});
|
||||
|
||||
it('returns admin permissions for owner role and calls output once', async () => {
|
||||
const league = { id: 'league1' } as any;
|
||||
const league = { id: 'league1' } as unknown as { id: string };
|
||||
mockFindById.mockResolvedValue(league);
|
||||
mockGetMembership.mockResolvedValue({ status: 'active', role: 'owner' });
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
GetLeagueDriverSeasonStatsUseCase,
|
||||
type GetLeagueDriverSeasonStatsResult,
|
||||
type GetLeagueDriverSeasonStatsInput,
|
||||
type GetLeagueDriverSeasonStatsErrorCode,
|
||||
} from './GetLeagueDriverSeasonStatsUseCase';
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
import {
|
||||
GetLeagueDriverSeasonStatsUseCase,
|
||||
type GetLeagueDriverSeasonStatsErrorCode,
|
||||
type GetLeagueDriverSeasonStatsInput,
|
||||
type GetLeagueDriverSeasonStatsResult,
|
||||
} from './GetLeagueDriverSeasonStatsUseCase';
|
||||
|
||||
describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
const mockStandingFindByLeagueId = vi.fn();
|
||||
@@ -30,7 +29,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
let penaltyRepository: IPenaltyRepository;
|
||||
let raceRepository: IRaceRepository;
|
||||
let driverRepository: IDriverRepository;
|
||||
let teamRepository: ITeamRepository;
|
||||
let driverRatingPort: DriverRatingPort;
|
||||
let output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult> & { present: ReturnType<typeof vi.fn> };
|
||||
|
||||
@@ -102,15 +100,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
exists: vi.fn(),
|
||||
existsByIRacingId: vi.fn(),
|
||||
};
|
||||
teamRepository = {
|
||||
findById: mockTeamFindById,
|
||||
findAll: vi.fn(),
|
||||
findByLeagueId: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
};
|
||||
driverRatingPort = {
|
||||
getDriverRating: mockDriverRatingGetRating,
|
||||
calculateRatingChange: vi.fn(),
|
||||
@@ -129,7 +118,6 @@ describe('GetLeagueDriverSeasonStatsUseCase', () => {
|
||||
penaltyRepository,
|
||||
raceRepository,
|
||||
driverRepository,
|
||||
teamRepository,
|
||||
driverRatingPort,
|
||||
output,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { DriverRatingPort } from '../ports/DriverRatingPort';
|
||||
|
||||
export type DriverSeasonStats = {
|
||||
@@ -56,7 +55,6 @@ export class GetLeagueDriverSeasonStatsUseCase {
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly driverRatingPort: DriverRatingPort,
|
||||
private readonly output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult>,
|
||||
) {}
|
||||
@@ -101,39 +99,32 @@ export class GetLeagueDriverSeasonStatsUseCase {
|
||||
|
||||
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
|
||||
for (const standing of standings) {
|
||||
const driverId = String(standing.driverId);
|
||||
const driverId = standing.driverId.toString();
|
||||
const rating = await this.driverRatingPort.getDriverRating(driverId);
|
||||
driverRatings.set(driverId, { rating, ratingChange: null });
|
||||
}
|
||||
|
||||
const driverResults = new Map<string, Array<{ position: number }>>();
|
||||
for (const standing of standings) {
|
||||
const driverId = String(standing.driverId);
|
||||
const driverId = standing.driverId.toString();
|
||||
const results = await this.resultRepository.findByDriverIdAndLeagueId(driverId, leagueId);
|
||||
driverResults.set(
|
||||
driverId,
|
||||
results.map(result => ({ position: Number((result as any).position) })),
|
||||
results.map(result => ({ position: result.position.toNumber() })),
|
||||
);
|
||||
}
|
||||
|
||||
const driverIds = standings.map(s => String(s.driverId));
|
||||
const driverIds = standings.map(s => s.driverId.toString());
|
||||
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));
|
||||
const driversMap = new Map(drivers.filter(d => d).map(d => [String(d!.id), d!]));
|
||||
const teamIds = Array.from(
|
||||
new Set(
|
||||
drivers
|
||||
.filter(d => (d as any)?.teamId)
|
||||
.map(d => (d as any).teamId as string),
|
||||
),
|
||||
const driversMap = new Map(
|
||||
drivers
|
||||
.filter((driver): driver is NonNullable<typeof driver> => driver !== null)
|
||||
.map(driver => [driver.id, driver]),
|
||||
);
|
||||
const teams = await Promise.all(teamIds.map(id => this.teamRepository.findById(id)));
|
||||
const teamsMap = new Map(teams.filter(t => t).map(t => [String(t!.id), t!]));
|
||||
|
||||
const stats: DriverSeasonStats[] = standings.map(standing => {
|
||||
const driverId = String(standing.driverId);
|
||||
const driver = driversMap.get(driverId) as any;
|
||||
const teamId = driver?.teamId as string | undefined;
|
||||
const team = teamId ? teamsMap.get(String(teamId)) : undefined;
|
||||
const driverId = standing.driverId.toString();
|
||||
const driver = driversMap.get(driverId);
|
||||
const penalties = penaltiesByDriver.get(driverId) ?? { baseDelta: 0, bonusDelta: 0 };
|
||||
const results = driverResults.get(driverId) ?? [];
|
||||
const rating = driverRatings.get(driverId);
|
||||
@@ -146,16 +137,16 @@ export class GetLeagueDriverSeasonStatsUseCase {
|
||||
results.length > 0
|
||||
? results.reduce((sum, r) => sum + r.position, 0) / results.length
|
||||
: null;
|
||||
const totalPoints = Number(standing.points);
|
||||
const totalPoints = standing.points.toNumber();
|
||||
const pointsPerRace = racesStarted > 0 ? totalPoints / racesStarted : 0;
|
||||
|
||||
return {
|
||||
leagueId,
|
||||
driverId,
|
||||
position: Number(standing.position),
|
||||
driverName: String(driver?.name ?? ''),
|
||||
teamId,
|
||||
teamName: (team as any)?.name as string | undefined,
|
||||
position: standing.position.toNumber(),
|
||||
driverName: driver ? driver.name.toString() : '',
|
||||
teamId: undefined,
|
||||
teamName: undefined,
|
||||
totalPoints,
|
||||
basePoints: totalPoints - penalties.baseDelta,
|
||||
penaltyPoints: penalties.baseDelta,
|
||||
|
||||
@@ -73,10 +73,7 @@ export class GetLeagueJoinRequestsUseCase {
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
|
||||
? (error as Error).message
|
||||
: 'Failed to load league join requests';
|
||||
const message = error instanceof Error ? error.message : 'Failed to load league join requests';
|
||||
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
|
||||
@@ -67,10 +67,7 @@ export class GetLeagueMembershipsUseCase {
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
|
||||
? (error as any).message
|
||||
: 'Failed to load league memberships';
|
||||
const message = error instanceof Error ? error.message : 'Failed to load league memberships';
|
||||
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
|
||||
@@ -94,8 +94,8 @@ export class GetLeagueProtestsUseCase {
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
|
||||
? (error as any).message
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: 'Failed to load league protests';
|
||||
|
||||
return Result.err({
|
||||
|
||||
@@ -27,11 +27,11 @@ describe('GetLeagueSeasonsUseCase', () => {
|
||||
beforeEach(() => {
|
||||
seasonRepository = {
|
||||
findByLeagueId: vi.fn(),
|
||||
} as unknown as ISeasonRepository as any;
|
||||
};
|
||||
|
||||
leagueRepository = {
|
||||
findById: vi.fn(),
|
||||
} as unknown as ILeagueRepository as any;
|
||||
};
|
||||
|
||||
output = {
|
||||
present: vi.fn(),
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorableEntityType, SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { Sponsor } from '../../domain/entities/sponsor/Sponsor';
|
||||
import { Money } from '../../domain/value-objects/Money';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import { Team } from '../../domain/entities/Team';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
@@ -29,9 +28,6 @@ describe('GetProfileOverviewUseCase', () => {
|
||||
let socialRepository: {
|
||||
getFriends: Mock;
|
||||
};
|
||||
let imageService: {
|
||||
getDriverAvatar: Mock;
|
||||
};
|
||||
let getDriverStats: Mock;
|
||||
let getAllDriverRankings: Mock;
|
||||
let driverExtendedProfileProvider: {
|
||||
@@ -52,9 +48,6 @@ describe('GetProfileOverviewUseCase', () => {
|
||||
socialRepository = {
|
||||
getFriends: vi.fn(),
|
||||
};
|
||||
imageService = {
|
||||
getDriverAvatar: vi.fn(),
|
||||
};
|
||||
getDriverStats = vi.fn();
|
||||
getAllDriverRankings = vi.fn();
|
||||
driverExtendedProfileProvider = {
|
||||
@@ -69,7 +62,6 @@ describe('GetProfileOverviewUseCase', () => {
|
||||
teamRepository as unknown as ITeamRepository,
|
||||
teamMembershipRepository as unknown as ITeamMembershipRepository,
|
||||
socialRepository as unknown as ISocialGraphRepository,
|
||||
imageService as unknown as IImageServicePort,
|
||||
driverExtendedProfileProvider,
|
||||
getDriverStats,
|
||||
getAllDriverRankings,
|
||||
@@ -117,7 +109,6 @@ describe('GetProfileOverviewUseCase', () => {
|
||||
teamRepository.findAll.mockResolvedValue(teams);
|
||||
teamMembershipRepository.getMembership.mockResolvedValue(null);
|
||||
socialRepository.getFriends.mockResolvedValue(friends);
|
||||
imageService.getDriverAvatar.mockReturnValue('avatar-url');
|
||||
getDriverStats.mockReturnValue(statsAdapter);
|
||||
getAllDriverRankings.mockReturnValue(rankings);
|
||||
driverExtendedProfileProvider.getExtendedProfile.mockReturnValue(null);
|
||||
@@ -127,8 +118,7 @@ describe('GetProfileOverviewUseCase', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = (output.present as unknown as Mock).mock
|
||||
.calls[0][0] as GetProfileOverviewResult;
|
||||
const presented = (output.present as unknown as Mock).mock.calls[0]?.[0] as GetProfileOverviewResult;
|
||||
expect(presented.driverInfo.driver.id).toBe(driverId);
|
||||
expect(presented.extendedProfile).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { DriverExtendedProfileProvider } from '../ports/DriverExtendedProfileProvider';
|
||||
import type { Driver } from '../../domain/entities/Driver';
|
||||
@@ -9,7 +8,7 @@ import type { Team } from '../../domain/entities/Team';
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort, UseCase } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
interface ProfileDriverStatsAdapter {
|
||||
rating: number | null;
|
||||
@@ -85,29 +84,29 @@ export type GetProfileOverviewResult = {
|
||||
finishDistribution: ProfileOverviewFinishDistribution | null;
|
||||
teamMemberships: ProfileOverviewTeamMembership[];
|
||||
socialSummary: ProfileOverviewSocialSummary;
|
||||
extendedProfile: unknown;
|
||||
extendedProfile: ReturnType<DriverExtendedProfileProvider['getExtendedProfile']>;
|
||||
};
|
||||
|
||||
export type GetProfileOverviewErrorCode =
|
||||
| 'DRIVER_NOT_FOUND'
|
||||
| 'REPOSITORY_ERROR';
|
||||
|
||||
export class GetProfileOverviewUseCase implements UseCase<GetProfileOverviewInput, GetProfileOverviewResult, GetProfileOverviewErrorCode> {
|
||||
export class GetProfileOverviewUseCase {
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly socialRepository: ISocialGraphRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
private readonly driverExtendedProfileProvider: DriverExtendedProfileProvider,
|
||||
private readonly getDriverStats: (driverId: string) => ProfileDriverStatsAdapter | null,
|
||||
private readonly getAllDriverRankings: () => DriverRankingEntry[],
|
||||
private readonly output: UseCaseOutputPort<GetProfileOverviewResult>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetProfileOverviewInput,
|
||||
): Promise<
|
||||
Result<GetProfileOverviewResult, ApplicationErrorCode<GetProfileOverviewErrorCode, { message: string }>>
|
||||
Result<void, ApplicationErrorCode<GetProfileOverviewErrorCode, { message: string }>>
|
||||
> {
|
||||
try {
|
||||
const { driverId } = input;
|
||||
@@ -130,10 +129,11 @@ export class GetProfileOverviewUseCase implements UseCase<GetProfileOverviewInpu
|
||||
const driverInfo = this.buildDriverInfo(driver, statsAdapter);
|
||||
const stats = this.buildStats(statsAdapter);
|
||||
const finishDistribution = this.buildFinishDistribution(statsAdapter);
|
||||
const teamMemberships = await this.buildTeamMemberships(driver.id, teams as Team[]);
|
||||
const socialSummary = this.buildSocialSummary(friends as Driver[]);
|
||||
const extendedProfile = this.driverExtendedProfileProvider.getExtendedProfile(driverId);
|
||||
|
||||
const teamMemberships = await this.buildTeamMemberships(driver.id, teams);
|
||||
const socialSummary = this.buildSocialSummary(friends);
|
||||
const extendedProfile =
|
||||
this.driverExtendedProfileProvider.getExtendedProfile(driverId);
|
||||
|
||||
const result: GetProfileOverviewResult = {
|
||||
driverInfo,
|
||||
stats,
|
||||
@@ -143,7 +143,9 @@ export class GetProfileOverviewUseCase implements UseCase<GetProfileOverviewInpu
|
||||
extendedProfile,
|
||||
};
|
||||
|
||||
return Result.ok(result);
|
||||
this.output.present(result);
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('GetRaceDetailUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
|
||||
const presented = output.present.mock.calls[0][0] as GetRaceDetailResult;
|
||||
const presented = output.present.mock.calls[0]?.[0] as GetRaceDetailResult;
|
||||
expect(presented.race).toEqual(race);
|
||||
expect(presented.league).toEqual(league);
|
||||
expect(presented.registrations).toEqual(registrations);
|
||||
@@ -145,7 +145,7 @@ describe('GetRaceDetailUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
|
||||
const presented = output.present.mock.calls[0][0] as GetRaceDetailResult;
|
||||
const presented = output.present.mock.calls[0]?.[0] as GetRaceDetailResult;
|
||||
expect(presented.userResult).toBe(userDomainResult);
|
||||
expect(presented.race).toEqual(race);
|
||||
expect(presented.league).toBeNull();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Result as DomainResult, Result } from '@core/shared/application/Result';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { League } from '../../domain/entities/League';
|
||||
import type { Race } from '../../domain/entities/Race';
|
||||
import type { RaceRegistration } from '../../domain/entities/RaceRegistration';
|
||||
import type { Result as RaceResult } from '../../domain/entities/result/Result';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
@@ -26,14 +27,12 @@ export type GetRaceDetailResult = {
|
||||
league: League | null;
|
||||
registrations: RaceRegistration[];
|
||||
drivers: NonNullable<Awaited<ReturnType<IDriverRepository['findById']>>>[];
|
||||
userResult: DomainResult | null;
|
||||
userResult: RaceResult | null;
|
||||
isUserRegistered: boolean;
|
||||
canRegister: boolean;
|
||||
};
|
||||
|
||||
export class GetRaceDetailUseCase {
|
||||
private output: UseCaseOutputPort<GetRaceDetailResult> | null = null; // TODO wtf this must be injected via constructor
|
||||
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
@@ -41,12 +40,9 @@ export class GetRaceDetailUseCase {
|
||||
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly output: UseCaseOutputPort<GetRaceDetailResult>,
|
||||
) {}
|
||||
|
||||
setOutput(output: UseCaseOutputPort<GetRaceDetailResult>) { // TODO must be removed
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
async execute(
|
||||
input: GetRaceDetailInput,
|
||||
): Promise<Result<void, ApplicationErrorCode<GetRaceDetailErrorCode, { message: string }>>> {
|
||||
@@ -75,9 +71,10 @@ export class GetRaceDetailUseCase {
|
||||
|
||||
const isUserRegistered = registrations.some(reg => reg.driverId.toString() === driverId);
|
||||
const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date();
|
||||
const canRegister = !!membership && membership.status === 'active' && isUpcoming;
|
||||
const canRegister =
|
||||
!!membership && membership.status.toString() === 'active' && isUpcoming;
|
||||
|
||||
let userResult: DomainResult | null = null;
|
||||
let userResult: RaceResult | null = null;
|
||||
|
||||
if (race.status === 'completed') {
|
||||
const results = await this.resultRepository.findByRaceId(race.id);
|
||||
@@ -94,9 +91,6 @@ export class GetRaceDetailUseCase {
|
||||
canRegister,
|
||||
};
|
||||
|
||||
if (!this.output) {
|
||||
throw new Error('Output not set');
|
||||
}
|
||||
this.output.present(result);
|
||||
|
||||
return Result.ok(undefined);
|
||||
|
||||
@@ -38,10 +38,10 @@ export class GetRacePenaltiesUseCase {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
penalties.forEach((penalty: any) => {
|
||||
for (const penalty of penalties) {
|
||||
driverIds.add(penalty.driverId);
|
||||
driverIds.add(penalty.issuedBy);
|
||||
});
|
||||
}
|
||||
|
||||
const drivers = await Promise.all(
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
@@ -52,16 +52,16 @@ export class GetRacePenaltiesUseCase {
|
||||
this.output.present({ penalties, drivers: validDrivers });
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error && error.message ? error.message : 'Failed to load race penalties';
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: 'Failed to load race penalties';
|
||||
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: {
|
||||
message,
|
||||
},
|
||||
} as ApplicationErrorCode<GetRacePenaltiesErrorCode, { message: string }>);
|
||||
details: { message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,9 @@ describe('GetRaceProtestsUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as GetRaceProtestsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetRaceProtestsResult;
|
||||
|
||||
expect(presented.protests).toHaveLength(1);
|
||||
expect(presented.protests[0]).toEqual(protest);
|
||||
@@ -96,7 +98,9 @@ describe('GetRaceProtestsUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as GetRaceProtestsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetRaceProtestsResult;
|
||||
|
||||
expect(presented.protests).toEqual([]);
|
||||
expect(presented.drivers).toEqual([]);
|
||||
|
||||
@@ -62,8 +62,8 @@ export class GetRaceProtestsUseCase {
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
|
||||
? (error as any).message
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: 'Failed to load race protests';
|
||||
|
||||
return Result.err({
|
||||
|
||||
@@ -57,12 +57,14 @@ describe('GetRaceRegistrationsUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as GetRaceRegistrationsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetRaceRegistrationsResult;
|
||||
|
||||
expect(presented.race).toEqual(race);
|
||||
expect(presented.registrations).toHaveLength(2);
|
||||
expect(presented.registrations[0].registration).toEqual(registrations[0]);
|
||||
expect(presented.registrations[1].registration).toEqual(registrations[1]);
|
||||
expect(presented.registrations[0]!.registration).toEqual(registrations[0]);
|
||||
expect(presented.registrations[1]!.registration).toEqual(registrations[1]);
|
||||
});
|
||||
|
||||
it('should return RACE_NOT_FOUND error when race does not exist', async () => {
|
||||
|
||||
@@ -62,13 +62,28 @@ describe('GetRacesPageDataUseCase', () => {
|
||||
});
|
||||
|
||||
it('should present races page data for a league', async () => {
|
||||
const races = [
|
||||
type RaceRow = {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: Date;
|
||||
status: 'scheduled' | 'completed';
|
||||
leagueId: string;
|
||||
strengthOfField: number;
|
||||
isUpcoming: () => boolean;
|
||||
isLive: () => boolean;
|
||||
isPast: () => boolean;
|
||||
};
|
||||
|
||||
type LeagueRow = { id: string; name: string };
|
||||
|
||||
const races: RaceRow[] = [
|
||||
{
|
||||
id: 'race-1',
|
||||
track: 'Track 1',
|
||||
car: 'Car 1',
|
||||
scheduledAt: new Date('2023-01-01T10:00:00Z'),
|
||||
status: 'scheduled' as const,
|
||||
status: 'scheduled',
|
||||
leagueId: 'league-1',
|
||||
strengthOfField: 1500,
|
||||
isUpcoming: () => true,
|
||||
@@ -80,16 +95,16 @@ describe('GetRacesPageDataUseCase', () => {
|
||||
track: 'Track 2',
|
||||
car: 'Car 2',
|
||||
scheduledAt: new Date('2023-01-02T10:00:00Z'),
|
||||
status: 'completed' as const,
|
||||
status: 'completed',
|
||||
leagueId: 'league-1',
|
||||
strengthOfField: 1600,
|
||||
isUpcoming: () => false,
|
||||
isLive: () => false,
|
||||
isPast: () => true,
|
||||
},
|
||||
] as any[];
|
||||
];
|
||||
|
||||
const leagues = [{ id: 'league-1', name: 'League 1' }] as any[];
|
||||
const leagues: LeagueRow[] = [{ id: 'league-1', name: 'League 1' }];
|
||||
|
||||
(raceRepository.findAll as Mock).mockResolvedValue(races);
|
||||
(leagueRepository.findAll as Mock).mockResolvedValue(leagues);
|
||||
@@ -103,14 +118,16 @@ describe('GetRacesPageDataUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0]! as GetRacesPageDataResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetRacesPageDataResult;
|
||||
|
||||
expect(presented.leagueId).toBe('league-1');
|
||||
expect(presented.races).toHaveLength(2);
|
||||
|
||||
expect(presented.races[0].race.id).toBe('race-1');
|
||||
expect(presented.races[0].leagueName).toBe('League 1');
|
||||
expect(presented.races[1].race.id).toBe('race-2');
|
||||
expect(presented.races[0]!.race.id).toBe('race-1');
|
||||
expect(presented.races[0]!.leagueName).toBe('League 1');
|
||||
expect(presented.races[1]!.race.id).toBe('race-2');
|
||||
});
|
||||
|
||||
it('should return repository error when repositories throw and not present data', async () => {
|
||||
|
||||
@@ -41,7 +41,9 @@ export class GetRacesPageDataUseCase {
|
||||
this.leagueRepository.findAll(),
|
||||
]);
|
||||
|
||||
const leagueMap = new Map(allLeagues.map(league => [league.id, league.name]));
|
||||
const leagueMap = new Map(
|
||||
allLeagues.map(league => [league.id.toString(), league.name.toString()]),
|
||||
);
|
||||
|
||||
const filteredRaces = allRaces
|
||||
.filter(race => race.leagueId === input.leagueId)
|
||||
|
||||
@@ -55,7 +55,7 @@ export class GetSeasonDetailsUseCase {
|
||||
}
|
||||
|
||||
const result: GetSeasonDetailsResult = {
|
||||
leagueId: league.id,
|
||||
leagueId: league.id.toString(),
|
||||
season,
|
||||
};
|
||||
|
||||
|
||||
@@ -23,13 +23,10 @@ export type SeasonSponsorshipFinancials = {
|
||||
currency: string;
|
||||
};
|
||||
|
||||
import type { LeagueId } from '../../domain/entities/LeagueId';
|
||||
import type { LeagueName } from '../../domain/entities/LeagueName';
|
||||
|
||||
export type SeasonSponsorshipDetail = {
|
||||
id: string;
|
||||
leagueId: LeagueId;
|
||||
leagueName: LeagueName;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
seasonName: string;
|
||||
seasonStartDate?: Date;
|
||||
@@ -101,20 +98,18 @@ export class GetSeasonSponsorshipsUseCase {
|
||||
const completedRaces = races.filter(r => r.status === 'completed').length;
|
||||
const impressions = completedRaces * driverCount * 100;
|
||||
|
||||
const sponsorshipDetails: SeasonSponsorshipDetail[] = sponsorships.map(sponsorship => {
|
||||
const sponsorshipDetails: SeasonSponsorshipDetail[] = sponsorships.map((sponsorship) => {
|
||||
const platformFee = sponsorship.getPlatformFee();
|
||||
const netAmount = sponsorship.getNetAmount();
|
||||
|
||||
return {
|
||||
const detail: SeasonSponsorshipDetail = {
|
||||
id: sponsorship.id,
|
||||
leagueId: league.id,
|
||||
leagueName: league.name,
|
||||
leagueId: league.id.toString(),
|
||||
leagueName: league.name.toString(),
|
||||
seasonId: season.id,
|
||||
seasonName: season.name,
|
||||
seasonStartDate: season.startDate,
|
||||
seasonEndDate: season.endDate,
|
||||
tier: sponsorship.tier,
|
||||
status: sponsorship.status,
|
||||
tier: sponsorship.tier.toString(),
|
||||
status: sponsorship.status.toString(),
|
||||
pricing: {
|
||||
amount: sponsorship.pricing.amount,
|
||||
currency: sponsorship.pricing.currency,
|
||||
@@ -134,8 +129,12 @@ export class GetSeasonSponsorshipsUseCase {
|
||||
impressions,
|
||||
},
|
||||
createdAt: sponsorship.createdAt,
|
||||
activatedAt: sponsorship.activatedAt,
|
||||
...(season.startDate ? { seasonStartDate: season.startDate } : {}),
|
||||
...(season.endDate ? { seasonEndDate: season.endDate } : {}),
|
||||
...(sponsorship.activatedAt ? { activatedAt: sponsorship.activatedAt } : {}),
|
||||
};
|
||||
|
||||
return detail;
|
||||
});
|
||||
|
||||
this.output.present({
|
||||
|
||||
@@ -119,7 +119,9 @@ describe('GetSponsorDashboardUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const dashboard = (output.present as Mock).mock.calls[0][0] as GetSponsorDashboardResult;
|
||||
const dashboardRaw = (output.present as Mock).mock.calls[0]?.[0];
|
||||
expect(dashboardRaw).toBeDefined();
|
||||
const dashboard = dashboardRaw as GetSponsorDashboardResult;
|
||||
|
||||
expect(dashboard).toBeDefined();
|
||||
expect(dashboard.sponsorId).toBe(sponsorId);
|
||||
|
||||
@@ -119,7 +119,9 @@ describe('GetSponsorSponsorshipsUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = (output.present as Mock).mock.calls[0][0] as GetSponsorSponsorshipsResult;
|
||||
const presentedRaw = (output.present as Mock).mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetSponsorSponsorshipsResult;
|
||||
|
||||
expect(presented.sponsor).toBe(sponsor);
|
||||
expect(presented.sponsorships).toHaveLength(1);
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('GetSponsorsUseCase', () => {
|
||||
};
|
||||
useCase = new GetSponsorsUseCase(
|
||||
sponsorRepository as unknown as ISponsorRepository,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
output as unknown as UseCaseOutputPort<unknown>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,9 @@ describe('GetTeamDetailsUseCase', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as GetTeamDetailsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetTeamDetailsResult;
|
||||
expect(presented.team).toBe(team);
|
||||
expect(presented.membership).toEqual(membership);
|
||||
expect(presented.canManage).toBe(false);
|
||||
@@ -103,7 +105,9 @@ describe('GetTeamDetailsUseCase', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as GetTeamDetailsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetTeamDetailsResult;
|
||||
expect(presented.canManage).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -82,17 +82,19 @@ describe('GetTeamJoinRequestsUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as GetTeamJoinRequestsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetTeamJoinRequestsResult;
|
||||
|
||||
expect(presented.team).toBe(team);
|
||||
expect(presented.joinRequests).toHaveLength(1);
|
||||
expect(presented.joinRequests[0]).toMatchObject({
|
||||
expect(presented.joinRequests[0]!).toMatchObject({
|
||||
id: 'req-1',
|
||||
teamId,
|
||||
driverId: 'driver-1',
|
||||
message: 'msg',
|
||||
});
|
||||
expect(presented.joinRequests[0].driver).toBe(driver);
|
||||
expect(presented.joinRequests[0]!.driver).toBe(driver);
|
||||
});
|
||||
|
||||
it('should return TEAM_NOT_FOUND error when team does not exist', async () => {
|
||||
|
||||
@@ -69,8 +69,8 @@ export class GetTeamJoinRequestsUseCase {
|
||||
return Result.ok(undefined);
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error && typeof error === 'object' && 'message' in error && typeof (error as any).message === 'string'
|
||||
? (error as any).message
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: 'Failed to load team join requests';
|
||||
|
||||
return Result.err({
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from './GetTeamsLeaderboardUseCase';
|
||||
import { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import { Team } from '../../domain/entities/Team';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
@@ -21,9 +20,6 @@ describe('GetTeamsLeaderboardUseCase', () => {
|
||||
let teamMembershipRepository: {
|
||||
getTeamMembers: Mock;
|
||||
};
|
||||
let driverRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
let getDriverStats: Mock;
|
||||
let logger: {
|
||||
debug: Mock;
|
||||
@@ -40,9 +36,6 @@ describe('GetTeamsLeaderboardUseCase', () => {
|
||||
teamMembershipRepository = {
|
||||
getTeamMembers: vi.fn(),
|
||||
};
|
||||
driverRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
getDriverStats = vi.fn();
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
@@ -52,12 +45,12 @@ describe('GetTeamsLeaderboardUseCase', () => {
|
||||
};
|
||||
output = {
|
||||
present: vi.fn(),
|
||||
} as any;
|
||||
} as unknown as UseCaseOutputPort<GetTeamsLeaderboardResult> & { present: Mock };
|
||||
|
||||
useCase = new GetTeamsLeaderboardUseCase(
|
||||
teamRepository as unknown as ITeamRepository,
|
||||
teamMembershipRepository as unknown as ITeamMembershipRepository,
|
||||
driverRepository as unknown as IDriverRepository,
|
||||
getDriverStats,
|
||||
getDriverStats as unknown as (driverId: string) => { rating: number | null; wins: number; totalRaces: number } | null,
|
||||
logger as unknown as Logger,
|
||||
output,
|
||||
);
|
||||
@@ -109,7 +102,9 @@ describe('GetTeamsLeaderboardUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = (output.present as unknown as Mock).mock.calls[0][0] as GetTeamsLeaderboardResult;
|
||||
const presentedRaw = (output.present as unknown as Mock).mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as GetTeamsLeaderboardResult;
|
||||
|
||||
expect(presented.recruitingCount).toBe(2); // both teams are recruiting
|
||||
expect(presented.items).toHaveLength(2);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import { SkillLevelService, type SkillLevel } from '@core/racing/domain/services/SkillLevelService';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -46,7 +45,6 @@ export class GetTeamsLeaderboardUseCase {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly getDriverStats: (driverId: string) => DriverStatsAdapter | null,
|
||||
private readonly logger: Logger,
|
||||
private readonly output: UseCaseOutputPort<GetTeamsLeaderboardResult>,
|
||||
|
||||
@@ -2,11 +2,9 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import {
|
||||
GetTotalDriversUseCase,
|
||||
GetTotalDriversInput,
|
||||
GetTotalDriversResult,
|
||||
GetTotalDriversErrorCode,
|
||||
} from './GetTotalDriversUseCase';
|
||||
import { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
describe('GetTotalDriversUseCase', () => {
|
||||
@@ -14,21 +12,12 @@ describe('GetTotalDriversUseCase', () => {
|
||||
let driverRepository: {
|
||||
findAll: Mock;
|
||||
};
|
||||
let output: UseCaseOutputPort<GetTotalDriversResult> & { present: Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
driverRepository = {
|
||||
findAll: vi.fn(),
|
||||
};
|
||||
|
||||
output = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<GetTotalDriversResult> & { present: Mock };
|
||||
|
||||
useCase = new GetTotalDriversUseCase(
|
||||
driverRepository as unknown as IDriverRepository,
|
||||
output,
|
||||
);
|
||||
useCase = new GetTotalDriversUseCase(driverRepository as unknown as IDriverRepository);
|
||||
});
|
||||
|
||||
it('should return total number of drivers', async () => {
|
||||
@@ -41,11 +30,7 @@ describe('GetTotalDriversUseCase', () => {
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
expect(output.present).toHaveBeenCalledWith<[{ totalDrivers: number }]>(
|
||||
expect.objectContaining({ totalDrivers: 2 }),
|
||||
);
|
||||
expect(result.unwrap()).toEqual({ totalDrivers: 2 });
|
||||
});
|
||||
|
||||
it('should return error on repository failure', async () => {
|
||||
@@ -66,6 +51,5 @@ describe('GetTotalDriversUseCase', () => {
|
||||
|
||||
expect(unwrappedError.code).toBe('REPOSITORY_ERROR');
|
||||
expect(unwrappedError.details.message).toBe(error.message);
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,9 @@ describe('GetTotalRacesUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
|
||||
const payload = output.present.mock.calls[0][0] as GetTotalRacesResult;
|
||||
const payloadRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(payloadRaw).toBeDefined();
|
||||
const payload = payloadRaw as GetTotalRacesResult;
|
||||
expect(payload.totalRaces).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -188,8 +188,9 @@ describe('ImportRaceResultsApiUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented =
|
||||
output.present.mock.calls[0][0] as ImportRaceResultsApiResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as ImportRaceResultsApiResult;
|
||||
|
||||
expect(presented.success).toBe(true);
|
||||
expect(presented.raceId).toBe('race-1');
|
||||
|
||||
@@ -172,16 +172,16 @@ export class ImportRaceResultsApiUseCase {
|
||||
|
||||
this.logger.info('ImportRaceResultsApiUseCase:race results created', { raceId });
|
||||
|
||||
await this.standingRepository.recalculate(league.id);
|
||||
await this.standingRepository.recalculate(league.id.toString());
|
||||
|
||||
this.logger.info('ImportRaceResultsApiUseCase:standings recalculated', {
|
||||
leagueId: league.id,
|
||||
leagueId: league.id.toString(),
|
||||
});
|
||||
|
||||
const result: ImportRaceResultsApiResult = {
|
||||
success: true,
|
||||
raceId,
|
||||
leagueId: league.id,
|
||||
leagueId: league.id.toString(),
|
||||
driversProcessed: results.length,
|
||||
resultsRecorded: validEntities.length,
|
||||
errors: [],
|
||||
|
||||
@@ -2,11 +2,10 @@ import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import {
|
||||
IsDriverRegisteredForRaceUseCase,
|
||||
type IsDriverRegisteredForRaceInput,
|
||||
type IsDriverRegisteredForRaceResult,
|
||||
type IsDriverRegisteredForRaceErrorCode,
|
||||
} from './IsDriverRegisteredForRaceUseCase';
|
||||
import { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
describe('IsDriverRegisteredForRaceUseCase', () => {
|
||||
@@ -20,10 +19,6 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
|
||||
warn: Mock;
|
||||
error: Mock;
|
||||
};
|
||||
let output: UseCaseOutputPort<IsDriverRegisteredForRaceResult> & {
|
||||
present: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
registrationRepository = {
|
||||
isRegistered: vi.fn(),
|
||||
@@ -34,13 +29,9 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
output = {
|
||||
present: vi.fn(),
|
||||
} as unknown as UseCaseOutputPort<IsDriverRegisteredForRaceResult> & { present: Mock };
|
||||
useCase = new IsDriverRegisteredForRaceUseCase(
|
||||
registrationRepository as unknown as IRaceRegistrationRepository,
|
||||
logger as unknown as Logger,
|
||||
output as UseCaseOutputPort<IsDriverRegisteredForRaceResult>,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -52,10 +43,7 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const [[presented]] = (output.present as Mock).mock.calls as [[IsDriverRegisteredForRaceResult]];
|
||||
expect(presented).toEqual({
|
||||
expect(result.unwrap()).toEqual({
|
||||
raceId: params.raceId,
|
||||
driverId: params.driverId,
|
||||
isRegistered: true,
|
||||
@@ -70,10 +58,7 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const [[presented]] = (output.present as Mock).mock.calls as [[IsDriverRegisteredForRaceResult]];
|
||||
expect(presented).toEqual({
|
||||
expect(result.unwrap()).toEqual({
|
||||
raceId: params.raceId,
|
||||
driverId: params.driverId,
|
||||
isRegistered: false,
|
||||
@@ -95,6 +80,5 @@ describe('IsDriverRegisteredForRaceUseCase', () => {
|
||||
>;
|
||||
expect(errorResult.code).toBe('REPOSITORY_ERROR');
|
||||
expect(errorResult.details?.message).toBe('Repository error');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { JoinLeagueUseCase, type JoinLeagueResult, type JoinLeagueInput, type JoinLeagueErrorCode } from './JoinLeagueUseCase';
|
||||
import { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
describe('JoinLeagueUseCase', () => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { LeagueMembership } from '../../domain/entities/LeagueMembership';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
|
||||
export type JoinLeagueErrorCode = 'ALREADY_MEMBER' | 'REPOSITORY_ERROR';
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
|
||||
describe('ListSeasonsForLeagueUseCase', () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
describe('ManageSeasonLifecycleUseCase', () => {
|
||||
let useCase: ManageSeasonLifecycleUseCase;
|
||||
@@ -107,7 +106,11 @@ describe('ManageSeasonLifecycleUseCase', () => {
|
||||
expect(archived.isOk()).toBe(true);
|
||||
expect(archived.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
presented = output.present.mock.calls[0][0] as ManageSeasonLifecycleResult;
|
||||
{
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
presented = presentedRaw as ManageSeasonLifecycleResult;
|
||||
}
|
||||
expect(presented.season.status).toBe('archived');
|
||||
});
|
||||
|
||||
|
||||
@@ -58,10 +58,12 @@ export class ManageSeasonLifecycleUseCase {
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(input.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
if (!season || season.leagueId !== league.id.toString()) {
|
||||
return Result.err({
|
||||
code: 'SEASON_NOT_FOUND',
|
||||
details: { message: `Season ${input.seasonId} does not belong to league ${league.id}` },
|
||||
details: {
|
||||
message: `Season ${input.seasonId} does not belong to league ${league.id.toString()}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,9 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented =
|
||||
output.present.mock.calls[0][0] as PreviewLeagueScheduleResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as PreviewLeagueScheduleResult;
|
||||
expect(presented.rounds.length).toBeGreaterThan(0);
|
||||
expect(presented.summary).toContain('Every Mon');
|
||||
});
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { SeasonScheduleGenerator } from '../../domain/services/SeasonScheduleGenerator';
|
||||
import { scheduleDTOToSeasonSchedule } from '../dto/LeagueScheduleDTO';
|
||||
import {
|
||||
scheduleDTOToSeasonSchedule,
|
||||
type SeasonScheduleConfigDTO,
|
||||
} from '../dto/LeagueScheduleDTO';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
export type PreviewLeagueScheduleSeasonConfig = {
|
||||
seasonStartDate: string;
|
||||
recurrenceStrategy: string;
|
||||
weekdays?: string[];
|
||||
raceStartTime: string;
|
||||
timezoneId: string;
|
||||
plannedRounds: number;
|
||||
intervalWeeks?: number;
|
||||
monthlyOrdinal?: 1 | 2 | 3 | 4;
|
||||
monthlyWeekday?: string;
|
||||
};
|
||||
export type PreviewLeagueScheduleSeasonConfig = SeasonScheduleConfigDTO;
|
||||
|
||||
export type PreviewLeagueScheduleInput = {
|
||||
schedule: PreviewLeagueScheduleSeasonConfig;
|
||||
@@ -61,7 +54,7 @@ export class PreviewLeagueScheduleUseCase {
|
||||
try {
|
||||
let seasonSchedule: SeasonSchedule;
|
||||
try {
|
||||
seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule as any);
|
||||
seasonSchedule = scheduleDTOToSeasonSchedule(params.schedule);
|
||||
} catch (error) {
|
||||
this.logger.warn('Invalid schedule data provided', {
|
||||
schedule: params.schedule,
|
||||
@@ -83,11 +76,11 @@ export class PreviewLeagueScheduleUseCase {
|
||||
maxRounds,
|
||||
);
|
||||
|
||||
const rounds: PreviewLeagueScheduleRound[] = slots.map((slot) => ({
|
||||
roundNumber: slot.roundNumber,
|
||||
scheduledAt: slot.scheduledAt.toISOString(),
|
||||
timezoneId: slot.timezone.id,
|
||||
}));
|
||||
const rounds: PreviewLeagueScheduleRound[] = slots.map(slot => ({
|
||||
roundNumber: slot.roundNumber,
|
||||
scheduledAt: slot.scheduledAt.toISOString(),
|
||||
timezoneId: slot.timezone.id,
|
||||
}));
|
||||
|
||||
const summary = this.buildSummary(params.schedule, rounds);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Designed for fast, common penalty scenarios like track limits, warnings, etc.
|
||||
*/
|
||||
|
||||
import { Penalty } from '../../domain/entities/Penalty';
|
||||
import { Penalty } from '../../domain/entities/penalty/Penalty';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
@@ -74,7 +74,11 @@ export class QuickPenaltyUseCase {
|
||||
);
|
||||
|
||||
if (!penaltyMapping) {
|
||||
this.logger.error('Unknown infraction type', { infractionType: input.infractionType, severity: input.severity });
|
||||
this.logger.error(
|
||||
'Unknown infraction type',
|
||||
undefined,
|
||||
{ infractionType: input.infractionType, severity: input.severity },
|
||||
);
|
||||
return Result.err({ code: 'UNKNOWN_INFRACTION', details: { message: 'Unknown infraction type' } });
|
||||
}
|
||||
|
||||
@@ -111,9 +115,16 @@ export class QuickPenaltyUseCase {
|
||||
|
||||
this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: input.raceId, driverId: input.driverId });
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to apply quick penalty', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error instanceof Error ? error.message : 'Unknown error' } });
|
||||
} catch (error: unknown) {
|
||||
const err =
|
||||
error instanceof Error ? error : new Error('Failed to apply quick penalty');
|
||||
|
||||
this.logger.error('Failed to apply quick penalty', err);
|
||||
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
|
||||
import type { IChampionshipStandingRepository } from '../../domain/repositories/IChampionshipStandingRepository';
|
||||
import type { Penalty } from '../../domain/entities/Penalty';
|
||||
import { EventScoringService } from '../../domain/services/EventScoringService';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import { ChampionshipAggregator } from '../../domain/services/ChampionshipAggregator';
|
||||
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -53,7 +54,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
|
||||
output = { present: vi.fn() } as unknown as typeof output;
|
||||
|
||||
useCase = new RecalculateChampionshipStandingsUseCase(
|
||||
leagueRepository as unknown as ISeasonRepository,
|
||||
leagueRepository as unknown as ILeagueRepository,
|
||||
seasonRepository as unknown as ISeasonRepository,
|
||||
leagueScoringConfigRepository as unknown as ILeagueScoringConfigRepository,
|
||||
raceRepository as unknown as IRaceRepository,
|
||||
@@ -172,7 +173,9 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
|
||||
const presented = output.present.mock.calls[0][0] as RecalculateChampionshipStandingsResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as RecalculateChampionshipStandingsResult;
|
||||
expect(presented.leagueId).toBe('league-1');
|
||||
expect(presented.seasonId).toBe('season-1');
|
||||
expect(presented.entries).toHaveLength(1);
|
||||
|
||||
@@ -97,7 +97,7 @@ export class RecalculateChampionshipStandingsUseCase {
|
||||
{};
|
||||
|
||||
for (const race of races) {
|
||||
const sessionType = this.mapRaceSessionType(race.sessionType);
|
||||
const sessionType = this.mapRaceSessionType(String(race.sessionType));
|
||||
if (!championship.sessionTypes.includes(sessionType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ describe('RegisterForRaceUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as RegisterForRaceResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as RegisterForRaceResult;
|
||||
expect(presented).toEqual<RegisterForRaceResult>({
|
||||
raceId: 'race-1',
|
||||
driverId: 'driver-1',
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
interface LeagueRepositoryMock {
|
||||
findById: Mock;
|
||||
@@ -31,13 +30,13 @@ describe('RejectLeagueJoinRequestUseCase', () => {
|
||||
beforeEach(() => {
|
||||
leagueRepository = {
|
||||
findById: vi.fn(),
|
||||
} as unknown as ILeagueRepository as any;
|
||||
};
|
||||
|
||||
leagueMembershipRepository = {
|
||||
getMembership: vi.fn(),
|
||||
getJoinRequests: vi.fn(),
|
||||
removeJoinRequest: vi.fn(),
|
||||
} as unknown as ILeagueMembershipRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
@@ -87,7 +86,9 @@ describe('RejectLeagueJoinRequestUseCase', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as RejectLeagueJoinRequestResult;
|
||||
const presentedRaw = output.present.mock.calls[0]?.[0];
|
||||
expect(presentedRaw).toBeDefined();
|
||||
const presented = presentedRaw as RejectLeagueJoinRequestResult;
|
||||
expect(presented.leagueId).toBe('league-1');
|
||||
expect(presented.requestId).toBe('req-1');
|
||||
expect(presented.status).toBe('rejected');
|
||||
|
||||
@@ -74,7 +74,12 @@ export class RejectLeagueJoinRequestUseCase {
|
||||
});
|
||||
}
|
||||
|
||||
const currentStatus = (joinRequest as any).status ?? 'pending';
|
||||
const currentStatus = (() => {
|
||||
const rawStatus = (joinRequest as unknown as { status?: unknown }).status;
|
||||
return rawStatus === 'pending' || rawStatus === 'approved' || rawStatus === 'rejected'
|
||||
? rawStatus
|
||||
: 'pending';
|
||||
})();
|
||||
if (currentStatus !== 'pending') {
|
||||
this.logger.warn('Join request is in invalid state for rejection', {
|
||||
leagueId,
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamM
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
interface TeamRepositoryMock {
|
||||
findById: Mock;
|
||||
@@ -31,13 +30,13 @@ describe('RejectTeamJoinRequestUseCase', () => {
|
||||
beforeEach(() => {
|
||||
teamRepository = {
|
||||
findById: vi.fn(),
|
||||
} as unknown as ITeamRepository as any;
|
||||
};
|
||||
|
||||
membershipRepository = {
|
||||
getMembership: vi.fn(),
|
||||
getJoinRequests: vi.fn(),
|
||||
removeJoinRequest: vi.fn(),
|
||||
} as unknown as ITeamMembershipRepository as any;
|
||||
};
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
|
||||
@@ -70,7 +70,12 @@ export class RejectTeamJoinRequestUseCase {
|
||||
});
|
||||
}
|
||||
|
||||
const currentStatus = (joinRequest as any).status ?? 'pending';
|
||||
const currentStatus = (() => {
|
||||
const rawStatus = (joinRequest as unknown as { status?: unknown }).status;
|
||||
return rawStatus === 'pending' || rawStatus === 'approved' || rawStatus === 'rejected'
|
||||
? rawStatus
|
||||
: 'pending';
|
||||
})();
|
||||
if (currentStatus !== 'pending') {
|
||||
this.logger.warn('Join request is in invalid state for rejection', {
|
||||
teamId,
|
||||
|
||||
@@ -50,8 +50,9 @@ describe('RemoveLeagueMemberUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(leagueMembershipRepository.saveMembership).toHaveBeenCalledTimes(1);
|
||||
const savedMembership = leagueMembershipRepository.saveMembership.mock.calls[0][0];
|
||||
expect(savedMembership.status.toString()).toBe('inactive');
|
||||
const savedMembership = leagueMembershipRepository.saveMembership.mock.calls[0]?.[0];
|
||||
expect(savedMembership).toBeDefined();
|
||||
expect(savedMembership!.status.toString()).toBe('inactive');
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user