refactor api modules

This commit is contained in:
2025-12-22 19:17:33 +01:00
parent c90b2166c1
commit 1333f5e907
100 changed files with 2226 additions and 1936 deletions

View File

@@ -1,32 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RaceController } from './RaceController';
import { RaceService } from './RaceService';
import { vi, Mocked } from 'vitest';
describe('RaceController', () => {
let controller: RaceController;
let service: jest.Mocked<RaceService>;
let service: Mocked<RaceService>;
beforeEach(async () => {
const mockService = {
getAllRaces: jest.fn(),
getTotalRaces: jest.fn(),
getRacesPageData: jest.fn(),
getAllRacesPageData: jest.fn(),
getRaceDetail: jest.fn(),
getRaceResultsDetail: jest.fn(),
getRaceWithSOF: jest.fn(),
getRaceProtests: jest.fn(),
getRacePenalties: jest.fn(),
registerForRace: jest.fn(),
withdrawFromRace: jest.fn(),
cancelRace: jest.fn(),
completeRace: jest.fn(),
importRaceResults: jest.fn(),
fileProtest: jest.fn(),
applyQuickPenalty: jest.fn(),
applyPenalty: jest.fn(),
requestProtestDefense: jest.fn(),
} as unknown as jest.Mocked<RaceService>;
getAllRaces: vi.fn(),
getTotalRaces: vi.fn(),
getRacesPageData: vi.fn(),
getAllRacesPageData: vi.fn(),
getRaceDetail: vi.fn(),
getRaceResultsDetail: vi.fn(),
getRaceWithSOF: vi.fn(),
getRaceProtests: vi.fn(),
getRacePenalties: vi.fn(),
registerForRace: vi.fn(),
withdrawFromRace: vi.fn(),
cancelRace: vi.fn(),
completeRace: vi.fn(),
importRaceResults: vi.fn(),
fileProtest: vi.fn(),
applyQuickPenalty: vi.fn(),
applyPenalty: vi.fn(),
requestProtestDefense: vi.fn(),
} as unknown as Mocked<RaceService>;
const module: TestingModule = await Test.createTestingModule({
controllers: [RaceController],
@@ -39,7 +40,7 @@ describe('RaceController', () => {
}).compile();
controller = module.get<RaceController>(RaceController);
service = module.get(RaceService) as jest.Mocked<RaceService>;
service = module.get(RaceService) as Mocked<RaceService>;
});
it('should be defined', () => {
@@ -48,25 +49,25 @@ describe('RaceController', () => {
describe('getAllRaces', () => {
it('should return all races view model', async () => {
const mockViewModel = { races: [], filters: { statuses: [], leagues: [] } } as { races: unknown[]; filters: { statuses: unknown[]; leagues: unknown[] } };
service.getAllRaces.mockResolvedValue({ viewModel: mockViewModel } as unknown as ReturnType<RaceService['getAllRaces']>);
const mockPresenter = { viewModel: { races: [], filters: { statuses: [], leagues: [] } } } as any;
service.getAllRaces.mockResolvedValue(mockPresenter);
const result = await controller.getAllRaces();
expect(service.getAllRaces).toHaveBeenCalled();
expect(result).toEqual(mockViewModel);
expect(result).toEqual(mockPresenter.viewModel);
});
});
describe('getTotalRaces', () => {
it('should return total races count view model', async () => {
const mockViewModel = { totalRaces: 5 } as { totalRaces: number };
service.getTotalRaces.mockResolvedValue({ viewModel: mockViewModel } as unknown as ReturnType<RaceService['getTotalRaces']>);
const mockPresenter = { viewModel: { totalRaces: 5 } } as any;
service.getTotalRaces.mockResolvedValue(mockPresenter);
const result = await controller.getTotalRaces();
expect(service.getTotalRaces).toHaveBeenCalled();
expect(result).toEqual(mockViewModel);
expect(result).toEqual(mockPresenter.viewModel);
});
});
});

View File

@@ -41,9 +41,10 @@ export class RaceController {
@Get('page-data')
@ApiOperation({ summary: 'Get races page data' })
@ApiQuery({ name: 'leagueId', description: 'League ID' })
@ApiResponse({ status: 200, description: 'Races page data', type: RacesPageDataDTO })
async getRacesPageData(): Promise<RacesPageDataDTO> {
const presenter = await this.raceService.getRacesPageData();
async getRacesPageData(@Query('leagueId') leagueId: string): Promise<RacesPageDataDTO> {
const presenter = await this.raceService.getRacesPageData(leagueId);
return presenter.viewModel;
}
@@ -144,7 +145,7 @@ export class RaceController {
@ApiParam({ name: 'raceId', description: 'Race ID' })
@ApiResponse({ status: 200, description: 'Successfully cancelled race' })
async cancelRace(@Param('raceId') raceId: string): Promise<void> {
const presenter = await this.raceService.cancelRace({ raceId });
const presenter = await this.raceService.cancelRace({ raceId }, '');
const viewModel = presenter.viewModel;
if (!viewModel.success) {
@@ -172,7 +173,7 @@ export class RaceController {
@ApiParam({ name: 'raceId', description: 'Race ID' })
@ApiResponse({ status: 200, description: 'Successfully re-opened race' })
async reopenRace(@Param('raceId') raceId: string): Promise<void> {
const presenter = await this.raceService.reopenRace({ raceId });
const presenter = await this.raceService.reopenRace({ raceId }, '');
const viewModel = presenter.viewModel;
if (!viewModel.success) {

View File

@@ -2,54 +2,68 @@ import type { Provider } from '@nestjs/common';
import { RaceService } from './RaceService';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
import type { Logger } from '@core/shared/application/Logger';
// Import concrete in-memory implementations
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository';
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository';
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
import { CompleteRaceUseCase } from '@core/racing/application/use-cases/CompleteRaceUseCase';
import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
import { FileProtestUseCase } from '@core/racing/application/use-cases/FileProtestUseCase';
import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
import { QuickPenaltyUseCase } from '@core/racing/application/use-cases/QuickPenaltyUseCase';
import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
import { ReopenRaceUseCase } from '@core/racing/application/use-cases/ReopenRaceUseCase';
import { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase';
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
import { ReopenRaceUseCase } from '@core/racing/application/use-cases/ReopenRaceUseCase';
import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
// Import presenters
import { AllRacesPageDataPresenter } from './presenters/AllRacesPageDataPresenter';
import { CommandResultPresenter } from './presenters/CommandResultPresenter';
import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter';
import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter';
import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter';
import { RaceDetailPresenter } from './presenters/RaceDetailPresenter';
import { RacePenaltiesPresenter } from './presenters/RacePenaltiesPresenter';
import { RaceProtestsPresenter } from './presenters/RaceProtestsPresenter';
import { RaceResultsDetailPresenter } from './presenters/RaceResultsDetailPresenter';
import { RacesPageDataPresenter } from './presenters/RacesPageDataPresenter';
import { RaceWithSOFPresenter } from './presenters/RaceWithSOFPresenter';
// Define injection tokens
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
@@ -65,6 +79,19 @@ export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const LOGGER_TOKEN = 'Logger';
// Presenter tokens
export const GET_ALL_RACES_PRESENTER_TOKEN = 'GetAllRacesPresenter';
export const GET_TOTAL_RACES_PRESENTER_TOKEN = 'GetTotalRacesPresenter';
export const IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN = 'ImportRaceResultsApiPresenter';
export const RACE_DETAIL_PRESENTER_TOKEN = 'RaceDetailPresenter';
export const RACES_PAGE_DATA_PRESENTER_TOKEN = 'RacesPageDataPresenter';
export const ALL_RACES_PAGE_DATA_PRESENTER_TOKEN = 'AllRacesPageDataPresenter';
export const RACE_RESULTS_DETAIL_PRESENTER_TOKEN = 'RaceResultsDetailPresenter';
export const RACE_WITH_SOF_PRESENTER_TOKEN = 'RaceWithSOFPresenter';
export const RACE_PROTESTS_PRESENTER_TOKEN = 'RaceProtestsPresenter';
export const RACE_PENALTIES_PRESENTER_TOKEN = 'RacePenaltiesPresenter';
export const COMMAND_RESULT_PRESENTER_TOKEN = 'CommandResultPresenter';
export const RaceProviders: Provider[] = [
RaceService,
{
@@ -109,7 +136,7 @@ export const RaceProviders: Provider[] = [
},
{
provide: STANDING_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger),
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger, getPointsSystems()),
inject: [LOGGER_TOKEN],
},
{
@@ -126,18 +153,105 @@ export const RaceProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
// Presenters
{
provide: GET_ALL_RACES_PRESENTER_TOKEN,
useClass: GetAllRacesPresenter,
},
{
provide: GET_TOTAL_RACES_PRESENTER_TOKEN,
useClass: GetTotalRacesPresenter,
},
{
provide: IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN,
useClass: ImportRaceResultsApiPresenter,
},
{
provide: RACE_DETAIL_PRESENTER_TOKEN,
useFactory: (driverRatingProvider: DriverRatingProvider, imageService: any) =>
new RaceDetailPresenter(driverRatingProvider, imageService, { raceId: '', driverId: '' }),
inject: [DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN],
},
{
provide: RACES_PAGE_DATA_PRESENTER_TOKEN,
useClass: RacesPageDataPresenter,
},
{
provide: ALL_RACES_PAGE_DATA_PRESENTER_TOKEN,
useClass: AllRacesPageDataPresenter,
},
{
provide: RACE_RESULTS_DETAIL_PRESENTER_TOKEN,
useFactory: (imageService: any) => new RaceResultsDetailPresenter(imageService),
inject: [IMAGE_SERVICE_TOKEN],
},
{
provide: RACE_WITH_SOF_PRESENTER_TOKEN,
useClass: RaceWithSOFPresenter,
},
{
provide: RACE_PROTESTS_PRESENTER_TOKEN,
useClass: RaceProtestsPresenter,
},
{
provide: RACE_PENALTIES_PRESENTER_TOKEN,
useClass: RacePenaltiesPresenter,
},
{
provide: COMMAND_RESULT_PRESENTER_TOKEN,
useClass: CommandResultPresenter,
},
// Use cases - using simplified approach since presenters need to be adapted
{
provide: GetAllRacesUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository, logger: Logger) =>
new GetAllRacesUseCase(raceRepo, leagueRepo, logger),
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
logger: Logger,
) => {
const useCase = new GetAllRacesUseCase(raceRepo, leagueRepo, logger);
// Create a simple wrapper that calls the presenter
const wrapper = {
present: (data: any) => {
const presenter = new GetAllRacesPresenter();
presenter.present(data);
}
};
useCase.setOutput(wrapper as any);
return useCase;
},
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTotalRacesUseCase,
useFactory: (raceRepo: IRaceRepository, logger: Logger) => new GetTotalRacesUseCase(raceRepo, logger),
useFactory: (raceRepo: IRaceRepository, logger: Logger) => {
const presenter = new GetTotalRacesPresenter();
return new GetTotalRacesUseCase(raceRepo, logger, presenter as any);
},
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: ImportRaceResultsApiUseCase,
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
resultRepo: IResultRepository,
driverRepo: IDriverRepository,
standingRepo: IStandingRepository,
logger: Logger,
) => {
const presenter = new ImportRaceResultsApiPresenter();
return new ImportRaceResultsApiUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger, presenter as any);
},
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
},
{
provide: GetRaceDetailUseCase,
useFactory: (
@@ -147,15 +261,24 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
) =>
new GetRaceDetailUseCase(
) => {
const useCase = new GetRaceDetailUseCase(
raceRepo,
leagueRepo,
driverRepo,
raceRegRepo,
resultRepo,
leagueMembershipRepo,
),
);
const wrapper = {
present: (data: any) => {
const presenter = new RaceDetailPresenter({} as any, {} as any, {} as any);
presenter.present(data);
}
};
useCase.setOutput(wrapper as any);
return useCase;
},
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
@@ -167,15 +290,27 @@ export const RaceProviders: Provider[] = [
},
{
provide: GetRacesPageDataUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) =>
new GetRacesPageDataUseCase(raceRepo, leagueRepo),
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
logger: Logger,
) => {
const presenter = new RacesPageDataPresenter();
return new GetRacesPageDataUseCase(raceRepo, leagueRepo, logger, presenter as any);
},
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetAllRacesPageDataUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) =>
new GetAllRacesPageDataUseCase(raceRepo, leagueRepo),
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
logger: Logger,
) => {
const presenter = new AllRacesPageDataPresenter();
return new GetAllRacesPageDataUseCase(raceRepo, leagueRepo, logger, presenter as any);
},
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetRaceResultsDetailUseCase,
@@ -185,7 +320,10 @@ export const RaceProviders: Provider[] = [
resultRepo: IResultRepository,
driverRepo: IDriverRepository,
penaltyRepo: IPenaltyRepository,
) => new GetRaceResultsDetailUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, penaltyRepo),
) => {
const presenter = new RaceResultsDetailPresenter({} as any);
return new GetRaceResultsDetailUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, penaltyRepo, presenter as any);
},
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
@@ -201,7 +339,10 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository,
driverRatingProvider: DriverRatingProvider,
) => new GetRaceWithSOFUseCase(raceRepo, raceRegRepo, resultRepo, driverRatingProvider),
) => {
const presenter = new RaceWithSOFPresenter();
return new GetRaceWithSOFUseCase(raceRepo, raceRegRepo, resultRepo, driverRatingProvider as any, presenter as any);
},
inject: [
RACE_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
@@ -211,14 +352,18 @@ export const RaceProviders: Provider[] = [
},
{
provide: GetRaceProtestsUseCase,
useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) =>
new GetRaceProtestsUseCase(protestRepo, driverRepo),
useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) => {
const presenter = new RaceProtestsPresenter();
return new GetRaceProtestsUseCase(protestRepo, driverRepo, presenter as any);
},
inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
},
{
provide: GetRacePenaltiesUseCase,
useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) =>
new GetRacePenaltiesUseCase(penaltyRepo, driverRepo),
useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) => {
const presenter = new RacePenaltiesPresenter();
return new GetRacePenaltiesUseCase(penaltyRepo, driverRepo, presenter as any);
},
inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
},
{
@@ -227,17 +372,30 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger,
) => new RegisterForRaceUseCase(raceRegRepo, leagueMembershipRepo, logger),
) => {
const presenter = new CommandResultPresenter();
return new RegisterForRaceUseCase(raceRegRepo, leagueMembershipRepo, logger, presenter as any);
},
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: WithdrawFromRaceUseCase,
useFactory: (raceRegRepo: IRaceRegistrationRepository) => new WithdrawFromRaceUseCase(raceRegRepo),
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
useFactory: (
raceRepo: IRaceRepository,
raceRegRepo: IRaceRegistrationRepository,
logger: Logger,
) => {
const presenter = new CommandResultPresenter();
return new WithdrawFromRaceUseCase(raceRepo, raceRegRepo, logger, presenter as any);
},
inject: [RACE_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: CancelRaceUseCase,
useFactory: (raceRepo: IRaceRepository, logger: Logger) => new CancelRaceUseCase(raceRepo, logger),
useFactory: (raceRepo: IRaceRepository, logger: Logger) => {
const presenter = new CommandResultPresenter();
return new CancelRaceUseCase(raceRepo, logger, presenter as any);
},
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
@@ -248,7 +406,10 @@ export const RaceProviders: Provider[] = [
resultRepo: IResultRepository,
standingRepo: IStandingRepository,
driverRatingProvider: DriverRatingProvider,
) => new CompleteRaceUseCase(raceRepo, raceRegRepo, resultRepo, standingRepo, driverRatingProvider),
) => {
const presenter = new CommandResultPresenter();
return new CompleteRaceUseCase(raceRepo, raceRegRepo, resultRepo, standingRepo, driverRatingProvider as any, presenter as any);
},
inject: [
RACE_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN,
@@ -259,28 +420,12 @@ export const RaceProviders: Provider[] = [
},
{
provide: ReopenRaceUseCase,
useFactory: (raceRepo: IRaceRepository, logger: Logger) => new ReopenRaceUseCase(raceRepo, logger),
useFactory: (raceRepo: IRaceRepository, logger: Logger) => {
const presenter = new CommandResultPresenter();
return new ReopenRaceUseCase(raceRepo, logger, presenter as any);
},
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: ImportRaceResultsApiUseCase,
useFactory: (
raceRepo: IRaceRepository,
leagueRepo: ILeagueRepository,
resultRepo: IResultRepository,
driverRepo: IDriverRepository,
standingRepo: IStandingRepository,
logger: Logger,
) => new ImportRaceResultsApiUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger),
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
RESULT_REPOSITORY_TOKEN,
DRIVER_REPOSITORY_TOKEN,
STANDING_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
},
{
provide: ImportRaceResultsUseCase,
useFactory: (
@@ -290,7 +435,10 @@ export const RaceProviders: Provider[] = [
driverRepo: IDriverRepository,
standingRepo: IStandingRepository,
logger: Logger,
) => new ImportRaceResultsUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger),
) => {
const presenter = new CommandResultPresenter();
return new ImportRaceResultsUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger, presenter as any);
},
inject: [
RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
@@ -306,7 +454,10 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository,
raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
) => new FileProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo),
) => {
const presenter = new CommandResultPresenter();
return new FileProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, presenter as any);
},
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
@@ -316,13 +467,11 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger,
) => new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger),
inject: [
PENALTY_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
) => {
const presenter = new CommandResultPresenter();
return new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger, presenter as any);
},
inject: [PENALTY_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: ApplyPenaltyUseCase,
@@ -332,14 +481,11 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger,
) => new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger),
inject: [
PENALTY_REPOSITORY_TOKEN,
PROTEST_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
) => {
const presenter = new CommandResultPresenter();
return new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any);
},
inject: [PENALTY_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: RequestProtestDefenseUseCase,
@@ -347,8 +493,12 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository,
raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
) => new RequestProtestDefenseUseCase(protestRepo, raceRepo, leagueMembershipRepo),
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
logger: Logger,
) => {
const presenter = new CommandResultPresenter();
return new RequestProtestDefenseUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any);
},
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: ReviewProtestUseCase,
@@ -356,7 +506,11 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository,
raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo),
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
logger: Logger,
) => {
const presenter = new CommandResultPresenter();
return new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any);
},
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
];
];

View File

@@ -1,168 +0,0 @@
import { RaceService } from './RaceService';
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase';
import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase';
import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase';
import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
import { CompleteRaceUseCase } from '@core/racing/application/use-cases/CompleteRaceUseCase';
import { FileProtestUseCase } from '@core/racing/application/use-cases/FileProtestUseCase';
import { QuickPenaltyUseCase } from '@core/racing/application/use-cases/QuickPenaltyUseCase';
import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
import { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase';
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
import { ReopenRaceUseCase } from '@core/racing/application/use-cases/ReopenRaceUseCase';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { Logger } from '@core/shared/application/Logger';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import { Result } from '@core/shared/application/Result';
// Minimal happy-path coverage to assert presenter usage
describe('RaceService', () => {
let service: RaceService;
let getAllRacesUseCase: jest.Mocked<GetAllRacesUseCase>;
let getTotalRacesUseCase: jest.Mocked<GetTotalRacesUseCase>;
let importRaceResultsApiUseCase: jest.Mocked<ImportRaceResultsApiUseCase>;
let getRaceDetailUseCase: jest.Mocked<GetRaceDetailUseCase>;
let getRacesPageDataUseCase: jest.Mocked<GetRacesPageDataUseCase>;
let getAllRacesPageDataUseCase: jest.Mocked<GetAllRacesPageDataUseCase>;
let getRaceResultsDetailUseCase: jest.Mocked<GetRaceResultsDetailUseCase>;
let getRaceWithSOFUseCase: jest.Mocked<GetRaceWithSOFUseCase>;
let getRaceProtestsUseCase: jest.Mocked<GetRaceProtestsUseCase>;
let getRacePenaltiesUseCase: jest.Mocked<GetRacePenaltiesUseCase>;
let registerForRaceUseCase: jest.Mocked<RegisterForRaceUseCase>;
let withdrawFromRaceUseCase: jest.Mocked<WithdrawFromRaceUseCase>;
let cancelRaceUseCase: jest.Mocked<CancelRaceUseCase>;
let completeRaceUseCase: jest.Mocked<CompleteRaceUseCase>;
let fileProtestUseCase: jest.Mocked<FileProtestUseCase>;
let quickPenaltyUseCase: jest.Mocked<QuickPenaltyUseCase>;
let applyPenaltyUseCase: jest.Mocked<ApplyPenaltyUseCase>;
let requestProtestDefenseUseCase: jest.Mocked<RequestProtestDefenseUseCase>;
let reviewProtestUseCase: jest.Mocked<ReviewProtestUseCase>;
let reopenRaceUseCase: jest.Mocked<ReopenRaceUseCase>;
let leagueRepository: jest.Mocked<ILeagueRepository>;
let logger: jest.Mocked<Logger>;
let driverRatingProvider: jest.Mocked<DriverRatingProvider>;
let imageService: jest.Mocked<IImageServicePort>;
beforeEach(() => {
getAllRacesUseCase = { execute: jest.fn() } as jest.Mocked<GetAllRacesUseCase>;
getTotalRacesUseCase = { execute: jest.fn() } as jest.Mocked<GetTotalRacesUseCase>;
importRaceResultsApiUseCase = { execute: jest.fn() } as jest.Mocked<ImportRaceResultsApiUseCase>;
getRaceDetailUseCase = { execute: jest.fn() } as jest.Mocked<GetRaceDetailUseCase>;
getRacesPageDataUseCase = { execute: jest.fn() } as jest.Mocked<GetRacesPageDataUseCase>;
getAllRacesPageDataUseCase = { execute: jest.fn() } as jest.Mocked<GetAllRacesPageDataUseCase>;
getRaceResultsDetailUseCase = { execute: jest.fn() } as jest.Mocked<GetRaceResultsDetailUseCase>;
getRaceWithSOFUseCase = { execute: jest.fn() } as jest.Mocked<GetRaceWithSOFUseCase>;
getRaceProtestsUseCase = { execute: jest.fn() } as jest.Mocked<GetRaceProtestsUseCase>;
getRacePenaltiesUseCase = { execute: jest.fn() } as jest.Mocked<GetRacePenaltiesUseCase>;
registerForRaceUseCase = { execute: jest.fn() } as jest.Mocked<RegisterForRaceUseCase>;
withdrawFromRaceUseCase = { execute: jest.fn() } as jest.Mocked<WithdrawFromRaceUseCase>;
cancelRaceUseCase = { execute: jest.fn() } as jest.Mocked<CancelRaceUseCase>;
completeRaceUseCase = { execute: jest.fn() } as jest.Mocked<CompleteRaceUseCase>;
fileProtestUseCase = { execute: jest.fn() } as jest.Mocked<FileProtestUseCase>;
quickPenaltyUseCase = { execute: jest.fn() } as jest.Mocked<QuickPenaltyUseCase>;
applyPenaltyUseCase = { execute: jest.fn() } as jest.Mocked<ApplyPenaltyUseCase>;
requestProtestDefenseUseCase = { execute: jest.fn() } as jest.Mocked<RequestProtestDefenseUseCase>;
reviewProtestUseCase = { execute: jest.fn() } as jest.Mocked<ReviewProtestUseCase>;
reopenRaceUseCase = { execute: jest.fn() } as jest.Mocked<ReopenRaceUseCase>;
leagueRepository = {
findAll: jest.fn(),
} as jest.Mocked<ILeagueRepository>;
logger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
} as jest.Mocked<Logger>;
driverRatingProvider = {
getDriverRating: jest.fn(),
} as jest.Mocked<DriverRatingProvider>;
imageService = {
getDriverAvatar: jest.fn(),
getTeamLogo: jest.fn(),
getLeagueCover: jest.fn(),
getLeagueLogo: jest.fn(),
} as jest.Mocked<IImageServicePort>;
service = new RaceService(
getAllRacesUseCase,
getTotalRacesUseCase,
importRaceResultsApiUseCase,
getRaceDetailUseCase,
getRacesPageDataUseCase,
getAllRacesPageDataUseCase,
getRaceResultsDetailUseCase,
getRaceWithSOFUseCase,
getRaceProtestsUseCase,
getRacePenaltiesUseCase,
registerForRaceUseCase,
withdrawFromRaceUseCase,
cancelRaceUseCase,
completeRaceUseCase,
fileProtestUseCase,
quickPenaltyUseCase,
applyPenaltyUseCase,
requestProtestDefenseUseCase,
reviewProtestUseCase,
reopenRaceUseCase,
leagueRepository,
logger,
driverRatingProvider,
imageService,
);
});
it('getAllRaces should return presenter with view model', async () => {
const output = {
races: [],
totalCount: 0,
};
(getAllRacesUseCase.execute as jest.Mock).mockResolvedValue(Result.ok(output));
const presenter = await service.getAllRaces();
const viewModel = presenter.getViewModel();
expect(getAllRacesUseCase.execute).toHaveBeenCalledWith();
expect(viewModel).not.toBeNull();
expect(viewModel).toMatchObject({ totalCount: 0 });
});
it('registerForRace should map success into CommandResultPresenter', async () => {
(registerForRaceUseCase.execute as jest.Mock).mockResolvedValue(Result.ok({}));
const presenter = await service.registerForRace({
raceId: 'race-1',
driverId: 'driver-1',
} as { raceId: string; driverId: string });
expect(registerForRaceUseCase.execute).toHaveBeenCalledWith({ raceId: 'race-1', driverId: 'driver-1' });
expect(presenter.viewModel.success).toBe(true);
});
it('registerForRace should map error into CommandResultPresenter', async () => {
(registerForRaceUseCase.execute as jest.Mock).mockResolvedValue(Result.err({ code: 'FAILED_TO_REGISTER_FOR_RACE' as const }));
const presenter = await service.registerForRace({
raceId: 'race-1',
driverId: 'driver-1',
} as { raceId: string; driverId: string });
expect(presenter.viewModel.success).toBe(false);
expect(presenter.viewModel.errorCode).toBe('FAILED_TO_REGISTER_FOR_RACE');
});
});

View File

@@ -1,9 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort';
import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort';
// DTOs
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
@@ -61,7 +56,23 @@ import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCom
import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO';
// Tokens
import { DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN } from './RaceProviders';
import {
DRIVER_RATING_PROVIDER_TOKEN,
IMAGE_SERVICE_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
LOGGER_TOKEN,
GET_ALL_RACES_PRESENTER_TOKEN,
GET_TOTAL_RACES_PRESENTER_TOKEN,
IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN,
RACE_DETAIL_PRESENTER_TOKEN,
RACES_PAGE_DATA_PRESENTER_TOKEN,
ALL_RACES_PAGE_DATA_PRESENTER_TOKEN,
RACE_RESULTS_DETAIL_PRESENTER_TOKEN,
RACE_WITH_SOF_PRESENTER_TOKEN,
RACE_PROTESTS_PRESENTER_TOKEN,
RACE_PENALTIES_PRESENTER_TOKEN,
COMMAND_RESULT_PRESENTER_TOKEN
} from './RaceProviders';
@Injectable()
export class RaceService {
@@ -90,305 +101,137 @@ export class RaceService {
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
// Injected presenters
@Inject(GET_ALL_RACES_PRESENTER_TOKEN) private readonly getAllRacesPresenter: GetAllRacesPresenter,
@Inject(GET_TOTAL_RACES_PRESENTER_TOKEN) private readonly getTotalRacesPresenter: GetTotalRacesPresenter,
@Inject(IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN) private readonly importRaceResultsApiPresenter: ImportRaceResultsApiPresenter,
@Inject(RACE_DETAIL_PRESENTER_TOKEN) private readonly raceDetailPresenter: RaceDetailPresenter,
@Inject(RACES_PAGE_DATA_PRESENTER_TOKEN) private readonly racesPageDataPresenter: RacesPageDataPresenter,
@Inject(ALL_RACES_PAGE_DATA_PRESENTER_TOKEN) private readonly allRacesPageDataPresenter: AllRacesPageDataPresenter,
@Inject(RACE_RESULTS_DETAIL_PRESENTER_TOKEN) private readonly raceResultsDetailPresenter: RaceResultsDetailPresenter,
@Inject(RACE_WITH_SOF_PRESENTER_TOKEN) private readonly raceWithSOFPresenter: RaceWithSOFPresenter,
@Inject(RACE_PROTESTS_PRESENTER_TOKEN) private readonly raceProtestsPresenter: RaceProtestsPresenter,
@Inject(RACE_PENALTIES_PRESENTER_TOKEN) private readonly racePenaltiesPresenter: RacePenaltiesPresenter,
@Inject(COMMAND_RESULT_PRESENTER_TOKEN) private readonly commandResultPresenter: CommandResultPresenter,
) {}
async getAllRaces(): Promise<GetAllRacesPresenter> {
this.logger.debug('[RaceService] Fetching all races.');
const presenter = new GetAllRacesPresenter();
this.getAllRacesUseCase.setOutput(presenter);
const result = await this.getAllRacesUseCase.execute({});
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return presenter;
await this.getAllRacesUseCase.execute({});
return this.getAllRacesPresenter;
}
async getTotalRaces(): Promise<GetTotalRacesPresenter> {
this.logger.debug('[RaceService] Fetching total races count.');
const result = await this.getTotalRacesUseCase.execute({});
const presenter = new GetTotalRacesPresenter();
presenter.present(result);
return presenter;
await this.getTotalRacesUseCase.execute({});
return this.getTotalRacesPresenter;
}
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsApiPresenter> {
this.logger.debug('Importing race results:', input);
const result = await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent });
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const presenter = new ImportRaceResultsApiPresenter();
presenter.present(result.unwrap());
return presenter;
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent });
return this.importRaceResultsApiPresenter;
}
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
this.logger.debug('[RaceService] Fetching race detail:', params);
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService, params);
this.getRaceDetailUseCase.setOutput(presenter);
const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) {
throw new Error('Failed to get race detail');
}
return presenter;
await this.getRaceDetailUseCase.execute(params);
return this.raceDetailPresenter;
}
async getRacesPageData(): Promise<RacesPageDataPresenter> {
async getRacesPageData(leagueId: string): Promise<RacesPageDataPresenter> {
this.logger.debug('[RaceService] Fetching races page data.');
const result = await this.getRacesPageDataUseCase.execute();
if (result.isErr()) {
throw new Error('Failed to get races page data');
}
const presenter = new RacesPageDataPresenter(this.leagueRepository);
await presenter.present(result.value as RacesPageOutputPort);
return presenter;
await this.getRacesPageDataUseCase.execute({ leagueId });
return this.racesPageDataPresenter;
}
async getAllRacesPageData(): Promise<AllRacesPageDataPresenter> {
this.logger.debug('[RaceService] Fetching all races page data.');
const result = await this.getAllRacesPageDataUseCase.execute();
if (result.isErr()) {
throw new Error('Failed to get all races page data');
}
const presenter = new AllRacesPageDataPresenter();
presenter.present(result.value);
return presenter;
await this.getAllRacesPageDataUseCase.execute({});
return this.allRacesPageDataPresenter;
}
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailPresenter> {
this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
if (result.isErr()) {
throw new Error('Failed to get race results detail');
}
const presenter = new RaceResultsDetailPresenter(this.imageService);
await presenter.present(result.value as RaceResultsDetailOutputPort);
return presenter;
await this.getRaceResultsDetailUseCase.execute({ raceId });
return this.raceResultsDetailPresenter;
}
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFPresenter> {
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
const result = await this.getRaceWithSOFUseCase.execute({ raceId });
if (result.isErr()) {
throw new Error('Failed to get race with SOF');
}
const presenter = new RaceWithSOFPresenter();
presenter.present(result.value as RaceWithSOFOutputPort);
return presenter;
await this.getRaceWithSOFUseCase.execute({ raceId });
return this.raceWithSOFPresenter;
}
async getRaceProtests(raceId: string): Promise<RaceProtestsPresenter> {
this.logger.debug('[RaceService] Fetching race protests:', { raceId });
const result = await this.getRaceProtestsUseCase.execute({ raceId });
if (result.isErr()) {
throw new Error('Failed to get race protests');
}
const presenter = new RaceProtestsPresenter();
presenter.present(result.value as RaceProtestsOutputPort);
return presenter;
await this.getRaceProtestsUseCase.execute({ raceId });
return this.raceProtestsPresenter;
}
async getRacePenalties(raceId: string): Promise<RacePenaltiesPresenter> {
this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
const result = await this.getRacePenaltiesUseCase.execute({ raceId });
if (result.isErr()) {
throw new Error('Failed to get race penalties');
}
const presenter = new RacePenaltiesPresenter();
presenter.present(result.value as RacePenaltiesOutputPort);
return presenter;
await this.getRacePenaltiesUseCase.execute({ raceId });
return this.racePenaltiesPresenter;
}
async registerForRace(params: RegisterForRaceParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Registering for race:', params);
const result = await this.registerForRaceUseCase.execute(params);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_REGISTER_FOR_RACE', 'Failed to register for race');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.registerForRaceUseCase.execute(params);
return this.commandResultPresenter;
}
async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Withdrawing from race:', params);
const result = await this.withdrawFromRaceUseCase.execute(params);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_WITHDRAW_FROM_RACE', 'Failed to withdraw from race');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.withdrawFromRaceUseCase.execute(params);
return this.commandResultPresenter;
}
async cancelRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> {
async cancelRace(params: RaceActionParamsDTO, cancelledById: string): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Cancelling race:', params);
const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId });
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_CANCEL_RACE', 'Failed to cancel race');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.cancelRaceUseCase.execute({ raceId: params.raceId, cancelledById });
return this.commandResultPresenter;
}
async completeRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Completing race:', params);
const result = await this.completeRaceUseCase.execute({ raceId: params.raceId });
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_COMPLETE_RACE', 'Failed to complete race');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.completeRaceUseCase.execute({ raceId: params.raceId });
return this.commandResultPresenter;
}
async reopenRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> {
async reopenRace(params: RaceActionParamsDTO, reopenedById: string): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Re-opening race:', params);
const result = await this.reopenRaceUseCase.execute({ raceId: params.raceId });
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const errorCode = result.unwrapErr().code;
if (errorCode === 'RACE_ALREADY_SCHEDULED') {
this.logger.debug('[RaceService] Race is already scheduled, treating reopen as success.');
presenter.presentSuccess('Race already scheduled');
return presenter;
}
presenter.presentFailure(errorCode ?? 'UNEXPECTED_ERROR', 'Unexpected error while reopening race');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.reopenRaceUseCase.execute({ raceId: params.raceId, reopenedById });
return this.commandResultPresenter;
}
async fileProtest(command: FileProtestCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Filing protest:', command);
const result = await this.fileProtestUseCase.execute(command);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_FILE_PROTEST', 'Failed to file protest');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.fileProtestUseCase.execute(command);
return this.commandResultPresenter;
}
async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Applying quick penalty:', command);
const result = await this.quickPenaltyUseCase.execute(command);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_APPLY_QUICK_PENALTY', 'Failed to apply quick penalty');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.quickPenaltyUseCase.execute(command);
return this.commandResultPresenter;
}
async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Applying penalty:', command);
const result = await this.applyPenaltyUseCase.execute(command);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_APPLY_PENALTY', 'Failed to apply penalty');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.applyPenaltyUseCase.execute(command);
return this.commandResultPresenter;
}
async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Requesting protest defense:', command);
const result = await this.requestProtestDefenseUseCase.execute(command);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_REQUEST_PROTEST_DEFENSE', 'Failed to request protest defense');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.requestProtestDefenseUseCase.execute(command);
return this.commandResultPresenter;
}
async reviewProtest(command: ReviewProtestCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Reviewing protest:', command);
const result = await this.reviewProtestUseCase.execute(command);
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_REVIEW_PROTEST', 'Failed to review protest');
return presenter;
}
presenter.presentSuccess();
return presenter;
await this.reviewProtestUseCase.execute(command);
return this.commandResultPresenter;
}
}
}

View File

@@ -28,12 +28,28 @@ export class AllRacesListItemDTO {
strengthOfField!: number | null;
}
export class AllRacesFilterOptionsDTO {
@ApiProperty({ type: [{ value: String, label: String }] })
statuses!: { value: AllRacesStatus; label: string }[];
export class AllRacesStatusFilterDTO {
@ApiProperty()
value!: AllRacesStatus;
@ApiProperty({ type: [{ id: String, name: String }] })
leagues!: { id: string; name: string }[];
@ApiProperty()
label!: string;
}
export class AllRacesLeagueFilterDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
}
export class AllRacesFilterOptionsDTO {
@ApiProperty({ type: [AllRacesStatusFilterDTO] })
statuses!: AllRacesStatusFilterDTO[];
@ApiProperty({ type: [AllRacesLeagueFilterDTO] })
leagues!: AllRacesLeagueFilterDTO[];
}
export class AllRacesPageDTO {

View File

@@ -7,19 +7,19 @@ export interface CommandResultDTO {
message?: string;
}
export type CommandApplicationError<E extends string = string> = ApplicationErrorCode<
E,
export type CommandApplicationError = ApplicationErrorCode<
string,
{ message: string }
>;
export class CommandResultPresenter<E extends string = string> {
export class CommandResultPresenter {
private model: CommandResultDTO | null = null;
reset(): void {
this.model = null;
}
present(result: Result<unknown, CommandApplicationError<E>>): void {
present(result: Result<unknown, CommandApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
@@ -36,7 +36,7 @@ export class CommandResultPresenter<E extends string = string> {
presentSuccess(message?: string): void {
this.model = {
success: true,
message,
...(message !== undefined && { message }),
};
}
@@ -44,7 +44,7 @@ export class CommandResultPresenter<E extends string = string> {
this.model = {
success: false,
errorCode,
message,
...(message !== undefined && { message }),
};
}
@@ -59,4 +59,8 @@ export class CommandResultPresenter<E extends string = string> {
return this.model;
}
get viewModel(): CommandResultDTO {
return this.responseModel;
}
}

View File

@@ -40,11 +40,11 @@ export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResul
track: output.race.track,
car: output.race.car,
scheduledAt: output.race.scheduledAt.toISOString(),
sessionType: output.race.sessionType,
status: output.race.status,
sessionType: output.race.sessionType.toString(),
status: output.race.status.toString(),
strengthOfField: output.race.strengthOfField ?? null,
registeredCount: output.race.registeredCount ?? undefined,
maxParticipants: output.race.maxParticipants ?? undefined,
...(output.race.registeredCount !== undefined && { registeredCount: output.race.registeredCount }),
...(output.race.maxParticipants !== undefined && { maxParticipants: output.race.maxParticipants }),
}
: null;
@@ -54,22 +54,22 @@ export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResul
name: output.league.name.toString(),
description: output.league.description.toString(),
settings: {
maxDrivers: output.league.settings.maxDrivers ?? undefined,
qualifyingFormat: output.league.settings.qualifyingFormat ?? undefined,
...(output.league.settings.maxDrivers !== undefined && { maxDrivers: output.league.settings.maxDrivers }),
...(output.league.settings.qualifyingFormat !== undefined && { qualifyingFormat: output.league.settings.qualifyingFormat.toString() }),
},
}
: null;
const entryListDTO: RaceDetailEntryDTO[] = await Promise.all(
output.drivers.map(async driver => {
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id });
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
const rating = this.driverRatingProvider.getRating(driver.id);
const avatarUrl = this.imageService.getDriverAvatar(driver.id);
return {
id: driver.id,
name: driver.name.toString(),
country: driver.country.toString(),
avatarUrl: avatarResult.avatarUrl,
rating: ratingResult.rating,
avatarUrl,
rating,
isCurrentUser: driver.id === params.driverId,
};
}),

View File

@@ -34,10 +34,10 @@ export class RaceProtestsPresenter {
protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId,
incident: {
lap: protest.incident.lap,
description: protest.incident.description,
lap: protest.incident.lap.toNumber(),
description: protest.incident.description.toString(),
},
status: protest.status,
status: protest.status.toString(),
filedAt: protest.filedAt.toISOString(),
} as RaceProtestDTO));

View File

@@ -39,12 +39,12 @@ export class RaceResultsDetailPresenter {
throw new Error(`Driver not found for result: ${singleResult.driverId}`);
}
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
const avatarUrl = this.imageService.getDriverAvatar(driver.id);
return {
driverId: singleResult.driverId.toString(),
driverName: driver.name.toString(),
avatarUrl: avatarResult.avatarUrl,
avatarUrl,
position: singleResult.position.toNumber(),
startPosition: singleResult.startPosition.toNumber(),
incidents: singleResult.incidents.toNumber(),