refactor api modules

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

View File

@@ -1,24 +1,24 @@
import { Module } from '@nestjs/common';
import { HelloController } from './presentation/hello.controller';
import { HelloService } from './application/hello/hello.service';
import { AnalyticsModule } from './domain/analytics/AnalyticsModule';
import { DatabaseModule } from './infrastructure/database/database.module';
import { LoggingModule } from './infrastructure/logging/LoggingModule';
import { BootstrapModule } from './infrastructure/bootstrap/BootstrapModule';
import { AuthModule } from './domain/auth/AuthModule';
import { BootstrapModule } from './domain/bootstrap/BootstrapModule';
import { DashboardModule } from './domain/dashboard/DashboardModule';
import { LeagueModule } from './domain/league/LeagueModule';
import { RaceModule } from './domain/race/RaceModule';
import { ProtestsModule } from './domain/protests/ProtestsModule';
import { TeamModule } from './domain/team/TeamModule';
import { SponsorModule } from './domain/sponsor/SponsorModule';
import { DatabaseModule } from './domain/database/DatabaseModule';
import { DriverModule } from './domain/driver/DriverModule';
import { HelloModule } from './domain/hello/HelloModule';
import { LeagueModule } from './domain/league/LeagueModule';
import { LoggingModule } from './domain/logging/LoggingModule';
import { MediaModule } from './domain/media/MediaModule';
import { PaymentsModule } from './domain/payments/PaymentsModule';
import { ProtestsModule } from './domain/protests/ProtestsModule';
import { RaceModule } from './domain/race/RaceModule';
import { SponsorModule } from './domain/sponsor/SponsorModule';
import { TeamModule } from './domain/team/TeamModule';
@Module({
imports: [
HelloModule,
DatabaseModule,
LoggingModule,
BootstrapModule,
@@ -34,7 +34,5 @@ import { PaymentsModule } from './domain/payments/PaymentsModule';
MediaModule,
PaymentsModule,
],
controllers: [HelloController],
providers: [HelloService],
})
export class AppModule {}

View File

@@ -1,25 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HelloService } from './hello.service';
import { HelloPresenter } from './presenters/HelloPresenter';
describe('HelloService', () => {
let service: HelloService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HelloService],
}).compile();
service = module.get<HelloService>(HelloService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should return "Hello World!"', () => {
const presenter = service.getHello();
expect(presenter.responseModel).toEqual({ message: 'Hello World!' });
});
});

View File

@@ -1,16 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Result } from '@core/shared/application/Result';
import { HelloPresenter, HelloResponseModel } from './presenters/HelloPresenter';
@Injectable()
export class HelloService {
constructor(private readonly presenter: HelloPresenter) {}
getHello(): HelloResponseModel {
const result = Result.ok('Hello World!');
this.presenter.present(result);
return this.presenter.responseModel;
}
}

View File

@@ -1,28 +0,0 @@
import type { Result } from '@core/shared/application/Result';
export interface HelloResponseModel {
message: string;
}
export class HelloPresenter {
private result: HelloResponseModel | null = null;
reset(): void {
this.result = null;
}
present(result: Result<string, Error>): void {
if (result.isErr()) {
throw result.unwrapErr();
}
const message = result.unwrap();
this.result = { message };
}
get responseModel(): HelloResponseModel {
if (!this.result) {
throw new Error('HelloPresenter not presented');
}
return this.result;
}
}

View File

@@ -1,5 +1,5 @@
import { EnsureInitialData } from '@/adapters/bootstrap/EnsureInitialData';
import { Module, OnModuleInit } from '@nestjs/common';
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import { BootstrapProviders } from './BootstrapProviders';
@Module({

View File

@@ -1,7 +1,5 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AnalyticsSnapshotOrmEntity } from '../../../../..//persistence/typeorm/analytics/AnalyticsSnapshotOrmEntity';
import { EngagementOrmEntity } from '../../../../..//persistence/typeorm/analytics/EngagementOrmEntity';
@Module({
imports: [
@@ -12,7 +10,7 @@ import { EngagementOrmEntity } from '../../../../..//persistence/typeorm/analyti
username: process.env.DATABASE_USER || 'user',
password: process.env.DATABASE_PASSWORD || 'password',
database: process.env.DATABASE_NAME || 'gridpilot',
entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity],
// entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity],
synchronize: true, // Use carefully in production
}),
],

View File

@@ -0,0 +1,14 @@
import { Controller, Get } from '@nestjs/common';
import { HelloService } from './HelloService';
@Controller()
export class HelloController {
constructor(private readonly helloService: HelloService) {}
@Get()
getHello() {
return this.helloService.getHello();
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { HelloController } from "./HelloController";
import { HelloService } from "./HelloService";
@Module({
controllers: [HelloController],
exports: [HelloService],
})
export class HelloModule {}

View File

@@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class HelloService {
getHello() {
return "Hello World";
}
}

View File

@@ -13,11 +13,15 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
// Import presenters
import { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter';
// Define injection tokens
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LOGGER_TOKEN = 'Logger';
export const REVIEW_PROTEST_PRESENTER_TOKEN = 'ReviewProtestPresenter';
export const ProtestsProviders: Provider[] = [
ProtestsService, // Provide the service itself
@@ -40,6 +44,26 @@ export const ProtestsProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
{
provide: REVIEW_PROTEST_PRESENTER_TOKEN,
useClass: ReviewProtestPresenter,
},
// Use cases
ReviewProtestUseCase,
{
provide: ReviewProtestUseCase,
useFactory: (
protestRepo: any,
raceRepo: any,
leagueMembershipRepo: any,
logger: Logger,
output: ReviewProtestPresenter,
) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, output),
inject: [
PROTEST_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN,
REVIEW_PROTEST_PRESENTER_TOKEN,
],
},
];

View File

@@ -3,10 +3,10 @@ import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application/Logger';
import type {
ReviewProtestUseCase,
ReviewProtestResult,
ReviewProtestApplicationError,
} from '@core/racing/application/use-cases/ReviewProtestUseCase';
import { ProtestsService } from './ProtestsService';
import type { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter';
import type { ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
describe('ProtestsService', () => {
@@ -17,6 +17,21 @@ describe('ProtestsService', () => {
beforeEach(() => {
executeMock = vi.fn();
const reviewProtestUseCase = { execute: executeMock } as unknown as ReviewProtestUseCase;
const reviewProtestPresenter = {
reset: vi.fn(),
setCommand: vi.fn(),
present: vi.fn(),
presentError: vi.fn(),
getResponseModel: vi.fn(),
get responseModel() {
return {
success: true,
protestId: 'test',
stewardId: 'test',
decision: 'uphold' as const,
};
},
} as unknown as ReviewProtestPresenter;
logger = {
debug: vi.fn(),
info: vi.fn(),
@@ -24,7 +39,7 @@ describe('ProtestsService', () => {
error: vi.fn(),
} as unknown as Logger;
service = new ProtestsService(reviewProtestUseCase, logger);
service = new ProtestsService(reviewProtestUseCase, reviewProtestPresenter, logger);
});
const baseCommand = {
@@ -35,15 +50,7 @@ describe('ProtestsService', () => {
};
it('returns DTO with success model on success', async () => {
const coreResult: ReviewProtestResult = {
leagueId: 'league-1',
protestId: baseCommand.protestId,
status: 'upheld',
stewardId: baseCommand.stewardId,
decision: baseCommand.decision,
};
executeMock.mockResolvedValue(Result.ok<ReviewProtestResult, ReviewProtestApplicationError>(coreResult));
executeMock.mockResolvedValue(Result.ok(undefined));
const dto = await service.reviewProtest(baseCommand);
@@ -62,7 +69,7 @@ describe('ProtestsService', () => {
details: { message: 'Protest not found' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);
@@ -79,7 +86,7 @@ describe('ProtestsService', () => {
details: { message: 'Race not found for protest' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);
@@ -96,7 +103,7 @@ describe('ProtestsService', () => {
details: { message: 'Steward is not authorized to review this protest' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);
@@ -114,7 +121,7 @@ describe('ProtestsService', () => {
details: { message: 'Failed to review protest' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);

View File

@@ -8,17 +8,14 @@ import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewP
import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
// Tokens
import { LOGGER_TOKEN } from './ProtestsProviders';
import { LOGGER_TOKEN, REVIEW_PROTEST_PRESENTER_TOKEN } from './ProtestsProviders';
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN } from './ProtestsProviders';
@Injectable()
export class ProtestsService {
constructor(
private readonly reviewProtestUseCase: ReviewProtestUseCase,
@Inject(REVIEW_PROTEST_PRESENTER_TOKEN) private readonly reviewProtestPresenter: ReviewProtestPresenter,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -30,11 +27,14 @@ export class ProtestsService {
}): Promise<ReviewProtestResponseDTO> {
this.logger.debug('[ProtestsService] Reviewing protest:', command);
const result = await this.reviewProtestUseCase.execute(command);
const presenter = new ReviewProtestPresenter();
// Set the command on the presenter so it can include stewardId and decision in the response
this.reviewProtestPresenter.setCommand({
stewardId: command.stewardId,
decision: command.decision,
});
presenter.present(result);
await this.reviewProtestUseCase.execute(command);
return presenter.responseModel;
return this.reviewProtestPresenter.responseModel;
}
}
}

View File

@@ -12,16 +12,27 @@ export interface ReviewProtestResponseDTO {
export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestResult> {
private model: ReviewProtestResponseDTO | null = null;
private command: { stewardId: string; decision: 'uphold' | 'dismiss' } | null = null;
reset(): void {
this.model = null;
this.command = null;
}
setCommand(command: { stewardId: string; decision: 'uphold' | 'dismiss' }): void {
this.command = command;
}
present(result: ReviewProtestResult): void {
if (!this.command) {
throw new Error('Command must be set before presenting result');
}
this.model = {
success: true,
protestId: result.protestId,
decision: result.status === 'upheld' ? 'uphold' : 'dismiss',
stewardId: this.command.stewardId,
decision: this.command.decision,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ import { SponsorProfileDTO } from './dtos/SponsorProfileDTO';
import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO';
import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO';
import type { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter';
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@ApiTags('sponsors')
@Controller('sponsors')
@@ -39,8 +39,7 @@ export class SponsorController {
type: GetEntitySponsorshipPricingResultDTO,
})
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> {
const presenter = await this.sponsorService.getEntitySponsorshipPricing();
return presenter.viewModel;
return await this.sponsorService.getEntitySponsorshipPricing();
}
@Get()
@@ -51,8 +50,7 @@ export class SponsorController {
type: GetSponsorsOutputDTO,
})
async getSponsors(): Promise<GetSponsorsOutputDTO> {
const presenter = await this.sponsorService.getSponsors();
return presenter.viewModel;
return await this.sponsorService.getSponsors();
}
@Post()
@@ -64,8 +62,7 @@ export class SponsorController {
type: CreateSponsorOutputDTO,
})
async createSponsor(@Body() input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> {
const presenter = await this.sponsorService.createSponsor(input);
return presenter.viewModel;
return await this.sponsorService.createSponsor(input);
}
@Get('dashboard/:sponsorId')
@@ -78,11 +75,10 @@ export class SponsorController {
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsorDashboard(
@Param('sponsorId') sponsorId: string,
): Promise<SponsorDashboardDTO | null> {
const presenter = await this.sponsorService.getSponsorDashboard({
): Promise<SponsorDashboardDTO> {
return await this.sponsorService.getSponsorDashboard({
sponsorId,
} as GetSponsorDashboardQueryParamsDTO);
return presenter.viewModel;
}
@Get(':sponsorId/sponsorships')
@@ -97,11 +93,10 @@ export class SponsorController {
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsorSponsorships(
@Param('sponsorId') sponsorId: string,
): Promise<SponsorSponsorshipsDTO | null> {
const presenter = await this.sponsorService.getSponsorSponsorships({
): Promise<SponsorSponsorshipsDTO> {
return await this.sponsorService.getSponsorSponsorships({
sponsorId,
} as GetSponsorSponsorshipsQueryParamsDTO);
return presenter.viewModel;
}
@Get(':sponsorId')
@@ -112,9 +107,8 @@ export class SponsorController {
type: GetSponsorOutputDTO,
})
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsor(@Param('sponsorId') sponsorId: string): Promise<GetSponsorOutputDTO | null> {
const presenter = await this.sponsorService.getSponsor(sponsorId);
return presenter.viewModel;
async getSponsor(@Param('sponsorId') sponsorId: string): Promise<GetSponsorOutputDTO> {
return await this.sponsorService.getSponsor(sponsorId);
}
@Get('requests')
@@ -126,14 +120,13 @@ export class SponsorController {
})
async getPendingSponsorshipRequests(
@Query() query: { entityType: string; entityId: string },
): Promise<GetPendingSponsorshipRequestsOutputDTO | null> {
const presenter = await this.sponsorService.getPendingSponsorshipRequests(
): Promise<GetPendingSponsorshipRequestsOutputDTO> {
return await this.sponsorService.getPendingSponsorshipRequests(
query as {
entityType: import('@core/racing/domain/entities/SponsorshipRequest').SponsorableEntityType;
entityId: string;
},
);
return presenter.viewModel;
}
@Post('requests/:requestId/accept')
@@ -146,11 +139,10 @@ export class SponsorController {
@Param('requestId') requestId: string,
@Body() input: AcceptSponsorshipRequestInputDTO,
): Promise<AcceptSponsorshipRequestResultViewModel | null> {
const presenter = await this.sponsorService.acceptSponsorshipRequest(
return await this.sponsorService.acceptSponsorshipRequest(
requestId,
input.respondedBy,
);
return presenter.viewModel;
}
@Post('requests/:requestId/reject')
@@ -162,13 +154,12 @@ export class SponsorController {
async rejectSponsorshipRequest(
@Param('requestId') requestId: string,
@Body() input: RejectSponsorshipRequestInputDTO,
): Promise<RejectSponsorshipRequestResultDTO | null> {
const presenter = await this.sponsorService.rejectSponsorshipRequest(
): Promise<RejectSponsorshipRequestResult | null> {
return await this.sponsorService.rejectSponsorshipRequest(
requestId,
input.respondedBy,
input.reason,
);
return presenter.viewModel;
}
@Get('billing/:sponsorId')
@@ -181,8 +172,7 @@ export class SponsorController {
invoices: InvoiceDTO[];
stats: BillingStatsDTO;
}> {
const presenter = await this.sponsorService.getSponsorBilling(sponsorId);
return presenter.viewModel;
return await this.sponsorService.getSponsorBilling(sponsorId);
}
@Get('leagues/available')

View File

@@ -5,7 +5,8 @@ import { SponsorService } from './SponsorService';
import { NotificationService } from '@core/notifications/application/ports/NotificationService';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import { IPaymentGateway } from '@core/payments/domain/ports/IPaymentGateway';
// Remove the missing import
// import { IPaymentGateway } from '@core/payments/domain/ports/IPaymentGateway';
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
@@ -15,7 +16,7 @@ import { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/I
import { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository';
import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository';
import type { Logger } from '@core/shared/application';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
@@ -40,6 +41,18 @@ import { InMemorySponsorRepository } from '@adapters/racing/persistence/inmemory
import { InMemorySponsorshipPricingRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository';
import { InMemorySponsorshipRequestRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository';
// Import presenters
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
// Define injection tokens
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository';
@@ -51,6 +64,18 @@ export const SPONSORSHIP_PRICING_REPOSITORY_TOKEN = 'ISponsorshipPricingReposito
export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestRepository';
export const LOGGER_TOKEN = 'Logger';
// Presenter tokens
export const GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN = 'GetEntitySponsorshipPricingPresenter';
export const GET_SPONSORS_PRESENTER_TOKEN = 'GetSponsorsPresenter';
export const CREATE_SPONSOR_PRESENTER_TOKEN = 'CreateSponsorPresenter';
export const GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN = 'GetSponsorDashboardPresenter';
export const GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN = 'GetSponsorSponsorshipsPresenter';
export const GET_SPONSOR_PRESENTER_TOKEN = 'GetSponsorPresenter';
export const GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN = 'GetPendingSponsorshipRequestsPresenter';
export const ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'AcceptSponsorshipRequestPresenter';
export const REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'RejectSponsorshipRequestPresenter';
export const GET_SPONSOR_BILLING_PRESENTER_TOKEN = 'SponsorBillingPresenter';
// Use case / application service tokens
export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase';
export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase';
@@ -64,6 +89,19 @@ export const ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'AcceptSponsorshipReque
export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase';
export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase';
// Output port tokens
export const GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetSponsorshipPricingOutputPort_TOKEN';
export const GET_SPONSORS_OUTPUT_PORT_TOKEN = 'GetSponsorsOutputPort_TOKEN';
export const CREATE_SPONSOR_OUTPUT_PORT_TOKEN = 'CreateSponsorOutputPort_TOKEN';
export const GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN = 'GetSponsorDashboardOutputPort_TOKEN';
export const GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSponsorSponsorshipsOutputPort_TOKEN';
export const GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetEntitySponsorshipPricingOutputPort_TOKEN';
export const GET_SPONSOR_OUTPUT_PORT_TOKEN = 'GetSponsorOutputPort_TOKEN';
export const GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN = 'GetPendingSponsorshipRequestsOutputPort_TOKEN';
export const ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'AcceptSponsorshipRequestOutputPort_TOKEN';
export const REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'RejectSponsorshipRequestOutputPort_TOKEN';
export const GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN = 'GetSponsorBillingOutputPort_TOKEN';
export const SponsorProviders: Provider[] = [
SponsorService,
// Repositories
@@ -111,27 +149,94 @@ export const SponsorProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Presenters
GetEntitySponsorshipPricingPresenter,
GetSponsorsPresenter,
CreateSponsorPresenter,
GetSponsorDashboardPresenter,
GetSponsorSponsorshipsPresenter,
GetSponsorPresenter,
GetPendingSponsorshipRequestsPresenter,
AcceptSponsorshipRequestPresenter,
RejectSponsorshipRequestPresenter,
SponsorBillingPresenter,
// Output ports
{
provide: GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN,
useExisting: GetEntitySponsorshipPricingPresenter,
},
{
provide: GET_SPONSORS_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorsPresenter,
},
{
provide: CREATE_SPONSOR_OUTPUT_PORT_TOKEN,
useExisting: CreateSponsorPresenter,
},
{
provide: GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorDashboardPresenter,
},
{
provide: GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorSponsorshipsPresenter,
},
{
provide: GET_SPONSOR_OUTPUT_PORT_TOKEN,
useExisting: GetSponsorPresenter,
},
{
provide: GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN,
useExisting: GetPendingSponsorshipRequestsPresenter,
},
{
provide: ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN,
useExisting: AcceptSponsorshipRequestPresenter,
},
{
provide: REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN,
useExisting: RejectSponsorshipRequestPresenter,
},
{
provide: GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN,
useExisting: SponsorBillingPresenter,
},
// Use cases
{
provide: GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
useFactory: () => new GetSponsorshipPricingUseCase(),
inject: [],
useFactory: (output: UseCaseOutputPort<any>) => new GetSponsorshipPricingUseCase(output),
inject: [GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN],
},
{
provide: GET_SPONSORS_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorsUseCase(sponsorRepo),
inject: [SPONSOR_REPOSITORY_TOKEN],
useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort<any>) => new GetSponsorsUseCase(sponsorRepo, output),
inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSORS_OUTPUT_PORT_TOKEN],
},
{
provide: CREATE_SPONSOR_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository) => new CreateSponsorUseCase(sponsorRepo),
inject: [SPONSOR_REPOSITORY_TOKEN],
useFactory: (sponsorRepo: ISponsorRepository, logger: Logger, output: UseCaseOutputPort<any>) => new CreateSponsorUseCase(sponsorRepo, logger, output),
inject: [SPONSOR_REPOSITORY_TOKEN, LOGGER_TOKEN, CREATE_SPONSOR_OUTPUT_PORT_TOKEN],
},
{
provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, leagueRepo: ILeagueRepository, leagueMembershipRepo: ILeagueMembershipRepository, raceRepo: IRaceRepository) =>
new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo),
inject: [SPONSOR_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN],
useFactory: (
sponsorRepo: ISponsorRepository,
seasonSponsorshipRepo: ISeasonSponsorshipRepository,
seasonRepo: ISeasonRepository,
leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
raceRepo: IRaceRepository,
output: UseCaseOutputPort<any>,
) => new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output),
inject: [
SPONSOR_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
SEASON_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN,
],
},
{
provide: GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN,
@@ -142,7 +247,8 @@ export const SponsorProviders: Provider[] = [
leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository,
raceRepo: IRaceRepository,
) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo),
output: UseCaseOutputPort<any>,
) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output),
inject: [
SPONSOR_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
@@ -150,41 +256,97 @@ export const SponsorProviders: Provider[] = [
LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
],
},
{
provide: GET_SPONSOR_BILLING_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository) =>
new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo),
useFactory: (
paymentRepo: IPaymentRepository,
seasonSponsorshipRepo: ISeasonSponsorshipRepository,
) => {
return new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo);
},
inject: ['IPaymentRepository', SEASON_SPONSORSHIP_REPOSITORY_TOKEN],
},
{
provide: GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
useFactory: (sponsorshipPricingRepo: ISponsorshipPricingRepository, sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, logger: Logger) =>
new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, sponsorshipRequestRepo, seasonSponsorshipRepo, logger),
inject: [SPONSORSHIP_PRICING_REPOSITORY_TOKEN, SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
useFactory: (
sponsorshipPricingRepo: ISponsorshipPricingRepository,
sponsorshipRequestRepo: ISponsorshipRequestRepository,
seasonSponsorshipRepo: ISeasonSponsorshipRepository,
logger: Logger,
output: UseCaseOutputPort<any>,
) => new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, sponsorshipRequestRepo, seasonSponsorshipRepo, logger, output),
inject: [
SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN,
GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN,
],
},
{
provide: GET_SPONSOR_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorUseCase(sponsorRepo),
inject: [SPONSOR_REPOSITORY_TOKEN],
useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort<any>) => new GetSponsorUseCase(sponsorRepo, output),
inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSOR_OUTPUT_PORT_TOKEN],
},
{
provide: GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, sponsorRepo: ISponsorRepository) =>
new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo),
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN],
useFactory: (
sponsorshipRequestRepo: ISponsorshipRequestRepository,
sponsorRepo: ISponsorRepository,
output: UseCaseOutputPort<any>,
) => new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo, output),
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN, GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN],
},
{
provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: NotificationService, paymentGateway: IPaymentGateway, walletRepository: IWalletRepository, leagueWalletRepository: ILeagueWalletRepository, logger: Logger) =>
new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepo, seasonSponsorshipRepo, seasonRepo, notificationService, paymentGateway, walletRepository, leagueWalletRepository, logger),
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, 'INotificationService', 'IPaymentGateway', 'IWalletRepository', 'ILeagueWalletRepository', LOGGER_TOKEN],
useFactory: (
sponsorshipRequestRepo: ISponsorshipRequestRepository,
seasonSponsorshipRepo: ISeasonSponsorshipRepository,
seasonRepo: ISeasonRepository,
notificationService: NotificationService,
walletRepository: IWalletRepository,
leagueWalletRepository: ILeagueWalletRepository,
logger: Logger,
output: UseCaseOutputPort<any>,
) => {
// Create a mock payment processor function
const paymentProcessor = async (_input: any) => {
return { success: true, transactionId: `txn_${Date.now()}` };
};
return new AcceptSponsorshipRequestUseCase(
sponsorshipRequestRepo,
seasonSponsorshipRepo,
seasonRepo,
notificationService,
paymentProcessor,
walletRepository,
leagueWalletRepository,
logger,
output
);
},
inject: [
SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
SEASON_REPOSITORY_TOKEN,
'INotificationService',
'IWalletRepository',
'ILeagueWalletRepository',
LOGGER_TOKEN,
ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN,
],
},
{
provide: REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, logger: Logger) =>
new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger),
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN],
useFactory: (
sponsorshipRequestRepo: ISponsorshipRequestRepository,
logger: Logger,
output: UseCaseOutputPort<any>,
) => new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger, output),
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN, REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN],
},
];
];

View File

@@ -1,15 +1,27 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application';
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import type { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import type { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import type { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
import type { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import type { GetSponsorDashboardInput, GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import type { GetSponsorSponsorshipsInput, GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import type { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import type { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
import { SponsorService } from './SponsorService';
describe('SponsorService', () => {
@@ -23,8 +35,21 @@ describe('SponsorService', () => {
let getPendingSponsorshipRequestsUseCase: { execute: Mock };
let acceptSponsorshipRequestUseCase: { execute: Mock };
let rejectSponsorshipRequestUseCase: { execute: Mock };
let getSponsorBillingUseCase: { execute: Mock };
let logger: Logger;
// Presenters
let getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter;
let getSponsorsPresenter: GetSponsorsPresenter;
let createSponsorPresenter: CreateSponsorPresenter;
let getSponsorDashboardPresenter: GetSponsorDashboardPresenter;
let getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter;
let getSponsorPresenter: GetSponsorPresenter;
let getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter;
let acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter;
let rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter;
let sponsorBillingPresenter: SponsorBillingPresenter;
beforeEach(() => {
getSponsorshipPricingUseCase = { execute: vi.fn() };
getSponsorsUseCase = { execute: vi.fn() };
@@ -35,6 +60,7 @@ describe('SponsorService', () => {
getPendingSponsorshipRequestsUseCase = { execute: vi.fn() };
acceptSponsorshipRequestUseCase = { execute: vi.fn() };
rejectSponsorshipRequestUseCase = { execute: vi.fn() };
getSponsorBillingUseCase = { execute: vi.fn() };
logger = {
debug: vi.fn(),
info: vi.fn(),
@@ -42,6 +68,18 @@ describe('SponsorService', () => {
error: vi.fn(),
} as unknown as Logger;
// Initialize presenters
getEntitySponsorshipPricingPresenter = new GetEntitySponsorshipPricingPresenter();
getSponsorsPresenter = new GetSponsorsPresenter();
createSponsorPresenter = new CreateSponsorPresenter();
getSponsorDashboardPresenter = new GetSponsorDashboardPresenter();
getSponsorSponsorshipsPresenter = new GetSponsorSponsorshipsPresenter();
getSponsorPresenter = new GetSponsorPresenter();
getPendingSponsorshipRequestsPresenter = new GetPendingSponsorshipRequestsPresenter();
acceptSponsorshipRequestPresenter = new AcceptSponsorshipRequestPresenter();
rejectSponsorshipRequestPresenter = new RejectSponsorshipRequestPresenter();
sponsorBillingPresenter = new SponsorBillingPresenter();
service = new SponsorService(
getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase,
getSponsorsUseCase as unknown as GetSponsorsUseCase,
@@ -52,28 +90,39 @@ describe('SponsorService', () => {
getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase,
acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase,
rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase,
getSponsorBillingUseCase as unknown as GetSponsorBillingUseCase,
logger,
getEntitySponsorshipPricingPresenter,
getSponsorsPresenter,
createSponsorPresenter,
getSponsorDashboardPresenter,
getSponsorSponsorshipsPresenter,
getSponsorPresenter,
getPendingSponsorshipRequestsPresenter,
acceptSponsorshipRequestPresenter,
rejectSponsorshipRequestPresenter,
sponsorBillingPresenter,
);
});
describe('getEntitySponsorshipPricing', () => {
it('returns presenter with pricing data on success', async () => {
it('returns pricing data on success', async () => {
const outputPort = {
entityType: 'season',
entityId: 'season-1',
pricing: [
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
tiers: [
{ name: 'Gold', price: 500, benefits: ['Main slot'] },
],
};
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getEntitySponsorshipPricing();
const result = await service.getEntitySponsorshipPricing();
expect(presenter.viewModel).toEqual({
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
pricing: [
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
{ id: 'Gold', level: 'Gold', price: 500, currency: 'USD' },
],
});
});
@@ -81,9 +130,9 @@ describe('SponsorService', () => {
it('returns empty pricing on error', async () => {
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getEntitySponsorshipPricing();
const result = await service.getEntitySponsorshipPricing();
expect(presenter.viewModel).toEqual({
expect(result).toEqual({
entityType: 'season',
entityId: '',
pricing: [],
@@ -92,82 +141,93 @@ describe('SponsorService', () => {
});
describe('getSponsors', () => {
it('returns sponsors in presenter on success', async () => {
const outputPort = { sponsors: [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }] };
it('returns sponsors on success', async () => {
const sponsors = [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }];
const outputPort = { sponsors };
getSponsorsUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getSponsors();
const result = await service.getSponsors();
expect(presenter.viewModel).toEqual({ sponsors: outputPort.sponsors });
expect(result).toEqual({ sponsors });
});
it('returns empty list on error', async () => {
getSponsorsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getSponsors();
const result = await service.getSponsors();
expect(presenter.viewModel).toEqual({ sponsors: [] });
expect(result).toEqual({ sponsors: [] });
});
});
describe('createSponsor', () => {
it('returns created sponsor in presenter on success', async () => {
const input = { name: 'Test', contactEmail: 'test@example.com' };
const outputPort = {
sponsor: {
id: 's1',
name: 'Test',
contactEmail: 'test@example.com',
createdAt: new Date(),
},
it('returns created sponsor on success', async () => {
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
const sponsor = {
id: 's1',
name: 'Test',
contactEmail: 'test@example.com',
createdAt: new Date(),
};
const outputPort = { sponsor };
createSponsorUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.createSponsor(input as any);
const result = await service.createSponsor(input);
expect(presenter.viewModel).toEqual({ sponsor: outputPort.sponsor });
expect(result).toEqual({ sponsor });
});
it('throws on error', async () => {
const input = { name: 'Test', contactEmail: 'test@example.com' };
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
createSponsorUseCase.execute.mockResolvedValue(
Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Invalid' } }),
);
await expect(service.createSponsor(input as any)).rejects.toThrow('Invalid');
await expect(service.createSponsor(input)).rejects.toThrow('Invalid');
});
});
describe('getSponsorDashboard', () => {
it('returns dashboard in presenter on success', async () => {
const params = { sponsorId: 's1' };
it('returns dashboard on success', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' };
const outputPort = {
sponsorId: 's1',
sponsorName: 'S1',
metrics: {} as any,
metrics: {
impressions: 0,
impressionsChange: 0,
uniqueViewers: 0,
viewersChange: 0,
races: 0,
drivers: 0,
exposure: 0,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {} as any,
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
};
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getSponsorDashboard(params as any);
const result = await service.getSponsorDashboard(params);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns null in presenter on error', async () => {
const params = { sponsorId: 's1' };
it('throws on error', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' };
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getSponsorDashboard(params as any);
expect(presenter.viewModel).toBeNull();
await expect(service.getSponsorDashboard(params)).rejects.toThrow('Sponsor dashboard not found');
});
});
describe('getSponsorSponsorships', () => {
it('returns sponsorships in presenter on success', async () => {
const params = { sponsorId: 's1' };
it('returns sponsorships on success', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
const outputPort = {
sponsorId: 's1',
sponsorName: 'S1',
@@ -182,46 +242,43 @@ describe('SponsorService', () => {
};
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getSponsorSponsorships(params as any);
const result = await service.getSponsorSponsorships(params);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns null in presenter on error', async () => {
const params = { sponsorId: 's1' };
it('throws on error', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(
Result.err({ code: 'REPOSITORY_ERROR' }),
);
const presenter = await service.getSponsorSponsorships(params as any);
expect(presenter.viewModel).toBeNull();
await expect(service.getSponsorSponsorships(params)).rejects.toThrow('Sponsor sponsorships not found');
});
});
describe('getSponsor', () => {
it('returns sponsor in presenter when found', async () => {
it('returns sponsor when found', async () => {
const sponsorId = 's1';
const output = { sponsor: { id: sponsorId, name: 'S1' } };
const sponsor = { id: sponsorId, name: 'S1' };
const output = { sponsor };
getSponsorUseCase.execute.mockResolvedValue(Result.ok(output));
const presenter = await service.getSponsor(sponsorId);
const result = await service.getSponsor(sponsorId);
expect(presenter.viewModel).toEqual({ sponsor: output.sponsor });
expect(result).toEqual({ sponsor });
});
it('returns null in presenter when not found', async () => {
it('throws when not found', async () => {
const sponsorId = 's1';
getSponsorUseCase.execute.mockResolvedValue(Result.ok(null));
const presenter = await service.getSponsor(sponsorId);
expect(presenter.viewModel).toBeNull();
await expect(service.getSponsor(sponsorId)).rejects.toThrow('Sponsor not found');
});
});
describe('getPendingSponsorshipRequests', () => {
it('returns requests in presenter on success', async () => {
it('returns requests on success', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
const outputPort = {
entityType: 'season',
@@ -231,9 +288,9 @@ describe('SponsorService', () => {
};
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getPendingSponsorshipRequests(params);
const result = await service.getPendingSponsorshipRequests(params);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns empty result on error', async () => {
@@ -242,9 +299,9 @@ describe('SponsorService', () => {
Result.err({ code: 'REPOSITORY_ERROR' }),
);
const presenter = await service.getPendingSponsorshipRequests(params);
const result = await service.getPendingSponsorshipRequests(params);
expect(presenter.viewModel).toEqual({
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
requests: [],
@@ -253,8 +310,8 @@ describe('SponsorService', () => {
});
});
describe('acceptSponsorshipRequest', () => {
it('returns accept result in presenter on success', async () => {
describe('SponsorshipRequest', () => {
it('returns accept result on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
const outputPort = {
@@ -267,100 +324,114 @@ describe('SponsorService', () => {
};
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy);
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns null in presenter on error', async () => {
it('throws on error', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
);
const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(presenter.viewModel).toBeNull();
await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Accept sponsorship request failed');
});
});
describe('rejectSponsorshipRequest', () => {
it('returns reject result in presenter on success', async () => {
it('returns reject result on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
const reason = 'Not interested';
const output = {
requestId,
status: 'rejected' as const,
rejectedAt: new Date(),
reason,
respondedAt: new Date(),
rejectionReason: reason,
};
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output));
const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
expect(presenter.viewModel).toEqual(output);
expect(result).toEqual(output);
});
it('returns null in presenter on error', async () => {
it('throws on error', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
);
const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy);
expect(presenter.viewModel).toBeNull();
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Reject sponsorship request failed');
});
});
describe('getSponsorBilling', () => {
it('returns mock billing data in presenter', async () => {
const presenter = await service.getSponsorBilling('s1');
it('returns billing data', async () => {
// Mock the use case to set up the presenter
getSponsorBillingUseCase.execute.mockImplementation(async () => {
sponsorBillingPresenter.present({
paymentMethods: [],
invoices: [],
stats: {
totalSpent: 0,
pendingAmount: 0,
nextPaymentDate: '',
nextPaymentAmount: 0,
activeSponsorships: 0,
averageMonthlySpend: 0,
},
});
return Result.ok(undefined);
});
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.paymentMethods).toBeInstanceOf(Array);
expect(presenter.viewModel?.invoices).toBeInstanceOf(Array);
expect(presenter.viewModel?.stats).toBeDefined();
const result = await service.getSponsorBilling('s1');
expect(result).not.toBeNull();
expect(result.paymentMethods).toBeInstanceOf(Array);
expect(result.invoices).toBeInstanceOf(Array);
expect(result.stats).toBeDefined();
});
});
describe('getAvailableLeagues', () => {
it('returns mock leagues in presenter', async () => {
const presenter = await service.getAvailableLeagues();
it('returns mock leagues', async () => {
const result = await service.getAvailableLeagues();
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.length).toBeGreaterThan(0);
expect(result).not.toBeNull();
expect(result.viewModel).not.toBeNull();
expect(result.viewModel?.length).toBeGreaterThan(0);
});
});
describe('getLeagueDetail', () => {
it('returns league detail in presenter', async () => {
const presenter = await service.getLeagueDetail('league-1');
it('returns league detail', async () => {
const result = await service.getLeagueDetail('league-1');
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.league.id).toBe('league-1');
expect(result).not.toBeNull();
expect(result.viewModel?.league.id).toBe('league-1');
});
});
describe('getSponsorSettings', () => {
it('returns settings in presenter', async () => {
const presenter = await service.getSponsorSettings('s1');
it('returns settings', async () => {
const result = await service.getSponsorSettings('s1');
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.profile).toBeDefined();
expect(presenter.viewModel?.notifications).toBeDefined();
expect(presenter.viewModel?.privacy).toBeDefined();
expect(result).not.toBeNull();
expect(result.viewModel?.profile).toBeDefined();
expect(result.viewModel?.notifications).toBeDefined();
expect(result.viewModel?.privacy).toBeDefined();
});
});
describe('updateSponsorSettings', () => {
it('returns success result in presenter', async () => {
const presenter = await service.updateSponsorSettings('s1', {});
it('returns success result', async () => {
const result = await service.updateSponsorSettings('s1', {});
expect(presenter.viewModel).toEqual({ success: true });
expect(result.viewModel).toEqual({ success: true });
});
});
});
});

View File

@@ -1,12 +1,14 @@
import { Injectable, Inject } from '@nestjs/common';
import { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
import { CreateSponsorOutputDTO } from './dtos/CreateSponsorOutputDTO';
import { GetSponsorDashboardQueryParamsDTO } from './dtos/GetSponsorDashboardQueryParamsDTO';
import { SponsorDashboardDTO } from './dtos/SponsorDashboardDTO';
import { GetSponsorSponsorshipsQueryParamsDTO } from './dtos/GetSponsorSponsorshipsQueryParamsDTO';
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO';
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO';
import { PaymentMethodDTO } from './dtos/PaymentMethodDTO';
import { InvoiceDTO } from './dtos/InvoiceDTO';
import { BillingStatsDTO } from './dtos/BillingStatsDTO';
import { SponsorSponsorshipsDTO } from './dtos/SponsorSponsorshipsDTO';
import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshipPricingResultDTO';
import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO';
import { AvailableLeagueDTO } from './dtos/AvailableLeagueDTO';
import { LeagueDetailDTO } from './dtos/LeagueDetailDTO';
import { DriverDTO } from './dtos/DriverDTO';
@@ -14,6 +16,9 @@ import { RaceDTO } from './dtos/RaceDTO';
import { SponsorProfileDTO } from './dtos/SponsorProfileDTO';
import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO';
import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO';
import { PaymentMethodDTO } from './dtos/PaymentMethodDTO';
import { InvoiceDTO } from './dtos/InvoiceDTO';
import { BillingStatsDTO } from './dtos/BillingStatsDTO';
// Use cases
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
@@ -24,7 +29,7 @@ import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-case
import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import {
GetPendingSponsorshipRequestsUseCase,
GetPendingSponsorshipRequestsDTO,
GetPendingSponsorshipRequestsInput,
} from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@@ -48,6 +53,8 @@ import { AvailableLeaguesPresenter } from './presenters/AvailableLeaguesPresente
import { LeagueDetailPresenter } from './presenters/LeagueDetailPresenter';
import { SponsorSettingsPresenter } from './presenters/SponsorSettingsPresenter';
import { SponsorSettingsUpdatePresenter } from './presenters/SponsorSettingsUpdatePresenter';
import { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
// Tokens
import {
@@ -61,6 +68,16 @@ import {
ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
LOGGER_TOKEN,
GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN,
GET_SPONSORS_PRESENTER_TOKEN,
CREATE_SPONSOR_PRESENTER_TOKEN,
GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN,
GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN,
GET_SPONSOR_PRESENTER_TOKEN,
GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN,
ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN,
REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN,
GET_SPONSOR_BILLING_PRESENTER_TOKEN,
} from './SponsorProviders';
@Injectable()
@@ -88,194 +105,152 @@ export class SponsorService {
private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase,
@Inject(LOGGER_TOKEN)
private readonly logger: Logger,
// Injected presenters
@Inject(GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN)
private readonly getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter,
@Inject(GET_SPONSORS_PRESENTER_TOKEN)
private readonly getSponsorsPresenter: GetSponsorsPresenter,
@Inject(CREATE_SPONSOR_PRESENTER_TOKEN)
private readonly createSponsorPresenter: CreateSponsorPresenter,
@Inject(GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN)
private readonly getSponsorDashboardPresenter: GetSponsorDashboardPresenter,
@Inject(GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN)
private readonly getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter,
@Inject(GET_SPONSOR_PRESENTER_TOKEN)
private readonly getSponsorPresenter: GetSponsorPresenter,
@Inject(GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN)
private readonly getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter,
@Inject(ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN)
private readonly acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter,
@Inject(REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN)
private readonly rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter,
@Inject(GET_SPONSOR_BILLING_PRESENTER_TOKEN)
private readonly sponsorBillingPresenter: SponsorBillingPresenter,
) {}
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingPresenter> {
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> {
this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
const presenter = new GetEntitySponsorshipPricingPresenter();
const result = await this.getSponsorshipPricingUseCase.execute();
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsorship pricing.', result.error);
presenter.present(null);
return presenter;
}
presenter.present(result.value);
return presenter;
await this.getSponsorshipPricingUseCase.execute({});
return this.getEntitySponsorshipPricingPresenter.viewModel;
}
async getSponsors(): Promise<GetSponsorsOutputDTO> {
this.logger.debug('[SponsorService] Fetching sponsors.');
const presenter = new GetSponsorsPresenter();
const result = await this.getSponsorsUseCase.execute();
presenter.present(result);
return presenter.responseModel;
await this.getSponsorsUseCase.execute();
return this.getSponsorsPresenter.responseModel;
}
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorPresenter> {
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> {
this.logger.debug('[SponsorService] Creating sponsor.', { input });
const presenter = new CreateSponsorPresenter();
const result = await this.createSponsorUseCase.execute(input);
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to create sponsor.', result.error);
throw new Error(result.error.details?.message || 'Failed to create sponsor');
}
presenter.present(result.value);
return presenter;
await this.createSponsorUseCase.execute(input);
return this.createSponsorPresenter.viewModel;
}
async getSponsorDashboard(
params: GetSponsorDashboardQueryParamsDTO,
): Promise<GetSponsorDashboardPresenter> {
): Promise<SponsorDashboardDTO> {
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
const presenter = new GetSponsorDashboardPresenter();
const result = await this.getSponsorDashboardUseCase.execute(params);
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor dashboard.', result.error);
presenter.present(null);
return presenter;
await this.getSponsorDashboardUseCase.execute(params);
const result = this.getSponsorDashboardPresenter.viewModel;
if (!result) {
throw new Error('Sponsor dashboard not found');
}
presenter.present(result.value);
return presenter;
return result;
}
async getSponsorSponsorships(
params: GetSponsorSponsorshipsQueryParamsDTO,
): Promise<GetSponsorSponsorshipsPresenter> {
): Promise<SponsorSponsorshipsDTO> {
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
const presenter = new GetSponsorSponsorshipsPresenter();
const result = await this.getSponsorSponsorshipsUseCase.execute(params);
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor sponsorships.', result.error);
presenter.present(null);
return presenter;
await this.getSponsorSponsorshipsUseCase.execute(params);
const result = this.getSponsorSponsorshipsPresenter.viewModel;
if (!result) {
throw new Error('Sponsor sponsorships not found');
}
presenter.present(result.value);
return presenter;
return result;
}
async getSponsor(sponsorId: string): Promise<GetSponsorPresenter> {
async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO> {
this.logger.debug('[SponsorService] Fetching sponsor.', { sponsorId });
const presenter = new GetSponsorPresenter();
const result = await this.getSponsorUseCase.execute({ sponsorId });
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor.', result.error);
presenter.present(null);
return presenter;
await this.getSponsorUseCase.execute({ sponsorId });
const result = this.getSponsorPresenter.viewModel;
if (!result) {
throw new Error('Sponsor not found');
}
presenter.present(result.value);
return presenter;
return result;
}
async getPendingSponsorshipRequests(params: {
entityType: SponsorableEntityType;
entityId: string;
}): Promise<GetPendingSponsorshipRequestsPresenter> {
}): Promise<GetPendingSponsorshipRequestsOutputDTO> {
this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params });
const presenter = new GetPendingSponsorshipRequestsPresenter();
const result = await this.getPendingSponsorshipRequestsUseCase.execute(
params as GetPendingSponsorshipRequestsDTO,
await this.getPendingSponsorshipRequestsUseCase.execute(
params as GetPendingSponsorshipRequestsInput,
);
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch pending sponsorship requests.', result.error);
presenter.present({
entityType: params.entityType,
entityId: params.entityId,
requests: [],
totalCount: 0,
});
return presenter;
const result = this.getPendingSponsorshipRequestsPresenter.viewModel;
if (!result) {
throw new Error('Pending sponsorship requests not found');
}
presenter.present(result.value);
return presenter;
return result;
}
async acceptSponsorshipRequest(
requestId: string,
respondedBy: string,
): Promise<AcceptSponsorshipRequestPresenter> {
): Promise<AcceptSponsorshipRequestResultViewModel> {
this.logger.debug('[SponsorService] Accepting sponsorship request.', {
requestId,
respondedBy,
});
const presenter = new AcceptSponsorshipRequestPresenter();
const result = await this.acceptSponsorshipRequestUseCase.execute({
await this.acceptSponsorshipRequestUseCase.execute({
requestId,
respondedBy,
} as AcceptSponsorshipRequestInputDTO);
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to accept sponsorship request.', result.error);
presenter.present(null);
return presenter;
});
const result = this.acceptSponsorshipRequestPresenter.viewModel;
if (!result) {
throw new Error('Accept sponsorship request failed');
}
presenter.present(result.value);
return presenter;
return result;
}
async rejectSponsorshipRequest(
requestId: string,
respondedBy: string,
reason?: string,
): Promise<RejectSponsorshipRequestPresenter> {
): Promise<RejectSponsorshipRequestResult> {
this.logger.debug('[SponsorService] Rejecting sponsorship request.', {
requestId,
respondedBy,
reason,
});
const presenter = new RejectSponsorshipRequestPresenter();
const result = await this.rejectSponsorshipRequestUseCase.execute({
const input: { requestId: string; respondedBy: string; reason?: string } = {
requestId,
respondedBy,
reason,
} as RejectSponsorshipRequestInputDTO);
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to reject sponsorship request.', result.error);
presenter.present(null);
return presenter;
};
if (reason !== undefined) {
input.reason = reason;
}
presenter.present(result.value);
return presenter;
await this.rejectSponsorshipRequestUseCase.execute(input);
const result = this.rejectSponsorshipRequestPresenter.viewModel;
if (!result) {
throw new Error('Reject sponsorship request failed');
}
return result;
}
async getSponsorBilling(sponsorId: string): Promise<GetSponsorBillingPresenter> {
async getSponsorBilling(sponsorId: string): Promise<{
paymentMethods: PaymentMethodDTO[];
invoices: InvoiceDTO[];
stats: BillingStatsDTO;
}> {
this.logger.debug('[SponsorService] Fetching sponsor billing.', { sponsorId });
const result = await this.getSponsorBillingUseCase.execute({ sponsorId });
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor billing.', result.error);
throw new Error(result.error.details?.message || 'Failed to fetch sponsor billing');
await this.getSponsorBillingUseCase.execute({ sponsorId });
const result = this.sponsorBillingPresenter.viewModel;
if (!result) {
throw new Error('Sponsor billing not found');
}
const presenter = new GetSponsorBillingPresenter();
presenter.present(result.value);
return presenter;
return result;
}
async getAvailableLeagues(): Promise<AvailableLeaguesPresenter> {
@@ -426,7 +401,7 @@ export class SponsorService {
website: 'https://acme-racing.com',
description:
'Premium sim racing equipment and accessories for competitive drivers.',
logoUrl: null,
logoUrl: '',
industry: 'Racing Equipment',
address: {
street: '123 Racing Boulevard',
@@ -479,4 +454,4 @@ export class SponsorService {
presenter.present({ success: true });
return presenter;
}
}
}

View File

@@ -4,19 +4,19 @@ import { IsString, IsEnum, IsOptional, IsNumber } from 'class-validator';
export class ActivityItemDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty({ enum: ['race', 'league', 'team', 'driver', 'platform'] })
@IsEnum(['race', 'league', 'team', 'driver', 'platform'])
type: 'race' | 'league' | 'team' | 'driver' | 'platform';
type: 'race' | 'league' | 'team' | 'driver' | 'platform' = 'race';
@ApiProperty()
@IsString()
message: string;
message: string = '';
@ApiProperty()
@IsString()
time: string;
time: string = '';
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -1,47 +1,47 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEnum, IsNumber, IsBoolean, IsOptional, IsDateString } from 'class-validator';
import { IsString, IsEnum, IsNumber, IsOptional, IsDateString } from 'class-validator';
export class AvailableLeagueDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
name: string;
name: string = '';
@ApiProperty()
@IsString()
game: string;
game: string = '';
@ApiProperty()
@IsNumber()
drivers: number;
drivers: number = 0;
@ApiProperty()
@IsNumber()
avgViewsPerRace: number;
avgViewsPerRace: number = 0;
@ApiProperty({ type: Object })
mainSponsorSlot: {
available: boolean;
price: number;
};
} = { available: false, price: 0 };
@ApiProperty({ type: Object })
secondarySlots: {
available: number;
total: number;
price: number;
};
} = { available: 0, total: 0, price: 0 };
@ApiProperty()
@IsNumber()
rating: number;
rating: number = 0;
@ApiProperty({ enum: ['premium', 'standard', 'starter'] })
@IsEnum(['premium', 'standard', 'starter'])
tier: 'premium' | 'standard' | 'starter';
tier: 'premium' | 'standard' | 'starter' = 'standard';
@ApiProperty({ required: false })
@IsOptional()
@@ -50,9 +50,9 @@ export class AvailableLeagueDTO {
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
@IsEnum(['active', 'upcoming', 'completed'])
seasonStatus: 'active' | 'upcoming' | 'completed';
seasonStatus: 'active' | 'upcoming' | 'completed' = 'active';
@ApiProperty()
@IsString()
description: string;
description: string = '';
}

View File

@@ -4,25 +4,25 @@ import { IsNumber, IsDateString } from 'class-validator';
export class BillingStatsDTO {
@ApiProperty()
@IsNumber()
totalSpent: number;
totalSpent: number = 0;
@ApiProperty()
@IsNumber()
pendingAmount: number;
pendingAmount: number = 0;
@ApiProperty()
@IsDateString()
nextPaymentDate: string;
nextPaymentDate: string = '';
@ApiProperty()
@IsNumber()
nextPaymentAmount: number;
nextPaymentAmount: number = 0;
@ApiProperty()
@IsNumber()
activeSponsorships: number;
activeSponsorships: number = 0;
@ApiProperty()
@IsNumber()
averageMonthlySpend: number;
averageMonthlySpend: number = 0;
}

View File

@@ -3,5 +3,5 @@ import { SponsorDTO } from './SponsorDTO';
export class CreateSponsorOutputDTO {
@ApiProperty({ type: SponsorDTO })
sponsor: SponsorDTO;
sponsor: SponsorDTO = new SponsorDTO();
}

View File

@@ -4,29 +4,29 @@ import { IsString, IsNumber } from 'class-validator';
export class DriverDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
name: string;
name: string = '';
@ApiProperty()
@IsString()
country: string;
country: string = '';
@ApiProperty()
@IsNumber()
position: number;
position: number = 0;
@ApiProperty()
@IsNumber()
races: number;
races: number = 0;
@ApiProperty()
@IsNumber()
impressions: number;
impressions: number = 0;
@ApiProperty()
@IsString()
team: string;
team: string = '';
}

View File

@@ -3,11 +3,11 @@ import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO';
export class GetEntitySponsorshipPricingResultDTO {
@ApiProperty()
entityType: string;
entityType: string = '';
@ApiProperty()
entityId: string;
entityId: string = '';
@ApiProperty({ type: [SponsorshipPricingItemDTO] })
pricing: SponsorshipPricingItemDTO[];
pricing: SponsorshipPricingItemDTO[] = [];
}

View File

@@ -3,14 +3,14 @@ import { SponsorshipRequestDTO } from './SponsorshipRequestDTO';
export class GetPendingSponsorshipRequestsOutputDTO {
@ApiProperty()
entityType: string;
entityType: string = '';
@ApiProperty()
entityId: string;
entityId: string = '';
@ApiProperty({ type: [SponsorshipRequestDTO] })
requests: SponsorshipRequestDTO[];
requests: SponsorshipRequestDTO[] = [];
@ApiProperty()
totalCount: number;
totalCount: number = 0;
}

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetSponsorDashboardQueryParamsDTO {
@ApiProperty()
@IsString()
sponsorId: string;
sponsorId: string = '';
}

View File

@@ -3,5 +3,5 @@ import { SponsorDTO } from './SponsorDTO';
export class GetSponsorOutputDTO {
@ApiProperty({ type: SponsorDTO })
sponsor: SponsorDTO;
sponsor: SponsorDTO = new SponsorDTO();
}

View File

@@ -4,5 +4,5 @@ import { IsString } from 'class-validator';
export class GetSponsorSponsorshipsQueryParamsDTO {
@ApiProperty()
@IsString()
sponsorId: string;
sponsorId: string = '';
}

View File

@@ -3,5 +3,5 @@ import { SponsorDTO } from './SponsorDTO';
export class GetSponsorsOutputDTO {
@ApiProperty({ type: [SponsorDTO] })
sponsors: SponsorDTO[];
sponsors: SponsorDTO[] = [];
}

View File

@@ -4,45 +4,45 @@ import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator';
export class InvoiceDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
invoiceNumber: string;
invoiceNumber: string = '';
@ApiProperty()
@IsDateString()
date: string;
date: string = '';
@ApiProperty()
@IsDateString()
dueDate: string;
dueDate: string = '';
@ApiProperty()
@IsNumber()
amount: number;
amount: number = 0;
@ApiProperty()
@IsNumber()
vatAmount: number;
vatAmount: number = 0;
@ApiProperty()
@IsNumber()
totalAmount: number;
totalAmount: number = 0;
@ApiProperty({ enum: ['paid', 'pending', 'overdue', 'failed'] })
@IsEnum(['paid', 'pending', 'overdue', 'failed'])
status: 'paid' | 'pending' | 'overdue' | 'failed';
status: 'paid' | 'pending' | 'overdue' | 'failed' = 'pending';
@ApiProperty()
@IsString()
description: string;
description: string = '';
@ApiProperty({ enum: ['league', 'team', 'driver', 'race', 'platform'] })
@IsEnum(['league', 'team', 'driver', 'race', 'platform'])
sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform';
sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform' = 'league';
@ApiProperty()
@IsString()
pdfUrl: string;
pdfUrl: string = '';
}

View File

@@ -1,68 +1,68 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEnum, IsNumber, IsDateString, IsOptional } from 'class-validator';
import { IsString, IsEnum, IsNumber, IsOptional } from 'class-validator';
export class LeagueDetailDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
name: string;
name: string = '';
@ApiProperty()
@IsString()
game: string;
game: string = '';
@ApiProperty({ enum: ['premium', 'standard', 'starter'] })
@IsEnum(['premium', 'standard', 'starter'])
tier: 'premium' | 'standard' | 'starter';
tier: 'premium' | 'standard' | 'starter' = 'standard';
@ApiProperty()
@IsString()
season: string;
season: string = '';
@ApiProperty()
@IsString()
description: string;
description: string = '';
@ApiProperty()
@IsNumber()
drivers: number;
drivers: number = 0;
@ApiProperty()
@IsNumber()
races: number;
races: number = 0;
@ApiProperty()
@IsNumber()
completedRaces: number;
completedRaces: number = 0;
@ApiProperty()
@IsNumber()
totalImpressions: number;
totalImpressions: number = 0;
@ApiProperty()
@IsNumber()
avgViewsPerRace: number;
avgViewsPerRace: number = 0;
@ApiProperty()
@IsNumber()
engagement: number;
engagement: number = 0;
@ApiProperty()
@IsNumber()
rating: number;
rating: number = 0;
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
@IsEnum(['active', 'upcoming', 'completed'])
seasonStatus: 'active' | 'upcoming' | 'completed';
seasonStatus: 'active' | 'upcoming' | 'completed' = 'active';
@ApiProperty({ type: Object })
seasonDates: {
start: string;
end: string;
};
} = { start: '', end: '' };
@ApiProperty({ type: Object, required: false })
@IsOptional()
@@ -84,5 +84,8 @@ export class LeagueDetailDTO {
price: number;
benefits: string[];
};
} = {
main: { available: false, price: 0, benefits: [] },
secondary: { available: 0, total: 0, price: 0, benefits: [] }
};
}

View File

@@ -4,25 +4,25 @@ import { IsBoolean } from 'class-validator';
export class NotificationSettingsDTO {
@ApiProperty()
@IsBoolean()
emailNewSponsorships: boolean;
emailNewSponsorships: boolean = false;
@ApiProperty()
@IsBoolean()
emailWeeklyReport: boolean;
emailWeeklyReport: boolean = false;
@ApiProperty()
@IsBoolean()
emailRaceAlerts: boolean;
emailRaceAlerts: boolean = false;
@ApiProperty()
@IsBoolean()
emailPaymentAlerts: boolean;
emailPaymentAlerts: boolean = false;
@ApiProperty()
@IsBoolean()
emailNewOpportunities: boolean;
emailNewOpportunities: boolean = false;
@ApiProperty()
@IsBoolean()
emailContractExpiry: boolean;
emailContractExpiry: boolean = false;
}

View File

@@ -4,15 +4,15 @@ import { IsString, IsEnum, IsBoolean, IsOptional, IsNumber } from 'class-validat
export class PaymentMethodDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty({ enum: ['card', 'bank', 'sepa'] })
@IsEnum(['card', 'bank', 'sepa'])
type: 'card' | 'bank' | 'sepa';
type: 'card' | 'bank' | 'sepa' = 'card';
@ApiProperty()
@IsString()
last4: string;
last4: string = '';
@ApiProperty({ required: false })
@IsOptional()
@@ -21,7 +21,7 @@ export class PaymentMethodDTO {
@ApiProperty()
@IsBoolean()
isDefault: boolean;
isDefault: boolean = false;
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -4,17 +4,17 @@ import { IsBoolean } from 'class-validator';
export class PrivacySettingsDTO {
@ApiProperty()
@IsBoolean()
publicProfile: boolean;
publicProfile: boolean = false;
@ApiProperty()
@IsBoolean()
showStats: boolean;
showStats: boolean = false;
@ApiProperty()
@IsBoolean()
showActiveSponsorships: boolean;
showActiveSponsorships: boolean = false;
@ApiProperty()
@IsBoolean()
allowDirectContact: boolean;
allowDirectContact: boolean = false;
}

View File

@@ -4,21 +4,21 @@ import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator';
export class RaceDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
name: string;
name: string = '';
@ApiProperty()
@IsDateString()
date: string;
date: string = '';
@ApiProperty()
@IsNumber()
views: number;
views: number = 0;
@ApiProperty({ enum: ['upcoming', 'completed'] })
@IsEnum(['upcoming', 'completed'])
status: 'upcoming' | 'completed';
status: 'upcoming' | 'completed' = 'upcoming';
}

View File

@@ -5,7 +5,7 @@ export class RejectSponsorshipRequestInputDTO {
@ApiProperty()
@IsString()
@IsNotEmpty()
respondedBy: string;
respondedBy: string = '';
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -4,21 +4,21 @@ import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator';
export class RenewalAlertDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
name: string;
name: string = '';
@ApiProperty({ enum: ['league', 'team', 'driver', 'race', 'platform'] })
@IsEnum(['league', 'team', 'driver', 'race', 'platform'])
type: 'league' | 'team' | 'driver' | 'race' | 'platform';
type: 'league' | 'team' | 'driver' | 'race' | 'platform' = 'league';
@ApiProperty()
@IsDateString()
renewDate: string;
renewDate: string = '';
@ApiProperty()
@IsNumber()
price: number;
price: number = 0;
}

View File

@@ -2,10 +2,10 @@ import { ApiProperty } from '@nestjs/swagger';
export class SponsorDTO {
@ApiProperty()
id: string;
id: string = '';
@ApiProperty()
name: string;
name: string = '';
@ApiProperty({ required: false })
contactEmail?: string;

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsArray, IsObject } from 'class-validator';
import { IsString } from 'class-validator';
import { SponsorDashboardMetricsDTO } from './SponsorDashboardMetricsDTO';
import { SponsoredLeagueDTO } from './SponsoredLeagueDTO';
import { SponsorDashboardInvestmentDTO } from './SponsorDashboardInvestmentDTO';
@@ -10,20 +10,20 @@ import { RenewalAlertDTO } from './RenewalAlertDTO';
export class SponsorDashboardDTO {
@ApiProperty()
@IsString()
sponsorId: string;
sponsorId: string = '';
@ApiProperty()
@IsString()
sponsorName: string;
sponsorName: string = '';
@ApiProperty({ type: SponsorDashboardMetricsDTO })
metrics: SponsorDashboardMetricsDTO;
metrics: SponsorDashboardMetricsDTO = new SponsorDashboardMetricsDTO();
@ApiProperty({ type: [SponsoredLeagueDTO] })
sponsoredLeagues: SponsoredLeagueDTO[];
sponsoredLeagues: SponsoredLeagueDTO[] = [];
@ApiProperty({ type: SponsorDashboardInvestmentDTO })
investment: SponsorDashboardInvestmentDTO;
investment: SponsorDashboardInvestmentDTO = new SponsorDashboardInvestmentDTO();
@ApiProperty({ type: Object })
sponsorships: {
@@ -32,11 +32,17 @@ export class SponsorDashboardDTO {
drivers: SponsorshipDTO[];
races: SponsorshipDTO[];
platform: SponsorshipDTO[];
} = {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: []
};
@ApiProperty({ type: [ActivityItemDTO] })
recentActivity: ActivityItemDTO[];
recentActivity: ActivityItemDTO[] = [];
@ApiProperty({ type: [RenewalAlertDTO] })
upcomingRenewals: RenewalAlertDTO[];
upcomingRenewals: RenewalAlertDTO[] = [];
}

View File

@@ -4,13 +4,13 @@ import { IsNumber } from 'class-validator';
export class SponsorDashboardInvestmentDTO {
@ApiProperty()
@IsNumber()
activeSponsorships: number;
activeSponsorships: number = 0;
@ApiProperty()
@IsNumber()
totalInvestment: number;
totalInvestment: number = 0;
@ApiProperty()
@IsNumber()
costPerThousandViews: number;
costPerThousandViews: number = 0;
}

View File

@@ -4,33 +4,33 @@ import { IsNumber } from 'class-validator';
export class SponsorDashboardMetricsDTO {
@ApiProperty()
@IsNumber()
impressions: number;
impressions: number = 0;
@ApiProperty()
@IsNumber()
impressionsChange: number;
impressionsChange: number = 0;
@ApiProperty()
@IsNumber()
uniqueViewers: number;
uniqueViewers: number = 0;
@ApiProperty()
@IsNumber()
viewersChange: number;
viewersChange: number = 0;
@ApiProperty()
@IsNumber()
races: number;
races: number = 0;
@ApiProperty()
@IsNumber()
drivers: number;
drivers: number = 0;
@ApiProperty()
@IsNumber()
exposure: number;
exposure: number = 0;
@ApiProperty()
@IsNumber()
exposureChange: number;
exposureChange: number = 0;
}

View File

@@ -1,30 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsObject } from 'class-validator';
import { IsString, IsOptional } from 'class-validator';
export class SponsorProfileDTO {
@ApiProperty()
@IsString()
companyName: string;
companyName: string = '';
@ApiProperty()
@IsString()
contactName: string;
contactName: string = '';
@ApiProperty()
@IsString()
contactEmail: string;
contactEmail: string = '';
@ApiProperty()
@IsString()
contactPhone: string;
contactPhone: string = '';
@ApiProperty()
@IsString()
website: string;
website: string = '';
@ApiProperty()
@IsString()
description: string;
description: string = '';
@ApiProperty({ required: false })
@IsOptional()
@@ -33,7 +33,7 @@ export class SponsorProfileDTO {
@ApiProperty()
@IsString()
industry: string;
industry: string = '';
@ApiProperty({ type: Object })
address: {
@@ -41,16 +41,16 @@ export class SponsorProfileDTO {
city: string;
country: string;
postalCode: string;
};
} = { street: '', city: '', country: '', postalCode: '' };
@ApiProperty()
@IsString()
taxId: string;
taxId: string = '';
@ApiProperty({ type: Object })
socialLinks: {
twitter: string;
linkedin: string;
instagram: string;
};
} = { twitter: '', linkedin: '', instagram: '' };
}

View File

@@ -5,14 +5,14 @@ import { SponsorshipDetailDTO } from './SponsorshipDetailDTO';
export class SponsorSponsorshipsDTO {
@ApiProperty()
@IsString()
sponsorId: string;
sponsorId: string = '';
@ApiProperty()
@IsString()
sponsorName: string;
sponsorName: string = '';
@ApiProperty({ type: [SponsorshipDetailDTO] })
sponsorships: SponsorshipDetailDTO[];
sponsorships: SponsorshipDetailDTO[] = [];
@ApiProperty()
summary: {
@@ -21,5 +21,11 @@ export class SponsorSponsorshipsDTO {
totalInvestment: number;
totalPlatformFees: number;
currency: string;
} = {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: 0,
totalPlatformFees: 0,
currency: ''
};
}

View File

@@ -4,29 +4,29 @@ import { IsString, IsEnum, IsNumber } from 'class-validator';
export class SponsoredLeagueDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
name: string;
name: string = '';
@ApiProperty({ enum: ['main', 'secondary'] })
@IsEnum(['main', 'secondary'])
tier: 'main' | 'secondary';
tier: 'main' | 'secondary' = 'main';
@ApiProperty()
@IsNumber()
drivers: number;
drivers: number = 0;
@ApiProperty()
@IsNumber()
races: number;
races: number = 0;
@ApiProperty()
@IsNumber()
impressions: number;
impressions: number = 0;
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
@IsEnum(['active', 'upcoming', 'completed'])
status: 'active' | 'upcoming' | 'completed';
status: 'active' | 'upcoming' | 'completed' = 'active';
}

View File

@@ -4,19 +4,19 @@ import { IsString, IsEnum, IsNumber, IsOptional, IsDateString } from 'class-vali
export class SponsorshipDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty({ enum: ['leagues', 'teams', 'drivers', 'races', 'platform'] })
@IsEnum(['leagues', 'teams', 'drivers', 'races', 'platform'])
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform';
type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform' = 'leagues';
@ApiProperty()
@IsString()
entityId: string;
entityId: string = '';
@ApiProperty()
@IsString()
entityName: string;
entityName: string = '';
@ApiProperty({ enum: ['main', 'secondary'], required: false })
@IsOptional()
@@ -25,7 +25,7 @@ export class SponsorshipDTO {
@ApiProperty({ enum: ['active', 'pending_approval', 'approved', 'rejected', 'expired'] })
@IsEnum(['active', 'pending_approval', 'approved', 'rejected', 'expired'])
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired';
status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired' = 'pending_approval';
@ApiProperty({ required: false })
@IsOptional()
@@ -44,19 +44,19 @@ export class SponsorshipDTO {
@ApiProperty()
@IsDateString()
startDate: string;
startDate: string = '';
@ApiProperty()
@IsDateString()
endDate: string;
endDate: string = '';
@ApiProperty()
@IsNumber()
price: number;
price: number = 0;
@ApiProperty()
@IsNumber()
impressions: number;
impressions: number = 0;
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -4,23 +4,23 @@ import { IsString, IsEnum, IsOptional, IsDate } from 'class-validator';
export class SponsorshipDetailDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
leagueId: string;
leagueId: string = '';
@ApiProperty()
@IsString()
leagueName: string;
leagueName: string = '';
@ApiProperty()
@IsString()
seasonId: string;
seasonId: string = '';
@ApiProperty()
@IsString()
seasonName: string;
seasonName: string = '';
@ApiProperty({ required: false })
@IsOptional()
@@ -34,29 +34,29 @@ export class SponsorshipDetailDTO {
@ApiProperty({ enum: ['main', 'secondary'] })
@IsEnum(['main', 'secondary'])
tier: 'main' | 'secondary';
tier: 'main' | 'secondary' = 'main';
@ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
@IsEnum(['pending', 'active', 'expired', 'cancelled'])
status: 'pending' | 'active' | 'expired' | 'cancelled';
status: 'pending' | 'active' | 'expired' | 'cancelled' = 'pending';
@ApiProperty()
pricing: {
amount: number;
currency: string;
};
} = { amount: 0, currency: '' };
@ApiProperty()
platformFee: {
amount: number;
currency: string;
};
} = { amount: 0, currency: '' };
@ApiProperty()
netAmount: {
amount: number;
currency: string;
};
} = { amount: 0, currency: '' };
@ApiProperty()
metrics: {
@@ -64,10 +64,10 @@ export class SponsorshipDetailDTO {
races: number;
completedRaces: number;
impressions: number;
};
} = { drivers: 0, races: 0, completedRaces: 0, impressions: 0 };
@ApiProperty()
createdAt: Date;
createdAt: Date = new Date();
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -4,17 +4,17 @@ import { IsString, IsNumber } from 'class-validator';
export class SponsorshipPricingItemDTO {
@ApiProperty()
@IsString()
id: string;
id: string = '';
@ApiProperty()
@IsString()
level: string;
level: string = '';
@ApiProperty()
@IsNumber()
price: number;
price: number = 0;
@ApiProperty()
@IsString()
currency: string;
currency: string = '';
}

View File

@@ -2,38 +2,38 @@ import { ApiProperty } from '@nestjs/swagger';
export class SponsorshipRequestDTO {
@ApiProperty()
id: string;
id: string = '';
@ApiProperty()
sponsorId: string;
sponsorId: string = '';
@ApiProperty()
sponsorName: string;
sponsorName: string = '';
@ApiProperty({ required: false })
sponsorLogo?: string;
@ApiProperty()
tier: string;
tier: string = '';
@ApiProperty()
offeredAmount: number;
offeredAmount: number = 0;
@ApiProperty()
currency: string;
currency: string = '';
@ApiProperty()
formattedAmount: string;
formattedAmount: string = '';
@ApiProperty({ required: false })
message?: string;
@ApiProperty()
createdAt: Date;
createdAt: Date = new Date();
@ApiProperty()
platformFee: number;
platformFee: number = 0;
@ApiProperty()
netAmount: number;
netAmount: number = 0;
}

View File

@@ -1,4 +1,4 @@
import type { AcceptSponsorshipOutputPort } from '@core/racing/application/ports/output/AcceptSponsorshipOutputPort';
import type { AcceptSponsorshipResult } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
export interface AcceptSponsorshipRequestResultViewModel {
requestId: string;
@@ -16,7 +16,7 @@ export class AcceptSponsorshipRequestPresenter {
this.result = null;
}
present(output: AcceptSponsorshipOutputPort | null) {
present(output: AcceptSponsorshipResult | null) {
if (!output) {
this.result = null;
return;

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CreateSponsorPresenter } from './CreateSponsorPresenter';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
describe('CreateSponsorPresenter', () => {
let presenter: CreateSponsorPresenter;
@@ -10,9 +11,23 @@ describe('CreateSponsorPresenter', () => {
describe('reset', () => {
it('should reset the result to null', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockPort);
expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
presenter.present(mockSponsor);
const expectedViewModel = {
sponsor: {
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: mockSponsor.createdAt.toDate()
}
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +36,24 @@ describe('CreateSponsorPresenter', () => {
describe('present', () => {
it('should store the result', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
presenter.present(mockPort);
presenter.present(mockSponsor);
expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
const expectedViewModel = {
sponsor: {
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: mockSponsor.createdAt.toDate()
}
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
@@ -35,10 +63,23 @@ describe('CreateSponsorPresenter', () => {
});
it('should return the result when presented', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockPort);
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
presenter.present(mockSponsor);
expect(presenter.getViewModel()).toEqual({ sponsor: mockPort.sponsor });
const expectedViewModel = {
sponsor: {
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: mockSponsor.createdAt.toDate()
}
};
expect(presenter.getViewModel()).toEqual(expectedViewModel);
});
});
@@ -48,10 +89,23 @@ describe('CreateSponsorPresenter', () => {
});
it('should return the result when presented', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } };
presenter.present(mockPort);
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
presenter.present(mockSponsor);
expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor });
const expectedViewModel = {
sponsor: {
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: mockSponsor.createdAt.toDate()
}
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
});

View File

@@ -1,4 +1,4 @@
import type { CreateSponsorOutputPort } from '@core/racing/application/ports/output/CreateSponsorOutputPort';
import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import type { CreateSponsorOutputDTO } from '../dtos/CreateSponsorOutputDTO';
export class CreateSponsorPresenter {
@@ -8,16 +8,24 @@ export class CreateSponsorPresenter {
this.result = null;
}
present(port: CreateSponsorOutputPort) {
present(sponsor: Sponsor) {
const sponsorData: any = {
id: sponsor.id.toString(),
name: sponsor.name.toString(),
contactEmail: sponsor.contactEmail.toString(),
createdAt: sponsor.createdAt.toDate(),
};
if (sponsor.logoUrl) {
sponsorData.logoUrl = sponsor.logoUrl.toString();
}
if (sponsor.websiteUrl) {
sponsorData.websiteUrl = sponsor.websiteUrl.toString();
}
this.result = {
sponsor: {
id: port.sponsor.id,
name: port.sponsor.name,
contactEmail: port.sponsor.contactEmail,
logoUrl: port.sponsor.logoUrl,
websiteUrl: port.sponsor.websiteUrl,
createdAt: port.sponsor.createdAt,
},
sponsor: sponsorData,
};
}

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetEntitySponsorshipPricingPresenter } from './GetEntitySponsorshipPricingPresenter';
import type { GetEntitySponsorshipPricingResult } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
describe('GetEntitySponsorshipPricingPresenter', () => {
let presenter: GetEntitySponsorshipPricingPresenter;
@@ -10,9 +11,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => {
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
const mockResult: GetEntitySponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
acceptingApplications: true,
tiers: []
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +33,21 @@ describe('GetEntitySponsorshipPricingPresenter', () => {
describe('present', () => {
it('should store the result', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
const mockResult: GetEntitySponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
acceptingApplications: true,
tiers: []
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
@@ -35,10 +57,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
const mockResult: GetEntitySponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
acceptingApplications: true,
tiers: []
};
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.getViewModel()).toEqual(expectedViewModel);
});
});
@@ -48,10 +80,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
const mockResult: GetEntitySponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
acceptingApplications: true,
tiers: []
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
});

View File

@@ -1,4 +1,4 @@
import type { GetSponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetSponsorshipPricingOutputPort';
import type { GetEntitySponsorshipPricingResult } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetEntitySponsorshipPricingPresenter {
@@ -8,7 +8,7 @@ export class GetEntitySponsorshipPricingPresenter {
this.result = null;
}
present(output: GetSponsorshipPricingOutputPort | null) {
present(output: GetEntitySponsorshipPricingResult | null) {
if (!output) {
this.result = {
entityType: 'season',
@@ -21,11 +21,11 @@ export class GetEntitySponsorshipPricingPresenter {
this.result = {
entityType: output.entityType,
entityId: output.entityId,
pricing: output.pricing.map(item => ({
id: item.id,
level: item.level,
pricing: output.tiers.map(item => ({
id: item.name,
level: item.name,
price: item.price,
currency: item.currency,
currency: 'USD',
})),
};
}

View File

@@ -1,4 +1,4 @@
import type { PendingSponsorshipRequestsOutputPort } from '@core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort';
import type { GetPendingSponsorshipRequestsResult } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import { GetPendingSponsorshipRequestsOutputDTO } from '../dtos/GetPendingSponsorshipRequestsOutputDTO';
export class GetPendingSponsorshipRequestsPresenter {
@@ -8,7 +8,7 @@ export class GetPendingSponsorshipRequestsPresenter {
this.result = null;
}
present(outputPort: PendingSponsorshipRequestsOutputPort | null) {
present(outputPort: GetPendingSponsorshipRequestsResult | null) {
if (!outputPort) {
this.result = null;
return;
@@ -17,7 +17,30 @@ export class GetPendingSponsorshipRequestsPresenter {
this.result = {
entityType: outputPort.entityType,
entityId: outputPort.entityId,
requests: outputPort.requests,
requests: outputPort.requests.map(r => {
const request: any = {
id: r.request.id,
sponsorId: r.request.sponsorId,
sponsorName: r.sponsor?.name?.toString() || 'Unknown Sponsor',
tier: r.request.tier,
offeredAmount: r.financials.offeredAmount.amount,
currency: r.financials.offeredAmount.currency,
formattedAmount: `${r.financials.offeredAmount.amount} ${r.financials.offeredAmount.currency}`,
createdAt: r.request.createdAt,
platformFee: r.financials.platformFee.amount,
netAmount: r.financials.netAmount.amount,
};
if (r.sponsor?.logoUrl) {
request.sponsorLogo = r.sponsor.logoUrl.toString();
}
if (r.request.message) {
request.message = r.request.message;
}
return request;
}),
totalCount: outputPort.totalCount,
};
}

View File

@@ -1,5 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorDashboardPresenter } from './GetSponsorDashboardPresenter';
import type { GetSponsorDashboardResult } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import { Money } from '@core/racing/domain/value-objects/Money';
describe('GetSponsorDashboardPresenter', () => {
let presenter: GetSponsorDashboardPresenter;
@@ -10,9 +12,58 @@ describe('GetSponsorDashboardPresenter', () => {
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
const mockResult: GetSponsorDashboardResult = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
costPerThousandViews: 0,
},
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +72,59 @@ describe('GetSponsorDashboardPresenter', () => {
describe('present', () => {
it('should store the result', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
const mockResult: GetSponsorDashboardResult = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
costPerThousandViews: 0,
},
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
@@ -35,10 +134,58 @@ describe('GetSponsorDashboardPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
const mockResult: GetSponsorDashboardResult = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
costPerThousandViews: 0,
},
};
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
};
expect(presenter.getViewModel()).toEqual(expectedViewModel);
});
});
@@ -48,10 +195,58 @@ describe('GetSponsorDashboardPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
const mockResult: GetSponsorDashboardResult = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
costPerThousandViews: 0,
},
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
metrics: {
impressions: 1000,
impressionsChange: 0,
uniqueViewers: 700,
viewersChange: 0,
races: 5,
drivers: 10,
exposure: 50,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
});

View File

@@ -1,4 +1,4 @@
import type { SponsorDashboardOutputPort } from '@core/racing/application/ports/output/SponsorDashboardOutputPort';
import type { GetSponsorDashboardResult } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import { SponsorDashboardDTO } from '../dtos/SponsorDashboardDTO';
export class GetSponsorDashboardPresenter {
@@ -8,8 +8,40 @@ export class GetSponsorDashboardPresenter {
this.result = null;
}
present(outputPort: SponsorDashboardOutputPort | null) {
this.result = outputPort ?? null;
present(outputPort: GetSponsorDashboardResult | null) {
if (!outputPort) {
this.result = null;
return;
}
this.result = {
sponsorId: outputPort.sponsorId,
sponsorName: outputPort.sponsorName,
metrics: outputPort.metrics,
sponsoredLeagues: outputPort.sponsoredLeagues.map(league => ({
id: league.leagueId,
name: league.leagueName,
tier: league.tier,
drivers: league.metrics.drivers,
races: league.metrics.races,
impressions: league.metrics.impressions,
status: league.status,
})),
investment: {
activeSponsorships: outputPort.investment.activeSponsorships,
totalInvestment: outputPort.investment.totalInvestment.amount,
costPerThousandViews: outputPort.investment.costPerThousandViews,
},
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
};
}
getViewModel(): SponsorDashboardDTO | null {

View File

@@ -1,5 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorSponsorshipsPresenter } from './GetSponsorSponsorshipsPresenter';
import type { GetSponsorSponsorshipsResult } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import { Money } from '@core/racing/domain/value-objects/Money';
describe('GetSponsorSponsorshipsPresenter', () => {
let presenter: GetSponsorSponsorshipsPresenter;
@@ -10,9 +13,38 @@ describe('GetSponsorSponsorshipsPresenter', () => {
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
const mockResult: GetSponsorSponsorshipsResult = {
sponsor: mockSponsor,
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
totalPlatformFees: Money.create(0, 'USD'),
},
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: 0,
totalPlatformFees: 0,
currency: 'USD',
},
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +53,39 @@ describe('GetSponsorSponsorshipsPresenter', () => {
describe('present', () => {
it('should store the result', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
const mockResult: GetSponsorSponsorshipsResult = {
sponsor: mockSponsor,
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
totalPlatformFees: Money.create(0, 'USD'),
},
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: 0,
totalPlatformFees: 0,
currency: 'USD',
},
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
@@ -35,10 +95,38 @@ describe('GetSponsorSponsorshipsPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
const mockResult: GetSponsorSponsorshipsResult = {
sponsor: mockSponsor,
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
totalPlatformFees: Money.create(0, 'USD'),
},
};
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: 0,
totalPlatformFees: 0,
currency: 'USD',
},
};
expect(presenter.getViewModel()).toEqual(expectedViewModel);
});
});
@@ -48,10 +136,38 @@ describe('GetSponsorSponsorshipsPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
const mockSponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test Sponsor',
contactEmail: 'test@example.com',
createdAt: new Date()
});
const mockResult: GetSponsorSponsorshipsResult = {
sponsor: mockSponsor,
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
totalPlatformFees: Money.create(0, 'USD'),
},
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: 0,
totalPlatformFees: 0,
currency: 'USD',
},
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
});

View File

@@ -1,4 +1,4 @@
import type { SponsorSponsorshipsOutputPort } from '@core/racing/application/ports/output/SponsorSponsorshipsOutputPort';
import type { GetSponsorSponsorshipsResult } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import { SponsorSponsorshipsDTO } from '../dtos/SponsorSponsorshipsDTO';
export class GetSponsorSponsorshipsPresenter {
@@ -8,8 +8,58 @@ export class GetSponsorSponsorshipsPresenter {
this.result = null;
}
present(outputPort: SponsorSponsorshipsOutputPort | null) {
this.result = outputPort ?? null;
present(outputPort: GetSponsorSponsorshipsResult | null) {
if (!outputPort) {
this.result = null;
return;
}
this.result = {
sponsorId: outputPort.sponsor.id.toString(),
sponsorName: outputPort.sponsor.name.toString(),
sponsorships: outputPort.sponsorships.map(s => {
// Map status to DTO expected values
let status: 'pending' | 'active' | 'expired' | 'cancelled';
if (s.sponsorship.status === 'ended') {
status = 'expired';
} else if (s.sponsorship.status === 'pending' || s.sponsorship.status === 'active' || s.sponsorship.status === 'cancelled') {
status = s.sponsorship.status;
} else {
status = 'pending';
}
return {
id: s.sponsorship.id.toString(),
leagueId: s.league.id.toString(),
leagueName: s.league.name.toString(),
seasonId: s.season.id.toString(),
seasonName: s.season.name.toString(),
tier: s.sponsorship.tier,
status,
pricing: {
amount: s.financials.pricing.amount,
currency: s.financials.pricing.currency,
},
platformFee: {
amount: s.financials.platformFee.amount,
currency: s.financials.platformFee.currency,
},
netAmount: {
amount: s.financials.netAmount.amount,
currency: s.financials.netAmount.currency,
},
metrics: s.metrics,
createdAt: s.sponsorship.createdAt,
};
}),
summary: {
totalSponsorships: outputPort.summary.totalSponsorships,
activeSponsorships: outputPort.summary.activeSponsorships,
totalInvestment: outputPort.summary.totalInvestment.amount,
totalPlatformFees: outputPort.summary.totalPlatformFees.amount,
currency: outputPort.summary.totalInvestment.currency,
},
};
}
getViewModel(): SponsorSponsorshipsDTO | null {

View File

@@ -1,11 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { Result } from '@core/shared/application/Result';
import type {
GetSponsorsResult,
GetSponsorsErrorCode,
} from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { GetSponsorsPresenter } from './GetSponsorsPresenter';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
describe('GetSponsorsPresenter', () => {
let presenter: GetSponsorsPresenter;
@@ -16,8 +11,8 @@ describe('GetSponsorsPresenter', () => {
describe('reset', () => {
it('should reset the model to null and cause responseModel to throw', () => {
const result = Result.ok<GetSponsorsResult, never>({ sponsors: [] });
presenter.present(result);
const sponsors: Sponsor[] = [];
presenter.present(sponsors);
expect(presenter.responseModel).toEqual({ sponsors: [] });
presenter.reset();
@@ -26,29 +21,24 @@ describe('GetSponsorsPresenter', () => {
});
describe('present', () => {
it('should map Result.ok sponsors to DTO responseModel', () => {
const result = Result.ok<GetSponsorsResult, never>({
sponsors: [
{
id: 'sponsor-1',
name: 'Sponsor One',
contactEmail: 's1@example.com',
logoUrl: 'logo1.png',
websiteUrl: 'https://one.example.com',
createdAt: new Date('2024-01-01T00:00:00Z'),
},
{
id: 'sponsor-2',
name: 'Sponsor Two',
contactEmail: 's2@example.com',
logoUrl: undefined,
websiteUrl: undefined,
createdAt: undefined,
},
],
});
it('should map sponsors to DTO responseModel', () => {
const sponsors: Sponsor[] = [
Sponsor.create({
id: 'sponsor-1',
name: 'Sponsor One',
contactEmail: 's1@example.com',
logoUrl: 'logo1.png',
websiteUrl: 'https://one.example.com',
createdAt: new Date('2024-01-01T00:00:00Z'),
}),
Sponsor.create({
id: 'sponsor-2',
name: 'Sponsor Two',
contactEmail: 's2@example.com',
}),
];
presenter.present(result);
presenter.present(sponsors);
expect(presenter.responseModel).toEqual({
sponsors: [
@@ -64,9 +54,7 @@ describe('GetSponsorsPresenter', () => {
id: 'sponsor-2',
name: 'Sponsor Two',
contactEmail: 's2@example.com',
logoUrl: undefined,
websiteUrl: undefined,
createdAt: undefined,
createdAt: expect.any(Date),
},
],
});
@@ -79,8 +67,8 @@ describe('GetSponsorsPresenter', () => {
});
it('should return the model when presented', () => {
const result = Result.ok<GetSponsorsResult, never>({ sponsors: [] });
presenter.present(result);
const sponsors: Sponsor[] = [];
presenter.present(sponsors);
expect(presenter.getResponseModel()).toEqual({ sponsors: [] });
});
@@ -91,16 +79,26 @@ describe('GetSponsorsPresenter', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented');
});
it('should fallback to empty sponsors list on error', () => {
const error = {
code: 'REPOSITORY_ERROR' as GetSponsorsErrorCode,
details: { message: 'DB error' },
} satisfies ApplicationErrorCode<GetSponsorsErrorCode, { message: string }>;
const result = Result.err<GetSponsorsResult, typeof error>(error);
it('should return the model when presented', () => {
const sponsors: Sponsor[] = [
Sponsor.create({
id: 'sponsor-1',
name: 'Sponsor One',
contactEmail: 's1@example.com',
}),
];
presenter.present(sponsors);
presenter.present(result);
expect(presenter.responseModel).toEqual({ sponsors: [] });
expect(presenter.responseModel).toEqual({
sponsors: [
{
id: 'sponsor-1',
name: 'Sponsor One',
contactEmail: 's1@example.com',
createdAt: expect.any(Date),
},
],
});
});
});
});

View File

@@ -1,9 +1,4 @@
import type { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetSponsorsResult,
GetSponsorsErrorCode,
} from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO';
import type { SponsorDTO } from '../dtos/SponsorDTO';
@@ -14,29 +9,26 @@ export class GetSponsorsPresenter {
this.model = null;
}
present(
result: Result<
GetSponsorsResult,
ApplicationErrorCode<GetSponsorsErrorCode, { message: string }>
>,
): void {
if (result.isErr()) {
// For sponsor listing, fall back to empty list on error
this.model = { sponsors: [] };
return;
}
const output = result.unwrap();
present(sponsors: Sponsor[]): void {
this.model = {
sponsors: output.sponsors.map<SponsorDTO>((sponsor) => ({
id: sponsor.id,
name: sponsor.name,
contactEmail: sponsor.contactEmail,
logoUrl: sponsor.logoUrl,
websiteUrl: sponsor.websiteUrl,
createdAt: sponsor.createdAt,
})),
sponsors: sponsors.map<SponsorDTO>((sponsor) => {
const sponsorData: any = {
id: sponsor.id.toString(),
name: sponsor.name.toString(),
contactEmail: sponsor.contactEmail.toString(),
createdAt: sponsor.createdAt.toDate(),
};
if (sponsor.logoUrl) {
sponsorData.logoUrl = sponsor.logoUrl.toString();
}
if (sponsor.websiteUrl) {
sponsorData.websiteUrl = sponsor.websiteUrl.toString();
}
return sponsorData;
}),
};
}

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter';
import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
describe('GetSponsorshipPricingPresenter', () => {
let presenter: GetSponsorshipPricingPresenter;
@@ -10,9 +11,19 @@ describe('GetSponsorshipPricingPresenter', () => {
describe('reset', () => {
it('should reset the result to null', () => {
const mockResult = { tiers: [] };
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +32,20 @@ describe('GetSponsorshipPricingPresenter', () => {
describe('present', () => {
it('should store the result', () => {
const mockResult = { tiers: [] };
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
@@ -35,10 +55,19 @@ describe('GetSponsorshipPricingPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { tiers: [] };
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
expect(presenter.getViewModel()).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.getViewModel()).toEqual(expectedViewModel);
});
});
@@ -48,10 +77,19 @@ describe('GetSponsorshipPricingPresenter', () => {
});
it('should return the result when presented', () => {
const mockResult = { tiers: [] };
const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
});
});
});

View File

@@ -1,12 +1,32 @@
import type { GetSponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetSponsorshipPricingOutputPort';
import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetSponsorshipPricingPresenter {
present(outputPort: GetSponsorshipPricingOutputPort): GetEntitySponsorshipPricingResultDTO {
return {
private result: GetEntitySponsorshipPricingResultDTO | null = null;
reset() {
this.result = null;
}
present(outputPort: GetSponsorshipPricingResult): void {
this.result = {
entityType: outputPort.entityType,
entityId: outputPort.entityId,
pricing: outputPort.pricing,
pricing: outputPort.pricing.map(item => ({
id: item.id,
level: item.level,
price: item.price,
currency: item.currency,
})),
};
}
getViewModel(): GetEntitySponsorshipPricingResultDTO | null {
return this.result;
}
get viewModel(): GetEntitySponsorshipPricingResultDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -1,21 +1,21 @@
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
export class RejectSponsorshipRequestPresenter {
private result: RejectSponsorshipRequestResultDTO | null = null;
private result: RejectSponsorshipRequestResult | null = null;
reset() {
this.result = null;
}
present(output: RejectSponsorshipRequestResultDTO | null) {
present(output: RejectSponsorshipRequestResult | null) {
this.result = output ?? null;
}
getViewModel(): RejectSponsorshipRequestResultDTO | null {
getViewModel(): RejectSponsorshipRequestResult | null {
return this.result;
}
get viewModel(): RejectSponsorshipRequestResultDTO | null {
get viewModel(): RejectSponsorshipRequestResult | null {
return this.result;
}
}

View File

@@ -3,7 +3,8 @@ import { vi } from 'vitest';
import { TeamController } from './TeamController';
import { TeamService } from './TeamService';
import type { Request } from 'express';
import { CreateTeamInputDTO, UpdateTeamInputDTO } from './dtos/CreateTeamInputDTO';
import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO';
import { UpdateTeamInput } from './dtos/TeamDto';
describe('TeamController', () => {
let controller: TeamController;
@@ -35,7 +36,7 @@ describe('TeamController', () => {
describe('getAll', () => {
it('should return all teams', async () => {
const result = { teams: [] };
const result = { teams: [], totalCount: 0 };
service.getAll.mockResolvedValue(result);
const response = await controller.getAll();
@@ -49,12 +50,12 @@ describe('TeamController', () => {
it('should return team details', async () => {
const teamId = 'team-123';
const userId = 'user-456';
const result = { id: teamId, name: 'Team' };
const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: null, canManage: false };
service.getDetails.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const mockReq = { user: { userId } } as any;
const response = await controller.getDetails(teamId, mockReq as Request);
const response = await controller.getDetails(teamId, mockReq);
expect(service.getDetails).toHaveBeenCalledWith(teamId, userId);
expect(response).toEqual(result);
@@ -64,7 +65,7 @@ describe('TeamController', () => {
describe('getMembers', () => {
it('should return team members', async () => {
const teamId = 'team-123';
const result = { members: [] };
const result = { members: [], totalCount: 0, ownerCount: 0, managerCount: 0, memberCount: 0 };
service.getMembers.mockResolvedValue(result);
const response = await controller.getMembers(teamId);
@@ -77,7 +78,7 @@ describe('TeamController', () => {
describe('getJoinRequests', () => {
it('should return join requests', async () => {
const teamId = 'team-123';
const result = { requests: [] };
const result = { requests: [], pendingCount: 0, totalCount: 0 };
service.getJoinRequests.mockResolvedValue(result);
const response = await controller.getJoinRequests(teamId);
@@ -89,14 +90,14 @@ describe('TeamController', () => {
describe('create', () => {
it('should create team', async () => {
const input: CreateTeamInputDTO = { name: 'New Team' };
const input: CreateTeamInputDTO = { name: 'New Team', tag: 'TAG' };
const userId = 'user-123';
const result = { teamId: 'team-456' };
const result = { id: 'team-456', success: true };
service.create.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const mockReq = { user: { userId } } as any;
const response = await controller.create(input, mockReq as Request);
const response = await controller.create(input, mockReq);
expect(service.create).toHaveBeenCalledWith(input, userId);
expect(response).toEqual(result);
@@ -106,14 +107,14 @@ describe('TeamController', () => {
describe('update', () => {
it('should update team', async () => {
const teamId = 'team-123';
const input: UpdateTeamInputDTO = { name: 'Updated Team' };
const userId = 'user-456';
const input: UpdateTeamInput = { name: 'Updated Team', updatedBy: userId };
const result = { success: true };
service.update.mockResolvedValue(result);
const mockReq: Partial<Request> = { ['user']: { userId } };
const mockReq = { user: { userId } } as any;
const response = await controller.update(teamId, input, mockReq as Request);
const response = await controller.update(teamId, input, mockReq);
expect(service.update).toHaveBeenCalledWith(teamId, input, userId);
expect(response).toEqual(result);
@@ -123,7 +124,7 @@ describe('TeamController', () => {
describe('getDriverTeam', () => {
it('should return driver team', async () => {
const driverId = 'driver-123';
const result = { teamId: 'team-456' };
const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false };
service.getDriverTeam.mockResolvedValue(result);
const response = await controller.getDriverTeam(driverId);
@@ -137,7 +138,7 @@ describe('TeamController', () => {
it('should return team membership', async () => {
const teamId = 'team-123';
const driverId = 'driver-456';
const result = { role: 'member' };
const result = { role: 'member' as const, joinedAt: '2023-01-01', isActive: true };
service.getMembership.mockResolvedValue(result);
const response = await controller.getMembership(teamId, driverId);

View File

@@ -8,7 +8,7 @@ import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO';
import { GetTeamJoinRequestsOutputDTO } from './dtos/GetTeamJoinRequestsOutputDTO';
import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO';
import { CreateTeamOutputDTO } from './dtos/CreateTeamOutputDTO';
import { UpdateTeamInputDTO } from './dtos/UpdateTeamInputDTO';
import { UpdateTeamInput } from './dtos/TeamDto';
import { UpdateTeamOutputDTO } from './dtos/UpdateTeamOutputDTO';
import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO';
import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO';
@@ -22,8 +22,7 @@ export class TeamController {
@ApiOperation({ summary: 'Get all teams' })
@ApiResponse({ status: 200, description: 'List of all teams', type: GetAllTeamsOutputDTO })
async getAll(): Promise<GetAllTeamsOutputDTO> {
const presenter = await this.teamService.getAll();
return presenter.responseModel;
return await this.teamService.getAll();
}
@Get(':teamId')
@@ -31,43 +30,38 @@ export class TeamController {
@ApiResponse({ status: 200, description: 'Team details', type: GetTeamDetailsOutputDTO })
@ApiResponse({ status: 404, description: 'Team not found' })
async getDetails(@Param('teamId') teamId: string, @Req() req: Request): Promise<GetTeamDetailsOutputDTO | null> {
const userId = req['user']?.userId;
const presenter = await this.teamService.getDetails(teamId, userId);
return presenter.getResponseModel();
const userId = (req as any)['user']?.userId;
return await this.teamService.getDetails(teamId, userId);
}
@Get(':teamId/members')
@ApiOperation({ summary: 'Get team members' })
@ApiResponse({ status: 200, description: 'Team members', type: GetTeamMembersOutputDTO })
async getMembers(@Param('teamId') teamId: string): Promise<GetTeamMembersOutputDTO> {
const presenter = await this.teamService.getMembers(teamId);
return presenter.getResponseModel()!;
return await this.teamService.getMembers(teamId);
}
@Get(':teamId/join-requests')
@ApiOperation({ summary: 'Get team join requests' })
@ApiResponse({ status: 200, description: 'Team join requests', type: GetTeamJoinRequestsOutputDTO })
async getJoinRequests(@Param('teamId') teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
const presenter = await this.teamService.getJoinRequests(teamId);
return presenter.getResponseModel()!;
return await this.teamService.getJoinRequests(teamId);
}
@Post()
@ApiOperation({ summary: 'Create a new team' })
@ApiResponse({ status: 201, description: 'Team created', type: CreateTeamOutputDTO })
async create(@Body() input: CreateTeamInputDTO, @Req() req: Request): Promise<CreateTeamOutputDTO> {
const userId = req['user']?.userId;
const presenter = await this.teamService.create(input, userId);
return presenter.responseModel;
const userId = (req as any)['user']?.userId;
return await this.teamService.create(input, userId);
}
@Patch(':teamId')
@ApiOperation({ summary: 'Update team' })
@ApiResponse({ status: 200, description: 'Team updated', type: UpdateTeamOutputDTO })
async update(@Param('teamId') teamId: string, @Body() input: UpdateTeamInputDTO, @Req() req: Request): Promise<UpdateTeamOutputDTO> {
const userId = req['user']?.userId;
const presenter = await this.teamService.update(teamId, input, userId);
return presenter.responseModel;
async update(@Param('teamId') teamId: string, @Body() input: UpdateTeamInput, @Req() req: Request): Promise<UpdateTeamOutputDTO> {
const userId = (req as any)['user']?.userId;
return await this.teamService.update(teamId, input, userId);
}
@Get('driver/:driverId')
@@ -75,8 +69,7 @@ export class TeamController {
@ApiResponse({ status: 200, description: 'Driver\'s team', type: GetDriverTeamOutputDTO })
@ApiResponse({ status: 404, description: 'Team not found' })
async getDriverTeam(@Param('driverId') driverId: string): Promise<GetDriverTeamOutputDTO | null> {
const presenter = await this.teamService.getDriverTeam(driverId);
return presenter.getResponseModel();
return await this.teamService.getDriverTeam(driverId);
}
@Get(':teamId/members/:driverId')
@@ -84,7 +77,6 @@ export class TeamController {
@ApiResponse({ status: 200, description: 'Team membership', type: GetTeamMembershipOutputDTO })
@ApiResponse({ status: 404, description: 'Membership not found' })
async getMembership(@Param('teamId') teamId: string, @Param('driverId') driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
const presenter = await this.teamService.getMembership(teamId, driverId);
return presenter.responseModel;
return await this.teamService.getMembership(teamId, driverId);
}
}

View File

@@ -3,10 +3,6 @@ import { TeamService } from './TeamService';
// Import core interfaces
import type { Logger } from '@core/shared/application/Logger';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Import concrete in-memory implementations
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
@@ -15,15 +11,7 @@ import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
// Use cases are imported and used directly in the service
// Define injection tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
@@ -58,53 +46,5 @@ export const TeamProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
// Use cases
{
provide: GetAllTeamsUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetAllTeamsUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamDetailsUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new GetTeamDetailsUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: GetTeamMembersUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamJoinRequestsUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: CreateTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new CreateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: UpdateTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
new UpdateTeamUseCase(teamRepo, membershipRepo),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
},
{
provide: GetDriverTeamUseCase,
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger),
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
{
provide: GetTeamMembershipUseCase,
useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) =>
new GetTeamMembershipUseCase(membershipRepo, logger),
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
},
// Use cases are created directly in the service
];

View File

@@ -1,4 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
@@ -11,20 +12,20 @@ import { DriverTeamViewModel } from './dtos/TeamDto';
describe('TeamService', () => {
let service: TeamService;
let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>;
let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>;
let getAllTeamsUseCase: ReturnType<typeof vi.mocked<GetAllTeamsUseCase>>;
let getDriverTeamUseCase: ReturnType<typeof vi.mocked<GetDriverTeamUseCase>>;
beforeEach(async () => {
const mockGetAllTeamsUseCase = {
execute: jest.fn(),
execute: vi.fn(),
};
const mockGetDriverTeamUseCase = {
execute: jest.fn(),
execute: vi.fn(),
};
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
};
const module: TestingModule = await Test.createTestingModule({
@@ -61,11 +62,11 @@ describe('TeamService', () => {
getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = {
present: jest.fn(),
getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }),
present: vi.fn(),
getViewModel: vi.fn().mockReturnValue({ teams: [], totalCount: 0 }),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
(AllTeamsPresenter as any) = vi.fn().mockImplementation(() => mockPresenter);
const result = await service.getAll();
@@ -81,11 +82,11 @@ describe('TeamService', () => {
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = {
present: jest.fn(),
getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel),
present: vi.fn(),
getViewModel: vi.fn().mockReturnValue({} as DriverTeamViewModel),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
(DriverTeamPresenter as any) = vi.fn().mockImplementation(() => mockPresenter);
const result = await service.getDriverTeam('driver1');

View File

@@ -15,7 +15,6 @@ import type { Logger } from '@core/shared/application/Logger';
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
@@ -38,7 +37,7 @@ import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
// Tokens
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN } from './TeamProviders';
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN } from './TeamProviders';
@Injectable()
export class TeamService {
@@ -46,7 +45,6 @@ export class TeamService {
@Inject(TEAM_REPOSITORY_TOKEN) private readonly teamRepository: ITeamRepository,
@Inject(TEAM_MEMBERSHIP_REPOSITORY_TOKEN) private readonly membershipRepository: ITeamMembershipRepository,
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -57,11 +55,11 @@ export class TeamService {
const useCase = new GetAllTeamsUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute();
if (result.isErr()) {
this.logger.error('Error fetching all teams', result.error?.details?.message || 'Unknown error');
this.logger.error('Error fetching all teams', new Error(result.error?.details?.message || 'Unknown error'));
return { teams: [], totalCount: 0 };
}
return presenter.responseModel;
return presenter.getResponseModel()!;
}
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
@@ -75,14 +73,14 @@ export class TeamService {
return null;
}
return presenter.getResponseModel();
return presenter.getResponseModel()!;
}
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter();
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.imageService, this.logger, presenter);
const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger, presenter);
const result = await useCase.execute({ teamId });
if (result.isErr()) {
this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`);
@@ -105,7 +103,7 @@ export class TeamService {
const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter);
const result = await useCase.execute({ teamId });
if (result.isErr()) {
this.logger.error(new Error(`Error fetching team join requests for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`));
this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, new Error(result.error?.details?.message || 'Unknown error'));
return {
requests: [],
pendingCount: 0,

View File

@@ -1,16 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTeamInputDTO {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
name!: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
tag: string;
tag!: string;
@ApiProperty({ required: false })
@IsOptional()

View File

@@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger';
export class CreateTeamOutputDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
success: boolean;
success!: boolean;
}

View File

@@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamListItemDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
memberCount: number;
memberCount!: number;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@@ -31,8 +31,8 @@ class TeamListItemDTO {
export class GetAllTeamsOutputDTO {
@ApiProperty({ type: [TeamListItemDTO] })
teams: TeamListItemDTO[];
teams!: TeamListItemDTO[];
@ApiProperty()
totalCount: number;
totalCount!: number;
}

View File

@@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
ownerId: string;
ownerId!: string;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@@ -34,25 +34,25 @@ class TeamDTO {
class MembershipDTO {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}
export class GetDriverTeamOutputDTO {
@ApiProperty({ type: TeamDTO })
team: TeamDTO;
team!: TeamDTO;
@ApiProperty({ type: MembershipDTO })
membership: MembershipDTO;
membership!: MembershipDTO;
@ApiProperty()
isOwner: boolean;
isOwner!: boolean;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}

View File

@@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
ownerId: string;
ownerId!: string;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@@ -34,22 +34,22 @@ class TeamDTO {
class MembershipDTO {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}
export class GetTeamDetailsOutputDTO {
@ApiProperty({ type: TeamDTO })
team: TeamDTO;
team!: TeamDTO;
@ApiProperty({ type: MembershipDTO, nullable: true })
membership: MembershipDTO | null;
membership!: MembershipDTO | null;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}

View File

@@ -2,34 +2,34 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamJoinRequestDTO {
@ApiProperty()
requestId: string;
requestId!: string;
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
teamId: string;
teamId!: string;
@ApiProperty()
status: 'pending' | 'approved' | 'rejected';
status!: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt: string;
requestedAt!: string;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class GetTeamJoinRequestsOutputDTO {
@ApiProperty({ type: [TeamJoinRequestDTO] })
requests: TeamJoinRequestDTO[];
requests!: TeamJoinRequestDTO[];
@ApiProperty()
pendingCount: number;
pendingCount!: number;
@ApiProperty()
totalCount: number;
totalCount!: number;
}

View File

@@ -2,37 +2,37 @@ import { ApiProperty } from '@nestjs/swagger';
class TeamMemberDTO {
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class GetTeamMembersOutputDTO {
@ApiProperty({ type: [TeamMemberDTO] })
members: TeamMemberDTO[];
members!: TeamMemberDTO[];
@ApiProperty()
totalCount: number;
totalCount!: number;
@ApiProperty()
ownerCount: number;
ownerCount!: number;
@ApiProperty()
managerCount: number;
managerCount!: number;
@ApiProperty()
memberCount: number;
memberCount!: number;
}

View File

@@ -2,11 +2,11 @@ import { ApiProperty } from '@nestjs/swagger';
export class GetTeamMembershipOutputDTO {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}

View File

@@ -4,31 +4,31 @@ export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
class TeamLeaderboardItemDTO {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
memberCount: number;
memberCount!: number;
@ApiProperty({ nullable: true })
rating: number | null;
rating!: number | null;
@ApiProperty()
totalWins: number;
totalWins!: number;
@ApiProperty()
totalRaces: number;
totalRaces!: number;
@ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel: SkillLevel;
performanceLevel!: SkillLevel;
@ApiProperty()
isRecruiting: boolean;
isRecruiting!: boolean;
@ApiProperty()
createdAt: string;
createdAt!: string;
@ApiProperty({ required: false })
description?: string;
@@ -45,14 +45,14 @@ class TeamLeaderboardItemDTO {
export class GetTeamsLeaderboardOutputDTO {
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
teams: TeamLeaderboardItemDTO[];
teams!: TeamLeaderboardItemDTO[];
@ApiProperty()
recruitingCount: number;
recruitingCount!: number;
@ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } })
groupsBySkillLevel: Record<SkillLevel, TeamLeaderboardItemDTO[]>;
groupsBySkillLevel!: Record<SkillLevel, TeamLeaderboardItemDTO[]>;
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
topTeams: TeamLeaderboardItemDTO[];
topTeams!: TeamLeaderboardItemDTO[];
}

View File

@@ -3,22 +3,22 @@ import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
export class TeamListItemViewModel {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
memberCount: number;
memberCount!: number;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@@ -32,30 +32,30 @@ export class TeamListItemViewModel {
export class AllTeamsViewModel {
@ApiProperty({ type: [TeamListItemViewModel] })
teams: TeamListItemViewModel[];
teams!: TeamListItemViewModel[];
@ApiProperty()
totalCount: number;
totalCount!: number;
}
export class TeamViewModel {
@ApiProperty()
id: string;
id!: string;
@ApiProperty()
name: string;
name!: string;
@ApiProperty()
tag: string;
tag!: string;
@ApiProperty()
description: string;
description!: string;
@ApiProperty()
ownerId: string;
ownerId!: string;
@ApiProperty({ type: [String] })
leagues: string[];
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@@ -85,131 +85,131 @@ export enum MembershipStatus {
export class MembershipViewModel {
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
}
export class DriverTeamViewModel {
@ApiProperty({ type: TeamViewModel })
team: TeamViewModel;
team!: TeamViewModel;
@ApiProperty({ type: MembershipViewModel })
membership: MembershipViewModel;
membership!: MembershipViewModel;
@ApiProperty()
isOwner: boolean;
isOwner!: boolean;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}
export class GetDriverTeamQuery {
@ApiProperty()
@IsString()
teamId: string;
teamId!: string;
@ApiProperty()
@IsString()
driverId: string;
driverId!: string;
}
export class TeamDetailsViewModel {
@ApiProperty({ type: TeamViewModel })
team: TeamViewModel;
team!: TeamViewModel;
@ApiProperty({ type: MembershipViewModel, nullable: true })
membership: MembershipViewModel | null;
membership!: MembershipViewModel | null;
@ApiProperty()
canManage: boolean;
canManage!: boolean;
}
export class TeamMemberViewModel {
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
role: 'owner' | 'manager' | 'member';
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt: string;
joinedAt!: string;
@ApiProperty()
isActive: boolean;
isActive!: boolean;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class TeamMembersViewModel {
@ApiProperty({ type: [TeamMemberViewModel] })
members: TeamMemberViewModel[];
members!: TeamMemberViewModel[];
@ApiProperty()
totalCount: number;
totalCount!: number;
@ApiProperty()
ownerCount: number;
ownerCount!: number;
@ApiProperty()
managerCount: number;
managerCount!: number;
@ApiProperty()
memberCount: number;
memberCount!: number;
}
export class TeamJoinRequestViewModel {
@ApiProperty()
requestId: string;
requestId!: string;
@ApiProperty()
driverId: string;
driverId!: string;
@ApiProperty()
driverName: string;
driverName!: string;
@ApiProperty()
teamId: string;
teamId!: string;
@ApiProperty()
status: 'pending' | 'approved' | 'rejected';
status!: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt: string;
requestedAt!: string;
@ApiProperty()
avatarUrl: string;
avatarUrl!: string;
}
export class TeamJoinRequestsViewModel {
@ApiProperty({ type: [TeamJoinRequestViewModel] })
requests: TeamJoinRequestViewModel[];
requests!: TeamJoinRequestViewModel[];
@ApiProperty()
pendingCount: number;
pendingCount!: number;
@ApiProperty()
totalCount: number;
totalCount!: number;
}
export class CreateTeamInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
name!: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
tag: string;
tag!: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -218,17 +218,17 @@ export class CreateTeamInput {
@ApiProperty()
@IsString()
ownerId: string;
ownerId!: string;
}
export class CreateTeamOutput {
@ApiProperty()
@IsString()
teamId: string;
teamId!: string;
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}
export class UpdateTeamInput {
@@ -254,19 +254,19 @@ export class UpdateTeamInput {
@ApiProperty()
@IsString()
updatedBy: string;
updatedBy!: string;
}
export class UpdateTeamOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}
export class ApproveTeamJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
requestId!: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -277,13 +277,13 @@ export class ApproveTeamJoinRequestInput {
export class ApproveTeamJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}
export class RejectTeamJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
requestId!: string;
@ApiProperty({ required: false })
@IsOptional()
@@ -294,5 +294,5 @@ export class RejectTeamJoinRequestInput {
export class RejectTeamJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
success!: boolean;
}

View File

@@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger';
export class UpdateTeamOutputDTO {
@ApiProperty()
success: boolean;
success!: boolean;
}

View File

@@ -16,4 +16,11 @@ export class TeamMembershipPresenter implements UseCaseOutputPort<GetTeamMembers
getResponseModel(): GetTeamMembershipOutputDTO | null {
return this.result;
}
get responseModel(): GetTeamMembershipOutputDTO {
if (!this.result) {
throw new Error('Presenter not presented');
}
return this.result;
}
}

View File

@@ -1,107 +1,90 @@
import type { TeamsLeaderboardOutputPort } from '@core/racing/application/ports/output/TeamsLeaderboardOutputPort';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetTeamsLeaderboardResult } from '@core/racing/application/use-cases/GetTeamsLeaderboardUseCase';
import type { GetTeamsLeaderboardOutputDTO } from '../dtos/GetTeamsLeaderboardOutputDTO';
export class TeamsLeaderboardPresenter {
export class TeamsLeaderboardPresenter implements UseCaseOutputPort<GetTeamsLeaderboardResult> {
private result: GetTeamsLeaderboardOutputDTO | null = null;
reset() {
this.result = null;
}
async present(outputPort: TeamsLeaderboardOutputPort): Promise<void> {
present(result: GetTeamsLeaderboardResult): void {
this.result = {
teams: outputPort.teams.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
teams: result.items.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
recruitingCount: outputPort.recruitingCount,
recruitingCount: result.recruitingCount,
groupsBySkillLevel: {
beginner: outputPort.groupsBySkillLevel.beginner.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
beginner: result.groupsBySkillLevel.beginner.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
intermediate: outputPort.groupsBySkillLevel.intermediate.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
intermediate: result.groupsBySkillLevel.intermediate.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
advanced: outputPort.groupsBySkillLevel.advanced.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
advanced: result.groupsBySkillLevel.advanced.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
pro: outputPort.groupsBySkillLevel.pro.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
pro: result.groupsBySkillLevel.pro.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
},
topTeams: outputPort.topTeams.map(team => ({
id: team.id,
name: team.name,
memberCount: team.memberCount,
rating: team.rating,
totalWins: team.totalWins,
totalRaces: team.totalRaces,
performanceLevel: team.performanceLevel,
isRecruiting: team.isRecruiting,
createdAt: team.createdAt.toISOString(),
description: team.description,
specialization: team.specialization,
region: team.region,
languages: team.languages,
topTeams: result.topItems.map(item => ({
id: item.team.id,
name: item.team.name.toString(),
memberCount: item.memberCount,
rating: item.rating,
totalWins: item.totalWins,
totalRaces: item.totalRaces,
performanceLevel: item.performanceLevel,
isRecruiting: item.isRecruiting,
createdAt: item.createdAt.toISOString(),
description: item.team.description?.toString() || '',
})),
};
}

View File

@@ -1,15 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { HelloService } from '../application/hello/hello.service';
@Controller()
export class HelloController {
constructor(private readonly helloService: HelloService) {}
@Get()
getHello(): { message: string } {
const presenter = this.helloService.getHello();
return presenter.responseModel;
}
}

View File

@@ -1,38 +0,0 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { CreatePaymentResult } from '@core/payments/application/use-cases/CreatePaymentUseCase';
import type { CreatePaymentViewModel, PaymentDto } from './types';
export class CreatePaymentPresenter implements UseCaseOutputPort<CreatePaymentResult> {
private viewModel: CreatePaymentViewModel | null = null;
present(result: CreatePaymentResult): void {
this.viewModel = {
payment: this.mapPaymentToDto(result.payment),
};
}
getViewModel(): CreatePaymentViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
private mapPaymentToDto(payment: CreatePaymentResult['payment']): PaymentDto {
return {
id: payment.id,
type: payment.type,
amount: payment.amount,
platformFee: payment.platformFee,
netAmount: payment.netAmount,
payerId: payment.payerId,
payerType: payment.payerType,
leagueId: payment.leagueId,
seasonId: payment.seasonId,
status: payment.status,
createdAt: payment.createdAt,
completedAt: payment.completedAt,
};
}
}

View File

@@ -1,38 +0,0 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetPaymentsResult } from '@core/payments/application/use-cases/GetPaymentsUseCase';
import type { GetPaymentsViewModel, PaymentDto } from './types';
export class GetPaymentsPresenter implements UseCaseOutputPort<GetPaymentsResult> {
private viewModel: GetPaymentsViewModel | null = null;
present(result: GetPaymentsResult): void {
this.viewModel = {
payments: result.payments.map(payment => this.mapPaymentToDto(payment)),
};
}
getViewModel(): GetPaymentsViewModel | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
private mapPaymentToDto(payment: GetPaymentsResult['payments'][0]): PaymentDto {
return {
id: payment.id,
type: payment.type,
amount: payment.amount,
platformFee: payment.platformFee,
netAmount: payment.netAmount,
payerId: payment.payerId,
payerType: payment.payerType,
leagueId: payment.leagueId,
seasonId: payment.seasonId,
status: payment.status,
createdAt: payment.createdAt,
completedAt: payment.completedAt,
};
}
}

View File

@@ -1,19 +0,0 @@
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { GetSponsorBillingResult } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
import type { SponsorBillingSummary } from './types';
export class GetSponsorBillingPresenter implements UseCaseOutputPort<GetSponsorBillingResult> {
private viewModel: SponsorBillingSummary | null = null;
present(result: GetSponsorBillingResult): void {
this.viewModel = result;
}
getViewModel(): SponsorBillingSummary | null {
return this.viewModel;
}
reset(): void {
this.viewModel = null;
}
}

View File

@@ -1,4 +0,0 @@
export * from './types';
export * from './CreatePaymentPresenter';
export * from './GetPaymentsPresenter';
export * from './GetSponsorBillingPresenter';

View File

@@ -1,177 +0,0 @@
import type { PaymentType, PayerType, PaymentStatus } from '@core/payments/domain/entities/Payment';
import type { PrizeType } from '@core/payments/domain/entities/Prize';
import type { TransactionType, ReferenceType } from '@core/payments/domain/entities/Wallet';
import type { MembershipFeeType } from '@core/payments/domain/entities/MembershipFee';
import type { MemberPaymentStatus } from '@core/payments/domain/entities/MemberPayment';
// DTOs for API responses
export interface PaymentDto {
id: string;
type: PaymentType;
amount: number;
platformFee: number;
netAmount: number;
payerId: string;
payerType: PayerType;
leagueId: string;
seasonId: string | undefined;
status: PaymentStatus;
createdAt: Date;
completedAt: Date | undefined;
}
export interface PrizeDto {
id: string;
leagueId: string;
seasonId: string;
position: number;
name: string;
amount: number;
type: PrizeType;
description: string | undefined;
awarded: boolean;
awardedTo: string | undefined;
awardedAt: Date | undefined;
createdAt: Date;
}
export interface WalletDto {
id: string;
leagueId: string;
balance: number;
totalRevenue: number;
totalPlatformFees: number;
totalWithdrawn: number;
currency: string;
createdAt: Date;
}
export interface TransactionDto {
id: string;
walletId: string;
type: TransactionType;
amount: number;
description: string;
referenceId: string | undefined;
referenceType: ReferenceType | undefined;
createdAt: Date;
}
export interface MembershipFeeDto {
id: string;
leagueId: string;
seasonId: string | undefined;
type: MembershipFeeType;
amount: number;
enabled: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface MemberPaymentDto {
id: string;
feeId: string;
driverId: string;
amount: number;
platformFee: number;
netAmount: number;
status: MemberPaymentStatus;
dueDate: Date;
paidAt: Date | undefined;
}
// View Models
export interface CreatePaymentViewModel {
payment: PaymentDto;
}
export interface GetPaymentsViewModel {
payments: PaymentDto[];
}
export interface GetPrizesViewModel {
prizes: PrizeDto[];
}
export interface CreatePrizeViewModel {
prize: PrizeDto;
}
export interface AwardPrizeViewModel {
prize: PrizeDto;
}
export interface DeletePrizeViewModel {
success: boolean;
}
export interface GetWalletViewModel {
wallet: WalletDto;
transactions: TransactionDto[];
}
export interface ProcessWalletTransactionViewModel {
wallet: WalletDto;
transaction: TransactionDto;
}
export interface GetMembershipFeesViewModel {
fee: MembershipFeeDto | null;
payments: MemberPaymentDto[];
}
export interface UpsertMembershipFeeViewModel {
fee: MembershipFeeDto;
}
export interface UpdateMemberPaymentViewModel {
payment: MemberPaymentDto;
}
export interface UpdatePaymentStatusViewModel {
payment: PaymentDto;
}
// Sponsor Billing
export interface SponsorBillingStats {
totalSpent: number;
pendingAmount: number;
nextPaymentDate: string | null;
nextPaymentAmount: number | null;
activeSponsorships: number;
averageMonthlySpend: number;
}
export interface SponsorInvoiceSummary {
id: string;
invoiceNumber: string;
date: string;
dueDate: string;
amount: number;
vatAmount: number;
totalAmount: number;
status: 'paid' | 'pending' | 'overdue' | 'failed';
description: string;
sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform';
pdfUrl: string;
}
export interface SponsorPaymentMethodSummary {
id: string;
type: 'card' | 'bank' | 'sepa';
last4: string;
brand?: string;
isDefault: boolean;
expiryMonth?: number;
expiryYear?: number;
bankName?: string;
}
export interface SponsorBillingSummary {
paymentMethods: SponsorPaymentMethodSummary[];
invoices: SponsorInvoiceSummary[];
stats: SponsorBillingStats;
}

View File

@@ -1,31 +1,54 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es2022", "dom"],
"moduleResolution": "node",
"baseUrl": ".",
"declaration": true,
"declarationMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"incremental": true,
"lib": [
"es2022",
"dom"
],
"module": "commonjs",
"moduleResolution": "node",
"noEmit": false,
"noEmitOnError": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"paths": {
"@/*": [
"./*"
],
"@adapters/*": [
"../../adapters/*"
],
"@core/*": [
"../../core/*"
],
"@testing/*": [
"../../testing/*"
]
},
"removeComments": true,
"sourceMap": true,
"outDir": "./dist",
"incremental": true,
"baseUrl": ".",
"types": ["node", "express", "vitest/globals"],
"paths": {
"@/*": ["./*"],
"@core/*": ["../../core/*"],
"@adapters/*": ["../../adapters/*"],
"@testing/*": ["../../testing/*"]
}
"strict": true,
"strictNullChecks": true,
"target": "es2017",
"types": [
"node",
"express",
"vitest/globals"
]
},
"include": ["src/**/*", "../../adapters/bootstrap/EnsureInitialData.ts"],
"exclude": ["node_modules", "dist", "**/*.mock.ts"]
}
"exclude": [
"node_modules",
"dist",
"**/*.mock.ts"
],
"extends": "../../tsconfig.base.json",
"include": [
"src/**/*"
]
}

View File

@@ -11,13 +11,14 @@ import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application';
import type { Driver } from '../../domain/entities/Driver';
import type { Penalty } from '../../domain/entities/penalty/Penalty';
export type GetRacePenaltiesInput = {
raceId: string;
};
export type GetRacePenaltiesResult = {
penalties: unknown[];
penalties: Penalty[];
drivers: Driver[];
};