This commit is contained in:
2025-12-21 19:53:22 +01:00
parent f2d8a23583
commit 3c64f328e2
105 changed files with 3191 additions and 1706 deletions

View File

@@ -96,25 +96,20 @@ export class RaceService {
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 result = await this.getAllRacesUseCase.execute({});
const presenter = new GetAllRacesPresenter();
await presenter.present(result.unwrap());
presenter.reset();
presenter.present(result);
return presenter;
}
async getTotalRaces(): Promise<GetTotalRacesPresenter> {
this.logger.debug('[RaceService] Fetching total races count.');
const result = await this.getTotalRacesUseCase.execute();
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
const result = await this.getTotalRacesUseCase.execute({});
const presenter = new GetTotalRacesPresenter();
presenter.present(result.unwrap());
presenter.present(result);
return presenter;
}

View File

@@ -1,21 +1,50 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetAllRacesPageDataResult,
GetAllRacesPageDataErrorCode,
} from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase';
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export type AllRacesPageDataResponseModel = AllRacesPageDTO;
export type GetAllRacesPageDataApplicationError = ApplicationErrorCode<
GetAllRacesPageDataErrorCode,
{ message: string }
>;
export class AllRacesPageDataPresenter {
private result: AllRacesPageDTO | null = null;
private model: AllRacesPageDataResponseModel | null = null;
present(output: AllRacesPageDTO): void {
this.result = output;
reset(): void {
this.model = null;
}
getViewModel(): AllRacesPageDTO | null {
return this.result;
present(
result: Result<GetAllRacesPageDataResult, GetAllRacesPageDataApplicationError>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get all races page data');
}
const output = result.unwrap();
this.model = {
races: output.races,
filters: output.filters,
};
}
get viewModel(): AllRacesPageDTO {
if (!this.result) {
getResponseModel(): AllRacesPageDataResponseModel | null {
return this.model;
}
get responseModel(): AllRacesPageDataResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}

View File

@@ -1,36 +1,62 @@
export interface CommandResultViewModel {
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
export interface CommandResultDTO {
success: boolean;
errorCode?: string;
message?: string;
}
export class CommandResultPresenter {
private result: CommandResultViewModel | null = null;
export type CommandApplicationError<E extends string = string> = ApplicationErrorCode<
E,
{ message: string }
>;
export class CommandResultPresenter<E extends string = string> {
private model: CommandResultDTO | null = null;
reset(): void {
this.model = null;
}
present(result: Result<unknown, CommandApplicationError<E>>): void {
if (result.isErr()) {
const error = result.unwrapErr();
this.model = {
success: false,
errorCode: error.code,
message: error.details?.message,
};
return;
}
this.model = { success: true };
}
presentSuccess(message?: string): void {
this.result = {
this.model = {
success: true,
message,
};
}
presentFailure(errorCode: string, message?: string): void {
this.result = {
this.model = {
success: false,
errorCode,
message,
};
}
getViewModel(): CommandResultViewModel | null {
return this.result;
getResponseModel(): CommandResultDTO | null {
return this.model;
}
get viewModel(): CommandResultViewModel {
if (!this.result) {
get responseModel(): CommandResultDTO {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}

View File

@@ -1,5 +1,6 @@
import { Result } from '@core/shared/application/Result';
import { GetAllRacesPresenter } from './GetAllRacesPresenter';
import type { GetAllRacesOutputPort } from '@core/racing/application/ports/output/GetAllRacesOutputPort';
import type { GetAllRacesResult } from '@core/racing/application/use-cases/GetAllRacesUseCase';
describe('GetAllRacesPresenter', () => {
it('should map races and distinct leagues into the DTO', async () => {

View File

@@ -1,33 +1,53 @@
import { GetAllRacesOutputPort } from '@core/racing/application/ports/output/GetAllRacesOutputPort';
import { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetAllRacesResult,
GetAllRacesErrorCode,
} from '@core/racing/application/use-cases/GetAllRacesUseCase';
import type { AllRacesPageDTO } from '../dtos/AllRacesPageDTO';
export type GetAllRacesResponseModel = AllRacesPageDTO;
export type GetAllRacesApplicationError = ApplicationErrorCode<
GetAllRacesErrorCode,
{ message: string }
>;
export class GetAllRacesPresenter {
private result: AllRacesPageDTO | null = null;
private model: GetAllRacesResponseModel | null = null;
reset() {
this.result = null;
reset(): void {
this.model = null;
}
async present(output: GetAllRacesOutputPort) {
const uniqueLeagues = new Map<string, { id: string; name: string }>();
for (const race of output.races) {
uniqueLeagues.set(race.leagueId, {
id: race.leagueId,
name: race.leagueName,
});
present(result: Result<GetAllRacesResult, GetAllRacesApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get all races');
}
this.result = {
const output = result.unwrap();
const leagueMap = new Map<string, string>();
const uniqueLeagues = new Map<string, { id: string; name: string }>();
for (const league of output.leagues) {
const id = league.id.toString();
const name = league.name.toString();
leagueMap.set(id, name);
uniqueLeagues.set(id, { id, name });
}
this.model = {
races: output.races.map(race => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
scheduledAt: race.scheduledAt.toISOString(),
status: race.status,
leagueId: race.leagueId,
leagueName: race.leagueName,
strengthOfField: race.strengthOfField,
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
strengthOfField: race.strengthOfField ?? null,
})),
filters: {
statuses: [
@@ -42,7 +62,15 @@ export class GetAllRacesPresenter {
};
}
getViewModel(): AllRacesPageDTO | null {
return this.result;
getResponseModel(): GetAllRacesResponseModel | null {
return this.model;
}
get responseModel(): GetAllRacesResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
}
}

View File

@@ -1,20 +1,47 @@
import { GetTotalRacesOutputPort } from '@core/racing/application/ports/output/GetTotalRacesOutputPort';
import { RaceStatsDTO } from '../dtos/RaceStatsDTO';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetTotalRacesResult,
GetTotalRacesErrorCode,
} from '@core/racing/application/use-cases/GetTotalRacesUseCase';
import type { RaceStatsDTO } from '../dtos/RaceStatsDTO';
export type GetTotalRacesResponseModel = RaceStatsDTO;
export type GetTotalRacesApplicationError = ApplicationErrorCode<
GetTotalRacesErrorCode,
{ message: string }
>;
export class GetTotalRacesPresenter {
private result: RaceStatsDTO | null = null;
private model: GetTotalRacesResponseModel | null = null;
reset() {
this.result = null;
reset(): void {
this.model = null;
}
present(output: GetTotalRacesOutputPort) {
this.result = {
present(result: Result<GetTotalRacesResult, GetTotalRacesApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get total races');
}
const output = result.unwrap();
this.model = {
totalRaces: output.totalRaces,
};
}
getViewModel(): RaceStatsDTO | null {
return this.result;
getResponseModel(): GetTotalRacesResponseModel | null {
return this.model;
}
get responseModel(): GetTotalRacesResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
}
}

View File

@@ -1,15 +1,36 @@
import { ImportRaceResultsApiOutputPort } from '@core/racing/application/ports/output/ImportRaceResultsApiOutputPort';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
ImportRaceResultsApiResult,
ImportRaceResultsApiErrorCode,
} from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase';
import { ImportRaceResultsSummaryDTO } from '../dtos/ImportRaceResultsSummaryDTO';
export class ImportRaceResultsApiPresenter {
private result: ImportRaceResultsSummaryDTO | null = null;
export type ImportRaceResultsApiResponseModel = ImportRaceResultsSummaryDTO;
reset() {
this.result = null;
export type ImportRaceResultsApiApplicationError = ApplicationErrorCode<
ImportRaceResultsApiErrorCode,
{ message: string }
>;
export class ImportRaceResultsApiPresenter {
private model: ImportRaceResultsApiResponseModel | null = null;
reset(): void {
this.model = null;
}
present(output: ImportRaceResultsApiOutputPort) {
this.result = {
present(
result: Result<ImportRaceResultsApiResult, ImportRaceResultsApiApplicationError>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to import race results');
}
const output = result.unwrap();
this.model = {
success: output.success,
raceId: output.raceId,
driversProcessed: output.driversProcessed,
@@ -18,7 +39,15 @@ export class ImportRaceResultsApiPresenter {
};
}
getViewModel(): ImportRaceResultsSummaryDTO | null {
return this.result;
getResponseModel(): ImportRaceResultsApiResponseModel | null {
return this.model;
}
get responseModel(): ImportRaceResultsApiResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.model;
}
}

View File

@@ -1,4 +1,9 @@
import type { RaceDetailOutputPort } from '@core/racing/application/ports/output/RaceDetailOutputPort';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceDetailResult,
GetRaceDetailErrorCode,
} from '@core/racing/application/use-cases/GetRaceDetailUseCase';
import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { GetRaceDetailParamsDTO } from '../dtos/GetRaceDetailParamsDTO';
@@ -9,44 +14,79 @@ import type { RaceDetailEntryDTO } from '../dtos/RaceDetailEntryDTO';
import type { RaceDetailRegistrationDTO } from '../dtos/RaceDetailRegistrationDTO';
import type { RaceDetailUserResultDTO } from '../dtos/RaceDetailUserResultDTO';
export type GetRaceDetailResponseModel = RaceDetailDTO;
export type GetRaceDetailApplicationError = ApplicationErrorCode<
GetRaceDetailErrorCode,
{ message: string }
>;
export class RaceDetailPresenter {
private result: RaceDetailDTO | null = null;
private model: GetRaceDetailResponseModel | 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
reset(): void {
this.model = null;
}
async present(
result: Result<GetRaceDetailResult, GetRaceDetailApplicationError>,
params: GetRaceDetailParamsDTO,
): Promise<void> {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
race: null,
league: null,
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
userResult: null,
} as RaceDetailDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race detail');
}
const output = result.unwrap();
const raceDTO: RaceDetailRaceDTO | null = output.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,
id: output.race.id,
leagueId: output.race.leagueId,
track: output.race.track,
car: output.race.car,
scheduledAt: output.race.scheduledAt.toISOString(),
sessionType: output.race.sessionType,
status: output.race.status,
strengthOfField: output.race.strengthOfField ?? null,
registeredCount: output.race.registeredCount ?? undefined,
maxParticipants: output.race.maxParticipants ?? undefined,
}
: null;
const leagueDTO: RaceDetailLeagueDTO | null = outputPort.league
const leagueDTO: RaceDetailLeagueDTO | null = output.league
? {
id: outputPort.league.id.toString(),
name: outputPort.league.name.toString(),
description: outputPort.league.description.toString(),
id: output.league.id.toString(),
name: output.league.name.toString(),
description: output.league.description.toString(),
settings: {
maxDrivers: outputPort.league.settings.maxDrivers ?? undefined,
qualifyingFormat: outputPort.league.settings.qualifyingFormat ?? undefined,
maxDrivers: output.league.settings.maxDrivers ?? undefined,
qualifyingFormat: output.league.settings.qualifyingFormat ?? undefined,
},
}
: null;
const entryListDTO: RaceDetailEntryDTO[] = await Promise.all(
outputPort.drivers.map(async driver => {
output.drivers.map(async driver => {
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id });
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id });
return {
@@ -61,24 +101,24 @@ export class RaceDetailPresenter {
);
const registrationDTO: RaceDetailRegistrationDTO = {
isUserRegistered: outputPort.isUserRegistered,
canRegister: outputPort.canRegister,
isUserRegistered: output.isUserRegistered,
canRegister: output.canRegister,
};
const userResultDTO: RaceDetailUserResultDTO | null = outputPort.userResult
const userResultDTO: RaceDetailUserResultDTO | null = output.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()),
position: output.userResult.position.toNumber(),
startPosition: output.userResult.startPosition.toNumber(),
incidents: output.userResult.incidents.toNumber(),
fastestLap: output.userResult.fastestLap.toNumber(),
positionChange: output.userResult.getPositionChange(),
isPodium: output.userResult.isPodium(),
isClean: output.userResult.isClean(),
ratingChange: this.calculateRatingChange(output.userResult.position.toNumber()),
}
: null;
this.result = {
this.model = {
race: raceDTO,
league: leagueDTO,
entryList: entryListDTO,
@@ -87,16 +127,16 @@ export class RaceDetailPresenter {
} as RaceDetailDTO;
}
getViewModel(): RaceDetailDTO | null {
return this.result;
getResponseModel(): GetRaceDetailResponseModel | null {
return this.model;
}
get viewModel(): RaceDetailDTO {
if (!this.result) {
get responseModel(): GetRaceDetailResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
private calculateRatingChange(position: number): number {

View File

@@ -1,12 +1,35 @@
import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRacePenaltiesResult,
GetRacePenaltiesErrorCode,
} from '@core/racing/application/use-cases/GetRacePenaltiesUseCase';
import type { RacePenaltiesDTO } from '../dtos/RacePenaltiesDTO';
import type { RacePenaltyDTO } from '../dtos/RacePenaltyDTO';
export class RacePenaltiesPresenter {
private result: RacePenaltiesDTO | null = null;
export type GetRacePenaltiesResponseModel = RacePenaltiesDTO;
present(outputPort: RacePenaltiesOutputPort): void {
const penalties: RacePenaltyDTO[] = outputPort.penalties.map(penalty => ({
export type GetRacePenaltiesApplicationError = ApplicationErrorCode<
GetRacePenaltiesErrorCode,
{ message: string }
>;
export class RacePenaltiesPresenter {
private model: GetRacePenaltiesResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<GetRacePenaltiesResult, GetRacePenaltiesApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get race penalties');
}
const output = result.unwrap();
const penalties: RacePenaltyDTO[] = output.penalties.map(penalty => ({
id: penalty.id,
driverId: penalty.driverId,
type: penalty.type,
@@ -18,25 +41,25 @@ export class RacePenaltiesPresenter {
} as RacePenaltyDTO));
const driverMap: Record<string, string> = {};
outputPort.drivers.forEach(driver => {
output.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString();
});
this.result = {
this.model = {
penalties,
driverMap,
} as RacePenaltiesDTO;
}
getViewModel(): RacePenaltiesDTO | null {
return this.result;
getResponseModel(): GetRacePenaltiesResponseModel | null {
return this.model;
}
get viewModel(): RacePenaltiesDTO {
if (!this.result) {
get responseModel(): GetRacePenaltiesResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}

View File

@@ -1,12 +1,35 @@
import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceProtestsResult,
GetRaceProtestsErrorCode,
} from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
import type { RaceProtestsDTO } from '../dtos/RaceProtestsDTO';
import type { RaceProtestDTO } from '../dtos/RaceProtestDTO';
export class RaceProtestsPresenter {
private result: RaceProtestsDTO | null = null;
export type GetRaceProtestsResponseModel = RaceProtestsDTO;
present(outputPort: RaceProtestsOutputPort): void {
const protests: RaceProtestDTO[] = outputPort.protests.map(protest => ({
export type GetRaceProtestsApplicationError = ApplicationErrorCode<
GetRaceProtestsErrorCode,
{ message: string }
>;
export class RaceProtestsPresenter {
private model: GetRaceProtestsResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<GetRaceProtestsResult, GetRaceProtestsApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get race protests');
}
const output = result.unwrap();
const protests: RaceProtestDTO[] = output.protests.map(protest => ({
id: protest.id,
protestingDriverId: protest.protestingDriverId,
accusedDriverId: protest.accusedDriverId,
@@ -19,25 +42,25 @@ export class RaceProtestsPresenter {
} as RaceProtestDTO));
const driverMap: Record<string, string> = {};
outputPort.drivers.forEach(driver => {
output.drivers.forEach(driver => {
driverMap[driver.id] = driver.name.toString();
});
this.result = {
this.model = {
protests,
driverMap,
} as RaceProtestsDTO;
}
getViewModel(): RaceProtestsDTO | null {
return this.result;
getResponseModel(): GetRaceProtestsResponseModel | null {
return this.model;
}
get viewModel(): RaceProtestsDTO {
if (!this.result) {
get responseModel(): GetRaceProtestsResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}

View File

@@ -1,18 +1,53 @@
import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceResultsDetailResult,
GetRaceResultsDetailErrorCode,
} from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import type { RaceResultsDetailDTO } from '../dtos/RaceResultsDetailDTO';
import type { RaceResultDTO } from '../dtos/RaceResultDTO';
export type GetRaceResultsDetailResponseModel = RaceResultsDetailDTO;
export type GetRaceResultsDetailApplicationError = ApplicationErrorCode<
GetRaceResultsDetailErrorCode,
{ message: string }
>;
export class RaceResultsDetailPresenter {
private result: RaceResultsDetailDTO | null = null;
private model: GetRaceResultsDetailResponseModel | null = null;
constructor(private readonly imageService: IImageServicePort) {}
async present(outputPort: RaceResultsDetailOutputPort): Promise<void> {
const driverMap = new Map(outputPort.drivers.map(driver => [driver.id, driver]));
reset(): void {
this.model = null;
}
async present(
result: Result<GetRaceResultsDetailResult, GetRaceResultsDetailApplicationError>,
): Promise<void> {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
raceId: '',
track: '',
results: [],
} as RaceResultsDetailDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race results detail');
}
const output = result.unwrap();
const driverMap = new Map(output.drivers.map(driver => [driver.id, driver]));
const results: RaceResultDTO[] = await Promise.all(
outputPort.results.map(async singleResult => {
output.results.map(async singleResult => {
const driver = driverMap.get(singleResult.driverId.toString());
if (!driver) {
throw new Error(`Driver not found for result: ${singleResult.driverId}`);
@@ -35,22 +70,22 @@ export class RaceResultsDetailPresenter {
}),
);
this.result = {
raceId: outputPort.race.id,
track: outputPort.race.track,
this.model = {
raceId: output.race.id,
track: output.race.track,
results,
} as RaceResultsDetailDTO;
}
getViewModel(): RaceResultsDetailDTO | null {
return this.result;
getResponseModel(): GetRaceResultsDetailResponseModel | null {
return this.model;
}
get viewModel(): RaceResultsDetailDTO {
if (!this.result) {
get responseModel(): GetRaceResultsDetailResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}

View File

@@ -1,26 +1,58 @@
import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRaceWithSOFResult,
GetRaceWithSOFErrorCode,
} from '@core/racing/application/use-cases/GetRaceWithSOFUseCase';
import type { RaceWithSOFDTO } from '../dtos/RaceWithSOFDTO';
export class RaceWithSOFPresenter {
private result: RaceWithSOFDTO | null = null;
export type GetRaceWithSOFResponseModel = RaceWithSOFDTO;
present(outputPort: RaceWithSOFOutputPort): void {
this.result = {
id: outputPort.id,
track: outputPort.track,
strengthOfField: outputPort.strengthOfField,
export type GetRaceWithSOFApplicationError = ApplicationErrorCode<
GetRaceWithSOFErrorCode,
{ message: string }
>;
export class RaceWithSOFPresenter {
private model: GetRaceWithSOFResponseModel | null = null;
reset(): void {
this.model = null;
}
present(result: Result<GetRaceWithSOFResult, GetRaceWithSOFApplicationError>): void {
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'RACE_NOT_FOUND') {
this.model = {
id: '',
track: '',
strengthOfField: null,
} as RaceWithSOFDTO;
return;
}
throw new Error(error.details?.message ?? 'Failed to get race with SOF');
}
const output = result.unwrap();
this.model = {
id: output.race.id,
track: output.race.track,
strengthOfField: output.strengthOfField,
} as RaceWithSOFDTO;
}
getViewModel(): RaceWithSOFDTO | null {
return this.result;
getResponseModel(): GetRaceWithSOFResponseModel | null {
return this.model;
}
get viewModel(): RaceWithSOFDTO {
if (!this.result) {
get responseModel(): GetRaceWithSOFResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}

View File

@@ -1,43 +1,62 @@
import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetRacesPageDataResult,
GetRacesPageDataErrorCode,
} from '@core/racing/application/use-cases/GetRacesPageDataUseCase';
import type { RacesPageDataDTO } from '../dtos/RacesPageDataDTO';
import type { RacesPageDataRaceDTO } from '../dtos/RacesPageDataRaceDTO';
export type GetRacesPageDataResponseModel = RacesPageDataDTO;
export type GetRacesPageDataApplicationError = ApplicationErrorCode<
GetRacesPageDataErrorCode,
{ message: string }
>;
export class RacesPageDataPresenter {
private result: RacesPageDataDTO | null = null;
private model: GetRacesPageDataResponseModel | null = null;
constructor(private readonly leagueRepository: ILeagueRepository) {}
reset(): void {
this.model = null;
}
async present(outputPort: RacesPageOutputPort): Promise<void> {
const allLeagues = await this.leagueRepository.findAll();
const leagueMap = new Map(allLeagues.map(l => [l.id, l.name]));
present(
result: Result<GetRacesPageDataResult, GetRacesPageDataApplicationError>,
): void {
if (result.isErr()) {
const error = result.unwrapErr();
throw new Error(error.details?.message ?? 'Failed to get races page data');
}
const races: RacesPageDataRaceDTO[] = outputPort.races.map(race => ({
const output = result.unwrap();
const races: RacesPageDataRaceDTO[] = output.races.map(({ race, leagueName }) => ({
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,
leagueName,
strengthOfField: race.strengthOfField ?? null,
isUpcoming: race.scheduledAt > new Date(),
isLive: race.status === 'running',
isPast: race.scheduledAt < new Date() && race.status === 'completed',
}));
this.result = { races } as RacesPageDataDTO;
this.model = { races } as RacesPageDataDTO;
}
getViewModel(): RacesPageDataDTO | null {
return this.result;
getResponseModel(): GetRacesPageDataResponseModel | null {
return this.model;
}
get viewModel(): RacesPageDataDTO {
if (!this.result) {
get responseModel(): GetRacesPageDataResponseModel {
if (!this.model) {
throw new Error('Presenter not presented');
}
return this.result;
return this.model;
}
}