refactor
This commit is contained in:
@@ -23,6 +23,14 @@ import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/U
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
|
||||
// Import presenters
|
||||
import { DriverStatsPresenter } from './presenters/DriverStatsPresenter';
|
||||
import { DriversLeaderboardPresenter } from './presenters/DriversLeaderboardPresenter';
|
||||
import { CompleteOnboardingPresenter } from './presenters/CompleteOnboardingPresenter';
|
||||
import { DriverRegistrationStatusPresenter } from './presenters/DriverRegistrationStatusPresenter';
|
||||
import { DriverPresenter } from './presenters/DriverPresenter';
|
||||
import { DriverProfilePresenter } from './presenters/DriverProfilePresenter';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankingService';
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
|
||||
import { GetDriverRegistrationStatusQueryDTO } from './dtos/GetDriverRegistrationStatusQueryDTO';
|
||||
import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
|
||||
import { DriverStatsDTO } from './dtos/DriverStatsDTO';
|
||||
import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
|
||||
import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
|
||||
import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO';
|
||||
import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO';
|
||||
|
||||
// Use cases
|
||||
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
@@ -51,37 +57,42 @@ export class DriverService {
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
@Inject(LOGGER_TOKEN)
|
||||
private readonly logger: Logger,
|
||||
private readonly driversLeaderboardPresenter: DriversLeaderboardPresenter,
|
||||
private readonly driverStatsPresenter: DriverStatsPresenter,
|
||||
private readonly completeOnboardingPresenter: CompleteOnboardingPresenter,
|
||||
private readonly driverRegistrationStatusPresenter: DriverRegistrationStatusPresenter,
|
||||
private readonly driverPresenter: DriverPresenter,
|
||||
private readonly driverProfilePresenter: DriverProfilePresenter,
|
||||
) {}
|
||||
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardPresenter> {
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardDTO> {
|
||||
this.logger.debug('[DriverService] Fetching drivers leaderboard.');
|
||||
|
||||
const result = await this.getDriversLeaderboardUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
throw new Error(`Failed to fetch drivers leaderboard: ${result.unwrapErr().details.message}`);
|
||||
}
|
||||
const result = await this.getDriversLeaderboardUseCase.execute({});
|
||||
|
||||
const presenter = new DriversLeaderboardPresenter();
|
||||
presenter.reset();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter;
|
||||
presenter.present(result);
|
||||
|
||||
return presenter.getResponseModel();
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsPresenter> {
|
||||
async getTotalDrivers(): Promise<DriverStatsDTO> {
|
||||
this.logger.debug('[DriverService] Fetching total drivers count.');
|
||||
|
||||
const result = await this.getTotalDriversUseCase.execute();
|
||||
const result = await this.getTotalDriversUseCase.execute({});
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
const error = result.unwrapErr();
|
||||
throw new Error(error.details?.message ?? 'Failed to load driver stats');
|
||||
}
|
||||
|
||||
const presenter = new DriverStatsPresenter();
|
||||
presenter.reset();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter;
|
||||
return this.driverStatsPresenter.getResponseModel();
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInputDTO): Promise<CompleteOnboardingPresenter> {
|
||||
async completeOnboarding(
|
||||
userId: string,
|
||||
input: CompleteOnboardingInputDTO,
|
||||
): Promise<CompleteOnboardingOutputDTO> {
|
||||
this.logger.debug('Completing onboarding for user:', userId);
|
||||
|
||||
const result = await this.completeDriverOnboardingUseCase.execute({
|
||||
@@ -95,20 +106,14 @@ export class DriverService {
|
||||
});
|
||||
|
||||
const presenter = new CompleteOnboardingPresenter();
|
||||
presenter.reset();
|
||||
presenter.present(result);
|
||||
|
||||
if (result.isOk()) {
|
||||
presenter.present(result.value);
|
||||
} else {
|
||||
presenter.presentError(result.error.code);
|
||||
}
|
||||
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
async getDriverRegistrationStatus(
|
||||
query: GetDriverRegistrationStatusQueryDTO,
|
||||
): Promise<DriverRegistrationStatusPresenter> {
|
||||
): Promise<DriverRegistrationStatusDTO> {
|
||||
this.logger.debug('Checking driver registration status:', query);
|
||||
|
||||
const result = await this.isDriverRegisteredForRaceUseCase.execute({
|
||||
@@ -116,77 +121,64 @@ export class DriverService {
|
||||
driverId: query.driverId,
|
||||
});
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(`Failed to check registration status: ${result.unwrapErr().code}`);
|
||||
}
|
||||
|
||||
const presenter = new DriverRegistrationStatusPresenter();
|
||||
presenter.reset();
|
||||
presenter.present(result);
|
||||
|
||||
const output = result.unwrap();
|
||||
presenter.present(output.isRegistered, output.raceId, output.driverId);
|
||||
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
async getCurrentDriver(userId: string): Promise<DriverPresenter> {
|
||||
async getCurrentDriver(userId: string): Promise<GetDriverOutputDTO | null> {
|
||||
this.logger.debug(`[DriverService] Fetching current driver for userId: ${userId}`);
|
||||
|
||||
const driver = await this.driverRepository.findById(userId);
|
||||
|
||||
const presenter = new DriverPresenter();
|
||||
presenter.reset();
|
||||
presenter.present(driver ?? null);
|
||||
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
async updateDriverProfile(
|
||||
driverId: string,
|
||||
bio?: string,
|
||||
country?: string,
|
||||
): Promise<DriverPresenter> {
|
||||
): Promise<GetDriverOutputDTO | null> {
|
||||
this.logger.debug(`[DriverService] Updating driver profile for driverId: ${driverId}`);
|
||||
|
||||
const result = await this.updateDriverProfileUseCase.execute({ driverId, bio, country });
|
||||
|
||||
const presenter = new DriverPresenter();
|
||||
presenter.reset();
|
||||
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Failed to update driver profile: ${result.error.code}`);
|
||||
presenter.present(null);
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
presenter.present(result.value);
|
||||
return presenter;
|
||||
const updatedDriver = await this.driverRepository.findById(driverId);
|
||||
presenter.present(updatedDriver ?? null);
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
async getDriver(driverId: string): Promise<DriverPresenter> {
|
||||
async getDriver(driverId: string): Promise<GetDriverOutputDTO | null> {
|
||||
this.logger.debug(`[DriverService] Fetching driver for driverId: ${driverId}`);
|
||||
|
||||
const driver = await this.driverRepository.findById(driverId);
|
||||
|
||||
const presenter = new DriverPresenter();
|
||||
presenter.reset();
|
||||
presenter.present(driver ?? null);
|
||||
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
async getDriverProfile(driverId: string): Promise<DriverProfilePresenter> {
|
||||
async getDriverProfile(driverId: string): Promise<GetDriverProfileOutputDTO> {
|
||||
this.logger.debug(`[DriverService] Fetching driver profile for driverId: ${driverId}`);
|
||||
|
||||
const result = await this.getProfileOverviewUseCase.execute({ driverId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(`Failed to fetch driver profile: ${result.error.code}`);
|
||||
}
|
||||
|
||||
const presenter = new DriverProfilePresenter();
|
||||
presenter.reset();
|
||||
presenter.present(result.value);
|
||||
presenter.present(result);
|
||||
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import type { CompleteDriverOnboardingOutputPort } from '@core/racing/application/ports/output/CompleteDriverOnboardingOutputPort';
|
||||
import type {
|
||||
CompleteDriverOnboardingResult,
|
||||
} from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import type { CompleteOnboardingOutputDTO } from '../dtos/CompleteOnboardingOutputDTO';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
export class CompleteOnboardingPresenter {
|
||||
private result: CompleteOnboardingOutputDTO | null = null;
|
||||
export class CompleteOnboardingPresenter
|
||||
implements UseCaseOutputPort<CompleteDriverOnboardingResult>
|
||||
{
|
||||
private responseModel: CompleteOnboardingOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: CompleteDriverOnboardingOutputPort): void {
|
||||
this.result = {
|
||||
present(result: CompleteDriverOnboardingResult): void {
|
||||
this.responseModel = {
|
||||
success: true,
|
||||
driverId: output.driverId,
|
||||
driverId: result.driver.id,
|
||||
};
|
||||
}
|
||||
|
||||
presentError(errorCode: string): void {
|
||||
this.result = {
|
||||
success: false,
|
||||
errorMessage: errorCode,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): CompleteOnboardingOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
getResponseModel(): CompleteOnboardingOutputDTO {
|
||||
if (!this.responseModel) throw new Error('Presenter not presented');
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@ import type { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import type { GetDriverOutputDTO } from '../dtos/GetDriverOutputDTO';
|
||||
|
||||
export class DriverPresenter {
|
||||
private result: GetDriverOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.result = null;
|
||||
}
|
||||
private responseModel: GetDriverOutputDTO | null = null;
|
||||
|
||||
present(driver: Driver | null): void {
|
||||
if (!driver) {
|
||||
this.result = null;
|
||||
this.responseModel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.result = {
|
||||
this.responseModel = {
|
||||
id: driver.id,
|
||||
iracingId: driver.iracingId.toString(),
|
||||
name: driver.name.toString(),
|
||||
@@ -24,7 +20,7 @@ export class DriverPresenter {
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): GetDriverOutputDTO | null {
|
||||
return this.result;
|
||||
getResponseModel(): GetDriverOutputDTO | null {
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,61 @@
|
||||
import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort';
|
||||
import type {
|
||||
GetProfileOverviewResult,
|
||||
} from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
import type { GetDriverProfileOutputDTO } from '../dtos/GetDriverProfileOutputDTO';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
export class DriverProfilePresenter {
|
||||
private result: GetDriverProfileOutputDTO | null = null;
|
||||
export class DriverProfilePresenter
|
||||
implements UseCaseOutputPort<GetProfileOverviewResult>
|
||||
{
|
||||
private responseModel: GetDriverProfileOutputDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: ProfileOverviewOutputPort): void {
|
||||
this.result = {
|
||||
currentDriver: output.driver
|
||||
present(result: GetProfileOverviewResult): void {
|
||||
this.responseModel = {
|
||||
currentDriver: result.driverInfo
|
||||
? {
|
||||
id: output.driver.id,
|
||||
name: output.driver.name,
|
||||
country: output.driver.country,
|
||||
avatarUrl: output.driver.avatarUrl,
|
||||
iracingId: output.driver.iracingId,
|
||||
joinedAt: output.driver.joinedAt.toISOString(),
|
||||
rating: output.driver.rating,
|
||||
globalRank: output.driver.globalRank,
|
||||
consistency: output.driver.consistency,
|
||||
bio: output.driver.bio,
|
||||
totalDrivers: output.driver.totalDrivers,
|
||||
id: result.driverInfo.driver.id,
|
||||
name: result.driverInfo.driver.name.toString(),
|
||||
country: result.driverInfo.driver.country.toString(),
|
||||
avatarUrl: this.getAvatarUrl(result.driverInfo.driver.id) || '',
|
||||
iracingId: result.driverInfo.driver.iracingId.toString(),
|
||||
joinedAt: result.driverInfo.driver.joinedAt.toDate().toISOString(),
|
||||
rating: result.driverInfo.rating,
|
||||
globalRank: result.driverInfo.globalRank,
|
||||
consistency: result.driverInfo.consistency,
|
||||
bio: result.driverInfo.driver.bio?.toString() || null,
|
||||
totalDrivers: result.driverInfo.totalDrivers,
|
||||
}
|
||||
: null,
|
||||
stats: output.stats,
|
||||
finishDistribution: output.finishDistribution,
|
||||
teamMemberships: output.teamMemberships.map(membership => ({
|
||||
teamId: membership.teamId,
|
||||
teamName: membership.teamName,
|
||||
teamTag: membership.teamTag,
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isCurrent: membership.isCurrent,
|
||||
stats: result.stats,
|
||||
finishDistribution: result.finishDistribution,
|
||||
teamMemberships: result.teamMemberships.map(membership => ({
|
||||
teamId: membership.team.id,
|
||||
teamName: membership.team.name.toString(),
|
||||
teamTag: membership.team.tag.toString(),
|
||||
role: membership.membership.role,
|
||||
joinedAt: membership.membership.joinedAt.toISOString(),
|
||||
isCurrent: true, // TODO: check membership status
|
||||
})),
|
||||
socialSummary: output.socialSummary,
|
||||
extendedProfile: output.extendedProfile,
|
||||
socialSummary: {
|
||||
friendsCount: result.socialSummary.friendsCount,
|
||||
friends: result.socialSummary.friends.map(friend => ({
|
||||
id: friend.id,
|
||||
name: friend.name.toString(),
|
||||
country: friend.country.toString(),
|
||||
avatarUrl: '', // TODO: get avatar
|
||||
})),
|
||||
},
|
||||
extendedProfile: result.extendedProfile as any,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): GetDriverProfileOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
getResponseModel(): GetDriverProfileOutputDTO {
|
||||
if (!this.responseModel) throw new Error('Presenter not presented');
|
||||
return this.responseModel;
|
||||
}
|
||||
|
||||
private getAvatarUrl(driverId: string): string | undefined {
|
||||
// Avatar resolution is delegated to infrastructure; keep as-is for now.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import type {
|
||||
IsDriverRegisteredForRaceResult,
|
||||
} from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { DriverRegistrationStatusDTO } from '../dtos/DriverRegistrationStatusDTO';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
export class DriverRegistrationStatusPresenter {
|
||||
private result: DriverRegistrationStatusDTO | null = null;
|
||||
export class DriverRegistrationStatusPresenter
|
||||
implements UseCaseOutputPort<IsDriverRegisteredForRaceResult>
|
||||
{
|
||||
private responseModel: DriverRegistrationStatusDTO | null = null;
|
||||
|
||||
reset(): void {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(isRegistered: boolean, raceId: string, driverId: string): void {
|
||||
this.result = {
|
||||
isRegistered,
|
||||
raceId,
|
||||
driverId,
|
||||
present(result: IsDriverRegisteredForRaceResult): void {
|
||||
this.responseModel = {
|
||||
isRegistered: result.isRegistered,
|
||||
raceId: result.raceId,
|
||||
driverId: result.driverId,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriverRegistrationStatusDTO {
|
||||
if (!this.result) {
|
||||
throw new Error('Presenter not presented');
|
||||
}
|
||||
|
||||
return this.result;
|
||||
getResponseModel(): DriverRegistrationStatusDTO {
|
||||
if (!this.responseModel) throw new Error('Presenter not presented');
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import { DriverStatsPresenter } from './DriverStatsPresenter';
|
||||
import type { TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
|
||||
import type { GetTotalDriversResult } from '../../../../../core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
|
||||
describe('DriverStatsPresenter', () => {
|
||||
let presenter: DriverStatsPresenter;
|
||||
@@ -10,16 +11,18 @@ describe('DriverStatsPresenter', () => {
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map core DTO to API view model correctly', () => {
|
||||
const dto: TotalDriversResultDTO = {
|
||||
it('should map core result to API response model correctly', () => {
|
||||
const output: GetTotalDriversResult = {
|
||||
totalDrivers: 42,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
const result = Result.ok<GetTotalDriversResult, never>(output);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
presenter.present(result);
|
||||
|
||||
expect(result).toEqual({
|
||||
const response = presenter.responseModel;
|
||||
|
||||
expect(response).toEqual({
|
||||
totalDrivers: 42,
|
||||
});
|
||||
});
|
||||
@@ -27,15 +30,17 @@ describe('DriverStatsPresenter', () => {
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result', () => {
|
||||
const dto: TotalDriversResultDTO = {
|
||||
const output: GetTotalDriversResult = {
|
||||
totalDrivers: 10,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
expect(presenter.viewModel).toBeDefined();
|
||||
const result = Result.ok<GetTotalDriversResult, never>(output);
|
||||
|
||||
presenter.present(result);
|
||||
expect(presenter.responseModel).toBeDefined();
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
expect(() => presenter.responseModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,22 @@
|
||||
import { DriverStatsDTO } from '../dtos/DriverStatsDTO';
|
||||
import type { TotalDriversOutputPort } from '../../../../../core/racing/application/ports/output/TotalDriversOutputPort';
|
||||
import type {
|
||||
GetTotalDriversResult,
|
||||
} from '@core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
export class DriverStatsPresenter {
|
||||
private result: DriverStatsDTO | null = null;
|
||||
export class DriverStatsPresenter
|
||||
implements UseCaseOutputPort<GetTotalDriversResult>
|
||||
{
|
||||
private responseModel: DriverStatsDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: TotalDriversOutputPort) {
|
||||
this.result = {
|
||||
totalDrivers: output.totalDrivers,
|
||||
present(result: GetTotalDriversResult): void {
|
||||
this.responseModel = {
|
||||
totalDrivers: result.totalDrivers,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriverStatsDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
getResponseModel(): DriverStatsDTO {
|
||||
if (!this.responseModel) throw new Error('Presenter not presented');
|
||||
return this.responseModel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import { DriversLeaderboardPresenter } from './DriversLeaderboardPresenter';
|
||||
import type { DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import type { GetDriversLeaderboardResult } from '../../../../../core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
|
||||
describe('DriversLeaderboardPresenter', () => {
|
||||
let presenter: DriversLeaderboardPresenter;
|
||||
@@ -10,41 +11,50 @@ describe('DriversLeaderboardPresenter', () => {
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should map core DTO to API view model correctly', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
it('should map core result to API response model correctly', () => {
|
||||
const coreResult: GetDriversLeaderboardResult = {
|
||||
items: [
|
||||
{
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
country: 'US',
|
||||
iracingId: '12345',
|
||||
joinedAt: new Date('2023-01-01'),
|
||||
driver: {
|
||||
id: 'driver-1',
|
||||
name: 'Driver One' as any,
|
||||
country: 'US' as any,
|
||||
} as any,
|
||||
rating: 2500,
|
||||
skillLevel: 'advanced' as any,
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
podiums: 20,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'https://example.com/avatar1.png',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
country: 'DE',
|
||||
iracingId: '67890',
|
||||
joinedAt: new Date('2023-01-02'),
|
||||
driver: {
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two' as any,
|
||||
country: 'DE' as any,
|
||||
} as any,
|
||||
rating: 2400,
|
||||
skillLevel: 'intermediate' as any,
|
||||
racesCompleted: 40,
|
||||
wins: 5,
|
||||
podiums: 15,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'https://example.com/avatar2.png',
|
||||
},
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
{ driverId: 'driver-2', rating: 2400, overallRank: 2 },
|
||||
],
|
||||
stats: {
|
||||
'driver-1': { racesCompleted: 50, wins: 10, podiums: 20 },
|
||||
'driver-2': { racesCompleted: 40, wins: 5, podiums: 15 },
|
||||
},
|
||||
avatarUrls: {
|
||||
'driver-1': 'https://example.com/avatar1.png',
|
||||
'driver-2': 'https://example.com/avatar2.png',
|
||||
},
|
||||
totalRaces: 90,
|
||||
totalWins: 15,
|
||||
activeCount: 2,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
const result = Result.ok<GetDriversLeaderboardResult, never>(coreResult);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
presenter.present(result);
|
||||
|
||||
const api = presenter.responseModel;
|
||||
|
||||
expect(result.drivers).toHaveLength(2);
|
||||
expect(result.drivers[0]).toEqual({
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
import type { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { DriversLeaderboardDTO } from '../dtos/DriversLeaderboardDTO';
|
||||
import type { DriversLeaderboardOutputPort } from '../../../../../core/racing/application/ports/output/DriversLeaderboardOutputPort';
|
||||
import type {
|
||||
GetDriversLeaderboardResult,
|
||||
GetDriversLeaderboardErrorCode,
|
||||
} from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
|
||||
export type DriversLeaderboardApplicationError = ApplicationErrorCode<
|
||||
GetDriversLeaderboardErrorCode,
|
||||
{ message: string }
|
||||
>;
|
||||
|
||||
export class DriversLeaderboardPresenter {
|
||||
private result: DriversLeaderboardDTO | null = null;
|
||||
present(
|
||||
result: Result<GetDriversLeaderboardResult, DriversLeaderboardApplicationError>,
|
||||
): DriversLeaderboardDTO {
|
||||
if (result.isErr()) {
|
||||
const error = result.unwrapErr();
|
||||
throw new Error(error.details?.message ?? 'Failed to load drivers leaderboard');
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.result = null;
|
||||
}
|
||||
const output = result.unwrap();
|
||||
|
||||
present(output: DriversLeaderboardOutputPort): void {
|
||||
this.result = {
|
||||
drivers: output.drivers.map(driver => ({
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating: driver.rating,
|
||||
skillLevel: driver.skillLevel,
|
||||
nationality: driver.nationality,
|
||||
racesCompleted: driver.racesCompleted,
|
||||
wins: driver.wins,
|
||||
podiums: driver.podiums,
|
||||
isActive: driver.isActive,
|
||||
rank: driver.rank,
|
||||
avatarUrl: driver.avatarUrl,
|
||||
return {
|
||||
drivers: output.items.map(item => ({
|
||||
id: item.driver.id,
|
||||
name: item.driver.name.toString(),
|
||||
rating: item.rating,
|
||||
skillLevel: item.skillLevel,
|
||||
nationality: item.driver.country.toString(),
|
||||
racesCompleted: item.racesCompleted,
|
||||
wins: item.wins,
|
||||
podiums: item.podiums,
|
||||
isActive: item.isActive,
|
||||
rank: item.rank,
|
||||
avatarUrl: item.avatarUrl,
|
||||
})),
|
||||
totalRaces: output.totalRaces,
|
||||
totalWins: output.totalWins,
|
||||
activeCount: output.activeCount,
|
||||
totalRaces: output.items.reduce((sum, d) => sum + d.racesCompleted, 0),
|
||||
totalWins: output.items.reduce((sum, d) => sum + d.wins, 0),
|
||||
activeCount: output.items.filter(d => d.isActive).length,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DriversLeaderboardDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user