import { Inject, Injectable } from '@nestjs/common'; import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort'; 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'; import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO'; import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO'; import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO'; import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO'; // Core imports import type { Logger } from '@core/shared/application/Logger'; import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; // 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 { 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'; // Presenters import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter'; import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter'; import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter'; import { RaceDetailPresenter } from './presenters/RaceDetailPresenter'; import { RacesPageDataPresenter } from './presenters/RacesPageDataPresenter'; import { AllRacesPageDataPresenter } from './presenters/AllRacesPageDataPresenter'; import { RaceResultsDetailPresenter } from './presenters/RaceResultsDetailPresenter'; import { RaceWithSOFPresenter } from './presenters/RaceWithSOFPresenter'; import { RaceProtestsPresenter } from './presenters/RaceProtestsPresenter'; import { RacePenaltiesPresenter } from './presenters/RacePenaltiesPresenter'; import { CommandResultPresenter } from './presenters/CommandResultPresenter'; // Command DTOs import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO'; import { QuickPenaltyCommandDTO } from './dtos/QuickPenaltyCommandDTO'; import { ApplyPenaltyCommandDTO } from './dtos/ApplyPenaltyCommandDTO'; import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCommandDTO'; import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO'; // Tokens import { DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN } from './RaceProviders'; @Injectable() export class RaceService { constructor( private readonly getAllRacesUseCase: GetAllRacesUseCase, private readonly getTotalRacesUseCase: GetTotalRacesUseCase, private readonly importRaceResultsApiUseCase: ImportRaceResultsApiUseCase, private readonly getRaceDetailUseCase: GetRaceDetailUseCase, private readonly getRacesPageDataUseCase: GetRacesPageDataUseCase, private readonly getAllRacesPageDataUseCase: GetAllRacesPageDataUseCase, private readonly getRaceResultsDetailUseCase: GetRaceResultsDetailUseCase, private readonly getRaceWithSOFUseCase: GetRaceWithSOFUseCase, private readonly getRaceProtestsUseCase: GetRaceProtestsUseCase, private readonly getRacePenaltiesUseCase: GetRacePenaltiesUseCase, private readonly registerForRaceUseCase: RegisterForRaceUseCase, private readonly withdrawFromRaceUseCase: WithdrawFromRaceUseCase, private readonly cancelRaceUseCase: CancelRaceUseCase, private readonly completeRaceUseCase: CompleteRaceUseCase, private readonly fileProtestUseCase: FileProtestUseCase, private readonly quickPenaltyUseCase: QuickPenaltyUseCase, private readonly applyPenaltyUseCase: ApplyPenaltyUseCase, private readonly requestProtestDefenseUseCase: RequestProtestDefenseUseCase, private readonly reviewProtestUseCase: ReviewProtestUseCase, private readonly reopenRaceUseCase: ReopenRaceUseCase, @Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository: ILeagueRepository, @Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider, @Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort, ) {} async getAllRaces(): Promise { 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; } async getTotalRaces(): Promise { this.logger.debug('[RaceService] Fetching total races count.'); const result = await this.getTotalRacesUseCase.execute({}); const presenter = new GetTotalRacesPresenter(); presenter.present(result); return presenter; } async importRaceResults(input: ImportRaceResultsDTO): Promise { 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; } async getRaceDetail(params: GetRaceDetailParamsDTO): Promise { this.logger.debug('[RaceService] Fetching race detail:', params); const result = await this.getRaceDetailUseCase.execute(params); if (result.isErr()) { throw new Error('Failed to get race detail'); } const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService); await presenter.present(result.value as RaceDetailOutputPort, params); return presenter; } async getRacesPageData(): Promise { 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; } async getAllRacesPageData(): Promise { 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; } async getRaceResultsDetail(raceId: string): Promise { 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; } async getRaceWithSOF(raceId: string): Promise { 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; } async getRaceProtests(raceId: string): Promise { 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; } async getRacePenalties(raceId: string): Promise { 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; } async registerForRace(params: RegisterForRaceParamsDTO): Promise { 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; } async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise { 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; } async cancelRace(params: RaceActionParamsDTO): Promise { 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; } async completeRace(params: RaceActionParamsDTO): Promise { 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; } async reopenRace(params: RaceActionParamsDTO): Promise { 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; } async fileProtest(command: FileProtestCommandDTO): Promise { 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; } async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise { 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; } async applyPenalty(command: ApplyPenaltyCommandDTO): Promise { 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; } async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise { 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; } async reviewProtest(command: ReviewProtestCommandDTO): Promise { 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; } }