presenter refactoring
This commit is contained in:
@@ -26,7 +26,7 @@ describe('RaceController', () => {
|
||||
applyQuickPenalty: jest.fn(),
|
||||
applyPenalty: jest.fn(),
|
||||
requestProtestDefense: jest.fn(),
|
||||
};
|
||||
} as unknown as jest.Mocked<RaceService>;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [RaceController],
|
||||
@@ -39,7 +39,7 @@ describe('RaceController', () => {
|
||||
}).compile();
|
||||
|
||||
controller = module.get<RaceController>(RaceController);
|
||||
service = module.get(RaceService);
|
||||
service = module.get(RaceService) as jest.Mocked<RaceService>;
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@@ -47,28 +47,26 @@ describe('RaceController', () => {
|
||||
});
|
||||
|
||||
describe('getAllRaces', () => {
|
||||
it('should return all races', async () => {
|
||||
const mockResult = { races: [], totalCount: 0 };
|
||||
service.getAllRaces.mockResolvedValue(mockResult);
|
||||
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 result = await controller.getAllRaces();
|
||||
|
||||
expect(service.getAllRaces).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalRaces', () => {
|
||||
it('should return total races count', async () => {
|
||||
const mockResult = { totalRaces: 5 };
|
||||
service.getTotalRaces.mockResolvedValue(mockResult);
|
||||
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 result = await controller.getTotalRaces();
|
||||
|
||||
expect(service.getTotalRaces).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(result).toEqual(mockViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
// Add more tests as needed
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, InternalServerErrorException, Param, Post, Query } from '@nestjs/common';
|
||||
import { ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { RaceService } from './RaceService';
|
||||
import { AllRacesPageDTO } from './dtos/AllRacesPageDTO';
|
||||
@@ -27,28 +27,32 @@ export class RaceController {
|
||||
@ApiOperation({ summary: 'Get all races' })
|
||||
@ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageDTO })
|
||||
async getAllRaces(): Promise<AllRacesPageDTO> {
|
||||
return this.raceService.getAllRaces();
|
||||
const presenter = await this.raceService.getAllRaces();
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get('total-races')
|
||||
@ApiOperation({ summary: 'Get the total number of races' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDTO })
|
||||
async getTotalRaces(): Promise<RaceStatsDTO> {
|
||||
return this.raceService.getTotalRaces();
|
||||
const presenter = await this.raceService.getTotalRaces();
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get('page-data')
|
||||
@ApiOperation({ summary: 'Get races page data' })
|
||||
@ApiResponse({ status: 200, description: 'Races page data', type: RacesPageDataDTO })
|
||||
async getRacesPageData(): Promise<RacesPageDataDTO> {
|
||||
return this.raceService.getRacesPageData();
|
||||
const presenter = await this.raceService.getRacesPageData();
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get('all/page-data')
|
||||
@ApiOperation({ summary: 'Get all races page data' })
|
||||
@ApiResponse({ status: 200, description: 'All races page data', type: AllRacesPageDTO })
|
||||
async getAllRacesPageData(): Promise<AllRacesPageDTO> {
|
||||
return this.raceService.getAllRacesPageData();
|
||||
const presenter = await this.raceService.getAllRacesPageData();
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get(':raceId')
|
||||
@@ -60,7 +64,8 @@ export class RaceController {
|
||||
@Param('raceId') raceId: string,
|
||||
@Query('driverId') driverId: string,
|
||||
): Promise<RaceDetailDTO> {
|
||||
return this.raceService.getRaceDetail({ raceId, driverId });
|
||||
const presenter = await this.raceService.getRaceDetail({ raceId, driverId });
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get(':raceId/results')
|
||||
@@ -68,7 +73,8 @@ export class RaceController {
|
||||
@ApiParam({ name: 'raceId', description: 'Race ID' })
|
||||
@ApiResponse({ status: 200, description: 'Race results detail', type: RaceResultsDetailDTO })
|
||||
async getRaceResultsDetail(@Param('raceId') raceId: string): Promise<RaceResultsDetailDTO> {
|
||||
return this.raceService.getRaceResultsDetail(raceId);
|
||||
const presenter = await this.raceService.getRaceResultsDetail(raceId);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get(':raceId/sof')
|
||||
@@ -76,7 +82,8 @@ export class RaceController {
|
||||
@ApiParam({ name: 'raceId', description: 'Race ID' })
|
||||
@ApiResponse({ status: 200, description: 'Race with SOF', type: RaceWithSOFDTO })
|
||||
async getRaceWithSOF(@Param('raceId') raceId: string): Promise<RaceWithSOFDTO> {
|
||||
return this.raceService.getRaceWithSOF(raceId);
|
||||
const presenter = await this.raceService.getRaceWithSOF(raceId);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get(':raceId/protests')
|
||||
@@ -84,7 +91,8 @@ export class RaceController {
|
||||
@ApiParam({ name: 'raceId', description: 'Race ID' })
|
||||
@ApiResponse({ status: 200, description: 'Race protests', type: RaceProtestsDTO })
|
||||
async getRaceProtests(@Param('raceId') raceId: string): Promise<RaceProtestsDTO> {
|
||||
return this.raceService.getRaceProtests(raceId);
|
||||
const presenter = await this.raceService.getRaceProtests(raceId);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Get(':raceId/penalties')
|
||||
@@ -92,7 +100,8 @@ export class RaceController {
|
||||
@ApiParam({ name: 'raceId', description: 'Race ID' })
|
||||
@ApiResponse({ status: 200, description: 'Race penalties', type: RacePenaltiesDTO })
|
||||
async getRacePenalties(@Param('raceId') raceId: string): Promise<RacePenaltiesDTO> {
|
||||
return this.raceService.getRacePenalties(raceId);
|
||||
const presenter = await this.raceService.getRacePenalties(raceId);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Post(':raceId/register')
|
||||
@@ -104,7 +113,12 @@ export class RaceController {
|
||||
@Param('raceId') raceId: string,
|
||||
@Body() body: Omit<RegisterForRaceParamsDTO, 'raceId'>,
|
||||
): Promise<void> {
|
||||
return this.raceService.registerForRace({ raceId, ...body });
|
||||
const presenter = await this.raceService.registerForRace({ raceId, ...body });
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to register for race');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':raceId/withdraw')
|
||||
@@ -116,7 +130,12 @@ export class RaceController {
|
||||
@Param('raceId') raceId: string,
|
||||
@Body() body: Omit<WithdrawFromRaceParamsDTO, 'raceId'>,
|
||||
): Promise<void> {
|
||||
return this.raceService.withdrawFromRace({ raceId, ...body });
|
||||
const presenter = await this.raceService.withdrawFromRace({ raceId, ...body });
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to withdraw from race');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':raceId/cancel')
|
||||
@@ -125,7 +144,12 @@ export class RaceController {
|
||||
@ApiParam({ name: 'raceId', description: 'Race ID' })
|
||||
@ApiResponse({ status: 200, description: 'Successfully cancelled race' })
|
||||
async cancelRace(@Param('raceId') raceId: string): Promise<void> {
|
||||
return this.raceService.cancelRace({ raceId });
|
||||
const presenter = await this.raceService.cancelRace({ raceId });
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to cancel race');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':raceId/complete')
|
||||
@@ -134,7 +158,12 @@ export class RaceController {
|
||||
@ApiParam({ name: 'raceId', description: 'Race ID' })
|
||||
@ApiResponse({ status: 200, description: 'Successfully completed race' })
|
||||
async completeRace(@Param('raceId') raceId: string): Promise<void> {
|
||||
return this.raceService.completeRace({ raceId });
|
||||
const presenter = await this.raceService.completeRace({ raceId });
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to complete race');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':raceId/reopen')
|
||||
@@ -143,7 +172,12 @@ 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> {
|
||||
return this.raceService.reopenRace({ raceId });
|
||||
const presenter = await this.raceService.reopenRace({ raceId });
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to re-open race');
|
||||
}
|
||||
}
|
||||
|
||||
@Post(':raceId/import-results')
|
||||
@@ -155,7 +189,8 @@ export class RaceController {
|
||||
@Param('raceId') raceId: string,
|
||||
@Body() body: Omit<ImportRaceResultsDTO, 'raceId'>,
|
||||
): Promise<ImportRaceResultsSummaryDTO> {
|
||||
return this.raceService.importRaceResults({ raceId, ...body });
|
||||
const presenter = await this.raceService.importRaceResults({ raceId, ...body });
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
@Post('protests/file')
|
||||
@@ -163,7 +198,12 @@ export class RaceController {
|
||||
@ApiOperation({ summary: 'File a protest' })
|
||||
@ApiResponse({ status: 200, description: 'Protest filed successfully' })
|
||||
async fileProtest(@Body() body: FileProtestCommandDTO): Promise<void> {
|
||||
return this.raceService.fileProtest(body);
|
||||
const presenter = await this.raceService.fileProtest(body);
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to file protest');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('penalties/quick')
|
||||
@@ -171,7 +211,12 @@ export class RaceController {
|
||||
@ApiOperation({ summary: 'Apply a quick penalty' })
|
||||
@ApiResponse({ status: 200, description: 'Penalty applied successfully' })
|
||||
async applyQuickPenalty(@Body() body: QuickPenaltyCommandDTO): Promise<void> {
|
||||
return this.raceService.applyQuickPenalty(body);
|
||||
const presenter = await this.raceService.applyQuickPenalty(body);
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to apply quick penalty');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('penalties/apply')
|
||||
@@ -179,7 +224,12 @@ export class RaceController {
|
||||
@ApiOperation({ summary: 'Apply a penalty' })
|
||||
@ApiResponse({ status: 200, description: 'Penalty applied successfully' })
|
||||
async applyPenalty(@Body() body: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
return this.raceService.applyPenalty(body);
|
||||
const presenter = await this.raceService.applyPenalty(body);
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to apply penalty');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('protests/defense/request')
|
||||
@@ -187,6 +237,11 @@ export class RaceController {
|
||||
@ApiOperation({ summary: 'Request protest defense' })
|
||||
@ApiResponse({ status: 200, description: 'Defense requested successfully' })
|
||||
async requestProtestDefense(@Body() body: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
return this.raceService.requestProtestDefense(body);
|
||||
const presenter = await this.raceService.requestProtestDefense(body);
|
||||
const viewModel = presenter.viewModel;
|
||||
|
||||
if (!viewModel.success) {
|
||||
throw new InternalServerErrorException(viewModel.message ?? 'Failed to request protest defense');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { ILeagueMembershipRepository } from '@core/racing/domain/repositori
|
||||
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 { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
|
||||
168
apps/api/src/domain/race/RaceService.test.ts
Normal file
168
apps/api/src/domain/race/RaceService.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConflictException, Inject, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
|
||||
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';
|
||||
@@ -13,17 +12,11 @@ import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
|
||||
import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
|
||||
import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
|
||||
import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
|
||||
import { AllRacesPageDTO } from './dtos/AllRacesPageDTO';
|
||||
import { RaceStatsDTO } from './dtos/RaceStatsDTO';
|
||||
import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO';
|
||||
import { RaceProtestsDTO } from './dtos/RaceProtestsDTO';
|
||||
import { RaceResultsDetailDTO } from './dtos/RaceResultsDetailDTO';
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
|
||||
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
|
||||
// Use cases
|
||||
@@ -41,7 +34,6 @@ import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/Regis
|
||||
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 { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase';
|
||||
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';
|
||||
@@ -53,6 +45,14 @@ import { ReopenRaceUseCase } from '@core/racing/application/use-cases/ReopenRace
|
||||
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';
|
||||
@@ -93,15 +93,21 @@ export class RaceService {
|
||||
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
|
||||
) {}
|
||||
|
||||
async getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
async getAllRaces(): Promise<GetAllRacesPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching all races.');
|
||||
|
||||
const result = await this.getAllRacesUseCase.execute();
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error('Failed to get all races');
|
||||
}
|
||||
|
||||
const presenter = new GetAllRacesPresenter();
|
||||
await this.getAllRacesUseCase.execute({}, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
await presenter.present(result.unwrap());
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getTotalRaces(): Promise<RaceStatsDTO> {
|
||||
async getTotalRaces(): Promise<GetTotalRacesPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching total races count.');
|
||||
const result = await this.getTotalRacesUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
@@ -109,10 +115,10 @@ export class RaceService {
|
||||
}
|
||||
const presenter = new GetTotalRacesPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsSummaryDTO> {
|
||||
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()) {
|
||||
@@ -120,10 +126,10 @@ export class RaceService {
|
||||
}
|
||||
const presenter = new ImportRaceResultsApiPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailDTO> {
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching race detail:', params);
|
||||
|
||||
const result = await this.getRaceDetailUseCase.execute(params);
|
||||
@@ -132,79 +138,12 @@ export class RaceService {
|
||||
throw new Error('Failed to get race detail');
|
||||
}
|
||||
|
||||
const outputPort = result.value as RaceDetailOutputPort;
|
||||
|
||||
// Map to DTO
|
||||
const raceDTO = outputPort.race
|
||||
? {
|
||||
id: outputPort.race.id,
|
||||
leagueId: outputPort.race.leagueId,
|
||||
track: outputPort.race.track,
|
||||
car: outputPort.race.car,
|
||||
scheduledAt: outputPort.race.scheduledAt.toISOString(),
|
||||
sessionType: outputPort.race.sessionType,
|
||||
status: outputPort.race.status,
|
||||
strengthOfField: outputPort.race.strengthOfField ?? null,
|
||||
registeredCount: outputPort.race.registeredCount ?? undefined,
|
||||
maxParticipants: outputPort.race.maxParticipants ?? undefined,
|
||||
}
|
||||
: null;
|
||||
|
||||
const leagueDTO = outputPort.league
|
||||
? {
|
||||
id: outputPort.league.id.toString(),
|
||||
name: outputPort.league.name.toString(),
|
||||
description: outputPort.league.description.toString(),
|
||||
settings: {
|
||||
maxDrivers: outputPort.league.settings.maxDrivers ?? undefined,
|
||||
qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
const entryListDTO = await Promise.all(
|
||||
outputPort.drivers.map(async driver => {
|
||||
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id });
|
||||
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name.toString(),
|
||||
country: driver.country.toString(),
|
||||
avatarUrl: avatarResult.avatarUrl,
|
||||
rating: ratingResult.rating,
|
||||
isCurrentUser: driver.id === params.driverId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const registrationDTO = {
|
||||
isUserRegistered: outputPort.isUserRegistered,
|
||||
canRegister: outputPort.canRegister,
|
||||
};
|
||||
|
||||
const userResultDTO = outputPort.userResult
|
||||
? {
|
||||
position: outputPort.userResult.position.toNumber(),
|
||||
startPosition: outputPort.userResult.startPosition.toNumber(),
|
||||
incidents: outputPort.userResult.incidents.toNumber(),
|
||||
fastestLap: outputPort.userResult.fastestLap.toNumber(),
|
||||
positionChange: outputPort.userResult.getPositionChange(),
|
||||
isPodium: outputPort.userResult.isPodium(),
|
||||
isClean: outputPort.userResult.isClean(),
|
||||
ratingChange: this.calculateRatingChange(outputPort.userResult.position.toNumber()),
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
race: raceDTO,
|
||||
league: leagueDTO,
|
||||
entryList: entryListDTO,
|
||||
registration: registrationDTO,
|
||||
userResult: userResultDTO,
|
||||
};
|
||||
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService);
|
||||
await presenter.present(result.value as RaceDetailOutputPort, params);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getRacesPageData(): Promise<RacesPageDataDTO> {
|
||||
async getRacesPageData(): Promise<RacesPageDataPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching races page data.');
|
||||
|
||||
const result = await this.getRacesPageDataUseCase.execute();
|
||||
@@ -213,33 +152,12 @@ export class RaceService {
|
||||
throw new Error('Failed to get races page data');
|
||||
}
|
||||
|
||||
const outputPort = result.value as RacesPageOutputPort;
|
||||
|
||||
// Fetch leagues for league names
|
||||
const allLeagues = await this.leagueRepository.findAll();
|
||||
const leagueMap = new Map(allLeagues.map(l => [l.id, l.name]));
|
||||
|
||||
// Map to DTO
|
||||
const racesDTO = outputPort.races.map(race => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
status: race.status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
|
||||
strengthOfField: race.strengthOfField,
|
||||
isUpcoming: race.scheduledAt > new Date(),
|
||||
isLive: race.status === 'running',
|
||||
isPast: race.scheduledAt < new Date() && race.status === 'completed',
|
||||
}));
|
||||
|
||||
return {
|
||||
races: racesDTO,
|
||||
};
|
||||
const presenter = new RacesPageDataPresenter(this.leagueRepository);
|
||||
await presenter.present(result.value as RacesPageOutputPort);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getAllRacesPageData(): Promise<AllRacesPageDTO> {
|
||||
async getAllRacesPageData(): Promise<AllRacesPageDataPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching all races page data.');
|
||||
|
||||
const result = await this.getAllRacesPageDataUseCase.execute();
|
||||
@@ -248,10 +166,12 @@ export class RaceService {
|
||||
throw new Error('Failed to get all races page data');
|
||||
}
|
||||
|
||||
return result.value as AllRacesPageDTO;
|
||||
const presenter = new AllRacesPageDataPresenter();
|
||||
presenter.present(result.value);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailDTO> {
|
||||
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
|
||||
|
||||
const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
|
||||
@@ -260,43 +180,12 @@ export class RaceService {
|
||||
throw new Error('Failed to get race results detail');
|
||||
}
|
||||
|
||||
const outputPort = result.value as RaceResultsDetailOutputPort;
|
||||
|
||||
// Create a map of driverId to driver for easy lookup
|
||||
const driverMap = new Map(outputPort.drivers.map(driver => [driver.id, driver]));
|
||||
|
||||
const resultsDTO = await Promise.all(
|
||||
outputPort.results.map(async singleResult => {
|
||||
const driver = driverMap.get(singleResult.driverId.toString());
|
||||
if (!driver) {
|
||||
throw new Error(`Driver not found for result: ${singleResult.driverId}`);
|
||||
}
|
||||
|
||||
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
|
||||
|
||||
return {
|
||||
driverId: singleResult.driverId.toString(),
|
||||
driverName: driver.name.toString(),
|
||||
avatarUrl: avatarResult.avatarUrl,
|
||||
position: singleResult.position.toNumber(),
|
||||
startPosition: singleResult.startPosition.toNumber(),
|
||||
incidents: singleResult.incidents.toNumber(),
|
||||
fastestLap: singleResult.fastestLap.toNumber(),
|
||||
positionChange: singleResult.getPositionChange(),
|
||||
isPodium: singleResult.isPodium(),
|
||||
isClean: singleResult.isClean(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
raceId: outputPort.race.id,
|
||||
track: outputPort.race.track,
|
||||
results: resultsDTO,
|
||||
};
|
||||
const presenter = new RaceResultsDetailPresenter(this.imageService);
|
||||
await presenter.present(result.value as RaceResultsDetailOutputPort);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFDTO> {
|
||||
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
|
||||
|
||||
const result = await this.getRaceWithSOFUseCase.execute({ raceId });
|
||||
@@ -305,17 +194,12 @@ export class RaceService {
|
||||
throw new Error('Failed to get race with SOF');
|
||||
}
|
||||
|
||||
const outputPort = result.value as RaceWithSOFOutputPort;
|
||||
|
||||
// Map to DTO
|
||||
return {
|
||||
id: outputPort.id,
|
||||
track: outputPort.track,
|
||||
strengthOfField: outputPort.strengthOfField,
|
||||
};
|
||||
const presenter = new RaceWithSOFPresenter();
|
||||
presenter.present(result.value as RaceWithSOFOutputPort);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getRaceProtests(raceId: string): Promise<RaceProtestsDTO> {
|
||||
async getRaceProtests(raceId: string): Promise<RaceProtestsPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching race protests:', { raceId });
|
||||
|
||||
const result = await this.getRaceProtestsUseCase.execute({ raceId });
|
||||
@@ -324,32 +208,12 @@ export class RaceService {
|
||||
throw new Error('Failed to get race protests');
|
||||
}
|
||||
|
||||
const outputPort = result.value as RaceProtestsOutputPort;
|
||||
|
||||
const protestsDTO = outputPort.protests.map(protest => ({
|
||||
id: protest.id,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
incident: {
|
||||
lap: protest.incident.lap,
|
||||
description: protest.incident.description,
|
||||
},
|
||||
status: protest.status,
|
||||
filedAt: protest.filedAt.toISOString(),
|
||||
}));
|
||||
|
||||
const driverMap: Record<string, string> = {};
|
||||
outputPort.drivers.forEach(driver => {
|
||||
driverMap[driver.id] = driver.name.toString();
|
||||
});
|
||||
|
||||
return {
|
||||
protests: protestsDTO,
|
||||
driverMap,
|
||||
};
|
||||
const presenter = new RaceProtestsPresenter();
|
||||
presenter.present(result.value as RaceProtestsOutputPort);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getRacePenalties(raceId: string): Promise<RacePenaltiesDTO> {
|
||||
async getRacePenalties(raceId: string): Promise<RacePenaltiesPresenter> {
|
||||
this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
|
||||
|
||||
const result = await this.getRacePenaltiesUseCase.execute({ raceId });
|
||||
@@ -358,148 +222,175 @@ export class RaceService {
|
||||
throw new Error('Failed to get race penalties');
|
||||
}
|
||||
|
||||
const outputPort = result.value as RacePenaltiesOutputPort;
|
||||
|
||||
const penaltiesDTO = outputPort.penalties.map(penalty => ({
|
||||
id: penalty.id,
|
||||
driverId: penalty.driverId,
|
||||
type: penalty.type,
|
||||
value: penalty.value ?? 0,
|
||||
reason: penalty.reason,
|
||||
issuedBy: penalty.issuedBy,
|
||||
issuedAt: penalty.issuedAt.toISOString(),
|
||||
notes: penalty.notes,
|
||||
}));
|
||||
|
||||
const driverMap: Record<string, string> = {};
|
||||
outputPort.drivers.forEach(driver => {
|
||||
driverMap[driver.id] = driver.name.toString();
|
||||
});
|
||||
|
||||
return {
|
||||
penalties: penaltiesDTO,
|
||||
driverMap,
|
||||
};
|
||||
const presenter = new RacePenaltiesPresenter();
|
||||
presenter.present(result.value as RacePenaltiesOutputPort);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async registerForRace(params: RegisterForRaceParamsDTO): Promise<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to register for race');
|
||||
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<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to withdraw from race');
|
||||
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<void> {
|
||||
async cancelRace(params: RaceActionParamsDTO): 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()) {
|
||||
throw new Error('Failed to cancel race');
|
||||
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<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to complete race');
|
||||
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<void> {
|
||||
async reopenRace(params: RaceActionParamsDTO): 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_NOT_FOUND') {
|
||||
throw new NotFoundException('Race not found');
|
||||
}
|
||||
|
||||
if (errorCode === 'CANNOT_REOPEN_RUNNING_RACE') {
|
||||
throw new ConflictException('Cannot re-open a running race');
|
||||
}
|
||||
|
||||
if (errorCode === 'RACE_ALREADY_SCHEDULED') {
|
||||
this.logger.debug('[RaceService] Race is already scheduled, treating reopen as success.');
|
||||
return;
|
||||
presenter.presentSuccess('Race already scheduled');
|
||||
return presenter;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(errorCode ?? 'UNEXPECTED_ERROR');
|
||||
presenter.presentFailure(errorCode ?? 'UNEXPECTED_ERROR', 'Unexpected error while reopening race');
|
||||
return presenter;
|
||||
}
|
||||
|
||||
presenter.presentSuccess();
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async fileProtest(command: FileProtestCommandDTO): Promise<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to file protest');
|
||||
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<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to apply quick penalty');
|
||||
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<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to apply penalty');
|
||||
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<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to request protest defense');
|
||||
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<void> {
|
||||
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()) {
|
||||
throw new Error('Failed to review protest');
|
||||
const error = result.unwrapErr();
|
||||
presenter.presentFailure(error.code ?? 'FAILED_TO_REVIEW_PROTEST', 'Failed to review protest');
|
||||
return presenter;
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRatingChange(position: number): number {
|
||||
const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5;
|
||||
const positionBonus = Math.max(0, (20 - position) * 2);
|
||||
return baseChange + positionBonus;
|
||||
presenter.presentSuccess();
|
||||
return presenter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO';
|
||||
import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
|
||||
import { DashboardRecentResultDTO } from './DashboardRecentResultDTO';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsNumber, IsUrl } from 'class-validator';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsUrl } from 'class-validator';
|
||||
|
||||
export class FileProtestCommandDTO {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsBoolean, IsNumber } from 'class-validator';
|
||||
import { IsString, IsBoolean } from 'class-validator';
|
||||
|
||||
export class RaceDetailEntryDTO {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
|
||||
|
||||
export class AllRacesPageDataPresenter {
|
||||
private result: AllRacesPageDTO | null = null;
|
||||
|
||||
present(output: AllRacesPageDTO): void {
|
||||
this.result = output;
|
||||
}
|
||||
|
||||
getViewModel(): AllRacesPageDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): AllRacesPageDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface CommandResultViewModel {
|
||||
success: boolean;
|
||||
errorCode?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class CommandResultPresenter {
|
||||
private result: CommandResultViewModel | null = null;
|
||||
|
||||
presentSuccess(message?: string): void {
|
||||
this.result = {
|
||||
success: true,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
presentFailure(errorCode: string, message?: string): void {
|
||||
this.result = {
|
||||
success: false,
|
||||
errorCode,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): CommandResultViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): CommandResultViewModel {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
107
apps/api/src/domain/race/presenters/RaceDetailPresenter.ts
Normal file
107
apps/api/src/domain/race/presenters/RaceDetailPresenter.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
|
||||
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO';
|
||||
import type { RaceDetailDTO } from '../dtos/RaceDetailDTO';
|
||||
import type { RaceDetailRaceDTO } from '../dtos/RaceDetailRaceDTO';
|
||||
import type { RaceDetailLeagueDTO } from '../dtos/RaceDetailLeagueDTO';
|
||||
import type { RaceDetailEntryDTO } from '../dtos/RaceDetailEntryDTO';
|
||||
import type { RaceDetailRegistrationDTO } from '../dtos/RaceDetailRegistrationDTO';
|
||||
import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO';
|
||||
|
||||
export class RaceDetailPresenter {
|
||||
private result: RaceDetailDTO | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
private readonly imageService: IImageServicePort,
|
||||
) {}
|
||||
|
||||
async present(outputPort: RaceDetailOutputPort, params: GetRaceDetailParamsDTO): Promise<void> {
|
||||
const raceDTO: RaceDetailRaceDTO | null = outputPort.race
|
||||
? {
|
||||
id: outputPort.race.id,
|
||||
leagueId: outputPort.race.leagueId,
|
||||
track: outputPort.race.track,
|
||||
car: outputPort.race.car,
|
||||
scheduledAt: outputPort.race.scheduledAt.toISOString(),
|
||||
sessionType: outputPort.race.sessionType,
|
||||
status: outputPort.race.status,
|
||||
strengthOfField: outputPort.race.strengthOfField ?? null,
|
||||
registeredCount: outputPort.race.registeredCount ?? undefined,
|
||||
maxParticipants: outputPort.race.maxParticipants ?? undefined,
|
||||
}
|
||||
: null;
|
||||
|
||||
const leagueDTO: RaceDetailLeagueDTO | null = outputPort.league
|
||||
? {
|
||||
id: outputPort.league.id.toString(),
|
||||
name: outputPort.league.name.toString(),
|
||||
description: outputPort.league.description.toString(),
|
||||
settings: {
|
||||
maxDrivers: outputPort.league.settings.maxDrivers ?? undefined,
|
||||
qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
const entryListDTO: RaceDetailEntryDTO[] = await Promise.all(
|
||||
outputPort.drivers.map(async driver => {
|
||||
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id });
|
||||
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name.toString(),
|
||||
country: driver.country.toString(),
|
||||
avatarUrl: avatarResult.avatarUrl,
|
||||
rating: ratingResult.rating,
|
||||
isCurrentUser: driver.id === params.driverId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const registrationDTO: RaceDetailRegistrationDTO = {
|
||||
isUserRegistered: outputPort.isUserRegistered,
|
||||
canRegister: outputPort.canRegister,
|
||||
};
|
||||
|
||||
const userResultDTO: RaceDetailUserResultDTO | null = outputPort.userResult
|
||||
? {
|
||||
position: outputPort.userResult.position.toNumber(),
|
||||
startPosition: outputPort.userResult.startPosition.toNumber(),
|
||||
incidents: outputPort.userResult.incidents.toNumber(),
|
||||
fastestLap: outputPort.userResult.fastestLap.toNumber(),
|
||||
positionChange: outputPort.userResult.getPositionChange(),
|
||||
isPodium: outputPort.userResult.isPodium(),
|
||||
isClean: outputPort.userResult.isClean(),
|
||||
ratingChange: this.calculateRatingChange(outputPort.userResult.position.toNumber()),
|
||||
}
|
||||
: null;
|
||||
|
||||
this.result = {
|
||||
race: raceDTO,
|
||||
league: leagueDTO,
|
||||
entryList: entryListDTO,
|
||||
registration: registrationDTO,
|
||||
userResult: userResultDTO,
|
||||
} as RaceDetailDTO;
|
||||
}
|
||||
|
||||
getViewModel(): RaceDetailDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): RaceDetailDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
|
||||
private calculateRatingChange(position: number): number {
|
||||
const baseChange = position <= 3 ? 25 : position <= 10 ? 10 : -5;
|
||||
const positionBonus = Math.max(0, (20 - position) * 2);
|
||||
return baseChange + positionBonus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort';
|
||||
import type { RacePenaltiesDTO } from '../dtos/RacePenaltiesDTO';
|
||||
import type { RacePenaltyDTO } from '../dtos/RacePenaltyDTO';
|
||||
|
||||
export class RacePenaltiesPresenter {
|
||||
private result: RacePenaltiesDTO | null = null;
|
||||
|
||||
present(outputPort: RacePenaltiesOutputPort): void {
|
||||
const penalties: RacePenaltyDTO[] = outputPort.penalties.map(penalty => ({
|
||||
id: penalty.id,
|
||||
driverId: penalty.driverId,
|
||||
type: penalty.type,
|
||||
value: penalty.value ?? 0,
|
||||
reason: penalty.reason,
|
||||
issuedBy: penalty.issuedBy,
|
||||
issuedAt: penalty.issuedAt.toISOString(),
|
||||
notes: penalty.notes,
|
||||
} as RacePenaltyDTO));
|
||||
|
||||
const driverMap: Record<string, string> = {};
|
||||
outputPort.drivers.forEach(driver => {
|
||||
driverMap[driver.id] = driver.name.toString();
|
||||
});
|
||||
|
||||
this.result = {
|
||||
penalties,
|
||||
driverMap,
|
||||
} as RacePenaltiesDTO;
|
||||
}
|
||||
|
||||
getViewModel(): RacePenaltiesDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): RacePenaltiesDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
43
apps/api/src/domain/race/presenters/RaceProtestsPresenter.ts
Normal file
43
apps/api/src/domain/race/presenters/RaceProtestsPresenter.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort';
|
||||
import type { RaceProtestsDTO } from '../dtos/RaceProtestsDTO';
|
||||
import type { RaceProtestDTO } from '../dtos/RaceProtestDTO';
|
||||
|
||||
export class RaceProtestsPresenter {
|
||||
private result: RaceProtestsDTO | null = null;
|
||||
|
||||
present(outputPort: RaceProtestsOutputPort): void {
|
||||
const protests: RaceProtestDTO[] = outputPort.protests.map(protest => ({
|
||||
id: protest.id,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
incident: {
|
||||
lap: protest.incident.lap,
|
||||
description: protest.incident.description,
|
||||
},
|
||||
status: protest.status,
|
||||
filedAt: protest.filedAt.toISOString(),
|
||||
} as RaceProtestDTO));
|
||||
|
||||
const driverMap: Record<string, string> = {};
|
||||
outputPort.drivers.forEach(driver => {
|
||||
driverMap[driver.id] = driver.name.toString();
|
||||
});
|
||||
|
||||
this.result = {
|
||||
protests,
|
||||
driverMap,
|
||||
} as RaceProtestsDTO;
|
||||
}
|
||||
|
||||
getViewModel(): RaceProtestsDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): RaceProtestsDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import type { RaceResultsDetailDTO } from '../dtos/RaceResultsDetailDTO';
|
||||
import type { RaceResultDTO } from '../dtos/RaceResultDTO';
|
||||
|
||||
export class RaceResultsDetailPresenter {
|
||||
private result: RaceResultsDetailDTO | null = null;
|
||||
|
||||
constructor(private readonly imageService: IImageServicePort) {}
|
||||
|
||||
async present(outputPort: RaceResultsDetailOutputPort): Promise<void> {
|
||||
const driverMap = new Map(outputPort.drivers.map(driver => [driver.id, driver]));
|
||||
|
||||
const results: RaceResultDTO[] = await Promise.all(
|
||||
outputPort.results.map(async singleResult => {
|
||||
const driver = driverMap.get(singleResult.driverId.toString());
|
||||
if (!driver) {
|
||||
throw new Error(`Driver not found for result: ${singleResult.driverId}`);
|
||||
}
|
||||
|
||||
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
|
||||
|
||||
return {
|
||||
driverId: singleResult.driverId.toString(),
|
||||
driverName: driver.name.toString(),
|
||||
avatarUrl: avatarResult.avatarUrl,
|
||||
position: singleResult.position.toNumber(),
|
||||
startPosition: singleResult.startPosition.toNumber(),
|
||||
incidents: singleResult.incidents.toNumber(),
|
||||
fastestLap: singleResult.fastestLap.toNumber(),
|
||||
positionChange: singleResult.getPositionChange(),
|
||||
isPodium: singleResult.isPodium(),
|
||||
isClean: singleResult.isClean(),
|
||||
} as RaceResultDTO;
|
||||
}),
|
||||
);
|
||||
|
||||
this.result = {
|
||||
raceId: outputPort.race.id,
|
||||
track: outputPort.race.track,
|
||||
results,
|
||||
} as RaceResultsDetailDTO;
|
||||
}
|
||||
|
||||
getViewModel(): RaceResultsDetailDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): RaceResultsDetailDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
26
apps/api/src/domain/race/presenters/RaceWithSOFPresenter.ts
Normal file
26
apps/api/src/domain/race/presenters/RaceWithSOFPresenter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
|
||||
import type { RaceWithSOFDTO } from '../dtos/RaceWithSOFDTO';
|
||||
|
||||
export class RaceWithSOFPresenter {
|
||||
private result: RaceWithSOFDTO | null = null;
|
||||
|
||||
present(outputPort: RaceWithSOFOutputPort): void {
|
||||
this.result = {
|
||||
id: outputPort.id,
|
||||
track: outputPort.track,
|
||||
strengthOfField: outputPort.strengthOfField,
|
||||
} as RaceWithSOFDTO;
|
||||
}
|
||||
|
||||
getViewModel(): RaceWithSOFDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): RaceWithSOFDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { RacesPageDataDTO } from '../dtos/RacesPageDataDTO';
|
||||
import type { RacesPageDataRaceDTO } from '../dtos/RacesPageDataRaceDTO';
|
||||
|
||||
export class RacesPageDataPresenter {
|
||||
private result: RacesPageDataDTO | null = null;
|
||||
|
||||
constructor(private readonly leagueRepository: ILeagueRepository) {}
|
||||
|
||||
async present(outputPort: RacesPageOutputPort): Promise<void> {
|
||||
const allLeagues = await this.leagueRepository.findAll();
|
||||
const leagueMap = new Map(allLeagues.map(l => [l.id, l.name]));
|
||||
|
||||
const races: RacesPageDataRaceDTO[] = outputPort.races.map(race => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
status: race.status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
|
||||
strengthOfField: race.strengthOfField,
|
||||
isUpcoming: race.scheduledAt > new Date(),
|
||||
isLive: race.status === 'running',
|
||||
isPast: race.scheduledAt < new Date() && race.status === 'completed',
|
||||
}));
|
||||
|
||||
this.result = { races } as RacesPageDataDTO;
|
||||
}
|
||||
|
||||
getViewModel(): RacesPageDataDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): RacesPageDataDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user