refactor league module (wip)

This commit is contained in:
2025-12-22 15:47:47 +01:00
parent 03dc81b0ba
commit f59e1b13e7
10 changed files with 444 additions and 819 deletions

View File

@@ -0,0 +1,48 @@
import { vi } from 'vitest';
import { DashboardService } from './DashboardService';
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import type { Logger } from '@core/shared/application/Logger';
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
describe('DashboardService', () => {
let service: DashboardService;
let mockUseCase: ReturnType<typeof vi.mocked<DashboardOverviewUseCase>>;
let mockPresenter: ReturnType<typeof vi.mocked<DashboardOverviewPresenter>>;
let mockLogger: ReturnType<typeof vi.mocked<Logger>>;
beforeEach(() => {
mockUseCase = {
execute: vi.fn(),
} as any;
mockPresenter = {
present: vi.fn(),
getResponseModel: vi.fn(),
} as any;
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as any;
service = new DashboardService(
mockLogger,
mockUseCase,
mockPresenter
);
});
it('should get dashboard overview', async () => {
const mockResult = { totalUsers: 100 };
mockUseCase.execute.mockResolvedValue(undefined);
mockPresenter.getResponseModel.mockReturnValue(mockResult);
const result = await service.getDashboardOverview('driver-1');
expect(mockUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver-1' });
expect(mockPresenter.getResponseModel).toHaveBeenCalled();
expect(result).toBe(mockResult);
});
});

View File

@@ -7,27 +7,20 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen
import type { Logger } from '@core/shared/application/Logger';
// Tokens
import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN } from './DashboardProviders';
import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN } from './DashboardProviders';
@Injectable()
export class DashboardService {
private readonly presenter = new DashboardOverviewPresenter();
constructor(
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
@Inject(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN) private readonly presenter: DashboardOverviewPresenter,
) {}
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
const result = await this.dashboardOverviewUseCase.execute({ driverId });
if (result.isErr()) {
throw new Error(result.unwrapErr().details?.message ?? 'Failed to get dashboard overview');
}
this.presenter.present(result);
await this.dashboardOverviewUseCase.execute({ driverId });
return this.presenter.getResponseModel();
}

View File

@@ -35,6 +35,17 @@ import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankin
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import { InMemorySocialGraphRepository } from '@core/social/infrastructure/inmemory/InMemorySocialAndFeed';
// Import presenters
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
import { DriverPresenter } from './presenters/DriverPresenter';
import { DriverProfilePresenter } from './presenters/DriverProfilePresenter';
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
// Import types for output ports
import type { UseCaseOutputPort } from '@core/shared/application';
// Define injection tokens
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const RANKING_SERVICE_TOKEN = 'IRankingService';
@@ -47,7 +58,7 @@ export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreference
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
export const LOGGER_TOKEN = 'Logger';
// Use case tokens
export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseCase';
@@ -57,11 +68,61 @@ export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredF
export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
// Output port tokens
export const GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN = 'GetDriversLeaderboardOutputPort_TOKEN';
export const GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN = 'GetTotalDriversOutputPort_TOKEN';
export const COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN = 'CompleteDriverOnboardingOutputPort_TOKEN';
export const IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN = 'IsDriverRegisteredForRaceOutputPort_TOKEN';
export const UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN = 'UpdateDriverProfileOutputPort_TOKEN';
export const GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN = 'GetProfileOverviewOutputPort_TOKEN';
export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself
DriverService,
// Presenters
DriversLeaderboardPresenter,
DriverStatsPresenter,
CompleteOnboardingPresenter,
DriverRegistrationStatusPresenter,
DriverPresenter,
DriverProfilePresenter,
// Output ports (point to presenters)
{
provide: GET_DRIVERS_LEADERBOARD_OUTPUT_PORT_TOKEN,
useExisting: DriversLeaderboardPresenter,
},
{
provide: GET_TOTAL_DRIVERS_OUTPUT_PORT_TOKEN,
useExisting: DriverStatsPresenter,
},
{
provide: COMPLETE_DRIVER_ONBOARDING_OUTPUT_PORT_TOKEN,
useExisting: CompleteOnboardingPresenter,
},
{
provide: IS_DRIVER_REGISTERED_FOR_RACE_OUTPUT_PORT_TOKEN,
useExisting: DriverRegistrationStatusPresenter,
},
{
provide: UPDATE_DRIVER_PROFILE_OUTPUT_PORT_TOKEN,
useExisting: DriverPresenter,
},
{
provide: GET_PROFILE_OVERVIEW_OUTPUT_PORT_TOKEN,
useExisting: DriverProfilePresenter,
},
// Logger
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Repositories
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
{
@@ -115,10 +176,7 @@ export const DriverProviders: Provider[] = [
new InMemorySocialGraphRepository(logger, { drivers: [], friendships: [], feedEvents: [] }),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
@@ -149,7 +207,8 @@ export const DriverProviders: Provider[] = [
},
{
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, logger: Logger) => new UpdateDriverProfileUseCase(driverRepo, logger),
useFactory: (driverRepo: IDriverRepository, logger: Logger) =>
new UpdateDriverProfileUseCase(driverRepo, logger),
inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
@@ -209,4 +268,4 @@ export const DriverProviders: Provider[] = [
RANKING_SERVICE_TOKEN,
],
},
];
];

View File

@@ -1,218 +1,31 @@
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
import type { Driver } from '@core/racing/domain/entities/Driver';
import { GetDriversLeaderboardUseCase, type GetDriversLeaderboardResult } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { DriverService } from './DriverService';
describe('DriverService', () => {
let service: DriverService;
let getDriversLeaderboardUseCase: ReturnType<typeof vi.mocked<GetDriversLeaderboardUseCase>>;
let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>;
let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>;
let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>;
let logger: ReturnType<typeof vi.mocked<Logger>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DriverService,
{
provide: 'GetDriversLeaderboardUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'GetTotalDriversUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'CompleteDriverOnboardingUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'IsDriverRegisteredForRaceUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'UpdateDriverProfileUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'GetProfileOverviewUseCase',
useValue: {
execute: vi.fn(),
},
},
{
provide: 'IDriverRepository',
useValue: {
findById: vi.fn(),
},
},
{
provide: 'Logger',
useValue: {
debug: vi.fn(),
error: vi.fn(),
},
},
],
}).compile();
service = module.get<DriverService>(DriverService);
getDriversLeaderboardUseCase = vi.mocked(module.get('GetDriversLeaderboardUseCase'));
getTotalDriversUseCase = vi.mocked(module.get('GetTotalDriversUseCase'));
completeDriverOnboardingUseCase = vi.mocked(module.get('CompleteDriverOnboardingUseCase'));
isDriverRegisteredForRaceUseCase = vi.mocked(module.get('IsDriverRegisteredForRaceUseCase'));
logger = vi.mocked(module.get('Logger'));
beforeEach(() => {
// Mock all dependencies
service = new DriverService(
{} as any, // getDriversLeaderboardUseCase
{} as any, // getTotalDriversUseCase
{} as any, // completeDriverOnboardingUseCase
{} as any, // isDriverRegisteredForRaceUseCase
{} as any, // updateDriverProfileUseCase
{} as any, // getProfileOverviewUseCase
{} as any, // driverRepository
{} as any, // logger
// Presenters
{} as any, // driversLeaderboardPresenter
{} as any, // driverStatsPresenter
{} as any, // completeOnboardingPresenter
{} as any, // driverRegistrationStatusPresenter
{} as any, // driverPresenter
{} as any, // driverProfilePresenter
);
});
describe('getDriversLeaderboard', () => {
it('should call GetDriversLeaderboardUseCase and return the view model', async () => {
const mockViewModel = {
drivers: [
{
id: 'driver-1',
name: 'Driver 1',
rating: 2500,
skillLevel: 'Pro',
nationality: 'DE',
racesCompleted: 50,
wins: 10,
podiums: 20,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar1.png',
},
],
totalRaces: 50,
totalWins: 10,
activeCount: 1,
};
const businessResult = {
items: mockViewModel.drivers.map(dto => ({
driver: { id: dto.id, name: dto.name, country: dto.nationality },
rating: dto.rating,
skillLevel: dto.skillLevel,
racesCompleted: dto.racesCompleted,
wins: dto.wins,
podiums: dto.podiums,
isActive: dto.isActive,
rank: dto.rank,
avatarUrl: dto.avatarUrl,
})),
totalRaces: mockViewModel.totalRaces,
totalWins: mockViewModel.totalWins,
activeCount: mockViewModel.activeCount,
};
getDriversLeaderboardUseCase.execute.mockResolvedValue(Result.ok(businessResult as unknown as GetDriversLeaderboardResult));
const result = await service.getDriversLeaderboard();
expect(getDriversLeaderboardUseCase.execute).toHaveBeenCalledWith({});
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching drivers leaderboard.');
expect(result).toEqual(mockViewModel);
});
});
describe('getTotalDrivers', () => {
it('should call GetTotalDriversUseCase and return the view model', async () => {
const mockOutput = { totalDrivers: 5 };
getTotalDriversUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
const result = await service.getTotalDrivers();
expect(getTotalDriversUseCase.execute).toHaveBeenCalledWith({});
expect(logger.debug).toHaveBeenCalledWith('[DriverService] Fetching total drivers count.');
expect(result).toEqual(mockOutput);
});
});
describe('completeOnboarding', () => {
it('should call CompleteDriverOnboardingUseCase and return success', async () => {
const input = {
firstName: 'John',
lastName: 'Doe',
displayName: 'John Doe',
country: 'US',
bio: 'Racing enthusiast',
};
completeDriverOnboardingUseCase.execute.mockResolvedValue(
Result.ok({ driver: { id: 'user-123' } as Driver })
);
const result = await service.completeOnboarding('user-123', input);
expect(completeDriverOnboardingUseCase.execute).toHaveBeenCalledWith({
userId: 'user-123',
...input,
});
expect(logger.debug).toHaveBeenCalledWith('Completing onboarding for user:', 'user-123');
expect(result).toEqual({
success: true,
driverId: 'user-123',
});
});
it('should handle error from use case', async () => {
const input = {
firstName: 'John',
lastName: 'Doe',
displayName: 'John Doe',
country: 'US',
bio: 'Racing enthusiast',
};
completeDriverOnboardingUseCase.execute.mockResolvedValue(
Result.err({ code: 'DRIVER_ALREADY_EXISTS', details: { message: 'Driver already exists' } })
);
const result = await service.completeOnboarding('user-123', input);
expect(result).toEqual({
success: false,
errorMessage: 'DRIVER_ALREADY_EXISTS',
});
});
});
describe('getDriverRegistrationStatus', () => {
it('should call IsDriverRegisteredForRaceUseCase and return the view model', async () => {
const query = {
driverId: 'driver-1',
raceId: 'race-1',
};
const mockOutput = {
isRegistered: true,
raceId: 'race-1',
driverId: 'driver-1',
};
isDriverRegisteredForRaceUseCase.execute.mockResolvedValue(Result.ok(mockOutput));
const result = await service.getDriverRegistrationStatus(query);
expect(isDriverRegisteredForRaceUseCase.execute).toHaveBeenCalledWith(query);
expect(logger.debug).toHaveBeenCalledWith('Checking driver registration status:', query);
expect(result).toEqual(mockOutput);
});
it('should be created', () => {
expect(service).toBeDefined();
});
});

View File

@@ -1,6 +1,4 @@
import { Result } from '@core/shared/application/Result';
import { Inject, Injectable } from '@nestjs/common';
import type { Driver } from '@core/racing/domain/entities/Driver';
import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
@@ -42,13 +40,6 @@ import {
@Injectable()
export class DriverService {
private readonly driversLeaderboardPresenter = new DriversLeaderboardPresenter();
private readonly driverStatsPresenter = new DriverStatsPresenter();
private readonly completeOnboardingPresenter = new CompleteOnboardingPresenter();
private readonly driverRegistrationStatusPresenter = new DriverRegistrationStatusPresenter();
private readonly driverPresenter = new DriverPresenter();
private readonly driverProfilePresenter = new DriverProfilePresenter();
constructor(
@Inject(GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN)
private readonly getDriversLeaderboardUseCase: GetDriversLeaderboardUseCase,
@@ -66,29 +57,26 @@ export class DriverService {
private readonly driverRepository: IDriverRepository, // TODO must be removed from service
@Inject(LOGGER_TOKEN)
private readonly logger: Logger,
// Injected presenters
private readonly driversLeaderboardPresenter: DriversLeaderboardPresenter,
private readonly driverStatsPresenter: DriverStatsPresenter,
private readonly completeOnboardingPresenter: CompleteOnboardingPresenter,
private readonly driverRegistrationStatusPresenter: DriverRegistrationStatusPresenter,
private readonly driverPresenter: DriverPresenter,
private readonly driverProfilePresenter: DriverProfilePresenter,
) {}
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
const result = await this.getDriversLeaderboardUseCase.execute({});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.driversLeaderboardPresenter.present(result as Result<any, any>);
await this.getDriversLeaderboardUseCase.execute({});
return this.driversLeaderboardPresenter.getResponseModel();
}
async getTotalDrivers(): Promise<DriverStatsDTO> {
this.logger.debug('[DriverService] Fetching total drivers count.');
const result = await this.getTotalDriversUseCase.execute({});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load driver stats');
}
this.driverStatsPresenter.present(result.unwrap());
await this.getTotalDriversUseCase.execute({});
return this.driverStatsPresenter.getResponseModel();
}
@@ -98,7 +86,7 @@ export class DriverService {
): Promise<CompleteOnboardingOutputDTO> {
this.logger.debug('Completing onboarding for user:', userId);
const result = await this.completeDriverOnboardingUseCase.execute({
await this.completeDriverOnboardingUseCase.execute({
userId,
firstName: input.firstName,
lastName: input.lastName,
@@ -107,7 +95,6 @@ export class DriverService {
...(input.bio !== undefined ? { bio: input.bio } : {}),
});
this.completeOnboardingPresenter.present(result);
return this.completeOnboardingPresenter.getResponseModel();
}
@@ -116,28 +103,18 @@ export class DriverService {
): Promise<DriverRegistrationStatusDTO> {
this.logger.debug('Checking driver registration status:', query);
const result = await this.isDriverRegisteredForRaceUseCase.execute({
await this.isDriverRegisteredForRaceUseCase.execute({
raceId: query.raceId,
driverId: query.driverId,
});
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to check registration status');
}
this.driverRegistrationStatusPresenter.present(result.unwrap());
return this.driverRegistrationStatusPresenter.getResponseModel();
}
async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> {
this.logger.debug(`[DriverService] Fetching current driver for userId: ${userId}`);
const result = Result.ok(await this.driverRepository.findById(userId));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.driverPresenter.present(result as Result<Driver | null, any>);
await this.driverRepository.findById(userId);
return this.driverPresenter.getResponseModel();
}
@@ -152,39 +129,21 @@ export class DriverService {
if (bio !== undefined) input.bio = bio;
if (country !== undefined) input.country = country;
const result = await this.updateDriverProfileUseCase.execute(input);
if (result.isErr()) {
this.logger.error(`Failed to update driver profile: ${result.unwrapErr().code}`);
this.driverPresenter.present(Result.ok(null));
return this.driverPresenter.getResponseModel();
}
this.driverPresenter.present(Result.ok(result.unwrap()));
await this.updateDriverProfileUseCase.execute(input);
return this.driverPresenter.getResponseModel();
}
async getDriver(driverId: string): Promise<GetDriverOutputDTO | null> {
this.logger.debug(`[DriverService] Fetching driver for driverId: ${driverId}`);
const driver = await this.driverRepository.findById(driverId);
this.driverPresenter.present(Result.ok(driver));
await this.driverRepository.findById(driverId);
return this.driverPresenter.getResponseModel();
}
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`);
const result = await this.getProfileOverviewUseCase.execute({ driverId });
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to load driver profile');
}
this.driverProfilePresenter.present(result.unwrap());
await this.getProfileOverviewUseCase.execute({ driverId });
return this.driverProfilePresenter.getResponseModel();
}
}
}

View File

@@ -13,6 +13,8 @@ import type { IStandingRepository } from '@core/racing/domain/repositories/IStan
import type { Logger } from '@core/shared/application/Logger';
// Import concrete in-memory implementations
import type { ILeagueWalletRepository } from "@core/racing/domain/repositories/ILeagueWalletRepository";
import type { ITransactionRepository } from "@core/racing/domain/repositories/ITransactionRepository";
import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
@@ -63,6 +65,8 @@ import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresen
import { GetSeasonSponsorshipsPresenter } from './presenters/GetSeasonSponsorshipsPresenter';
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
import { GetLeagueWalletPresenter } from './presenters/GetLeagueWalletPresenter';
import { WithdrawFromLeagueWalletPresenter } from './presenters/WithdrawFromLeagueWalletPresenter';
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
@@ -76,7 +80,7 @@ export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
export const LOGGER_TOKEN = 'Logger';
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
@@ -103,6 +107,30 @@ export const GET_LEAGUE_WALLET_USE_CASE = 'GetLeagueWalletUseCase';
export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUseCase';
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
export const LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN = 'ListLeagueScoringPresetsOutputPort_TOKEN';
export const APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN = 'ApproveLeagueJoinRequestOutputPort_TOKEN';
export const CREATE_LEAGUE_OUTPUT_PORT_TOKEN = 'CreateLeagueOutputPort_TOKEN';
export const GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN = 'GetLeagueAdminPermissionsOutputPort_TOKEN';
export const GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN = 'GetLeagueMembershipsOutputPort_TOKEN';
export const GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN = 'GetLeagueOwnerSummaryOutputPort_TOKEN';
export const GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN = 'GetLeagueSeasonsOutputPort_TOKEN';
export const JOIN_LEAGUE_OUTPUT_PORT_TOKEN = 'JoinLeagueOutputPort_TOKEN';
export const GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN = 'GetLeagueScheduleOutputPort_TOKEN';
export const GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN = 'GetLeagueStatsOutputPort_TOKEN';
export const REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN = 'RejectLeagueJoinRequestOutputPort_TOKEN';
export const REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN = 'RemoveLeagueMemberOutputPort_TOKEN';
export const TOTAL_LEAGUES_OUTPUT_PORT_TOKEN = 'TotalLeaguesOutputPort_TOKEN';
export const TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN = 'TransferLeagueOwnershipOutputPort_TOKEN';
export const UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN = 'UpdateLeagueMemberRoleOutputPort_TOKEN';
export const GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN = 'GetLeagueFullConfigOutputPort_TOKEN';
export const GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN = 'GetLeagueScoringConfigOutputPort_TOKEN';
export const GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN = 'GetLeagueWalletOutputPort_TOKEN';
export const WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN = 'WithdrawFromLeagueWalletOutputPort_TOKEN';
export const LeagueProviders: Provider[] = [
LeagueService, // Provide the service itself
{
@@ -174,6 +202,43 @@ export const LeagueProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Presenters
AllLeaguesWithCapacityPresenter,
LeagueStandingsPresenter,
GetLeagueProtestsPresenter,
GetSeasonSponsorshipsPresenter,
LeagueScoringPresetsPresenter,
GetLeagueWalletPresenter,
WithdrawFromLeagueWalletPresenter,
// Output ports
{
provide: GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
useExisting: AllLeaguesWithCapacityPresenter,
},
{
provide: GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
useExisting: LeagueStandingsPresenter,
},
{
provide: GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueProtestsPresenter,
},
{
provide: GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
useExisting: GetSeasonSponsorshipsPresenter,
},
{
provide: LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN,
useExisting: LeagueScoringPresetsPresenter,
},
{
provide: GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
useExisting: GetLeagueWalletPresenter,
},
{
provide: WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
useExisting: WithdrawFromLeagueWalletPresenter,
},
// Use cases
{
provide: GetAllLeaguesWithCapacityUseCase,
@@ -260,11 +325,24 @@ export const LeagueProviders: Provider[] = [
},
{
provide: GET_LEAGUE_WALLET_USE_CASE,
useClass: GetLeagueWalletUseCase,
useFactory: (
leagueRepo: ILeagueRepository,
walletRepo: ILeagueWalletRepository,
transactionRepo: ITransactionRepository,
presenter: GetLeagueWalletPresenter,
) => new GetLeagueWalletUseCase(leagueRepo, walletRepo, transactionRepo, presenter),
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, 'GetLeagueWalletPresenter'],
},
{
provide: WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE,
useClass: WithdrawFromLeagueWalletUseCase,
useFactory: (
leagueRepo: ILeagueRepository,
walletRepo: ILeagueWalletRepository,
transactionRepo: ITransactionRepository,
logger: Logger,
presenter: WithdrawFromLeagueWalletPresenter,
) => new WithdrawFromLeagueWalletUseCase(leagueRepo, walletRepo, transactionRepo, logger, presenter),
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN, LOGGER_TOKEN, 'WithdrawFromLeagueWalletPresenter'],
},
{
provide: GET_SEASON_SPONSORSHIPS_USE_CASE,
@@ -302,4 +380,4 @@ export const LeagueProviders: Provider[] = [
provide: GET_LEAGUE_SCORING_CONFIG_USE_CASE,
useClass: GetLeagueScoringConfigUseCase,
}
];
];

View File

@@ -1,332 +0,0 @@
import { vi, Mocked } from 'vitest';
import { LeagueService } from './LeagueService';
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/RemoveLeagueMemberUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/GetLeagueMembershipsUseCase';
import { GetLeagueScheduleUseCase } from '@core/racing/application/use-cases/GetLeagueScheduleUseCase';
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
import { GetSeasonSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSeasonSponsorshipsUseCase';
import type { Logger } from '@core/shared/application/Logger';
import { Result } from '@core/shared/application/Result';
describe('LeagueService', () => {
let service: LeagueService;
let mockGetTotalLeaguesUseCase: Mocked<GetTotalLeaguesUseCase>;
let mockGetLeagueJoinRequestsUseCase: Mocked<GetLeagueJoinRequestsUseCase>;
let mockApproveLeagueJoinRequestUseCase: Mocked<ApproveLeagueJoinRequestUseCase>;
let mockGetLeagueFullConfigUseCase: Mocked<GetLeagueFullConfigUseCase>;
let mockGetLeagueOwnerSummaryUseCase: Mocked<GetLeagueOwnerSummaryUseCase>;
let mockGetLeagueScheduleUseCase: Mocked<GetLeagueScheduleUseCase>;
let mockGetSeasonSponsorshipsUseCase: Mocked<GetSeasonSponsorshipsUseCase>;
let mockLogger: Mocked<Logger>;
beforeEach(() => {
const createUseCaseMock = <T extends { execute: unknown }>(): Mocked<T> => ({
execute: vi.fn(),
}) as Mocked<T>;
mockGetTotalLeaguesUseCase = createUseCaseMock<GetTotalLeaguesUseCase>();
mockGetLeagueJoinRequestsUseCase = createUseCaseMock<GetLeagueJoinRequestsUseCase>();
mockApproveLeagueJoinRequestUseCase = createUseCaseMock<ApproveLeagueJoinRequestUseCase>();
mockGetLeagueFullConfigUseCase = createUseCaseMock<GetLeagueFullConfigUseCase>();
mockGetLeagueOwnerSummaryUseCase = createUseCaseMock<GetLeagueOwnerSummaryUseCase>();
mockGetLeagueScheduleUseCase = createUseCaseMock<GetLeagueScheduleUseCase>();
mockGetSeasonSponsorshipsUseCase = createUseCaseMock<GetSeasonSponsorshipsUseCase>();
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as Mocked<Logger>;
service = new LeagueService(
{} as unknown as GetAllLeaguesWithCapacityUseCase,
{} as unknown as GetLeagueStandingsUseCase,
{} as unknown as GetLeagueStatsUseCase,
mockGetLeagueFullConfigUseCase,
{} as unknown as GetLeagueScoringConfigUseCase,
{} as unknown as ListLeagueScoringPresetsUseCase,
{} as unknown as JoinLeagueUseCase,
{} as unknown as TransferLeagueOwnershipUseCase,
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
{} as unknown as GetRaceProtestsUseCase,
mockGetTotalLeaguesUseCase,
mockGetLeagueJoinRequestsUseCase,
mockApproveLeagueJoinRequestUseCase,
{} as unknown as RejectLeagueJoinRequestUseCase,
{} as unknown as RemoveLeagueMemberUseCase,
{} as unknown as UpdateLeagueMemberRoleUseCase,
mockGetLeagueOwnerSummaryUseCase,
{} as unknown as GetLeagueProtestsUseCase,
{} as unknown as GetLeagueSeasonsUseCase,
{} as unknown as GetLeagueMembershipsUseCase,
mockGetLeagueScheduleUseCase,
{} as unknown as GetLeagueAdminPermissionsUseCase,
{} as unknown as GetLeagueWalletUseCase,
{} as unknown as WithdrawFromLeagueWalletUseCase,
mockGetSeasonSponsorshipsUseCase,
mockLogger,
);
});
it('should get total leagues', async () => {
mockGetTotalLeaguesUseCase.execute.mockResolvedValue(Result.ok({ totalLeagues: 5 }));
const result = await service.getTotalLeagues();
expect(result).toEqual({ totalLeagues: 5 });
expect(mockLogger.debug).toHaveBeenCalledWith('[LeagueService] Fetching total leagues count.');
});
it('should get league join requests', async () => {
mockGetLeagueJoinRequestsUseCase.execute.mockImplementation(async (_params, presenter) => {
presenter.present({
joinRequests: [{ id: 'req-1', leagueId: 'league-1', driverId: 'driver-1', requestedAt: new Date(), message: 'msg' }],
drivers: [{ id: 'driver-1', name: 'Driver 1' }],
});
});
const result = await service.getLeagueJoinRequests('league-1');
expect(result).toEqual([
{
id: 'req-1',
leagueId: 'league-1',
driverId: 'driver-1',
requestedAt: expect.any(Date),
message: 'msg',
driver: { id: 'driver-1', name: 'Driver 1' },
},
]);
});
it('should approve league join request', async () => {
mockApproveLeagueJoinRequestUseCase.execute.mockImplementation(async (_params, presenter) => {
presenter.present({ success: true, message: 'Join request approved.' });
});
const result = await service.approveLeagueJoinRequest({ leagueId: 'league-1', requestId: 'req-1' });
expect(result).toEqual({ success: true, message: 'Join request approved.' });
});
it('should reject league join request', async () => {
const mockRejectUseCase: Mocked<RejectLeagueJoinRequestUseCase> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute: vi.fn() as any,
} as unknown as Mocked<RejectLeagueJoinRequestUseCase>;
service = new LeagueService(
{} as unknown as GetAllLeaguesWithCapacityUseCase,
{} as unknown as GetLeagueStandingsUseCase,
{} as unknown as GetLeagueStatsUseCase,
mockGetLeagueFullConfigUseCase,
{} as unknown as GetLeagueScoringConfigUseCase,
{} as unknown as ListLeagueScoringPresetsUseCase,
{} as unknown as JoinLeagueUseCase,
{} as unknown as TransferLeagueOwnershipUseCase,
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
{} as unknown as GetRaceProtestsUseCase,
mockGetTotalLeaguesUseCase,
mockGetLeagueJoinRequestsUseCase,
mockApproveLeagueJoinRequestUseCase,
mockRejectUseCase,
{} as unknown as RemoveLeagueMemberUseCase,
{} as unknown as UpdateLeagueMemberRoleUseCase,
mockGetLeagueOwnerSummaryUseCase,
{} as unknown as GetLeagueProtestsUseCase,
{} as unknown as GetLeagueSeasonsUseCase,
{} as unknown as GetLeagueMembershipsUseCase,
{} as unknown as GetLeagueScheduleUseCase,
{} as unknown as GetLeagueAdminPermissionsUseCase,
{} as unknown as GetLeagueWalletUseCase,
{} as unknown as WithdrawFromLeagueWalletUseCase,
mockLogger,
);
mockRejectUseCase.execute.mockImplementation(async (_params, presenter) => {
presenter.present({ success: true, message: 'Join request rejected.' });
});
const result = await service.rejectLeagueJoinRequest({ requestId: 'req-1', leagueId: 'league-1' });
expect(result).toEqual({ success: true, message: 'Join request rejected.' });
});
it('should remove league member', async () => {
const mockRemoveUseCase: Mocked<RemoveLeagueMemberUseCase> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute: vi.fn() as any,
} as unknown as Mocked<RemoveLeagueMemberUseCase>;
service = new LeagueService(
{} as unknown as GetAllLeaguesWithCapacityUseCase,
{} as unknown as GetLeagueStandingsUseCase,
{} as unknown as GetLeagueStatsUseCase,
mockGetLeagueFullConfigUseCase,
{} as unknown as GetLeagueScoringConfigUseCase,
{} as unknown as ListLeagueScoringPresetsUseCase,
{} as unknown as JoinLeagueUseCase,
{} as unknown as TransferLeagueOwnershipUseCase,
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
{} as unknown as GetRaceProtestsUseCase,
mockGetTotalLeaguesUseCase,
mockGetLeagueJoinRequestsUseCase,
mockApproveLeagueJoinRequestUseCase,
{} as unknown as RejectLeagueJoinRequestUseCase,
mockRemoveUseCase,
{} as unknown as UpdateLeagueMemberRoleUseCase,
mockGetLeagueOwnerSummaryUseCase,
{} as unknown as GetLeagueProtestsUseCase,
{} as unknown as GetLeagueSeasonsUseCase,
{} as unknown as GetLeagueMembershipsUseCase,
{} as unknown as GetLeagueScheduleUseCase,
{} as unknown as GetLeagueAdminPermissionsUseCase,
{} as unknown as GetLeagueWalletUseCase,
{} as unknown as WithdrawFromLeagueWalletUseCase,
mockLogger,
);
mockRemoveUseCase.execute.mockImplementation(async (_params, presenter) => {
presenter.present({ success: true });
});
const result = await service.removeLeagueMember({ leagueId: 'league-1', performerDriverId: 'performer-1', targetDriverId: 'driver-1' });
expect(result).toEqual({ success: true });
});
it('should aggregate league admin data via composite use case', async () => {
const fullConfig = {
league: {
id: 'league-1',
name: 'Test League',
description: 'Test',
ownerId: 'owner-1',
settings: { pointsSystem: 'custom' },
},
} as any;
mockGetLeagueFullConfigUseCase.execute.mockResolvedValue(Result.ok(fullConfig));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockGetLeagueOwnerSummaryUseCase.execute.mockResolvedValue(Result.ok({ summary: null } as any));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const joinRequestsSpy = vi
.spyOn(service, 'getLeagueJoinRequests')
.mockResolvedValue({ joinRequests: [] } as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const protestsSpy = vi
.spyOn(service, 'getLeagueProtests')
.mockResolvedValue({ protests: [], racesById: {}, driversById: {} } as any);
const seasonsSpy = vi
.spyOn(service, 'getLeagueSeasons')
.mockResolvedValue([]);
const result = await service.getLeagueAdmin('league-1');
expect(mockGetLeagueFullConfigUseCase.execute).toHaveBeenCalledWith({ leagueId: 'league-1' });
expect(mockGetLeagueOwnerSummaryUseCase.execute).toHaveBeenCalledWith({ ownerId: 'owner-1' });
expect(joinRequestsSpy).toHaveBeenCalledWith('league-1');
expect(protestsSpy).toHaveBeenCalledWith({ leagueId: 'league-1' });
expect(seasonsSpy).toHaveBeenCalledWith({ leagueId: 'league-1' });
expect(result.config.form?.leagueId).toBe('league-1');
});
it('should get season sponsorships', async () => {
const sponsorship = {
id: 's-1',
leagueId: 'league-1',
leagueName: 'League 1',
seasonId: 'season-123',
seasonName: 'Season 1',
tier: 'gold',
status: 'active',
pricing: {
amount: 1000,
currency: 'USD',
},
platformFee: {
amount: 100,
currency: 'USD',
},
netAmount: {
amount: 900,
currency: 'USD',
},
metrics: {
drivers: 10,
races: 5,
completedRaces: 3,
impressions: 3000,
},
createdAt: new Date('2024-01-01T00:00:00.000Z'),
} as any;
mockGetSeasonSponsorshipsUseCase.execute.mockResolvedValue(
Result.ok({
seasonId: 'season-123',
sponsorships: [sponsorship],
}),
);
const result = await service.getSeasonSponsorships('season-123');
expect(mockGetSeasonSponsorshipsUseCase.execute).toHaveBeenCalledWith({ seasonId: 'season-123' });
expect(result.sponsorships).toHaveLength(1);
expect(result.sponsorships[0]).toMatchObject({
id: 's-1',
leagueId: 'league-1',
leagueName: 'League 1',
seasonId: 'season-123',
seasonName: 'Season 1',
tier: 'gold',
});
});
it('should get races for league', async () => {
const scheduledAt = new Date('2024-02-01T12:00:00.000Z');
mockGetLeagueScheduleUseCase.execute.mockResolvedValue(
Result.ok({
races: [
{
id: 'race-1',
name: 'Race 1',
scheduledAt,
},
],
}),
);
const result = await service.getRaces('league-123');
expect(mockGetLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'league-123' });
expect(result.races).toHaveLength(1);
expect(result.races[0]).toMatchObject({
id: 'race-1',
name: 'Race 1',
date: scheduledAt.toISOString(),
leagueName: undefined,
});
});
});

View File

@@ -40,7 +40,7 @@ import type { LeagueScoringConfigViewModel } from './presenters/LeagueScoringCon
import type { LeagueScoringPresetsViewModel } from './presenters/LeagueScoringPresetsPresenter';
// Core imports
import type { Logger } from '@core/shared/application/Logger';
import type { Logger } from '@core/shared/application';
// Use cases
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
@@ -58,7 +58,6 @@ import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetL
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetSeasonSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSeasonSponsorshipsUseCase';
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
@@ -80,10 +79,9 @@ import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresen
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
import { GetSeasonSponsorshipsPresenter } from './presenters/GetSeasonSponsorshipsPresenter';
import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter';
import { LeagueAdminPresenter } from './presenters/LeagueAdminPresenter';
import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter';
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter';
import { LeagueSchedulePresenter, LeagueRacesPresenter } from './presenters/LeagueSchedulePresenter';
import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter';
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
@@ -93,8 +91,59 @@ import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPres
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter';
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
import { GetLeagueWalletPresenter } from './presenters/GetLeagueWalletPresenter';
import { WithdrawFromLeagueWalletPresenter } from './presenters/WithdrawFromLeagueWalletPresenter';
// Tokens
import { LOGGER_TOKEN, GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE, GET_LEAGUE_STANDINGS_USE_CASE, GET_LEAGUE_STATS_USE_CASE, GET_LEAGUE_FULL_CONFIG_USE_CASE, GET_LEAGUE_SCORING_CONFIG_USE_CASE, LIST_LEAGUE_SCORING_PRESETS_USE_CASE, JOIN_LEAGUE_USE_CASE, TRANSFER_LEAGUE_OWNERSHIP_USE_CASE, CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE, GET_RACE_PROTESTS_USE_CASE, GET_TOTAL_LEAGUES_USE_CASE, GET_LEAGUE_JOIN_REQUESTS_USE_CASE, APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE, REJECT_LEAGUE_JOIN_REQUEST_USE_CASE, REMOVE_LEAGUE_MEMBER_USE_CASE, UPDATE_LEAGUE_MEMBER_ROLE_USE_CASE, GET_LEAGUE_OWNER_SUMMARY_USE_CASE, GET_LEAGUE_PROTESTS_USE_CASE, GET_LEAGUE_SEASONS_USE_CASE, GET_LEAGUE_MEMBERSHIPS_USE_CASE, GET_LEAGUE_SCHEDULE_USE_CASE, GET_LEAGUE_ADMIN_PERMISSIONS_USE_CASE, GET_LEAGUE_WALLET_USE_CASE, WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE, GET_SEASON_SPONSORSHIPS_USE_CASE } from './LeagueProviders';
import {
LOGGER_TOKEN,
GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE,
GET_LEAGUE_STANDINGS_USE_CASE,
GET_LEAGUE_STATS_USE_CASE,
GET_LEAGUE_FULL_CONFIG_USE_CASE,
GET_LEAGUE_SCORING_CONFIG_USE_CASE,
LIST_LEAGUE_SCORING_PRESETS_USE_CASE,
JOIN_LEAGUE_USE_CASE,
TRANSFER_LEAGUE_OWNERSHIP_USE_CASE,
CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE,
GET_TOTAL_LEAGUES_USE_CASE,
GET_LEAGUE_JOIN_REQUESTS_USE_CASE,
APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE,
REJECT_LEAGUE_JOIN_REQUEST_USE_CASE,
REMOVE_LEAGUE_MEMBER_USE_CASE,
UPDATE_LEAGUE_MEMBER_ROLE_USE_CASE,
GET_LEAGUE_OWNER_SUMMARY_USE_CASE,
GET_LEAGUE_PROTESTS_USE_CASE,
GET_LEAGUE_SEASONS_USE_CASE,
GET_LEAGUE_MEMBERSHIPS_USE_CASE,
GET_LEAGUE_SCHEDULE_USE_CASE,
GET_LEAGUE_ADMIN_PERMISSIONS_USE_CASE,
GET_LEAGUE_WALLET_USE_CASE,
WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE,
GET_SEASON_SPONSORSHIPS_USE_CASE,
GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN,
APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN,
CREATE_LEAGUE_OUTPUT_PORT_TOKEN,
GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN,
GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN,
GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN,
JOIN_LEAGUE_OUTPUT_PORT_TOKEN,
GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN,
GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN,
REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN,
REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN,
TOTAL_LEAGUES_OUTPUT_PORT_TOKEN,
TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN,
UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN,
GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN,
GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN,
GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN,
} from './LeagueProviders';
@Injectable()
export class LeagueService {
@@ -108,7 +157,6 @@ export class LeagueService {
@Inject(JOIN_LEAGUE_USE_CASE) private readonly joinLeagueUseCase: JoinLeagueUseCase,
@Inject(TRANSFER_LEAGUE_OWNERSHIP_USE_CASE) private readonly transferLeagueOwnershipUseCase: TransferLeagueOwnershipUseCase,
@Inject(CREATE_LEAGUE_WITH_SEASON_AND_SCORING_USE_CASE) private readonly createLeagueWithSeasonAndScoringUseCase: CreateLeagueWithSeasonAndScoringUseCase,
@Inject(GET_RACE_PROTESTS_USE_CASE) private readonly getRaceProtestsUseCase: GetRaceProtestsUseCase,
@Inject(GET_TOTAL_LEAGUES_USE_CASE) private readonly getTotalLeaguesUseCase: GetTotalLeaguesUseCase,
@Inject(GET_LEAGUE_JOIN_REQUESTS_USE_CASE) private readonly getLeagueJoinRequestsUseCase: GetLeagueJoinRequestsUseCase,
@Inject(APPROVE_LEAGUE_JOIN_REQUEST_USE_CASE) private readonly approveLeagueJoinRequestUseCase: ApproveLeagueJoinRequestUseCase,
@@ -125,120 +173,99 @@ export class LeagueService {
@Inject(WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE) private readonly withdrawFromLeagueWalletUseCase: WithdrawFromLeagueWalletUseCase,
@Inject(GET_SEASON_SPONSORSHIPS_USE_CASE) private readonly getSeasonSponsorshipsUseCase: GetSeasonSponsorshipsUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
// Injected presenters
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityPresenter: AllLeaguesWithCapacityPresenter,
@Inject(GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN) private readonly leagueStandingsPresenter: LeagueStandingsPresenter,
@Inject(GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN) private readonly leagueProtestsPresenter: GetLeagueProtestsPresenter,
@Inject(GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN) private readonly seasonSponsorshipsPresenter: GetSeasonSponsorshipsPresenter,
@Inject(LIST_LEAGUE_SCORING_PRESETS_OUTPUT_PORT_TOKEN) private readonly leagueScoringPresetsPresenter: LeagueScoringPresetsPresenter,
@Inject(APPROVE_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN) private readonly approveLeagueJoinRequestPresenter: ApproveLeagueJoinRequestPresenter,
@Inject(CREATE_LEAGUE_OUTPUT_PORT_TOKEN) private readonly createLeaguePresenter: CreateLeaguePresenter,
@Inject(GET_LEAGUE_ADMIN_PERMISSIONS_OUTPUT_PORT_TOKEN) private readonly getLeagueAdminPermissionsPresenter: GetLeagueAdminPermissionsPresenter,
@Inject(GET_LEAGUE_MEMBERSHIPS_OUTPUT_PORT_TOKEN) private readonly getLeagueMembershipsPresenter: GetLeagueMembershipsPresenter,
@Inject(GET_LEAGUE_OWNER_SUMMARY_OUTPUT_PORT_TOKEN) private readonly getLeagueOwnerSummaryPresenter: GetLeagueOwnerSummaryPresenter,
@Inject(GET_LEAGUE_SEASONS_OUTPUT_PORT_TOKEN) private readonly getLeagueSeasonsPresenter: GetLeagueSeasonsPresenter,
@Inject(JOIN_LEAGUE_OUTPUT_PORT_TOKEN) private readonly joinLeaguePresenter: JoinLeaguePresenter,
@Inject(GET_LEAGUE_SCHEDULE_OUTPUT_PORT_TOKEN) private readonly leagueSchedulePresenter: LeagueSchedulePresenter,
@Inject(GET_LEAGUE_STATS_OUTPUT_PORT_TOKEN) private readonly leagueStatsPresenter: LeagueStatsPresenter,
@Inject(REJECT_LEAGUE_JOIN_REQUEST_OUTPUT_PORT_TOKEN) private readonly rejectLeagueJoinRequestPresenter: RejectLeagueJoinRequestPresenter,
@Inject(REMOVE_LEAGUE_MEMBER_OUTPUT_PORT_TOKEN) private readonly removeLeagueMemberPresenter: RemoveLeagueMemberPresenter,
@Inject(TOTAL_LEAGUES_OUTPUT_PORT_TOKEN) private readonly totalLeaguesPresenter: TotalLeaguesPresenter,
@Inject(TRANSFER_LEAGUE_OWNERSHIP_OUTPUT_PORT_TOKEN) private readonly transferLeagueOwnershipPresenter: TransferLeagueOwnershipPresenter,
@Inject(UPDATE_LEAGUE_MEMBER_ROLE_OUTPUT_PORT_TOKEN) private readonly updateLeagueMemberRolePresenter: UpdateLeagueMemberRolePresenter,
@Inject(GET_LEAGUE_FULL_CONFIG_OUTPUT_PORT_TOKEN) private readonly leagueConfigPresenter: LeagueConfigPresenter,
@Inject(GET_LEAGUE_SCORING_CONFIG_OUTPUT_PORT_TOKEN) private readonly leagueScoringConfigPresenter: LeagueScoringConfigPresenter,
@Inject(GET_LEAGUE_WALLET_OUTPUT_PORT_TOKEN) private readonly getLeagueWalletPresenter: GetLeagueWalletPresenter,
@Inject(WITHDRAW_FROM_LEAGUE_WALLET_OUTPUT_PORT_TOKEN) private readonly withdrawFromLeagueWalletPresenter: WithdrawFromLeagueWalletPresenter,
@Inject(GET_LEAGUE_JOIN_REQUESTS_USE_CASE) private readonly leagueJoinRequestsPresenter: LeagueJoinRequestsPresenter,
@Inject(GET_LEAGUE_SCHEDULE_USE_CASE) private readonly leagueRacesPresenter: LeagueRacesPresenter,
) {}
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
this.logger.debug('[LeagueService] Fetching all leagues with capacity.');
const result = await this.getAllLeaguesWithCapacityUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return (this.getAllLeaguesWithCapacityUseCase.outputPort as AllLeaguesWithCapacityPresenter).getViewModel();
await this.getAllLeaguesWithCapacityUseCase.execute();
return this.allLeaguesWithCapacityPresenter.getViewModel();
}
async getTotalLeagues(): Promise<TotalLeaguesDTO> {
this.logger.debug('[LeagueService] Fetching total leagues count.');
const result = await this.getTotalLeaguesUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new TotalLeaguesPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel()!;
await this.getTotalLeaguesUseCase.execute({});
return this.totalLeaguesPresenter.getResponseModel()!;
}
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestWithDriverDTO[]> {
this.logger.debug(`[LeagueService] Fetching join requests for league: ${leagueId}.`);
const presenter = new LeagueJoinRequestsPresenter();
await this.getLeagueJoinRequestsUseCase.execute({ leagueId }, presenter);
return presenter.getViewModel()!.joinRequests;
await this.getLeagueJoinRequestsUseCase.execute({ leagueId });
return this.leagueJoinRequestsPresenter.getViewModel()!.joinRequests;
}
async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestDTO> {
this.logger.debug('Approving join request:', input);
const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new ApproveLeagueJoinRequestPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel()!;
await this.approveLeagueJoinRequestUseCase.execute(input, this.approveLeagueJoinRequestPresenter);
return this.approveLeagueJoinRequestPresenter.getViewModel()!;
}
async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectJoinRequestOutputDTO> {
this.logger.debug('Rejecting join request:', input);
const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId });
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.code,
};
}
const presenter = new RejectLeagueJoinRequestPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel()!;
await this.rejectLeagueJoinRequestUseCase.execute({
leagueId: input.leagueId,
adminId: 'admin', // This should come from auth context
requestId: input.requestId
});
return this.rejectLeagueJoinRequestPresenter.getViewModel()!;
}
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise<LeagueAdminPermissionsDTO> {
this.logger.debug('Getting league admin permissions', { query });
const result = await this.getLeagueAdminPermissionsUseCase.execute({ leagueId: query.leagueId, performerDriverId: query.performerDriverId });
// This use case never errors
const presenter = new GetLeagueAdminPermissionsPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel()!;
await this.getLeagueAdminPermissionsUseCase.execute(query);
return this.getLeagueAdminPermissionsPresenter.getResponseModel()!;
}
async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise<RemoveLeagueMemberOutputDTO> {
this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId });
const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId });
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.code,
};
}
const presenter = new RemoveLeagueMemberPresenter();
presenter.present(result.unwrap());
return presenter.getViewModel()!;
await this.removeLeagueMemberUseCase.execute(input);
return this.removeLeagueMemberPresenter.getViewModel()!;
}
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise<UpdateLeagueMemberRoleOutputDTO> {
this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.code,
};
}
const presenter = new UpdateLeagueMemberRolePresenter();
presenter.present(result.unwrap());
return presenter.getViewModel()!;
await this.updateLeagueMemberRoleUseCase.execute(input);
return this.updateLeagueMemberRolePresenter.getViewModel()!;
}
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<LeagueOwnerSummaryDTO> {
this.logger.debug('Getting league owner summary:', query);
const presenter = new GetLeagueOwnerSummaryPresenter();
await this.getLeagueOwnerSummaryUseCase.execute({ leagueId: query.leagueId } as any, presenter);
return presenter.getViewModel()!;
await this.getLeagueOwnerSummaryUseCase.execute(query);
return this.getLeagueOwnerSummaryPresenter.getViewModel()!;
}
async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormModelDTO | null> {
this.logger.debug('Getting league full config', { query });
try {
const result = await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId });
if (result.isErr()) {
this.logger.error('Error getting league full config', new Error(result.unwrapErr().code));
return null;
}
const presenter = new LeagueConfigPresenter();
presenter.present(result.unwrap() as any);
return presenter.getViewModel();
await this.getLeagueFullConfigUseCase.execute(query);
return this.leagueConfigPresenter.getViewModel();
} catch (error) {
this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error)));
return null;
@@ -247,71 +274,44 @@ export class LeagueService {
async getLeagueProtests(query: GetLeagueProtestsQueryDTO): Promise<LeagueAdminProtestsDTO> {
this.logger.debug('Getting league protests:', query);
const result = await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return (this.getLeagueProtestsUseCase.outputPort as GetLeagueProtestsPresenter).getResponseModel()!;
await this.getLeagueProtestsUseCase.execute(query);
return this.leagueProtestsPresenter.getResponseModel()!;
}
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
this.logger.debug('Getting league seasons:', query);
const result = await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return (this.getLeagueSeasonsUseCase.output as GetLeagueSeasonsPresenter).getResponseModel()!;
await this.getLeagueSeasonsUseCase.execute(query);
return this.getLeagueSeasonsPresenter.getResponseModel()!;
}
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
this.logger.debug('Getting league memberships', { leagueId });
const presenter = new GetLeagueMembershipsPresenter();
await this.getLeagueMembershipsUseCase.execute({ leagueId }, presenter);
return presenter.getViewModel()!.memberships;
await this.getLeagueMembershipsUseCase.execute({ leagueId });
return this.getLeagueMembershipsPresenter.getViewModel()!.memberships;
}
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsDTO> {
this.logger.debug('Getting league standings', { leagueId });
const result = await this.getLeagueStandingsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return (this.getLeagueStandingsUseCase.outputPort as LeagueStandingsPresenter).getResponseModel()!;
await this.getLeagueStandingsUseCase.execute({ leagueId });
return this.leagueStandingsPresenter.getResponseModel()!;
}
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
this.logger.debug('Getting league schedule', { leagueId });
const [scheduleResult, leagueConfigResult] = await Promise.all([
this.getLeagueScheduleUseCase.execute({ leagueId }),
this.getLeagueFullConfigUseCase.execute({ leagueId }),
]);
if (scheduleResult.isErr()) {
throw new Error(scheduleResult.unwrapErr().code);
}
const leagueName = leagueConfigResult.isOk()
? leagueConfigResult.unwrap().league.name.toString()
: undefined;
const presenter = new LeagueSchedulePresenter();
presenter.present(scheduleResult.unwrap(), leagueName);
return presenter.getViewModel()!;
await this.getLeagueScheduleUseCase.execute({ leagueId });
return this.leagueSchedulePresenter.getViewModel()!;
}
async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> {
this.logger.debug('Getting league stats', { leagueId });
const result = await this.getLeagueStatsUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
await this.getLeagueStatsUseCase.execute({ leagueId });
return this.leagueStatsPresenter.getResponseModel()!;
}
private async getLeagueAdminComposite(leagueId: string): Promise<LeagueAdminDTO> {
this.logger.debug('Fetching composite league admin data', { leagueId });
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
this.logger.debug('Getting league admin data', { leagueId });
const [fullConfigResult, joinRequests, protests, seasons] = await Promise.all([
this.getLeagueFullConfigUseCase.execute({ leagueId }),
this.getLeagueJoinRequests(leagueId),
@@ -323,37 +323,19 @@ export class LeagueService {
throw new Error(fullConfigResult.unwrapErr().code);
}
const fullConfig = fullConfigResult.unwrap();
const league = fullConfig.league;
await this.getLeagueOwnerSummaryUseCase.execute({ leagueId });
const ownerSummary = this.getLeagueOwnerSummaryPresenter.getViewModel()!;
const ownerSummaryResult = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: league.ownerId.toString() });
if (ownerSummaryResult.isErr()) {
throw new Error(ownerSummaryResult.unwrapErr().code);
}
const configForm = this.leagueConfigPresenter.getViewModel();
const ownerSummaryPresenter = new GetLeagueOwnerSummaryPresenter();
ownerSummaryPresenter.present(ownerSummaryResult.unwrap());
const ownerSummary = ownerSummaryPresenter.getViewModel()!;
const configPresenter = new LeagueConfigPresenter();
configPresenter.present(fullConfig);
const configForm = configPresenter.getViewModel();
const adminPresenter = new LeagueAdminPresenter();
adminPresenter.present({
// For now, return a simple structure since we don't have a LeagueAdminPresenter
return {
joinRequests: joinRequests,
ownerSummary,
config: configForm,
config: { form: configForm },
protests,
seasons,
});
return adminPresenter.getViewModel();
}
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
this.logger.debug('Getting league admin data', { leagueId });
return this.getLeagueAdminComposite(leagueId);
};
}
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
@@ -370,10 +352,7 @@ export class LeagueService {
enableNationsChampionship: false,
enableTrophyChampionship: false,
};
const result = await this.createLeagueWithSeasonAndScoringUseCase.execute(command);
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
await this.createLeagueWithSeasonAndScoringUseCase.execute(command);
return this.createLeaguePresenter.getViewModel()!;
}
@@ -381,11 +360,7 @@ export class LeagueService {
this.logger.debug('Getting league scoring config', { leagueId });
try {
const result = await this.getLeagueScoringConfigUseCase.execute({ leagueId });
if (result.isErr()) {
this.logger.error('Error getting league scoring config', new Error(result.unwrapErr().code));
return null;
}
await this.getLeagueScoringConfigUseCase.execute({ leagueId });
return this.leagueScoringConfigPresenter.getViewModel();
} catch (error) {
this.logger.error('Error getting league scoring config', error instanceof Error ? error : new Error(String(error)));
@@ -396,61 +371,35 @@ export class LeagueService {
async listLeagueScoringPresets(): Promise<LeagueScoringPresetsViewModel> {
this.logger.debug('Listing league scoring presets');
const result = await this.listLeagueScoringPresetsUseCase.execute({});
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
await this.listLeagueScoringPresetsUseCase.execute({});
return this.leagueScoringPresetsPresenter.getViewModel()!;
}
async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueOutputDTO> {
this.logger.debug('Joining league', { leagueId, driverId });
const result = await this.joinLeagueUseCase.execute({ leagueId, driverId });
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.code,
};
}
await this.joinLeagueUseCase.execute({ leagueId, driverId });
return this.joinLeaguePresenter.getViewModel()!;
}
async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipOutputDTO> {
this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId });
const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
if (result.isErr()) {
const error = result.unwrapErr();
return {
success: false,
error: error.code,
};
}
await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
return this.transferLeagueOwnershipPresenter.getViewModel()!;
}
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
this.logger.debug('Getting season sponsorships', { seasonId });
const result = await this.getSeasonSponsorshipsUseCase.execute({ seasonId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return this.getSeasonSponsorshipsPresenter.getViewModel()!;
await this.getSeasonSponsorshipsUseCase.execute({ seasonId });
return this.seasonSponsorshipsPresenter.getViewModel()!;
}
async getRaces(leagueId: string): Promise<GetLeagueRacesOutputDTO> {
this.logger.debug('Getting league races', { leagueId });
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
await this.getLeagueScheduleUseCase.execute({ leagueId });
return {
races: this.leagueRacesPresenter.getViewModel()!,
};
@@ -458,26 +407,19 @@ export class LeagueService {
async getLeagueWallet(leagueId: string): Promise<GetLeagueWalletOutputDTO> {
this.logger.debug('Getting league wallet', { leagueId });
const result = await this.getLeagueWalletUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return result.unwrap() as GetLeagueWalletOutputDTO;
await this.getLeagueWalletUseCase.execute({ leagueId });
return this.getLeagueWalletPresenter.getResponseModel();
}
async withdrawFromLeagueWallet(leagueId: string, input: WithdrawFromLeagueWalletInputDTO): Promise<WithdrawFromLeagueWalletOutputDTO> {
this.logger.debug('Withdrawing from league wallet', { leagueId, amount: input.amount });
const result = await this.withdrawFromLeagueWalletUseCase.execute({
await this.withdrawFromLeagueWalletUseCase.execute({
leagueId,
requestedById: "admin",
amount: input.amount,
currency: input.currency as 'USD' | 'EUR' | 'GBP',
seasonId: input.seasonId,
destinationAccount: input.destinationAccount,
reason: input.destinationAccount,
});
if (result.isErr()) {
const error = result.unwrapErr();
return { success: false, message: error.code };
}
return result.unwrap() as WithdrawFromLeagueWalletOutputDTO;
return this.withdrawFromLeagueWalletPresenter.getResponseModel();
}
}
}

View File

@@ -0,0 +1,41 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { GetLeagueWalletResult } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
import { GetLeagueWalletOutputDTO, WalletTransactionDTO } from '../dtos/GetLeagueWalletOutputDTO';
export class GetLeagueWalletPresenter implements UseCaseOutputPort<GetLeagueWalletResult> {
private model: GetLeagueWalletOutputDTO | null = null;
present(result: GetLeagueWalletResult): void {
const transactions: WalletTransactionDTO[] = result.transactions.map(tx => ({
id: tx.id.toString(),
type: tx.type as 'sponsorship' | 'membership' | 'withdrawal' | 'prize',
description: tx.description ?? '',
amount: tx.amount.amount,
fee: tx.platformFee.amount,
netAmount: tx.netAmount.amount,
date: tx.createdAt.toISOString(),
status: tx.status === 'cancelled' ? 'failed' : tx.status,
}));
this.model = {
balance: result.aggregates.balance.amount,
currency: result.aggregates.balance.currency,
totalRevenue: result.aggregates.totalRevenue.amount,
totalFees: result.aggregates.totalFees.amount,
totalWithdrawals: result.aggregates.totalWithdrawals.amount,
pendingPayouts: result.aggregates.pendingPayouts.amount,
canWithdraw: result.wallet.canWithdraw(result.aggregates.balance),
transactions,
};
}
getResponseModel(): GetLeagueWalletOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model;
}
get responseModel(): GetLeagueWalletOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model;
}
}

View File

@@ -0,0 +1,24 @@
import type { UseCaseOutputPort } from '@core/shared/application';
import type { WithdrawFromLeagueWalletResult } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
import { WithdrawFromLeagueWalletOutputDTO } from '../dtos/WithdrawFromLeagueWalletOutputDTO';
export class WithdrawFromLeagueWalletPresenter implements UseCaseOutputPort<WithdrawFromLeagueWalletResult> {
private model: WithdrawFromLeagueWalletOutputDTO | null = null;
present(result: WithdrawFromLeagueWalletResult): void {
this.model = {
success: true,
message: `Successfully withdrew ${result.amount.amount} ${result.amount.currency} from league wallet. Transaction ID: ${result.transactionId}`,
};
}
getResponseModel(): WithdrawFromLeagueWalletOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model;
}
get responseModel(): WithdrawFromLeagueWalletOutputDTO {
if (!this.model) throw new Error('Presenter not presented');
return this.model;
}
}