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 { Module } from '@nestjs/common';
import { HelloController } from './presentation/hello.controller';
import { HelloService } from './application/hello/hello.service';
import { AnalyticsModule } from './domain/analytics/AnalyticsModule'; 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 { AuthModule } from './domain/auth/AuthModule';
import { BootstrapModule } from './domain/bootstrap/BootstrapModule';
import { DashboardModule } from './domain/dashboard/DashboardModule'; import { DashboardModule } from './domain/dashboard/DashboardModule';
import { LeagueModule } from './domain/league/LeagueModule'; import { DatabaseModule } from './domain/database/DatabaseModule';
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 { DriverModule } from './domain/driver/DriverModule'; 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 { MediaModule } from './domain/media/MediaModule';
import { PaymentsModule } from './domain/payments/PaymentsModule'; 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({ @Module({
imports: [ imports: [
HelloModule,
DatabaseModule, DatabaseModule,
LoggingModule, LoggingModule,
BootstrapModule, BootstrapModule,
@@ -34,7 +34,5 @@ import { PaymentsModule } from './domain/payments/PaymentsModule';
MediaModule, MediaModule,
PaymentsModule, PaymentsModule,
], ],
controllers: [HelloController],
providers: [HelloService],
}) })
export class AppModule {} 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 { Module, OnModuleInit } from '@nestjs/common';
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import { BootstrapProviders } from './BootstrapProviders'; import { BootstrapProviders } from './BootstrapProviders';
@Module({ @Module({

View File

@@ -1,7 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AnalyticsSnapshotOrmEntity } from '../../../../..//persistence/typeorm/analytics/AnalyticsSnapshotOrmEntity';
import { EngagementOrmEntity } from '../../../../..//persistence/typeorm/analytics/EngagementOrmEntity';
@Module({ @Module({
imports: [ imports: [
@@ -12,7 +10,7 @@ import { EngagementOrmEntity } from '../../../../..//persistence/typeorm/analyti
username: process.env.DATABASE_USER || 'user', username: process.env.DATABASE_USER || 'user',
password: process.env.DATABASE_PASSWORD || 'password', password: process.env.DATABASE_PASSWORD || 'password',
database: process.env.DATABASE_NAME || 'gridpilot', database: process.env.DATABASE_NAME || 'gridpilot',
entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity], // entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity],
synchronize: true, // Use carefully in production 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 use cases
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase'; import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
// Import presenters
import { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter';
// Define injection tokens // Define injection tokens
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository'; export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LOGGER_TOKEN = 'Logger'; export const LOGGER_TOKEN = 'Logger';
export const REVIEW_PROTEST_PRESENTER_TOKEN = 'ReviewProtestPresenter';
export const ProtestsProviders: Provider[] = [ export const ProtestsProviders: Provider[] = [
ProtestsService, // Provide the service itself ProtestsService, // Provide the service itself
@@ -40,6 +44,26 @@ export const ProtestsProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
}, },
{
provide: REVIEW_PROTEST_PRESENTER_TOKEN,
useClass: ReviewProtestPresenter,
},
// Use cases // 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 { Logger } from '@core/shared/application/Logger';
import type { import type {
ReviewProtestUseCase, ReviewProtestUseCase,
ReviewProtestResult,
ReviewProtestApplicationError, ReviewProtestApplicationError,
} from '@core/racing/application/use-cases/ReviewProtestUseCase'; } from '@core/racing/application/use-cases/ReviewProtestUseCase';
import { ProtestsService } from './ProtestsService'; import { ProtestsService } from './ProtestsService';
import type { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter';
import type { ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter'; import type { ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
describe('ProtestsService', () => { describe('ProtestsService', () => {
@@ -17,6 +17,21 @@ describe('ProtestsService', () => {
beforeEach(() => { beforeEach(() => {
executeMock = vi.fn(); executeMock = vi.fn();
const reviewProtestUseCase = { execute: executeMock } as unknown as ReviewProtestUseCase; 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 = { logger = {
debug: vi.fn(), debug: vi.fn(),
info: vi.fn(), info: vi.fn(),
@@ -24,7 +39,7 @@ describe('ProtestsService', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } as unknown as Logger;
service = new ProtestsService(reviewProtestUseCase, logger); service = new ProtestsService(reviewProtestUseCase, reviewProtestPresenter, logger);
}); });
const baseCommand = { const baseCommand = {
@@ -35,15 +50,7 @@ describe('ProtestsService', () => {
}; };
it('returns DTO with success model on success', async () => { it('returns DTO with success model on success', async () => {
const coreResult: ReviewProtestResult = { executeMock.mockResolvedValue(Result.ok(undefined));
leagueId: 'league-1',
protestId: baseCommand.protestId,
status: 'upheld',
stewardId: baseCommand.stewardId,
decision: baseCommand.decision,
};
executeMock.mockResolvedValue(Result.ok<ReviewProtestResult, ReviewProtestApplicationError>(coreResult));
const dto = await service.reviewProtest(baseCommand); const dto = await service.reviewProtest(baseCommand);
@@ -62,7 +69,7 @@ describe('ProtestsService', () => {
details: { message: 'Protest not found' }, 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); const dto = await service.reviewProtest(baseCommand);
@@ -79,7 +86,7 @@ describe('ProtestsService', () => {
details: { message: 'Race not found for protest' }, 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); const dto = await service.reviewProtest(baseCommand);
@@ -96,7 +103,7 @@ describe('ProtestsService', () => {
details: { message: 'Steward is not authorized to review this protest' }, 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); const dto = await service.reviewProtest(baseCommand);
@@ -114,7 +121,7 @@ describe('ProtestsService', () => {
details: { message: 'Failed to review protest' }, 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); 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'; import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
// Tokens // 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() @Injectable()
export class ProtestsService { export class ProtestsService {
constructor( constructor(
private readonly reviewProtestUseCase: ReviewProtestUseCase, private readonly reviewProtestUseCase: ReviewProtestUseCase,
@Inject(REVIEW_PROTEST_PRESENTER_TOKEN) private readonly reviewProtestPresenter: ReviewProtestPresenter,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {} ) {}
@@ -30,11 +27,14 @@ export class ProtestsService {
}): Promise<ReviewProtestResponseDTO> { }): Promise<ReviewProtestResponseDTO> {
this.logger.debug('[ProtestsService] Reviewing protest:', command); this.logger.debug('[ProtestsService] Reviewing protest:', command);
const result = await this.reviewProtestUseCase.execute(command); // Set the command on the presenter so it can include stewardId and decision in the response
const presenter = new ReviewProtestPresenter(); 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> { export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestResult> {
private model: ReviewProtestResponseDTO | null = null; private model: ReviewProtestResponseDTO | null = null;
private command: { stewardId: string; decision: 'uphold' | 'dismiss' } | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
this.command = null;
}
setCommand(command: { stewardId: string; decision: 'uphold' | 'dismiss' }): void {
this.command = command;
} }
present(result: ReviewProtestResult): void { present(result: ReviewProtestResult): void {
if (!this.command) {
throw new Error('Command must be set before presenting result');
}
this.model = { this.model = {
success: true, success: true,
protestId: result.protestId, 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 { Test, TestingModule } from '@nestjs/testing';
import { RaceController } from './RaceController'; import { RaceController } from './RaceController';
import { RaceService } from './RaceService'; import { RaceService } from './RaceService';
import { vi, Mocked } from 'vitest';
describe('RaceController', () => { describe('RaceController', () => {
let controller: RaceController; let controller: RaceController;
let service: jest.Mocked<RaceService>; let service: Mocked<RaceService>;
beforeEach(async () => { beforeEach(async () => {
const mockService = { const mockService = {
getAllRaces: jest.fn(), getAllRaces: vi.fn(),
getTotalRaces: jest.fn(), getTotalRaces: vi.fn(),
getRacesPageData: jest.fn(), getRacesPageData: vi.fn(),
getAllRacesPageData: jest.fn(), getAllRacesPageData: vi.fn(),
getRaceDetail: jest.fn(), getRaceDetail: vi.fn(),
getRaceResultsDetail: jest.fn(), getRaceResultsDetail: vi.fn(),
getRaceWithSOF: jest.fn(), getRaceWithSOF: vi.fn(),
getRaceProtests: jest.fn(), getRaceProtests: vi.fn(),
getRacePenalties: jest.fn(), getRacePenalties: vi.fn(),
registerForRace: jest.fn(), registerForRace: vi.fn(),
withdrawFromRace: jest.fn(), withdrawFromRace: vi.fn(),
cancelRace: jest.fn(), cancelRace: vi.fn(),
completeRace: jest.fn(), completeRace: vi.fn(),
importRaceResults: jest.fn(), importRaceResults: vi.fn(),
fileProtest: jest.fn(), fileProtest: vi.fn(),
applyQuickPenalty: jest.fn(), applyQuickPenalty: vi.fn(),
applyPenalty: jest.fn(), applyPenalty: vi.fn(),
requestProtestDefense: jest.fn(), requestProtestDefense: vi.fn(),
} as unknown as jest.Mocked<RaceService>; } as unknown as Mocked<RaceService>;
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [RaceController], controllers: [RaceController],
@@ -39,7 +40,7 @@ describe('RaceController', () => {
}).compile(); }).compile();
controller = module.get<RaceController>(RaceController); controller = module.get<RaceController>(RaceController);
service = module.get(RaceService) as jest.Mocked<RaceService>; service = module.get(RaceService) as Mocked<RaceService>;
}); });
it('should be defined', () => { it('should be defined', () => {
@@ -48,25 +49,25 @@ describe('RaceController', () => {
describe('getAllRaces', () => { describe('getAllRaces', () => {
it('should return all races view model', async () => { it('should return all races view model', async () => {
const mockViewModel = { races: [], filters: { statuses: [], leagues: [] } } as { races: unknown[]; filters: { statuses: unknown[]; leagues: unknown[] } }; const mockPresenter = { viewModel: { races: [], filters: { statuses: [], leagues: [] } } } as any;
service.getAllRaces.mockResolvedValue({ viewModel: mockViewModel } as unknown as ReturnType<RaceService['getAllRaces']>); service.getAllRaces.mockResolvedValue(mockPresenter);
const result = await controller.getAllRaces(); const result = await controller.getAllRaces();
expect(service.getAllRaces).toHaveBeenCalled(); expect(service.getAllRaces).toHaveBeenCalled();
expect(result).toEqual(mockViewModel); expect(result).toEqual(mockPresenter.viewModel);
}); });
}); });
describe('getTotalRaces', () => { describe('getTotalRaces', () => {
it('should return total races count view model', async () => { it('should return total races count view model', async () => {
const mockViewModel = { totalRaces: 5 } as { totalRaces: number }; const mockPresenter = { viewModel: { totalRaces: 5 } } as any;
service.getTotalRaces.mockResolvedValue({ viewModel: mockViewModel } as unknown as ReturnType<RaceService['getTotalRaces']>); service.getTotalRaces.mockResolvedValue(mockPresenter);
const result = await controller.getTotalRaces(); const result = await controller.getTotalRaces();
expect(service.getTotalRaces).toHaveBeenCalled(); 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') @Get('page-data')
@ApiOperation({ summary: 'Get races page data' }) @ApiOperation({ summary: 'Get races page data' })
@ApiQuery({ name: 'leagueId', description: 'League ID' })
@ApiResponse({ status: 200, description: 'Races page data', type: RacesPageDataDTO }) @ApiResponse({ status: 200, description: 'Races page data', type: RacesPageDataDTO })
async getRacesPageData(): Promise<RacesPageDataDTO> { async getRacesPageData(@Query('leagueId') leagueId: string): Promise<RacesPageDataDTO> {
const presenter = await this.raceService.getRacesPageData(); const presenter = await this.raceService.getRacesPageData(leagueId);
return presenter.viewModel; return presenter.viewModel;
} }
@@ -144,7 +145,7 @@ export class RaceController {
@ApiParam({ name: 'raceId', description: 'Race ID' }) @ApiParam({ name: 'raceId', description: 'Race ID' })
@ApiResponse({ status: 200, description: 'Successfully cancelled race' }) @ApiResponse({ status: 200, description: 'Successfully cancelled race' })
async cancelRace(@Param('raceId') raceId: string): Promise<void> { 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; const viewModel = presenter.viewModel;
if (!viewModel.success) { if (!viewModel.success) {
@@ -172,7 +173,7 @@ export class RaceController {
@ApiParam({ name: 'raceId', description: 'Race ID' }) @ApiParam({ name: 'raceId', description: 'Race ID' })
@ApiResponse({ status: 200, description: 'Successfully re-opened race' }) @ApiResponse({ status: 200, description: 'Successfully re-opened race' })
async reopenRace(@Param('raceId') raceId: string): Promise<void> { 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; const viewModel = presenter.viewModel;
if (!viewModel.success) { if (!viewModel.success) {

View File

@@ -2,54 +2,68 @@ import type { Provider } from '@nestjs/common';
import { RaceService } from './RaceService'; import { RaceService } from './RaceService';
// Import core interfaces // Import core interfaces
import type { Logger } from '@core/shared/application/Logger'; import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; 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 { 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 { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository'; 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 concrete in-memory implementations
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository'; import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; 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 { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository'; import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository'; 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 { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider'; import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases // Import use cases
import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase'; import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase';
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 { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase';
import { CompleteRaceUseCase } from '@core/racing/application/use-cases/CompleteRaceUseCase'; 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 { 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 { 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 { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase';
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase'; 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 // Define injection tokens
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; 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 IMAGE_SERVICE_TOKEN = 'IImageServicePort';
export const LOGGER_TOKEN = 'Logger'; 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[] = [ export const RaceProviders: Provider[] = [
RaceService, RaceService,
{ {
@@ -109,7 +136,7 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: STANDING_REPOSITORY_TOKEN, provide: STANDING_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger), useFactory: (logger: Logger) => new InMemoryStandingRepository(logger, getPointsSystems()),
inject: [LOGGER_TOKEN], inject: [LOGGER_TOKEN],
}, },
{ {
@@ -126,18 +153,105 @@ export const RaceProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, 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, provide: GetAllRacesUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository, logger: Logger) => useFactory: (
new GetAllRacesUseCase(raceRepo, leagueRepo, logger), 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], inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: GetTotalRacesUseCase, 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], 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, provide: GetRaceDetailUseCase,
useFactory: ( useFactory: (
@@ -147,15 +261,24 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository, resultRepo: IResultRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
) => ) => {
new GetRaceDetailUseCase( const useCase = new GetRaceDetailUseCase(
raceRepo, raceRepo,
leagueRepo, leagueRepo,
driverRepo, driverRepo,
raceRegRepo, raceRegRepo,
resultRepo, resultRepo,
leagueMembershipRepo, 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: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
@@ -167,15 +290,27 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: GetRacesPageDataUseCase, provide: GetRacesPageDataUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => useFactory: (
new GetRacesPageDataUseCase(raceRepo, leagueRepo), raceRepo: IRaceRepository,
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN], 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, provide: GetAllRacesPageDataUseCase,
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => useFactory: (
new GetAllRacesPageDataUseCase(raceRepo, leagueRepo), raceRepo: IRaceRepository,
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN], 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, provide: GetRaceResultsDetailUseCase,
@@ -185,7 +320,10 @@ export const RaceProviders: Provider[] = [
resultRepo: IResultRepository, resultRepo: IResultRepository,
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
penaltyRepo: IPenaltyRepository, 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: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
@@ -201,7 +339,10 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
resultRepo: IResultRepository, resultRepo: IResultRepository,
driverRatingProvider: DriverRatingProvider, 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: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN,
@@ -211,14 +352,18 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: GetRaceProtestsUseCase, provide: GetRaceProtestsUseCase,
useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) => useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) => {
new GetRaceProtestsUseCase(protestRepo, driverRepo), const presenter = new RaceProtestsPresenter();
return new GetRaceProtestsUseCase(protestRepo, driverRepo, presenter as any);
},
inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN], inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
}, },
{ {
provide: GetRacePenaltiesUseCase, provide: GetRacePenaltiesUseCase,
useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) => useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) => {
new GetRacePenaltiesUseCase(penaltyRepo, driverRepo), const presenter = new RacePenaltiesPresenter();
return new GetRacePenaltiesUseCase(penaltyRepo, driverRepo, presenter as any);
},
inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN], inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN],
}, },
{ {
@@ -227,17 +372,30 @@ export const RaceProviders: Provider[] = [
raceRegRepo: IRaceRegistrationRepository, raceRegRepo: IRaceRegistrationRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, 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], inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
provide: WithdrawFromRaceUseCase, provide: WithdrawFromRaceUseCase,
useFactory: (raceRegRepo: IRaceRegistrationRepository) => new WithdrawFromRaceUseCase(raceRegRepo), useFactory: (
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN], 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, 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], inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
}, },
{ {
@@ -248,7 +406,10 @@ export const RaceProviders: Provider[] = [
resultRepo: IResultRepository, resultRepo: IResultRepository,
standingRepo: IStandingRepository, standingRepo: IStandingRepository,
driverRatingProvider: DriverRatingProvider, 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: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
RACE_REGISTRATION_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN,
@@ -259,28 +420,12 @@ export const RaceProviders: Provider[] = [
}, },
{ {
provide: ReopenRaceUseCase, 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], 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, provide: ImportRaceResultsUseCase,
useFactory: ( useFactory: (
@@ -290,7 +435,10 @@ export const RaceProviders: Provider[] = [
driverRepo: IDriverRepository, driverRepo: IDriverRepository,
standingRepo: IStandingRepository, standingRepo: IStandingRepository,
logger: Logger, 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: [ inject: [
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
@@ -306,7 +454,10 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository, protestRepo: IProtestRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, 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], inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
}, },
{ {
@@ -316,13 +467,11 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
) => new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger), ) => {
inject: [ const presenter = new CommandResultPresenter();
PENALTY_REPOSITORY_TOKEN, return new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger, presenter as any);
RACE_REPOSITORY_TOKEN, },
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, inject: [PENALTY_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
LOGGER_TOKEN,
],
}, },
{ {
provide: ApplyPenaltyUseCase, provide: ApplyPenaltyUseCase,
@@ -332,14 +481,11 @@ export const RaceProviders: Provider[] = [
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
logger: Logger, logger: Logger,
) => new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger), ) => {
inject: [ const presenter = new CommandResultPresenter();
PENALTY_REPOSITORY_TOKEN, return new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any);
PROTEST_REPOSITORY_TOKEN, },
RACE_REPOSITORY_TOKEN, inject: [PENALTY_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN,
],
}, },
{ {
provide: RequestProtestDefenseUseCase, provide: RequestProtestDefenseUseCase,
@@ -347,8 +493,12 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository, protestRepo: IProtestRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
) => new RequestProtestDefenseUseCase(protestRepo, raceRepo, leagueMembershipRepo), logger: Logger,
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN], ) => {
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, provide: ReviewProtestUseCase,
@@ -356,7 +506,11 @@ export const RaceProviders: Provider[] = [
protestRepo: IProtestRepository, protestRepo: IProtestRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo), logger: Logger,
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN], ) => {
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 { 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 // DTOs
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO'; import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
@@ -61,7 +56,23 @@ import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCom
import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO'; import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO';
// Tokens // 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() @Injectable()
export class RaceService { export class RaceService {
@@ -90,305 +101,137 @@ export class RaceService {
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(LOGGER_TOKEN) private readonly logger: Logger,
@Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider, @Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort, @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> { async getAllRaces(): Promise<GetAllRacesPresenter> {
this.logger.debug('[RaceService] Fetching all races.'); this.logger.debug('[RaceService] Fetching all races.');
await this.getAllRacesUseCase.execute({});
const presenter = new GetAllRacesPresenter(); return this.getAllRacesPresenter;
this.getAllRacesUseCase.setOutput(presenter);
const result = await this.getAllRacesUseCase.execute({});
if (result.isErr()) {
throw new Error(result.unwrapErr().code);
}
return presenter;
} }
async getTotalRaces(): Promise<GetTotalRacesPresenter> { async getTotalRaces(): Promise<GetTotalRacesPresenter> {
this.logger.debug('[RaceService] Fetching total races count.'); this.logger.debug('[RaceService] Fetching total races count.');
const result = await this.getTotalRacesUseCase.execute({}); await this.getTotalRacesUseCase.execute({});
const presenter = new GetTotalRacesPresenter(); return this.getTotalRacesPresenter;
presenter.present(result);
return presenter;
} }
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsApiPresenter> { async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsApiPresenter> {
this.logger.debug('Importing race results:', input); this.logger.debug('Importing race results:', input);
const result = await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }); await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent });
if (result.isErr()) { return this.importRaceResultsApiPresenter;
throw new Error(result.unwrapErr().code);
}
const presenter = new ImportRaceResultsApiPresenter();
presenter.present(result.unwrap());
return presenter;
} }
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> { async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailPresenter> {
this.logger.debug('[RaceService] Fetching race detail:', params); this.logger.debug('[RaceService] Fetching race detail:', params);
await this.getRaceDetailUseCase.execute(params);
const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService, params); return this.raceDetailPresenter;
this.getRaceDetailUseCase.setOutput(presenter);
const result = await this.getRaceDetailUseCase.execute(params);
if (result.isErr()) {
throw new Error('Failed to get race detail');
}
return presenter;
} }
async getRacesPageData(): Promise<RacesPageDataPresenter> { async getRacesPageData(leagueId: string): Promise<RacesPageDataPresenter> {
this.logger.debug('[RaceService] Fetching races page data.'); this.logger.debug('[RaceService] Fetching races page data.');
await this.getRacesPageDataUseCase.execute({ leagueId });
const result = await this.getRacesPageDataUseCase.execute(); return this.racesPageDataPresenter;
if (result.isErr()) {
throw new Error('Failed to get races page data');
}
const presenter = new RacesPageDataPresenter(this.leagueRepository);
await presenter.present(result.value as RacesPageOutputPort);
return presenter;
} }
async getAllRacesPageData(): Promise<AllRacesPageDataPresenter> { async getAllRacesPageData(): Promise<AllRacesPageDataPresenter> {
this.logger.debug('[RaceService] Fetching all races page data.'); this.logger.debug('[RaceService] Fetching all races page data.');
await this.getAllRacesPageDataUseCase.execute({});
const result = await this.getAllRacesPageDataUseCase.execute(); return this.allRacesPageDataPresenter;
if (result.isErr()) {
throw new Error('Failed to get all races page data');
}
const presenter = new AllRacesPageDataPresenter();
presenter.present(result.value);
return presenter;
} }
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailPresenter> { async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailPresenter> {
this.logger.debug('[RaceService] Fetching race results detail:', { raceId }); this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
await this.getRaceResultsDetailUseCase.execute({ raceId });
const result = await this.getRaceResultsDetailUseCase.execute({ raceId }); return this.raceResultsDetailPresenter;
if (result.isErr()) {
throw new Error('Failed to get race results detail');
}
const presenter = new RaceResultsDetailPresenter(this.imageService);
await presenter.present(result.value as RaceResultsDetailOutputPort);
return presenter;
} }
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFPresenter> { async getRaceWithSOF(raceId: string): Promise<RaceWithSOFPresenter> {
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId }); this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
await this.getRaceWithSOFUseCase.execute({ raceId });
const result = await this.getRaceWithSOFUseCase.execute({ raceId }); return this.raceWithSOFPresenter;
if (result.isErr()) {
throw new Error('Failed to get race with SOF');
}
const presenter = new RaceWithSOFPresenter();
presenter.present(result.value as RaceWithSOFOutputPort);
return presenter;
} }
async getRaceProtests(raceId: string): Promise<RaceProtestsPresenter> { async getRaceProtests(raceId: string): Promise<RaceProtestsPresenter> {
this.logger.debug('[RaceService] Fetching race protests:', { raceId }); this.logger.debug('[RaceService] Fetching race protests:', { raceId });
await this.getRaceProtestsUseCase.execute({ raceId });
const result = await this.getRaceProtestsUseCase.execute({ raceId }); return this.raceProtestsPresenter;
if (result.isErr()) {
throw new Error('Failed to get race protests');
}
const presenter = new RaceProtestsPresenter();
presenter.present(result.value as RaceProtestsOutputPort);
return presenter;
} }
async getRacePenalties(raceId: string): Promise<RacePenaltiesPresenter> { async getRacePenalties(raceId: string): Promise<RacePenaltiesPresenter> {
this.logger.debug('[RaceService] Fetching race penalties:', { raceId }); this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
await this.getRacePenaltiesUseCase.execute({ raceId });
const result = await this.getRacePenaltiesUseCase.execute({ raceId }); return this.racePenaltiesPresenter;
if (result.isErr()) {
throw new Error('Failed to get race penalties');
}
const presenter = new RacePenaltiesPresenter();
presenter.present(result.value as RacePenaltiesOutputPort);
return presenter;
} }
async registerForRace(params: RegisterForRaceParamsDTO): Promise<CommandResultPresenter> { async registerForRace(params: RegisterForRaceParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Registering for race:', params); this.logger.debug('[RaceService] Registering for race:', params);
await this.registerForRaceUseCase.execute(params);
const result = await this.registerForRaceUseCase.execute(params); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_REGISTER_FOR_RACE', 'Failed to register for race');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<CommandResultPresenter> { async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Withdrawing from race:', params); this.logger.debug('[RaceService] Withdrawing from race:', params);
await this.withdrawFromRaceUseCase.execute(params);
const result = await this.withdrawFromRaceUseCase.execute(params); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_WITHDRAW_FROM_RACE', 'Failed to withdraw from race');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async cancelRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> { async cancelRace(params: RaceActionParamsDTO, cancelledById: string): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Cancelling race:', params); this.logger.debug('[RaceService] Cancelling race:', params);
await this.cancelRaceUseCase.execute({ raceId: params.raceId, cancelledById });
const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId }); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_CANCEL_RACE', 'Failed to cancel race');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async completeRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> { async completeRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Completing race:', params); this.logger.debug('[RaceService] Completing race:', params);
await this.completeRaceUseCase.execute({ raceId: params.raceId });
const result = await this.completeRaceUseCase.execute({ raceId: params.raceId }); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_COMPLETE_RACE', 'Failed to complete race');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async reopenRace(params: RaceActionParamsDTO): Promise<CommandResultPresenter> { async reopenRace(params: RaceActionParamsDTO, reopenedById: string): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Re-opening race:', params); this.logger.debug('[RaceService] Re-opening race:', params);
await this.reopenRaceUseCase.execute({ raceId: params.raceId, reopenedById });
const result = await this.reopenRaceUseCase.execute({ raceId: params.raceId }); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const errorCode = result.unwrapErr().code;
if (errorCode === 'RACE_ALREADY_SCHEDULED') {
this.logger.debug('[RaceService] Race is already scheduled, treating reopen as success.');
presenter.presentSuccess('Race already scheduled');
return presenter;
}
presenter.presentFailure(errorCode ?? 'UNEXPECTED_ERROR', 'Unexpected error while reopening race');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async fileProtest(command: FileProtestCommandDTO): Promise<CommandResultPresenter> { async fileProtest(command: FileProtestCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Filing protest:', command); this.logger.debug('[RaceService] Filing protest:', command);
await this.fileProtestUseCase.execute(command);
const result = await this.fileProtestUseCase.execute(command); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_FILE_PROTEST', 'Failed to file protest');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<CommandResultPresenter> { async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Applying quick penalty:', command); this.logger.debug('[RaceService] Applying quick penalty:', command);
await this.quickPenaltyUseCase.execute(command);
const result = await this.quickPenaltyUseCase.execute(command); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_APPLY_QUICK_PENALTY', 'Failed to apply quick penalty');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<CommandResultPresenter> { async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Applying penalty:', command); this.logger.debug('[RaceService] Applying penalty:', command);
await this.applyPenaltyUseCase.execute(command);
const result = await this.applyPenaltyUseCase.execute(command); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_APPLY_PENALTY', 'Failed to apply penalty');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<CommandResultPresenter> { async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Requesting protest defense:', command); this.logger.debug('[RaceService] Requesting protest defense:', command);
await this.requestProtestDefenseUseCase.execute(command);
const result = await this.requestProtestDefenseUseCase.execute(command); return this.commandResultPresenter;
const presenter = new CommandResultPresenter();
if (result.isErr()) {
const error = result.unwrapErr();
presenter.presentFailure(error.code ?? 'FAILED_TO_REQUEST_PROTEST_DEFENSE', 'Failed to request protest defense');
return presenter;
}
presenter.presentSuccess();
return presenter;
} }
async reviewProtest(command: ReviewProtestCommandDTO): Promise<CommandResultPresenter> { async reviewProtest(command: ReviewProtestCommandDTO): Promise<CommandResultPresenter> {
this.logger.debug('[RaceService] Reviewing protest:', command); this.logger.debug('[RaceService] Reviewing protest:', command);
await this.reviewProtestUseCase.execute(command);
const result = await this.reviewProtestUseCase.execute(command); return this.commandResultPresenter;
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;
} }
} }

View File

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

View File

@@ -7,19 +7,19 @@ export interface CommandResultDTO {
message?: string; message?: string;
} }
export type CommandApplicationError<E extends string = string> = ApplicationErrorCode< export type CommandApplicationError = ApplicationErrorCode<
E, string,
{ message: string } { message: string }
>; >;
export class CommandResultPresenter<E extends string = string> { export class CommandResultPresenter {
private model: CommandResultDTO | null = null; private model: CommandResultDTO | null = null;
reset(): void { reset(): void {
this.model = null; this.model = null;
} }
present(result: Result<unknown, CommandApplicationError<E>>): void { present(result: Result<unknown, CommandApplicationError>): void {
if (result.isErr()) { if (result.isErr()) {
const error = result.unwrapErr(); const error = result.unwrapErr();
this.model = { this.model = {
@@ -36,7 +36,7 @@ export class CommandResultPresenter<E extends string = string> {
presentSuccess(message?: string): void { presentSuccess(message?: string): void {
this.model = { this.model = {
success: true, success: true,
message, ...(message !== undefined && { message }),
}; };
} }
@@ -44,7 +44,7 @@ export class CommandResultPresenter<E extends string = string> {
this.model = { this.model = {
success: false, success: false,
errorCode, errorCode,
message, ...(message !== undefined && { message }),
}; };
} }
@@ -59,4 +59,8 @@ export class CommandResultPresenter<E extends string = string> {
return this.model; 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, track: output.race.track,
car: output.race.car, car: output.race.car,
scheduledAt: output.race.scheduledAt.toISOString(), scheduledAt: output.race.scheduledAt.toISOString(),
sessionType: output.race.sessionType, sessionType: output.race.sessionType.toString(),
status: output.race.status, status: output.race.status.toString(),
strengthOfField: output.race.strengthOfField ?? null, strengthOfField: output.race.strengthOfField ?? null,
registeredCount: output.race.registeredCount ?? undefined, ...(output.race.registeredCount !== undefined && { registeredCount: output.race.registeredCount }),
maxParticipants: output.race.maxParticipants ?? undefined, ...(output.race.maxParticipants !== undefined && { maxParticipants: output.race.maxParticipants }),
} }
: null; : null;
@@ -54,22 +54,22 @@ export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResul
name: output.league.name.toString(), name: output.league.name.toString(),
description: output.league.description.toString(), description: output.league.description.toString(),
settings: { settings: {
maxDrivers: output.league.settings.maxDrivers ?? undefined, ...(output.league.settings.maxDrivers !== undefined && { maxDrivers: output.league.settings.maxDrivers }),
qualifyingFormat: output.league.settings.qualifyingFormat ?? undefined, ...(output.league.settings.qualifyingFormat !== undefined && { qualifyingFormat: output.league.settings.qualifyingFormat.toString() }),
}, },
} }
: null; : null;
const entryListDTO: RaceDetailEntryDTO[] = await Promise.all( const entryListDTO: RaceDetailEntryDTO[] = await Promise.all(
output.drivers.map(async driver => { output.drivers.map(async driver => {
const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id }); const rating = this.driverRatingProvider.getRating(driver.id);
const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id }); const avatarUrl = this.imageService.getDriverAvatar(driver.id);
return { return {
id: driver.id, id: driver.id,
name: driver.name.toString(), name: driver.name.toString(),
country: driver.country.toString(), country: driver.country.toString(),
avatarUrl: avatarResult.avatarUrl, avatarUrl,
rating: ratingResult.rating, rating,
isCurrentUser: driver.id === params.driverId, isCurrentUser: driver.id === params.driverId,
}; };
}), }),

View File

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

View File

@@ -39,12 +39,12 @@ export class RaceResultsDetailPresenter {
throw new Error(`Driver not found for result: ${singleResult.driverId}`); 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 { return {
driverId: singleResult.driverId.toString(), driverId: singleResult.driverId.toString(),
driverName: driver.name.toString(), driverName: driver.name.toString(),
avatarUrl: avatarResult.avatarUrl, avatarUrl,
position: singleResult.position.toNumber(), position: singleResult.position.toNumber(),
startPosition: singleResult.startPosition.toNumber(), startPosition: singleResult.startPosition.toNumber(),
incidents: singleResult.incidents.toNumber(), incidents: singleResult.incidents.toNumber(),

View File

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

View File

@@ -5,7 +5,8 @@ import { SponsorService } from './SponsorService';
import { NotificationService } from '@core/notifications/application/ports/NotificationService'; import { NotificationService } from '@core/notifications/application/ports/NotificationService';
import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository'; import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository';
import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository'; 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 { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository'; 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 { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository'; import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository';
import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository'; 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 { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; 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 { InMemorySponsorshipPricingRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository';
import { InMemorySponsorshipRequestRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository'; 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 // Define injection tokens
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository'; export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository'; 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 SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestRepository';
export const LOGGER_TOKEN = 'Logger'; 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 // Use case / application service tokens
export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase'; export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase';
export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase'; 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 REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase';
export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase'; 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[] = [ export const SponsorProviders: Provider[] = [
SponsorService, SponsorService,
// Repositories // Repositories
@@ -111,27 +149,94 @@ export const SponsorProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, 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 // Use cases
{ {
provide: GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN, provide: GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
useFactory: () => new GetSponsorshipPricingUseCase(), useFactory: (output: UseCaseOutputPort<any>) => new GetSponsorshipPricingUseCase(output),
inject: [], inject: [GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_SPONSORS_USE_CASE_TOKEN, provide: GET_SPONSORS_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorsUseCase(sponsorRepo), useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort<any>) => new GetSponsorsUseCase(sponsorRepo, output),
inject: [SPONSOR_REPOSITORY_TOKEN], inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSORS_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: CREATE_SPONSOR_USE_CASE_TOKEN, provide: CREATE_SPONSOR_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository) => new CreateSponsorUseCase(sponsorRepo), useFactory: (sponsorRepo: ISponsorRepository, logger: Logger, output: UseCaseOutputPort<any>) => new CreateSponsorUseCase(sponsorRepo, logger, output),
inject: [SPONSOR_REPOSITORY_TOKEN], inject: [SPONSOR_REPOSITORY_TOKEN, LOGGER_TOKEN, CREATE_SPONSOR_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, leagueRepo: ILeagueRepository, leagueMembershipRepo: ILeagueMembershipRepository, raceRepo: IRaceRepository) => useFactory: (
new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo), sponsorRepo: ISponsorRepository,
inject: [SPONSOR_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN], 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, provide: GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN,
@@ -142,7 +247,8 @@ export const SponsorProviders: Provider[] = [
leagueRepo: ILeagueRepository, leagueRepo: ILeagueRepository,
leagueMembershipRepo: ILeagueMembershipRepository, leagueMembershipRepo: ILeagueMembershipRepository,
raceRepo: IRaceRepository, raceRepo: IRaceRepository,
) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo), output: UseCaseOutputPort<any>,
) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output),
inject: [ inject: [
SPONSOR_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN,
SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
@@ -150,41 +256,97 @@ export const SponsorProviders: Provider[] = [
LEAGUE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN,
GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
], ],
}, },
{ {
provide: GET_SPONSOR_BILLING_USE_CASE_TOKEN, provide: GET_SPONSOR_BILLING_USE_CASE_TOKEN,
useFactory: (paymentRepo: IPaymentRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository) => useFactory: (
new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo), paymentRepo: IPaymentRepository,
seasonSponsorshipRepo: ISeasonSponsorshipRepository,
) => {
return new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo);
},
inject: ['IPaymentRepository', SEASON_SPONSORSHIP_REPOSITORY_TOKEN], inject: ['IPaymentRepository', SEASON_SPONSORSHIP_REPOSITORY_TOKEN],
}, },
{ {
provide: GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN, provide: GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
useFactory: (sponsorshipPricingRepo: ISponsorshipPricingRepository, sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, logger: Logger) => useFactory: (
new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, sponsorshipRequestRepo, seasonSponsorshipRepo, logger), sponsorshipPricingRepo: ISponsorshipPricingRepository,
inject: [SPONSORSHIP_PRICING_REPOSITORY_TOKEN, SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], 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, provide: GET_SPONSOR_USE_CASE_TOKEN,
useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorUseCase(sponsorRepo), useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort<any>) => new GetSponsorUseCase(sponsorRepo, output),
inject: [SPONSOR_REPOSITORY_TOKEN], inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSOR_OUTPUT_PORT_TOKEN],
}, },
{ {
provide: GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN, provide: GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, sponsorRepo: ISponsorRepository) => useFactory: (
new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo), sponsorshipRequestRepo: ISponsorshipRequestRepository,
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN], 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, provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: NotificationService, paymentGateway: IPaymentGateway, walletRepository: IWalletRepository, leagueWalletRepository: ILeagueWalletRepository, logger: Logger) => useFactory: (
new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepo, seasonSponsorshipRepo, seasonRepo, notificationService, paymentGateway, walletRepository, leagueWalletRepository, logger), sponsorshipRequestRepo: ISponsorshipRequestRepository,
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, 'INotificationService', 'IPaymentGateway', 'IWalletRepository', 'ILeagueWalletRepository', LOGGER_TOKEN], 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, provide: REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, logger: Logger) => useFactory: (
new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger), sponsorshipRequestRepo: ISponsorshipRequestRepository,
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN], 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 type { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
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 { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; 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 { 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'; import { SponsorService } from './SponsorService';
describe('SponsorService', () => { describe('SponsorService', () => {
@@ -23,8 +35,21 @@ describe('SponsorService', () => {
let getPendingSponsorshipRequestsUseCase: { execute: Mock }; let getPendingSponsorshipRequestsUseCase: { execute: Mock };
let acceptSponsorshipRequestUseCase: { execute: Mock }; let acceptSponsorshipRequestUseCase: { execute: Mock };
let rejectSponsorshipRequestUseCase: { execute: Mock }; let rejectSponsorshipRequestUseCase: { execute: Mock };
let getSponsorBillingUseCase: { execute: Mock };
let logger: Logger; 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(() => { beforeEach(() => {
getSponsorshipPricingUseCase = { execute: vi.fn() }; getSponsorshipPricingUseCase = { execute: vi.fn() };
getSponsorsUseCase = { execute: vi.fn() }; getSponsorsUseCase = { execute: vi.fn() };
@@ -35,6 +60,7 @@ describe('SponsorService', () => {
getPendingSponsorshipRequestsUseCase = { execute: vi.fn() }; getPendingSponsorshipRequestsUseCase = { execute: vi.fn() };
acceptSponsorshipRequestUseCase = { execute: vi.fn() }; acceptSponsorshipRequestUseCase = { execute: vi.fn() };
rejectSponsorshipRequestUseCase = { execute: vi.fn() }; rejectSponsorshipRequestUseCase = { execute: vi.fn() };
getSponsorBillingUseCase = { execute: vi.fn() };
logger = { logger = {
debug: vi.fn(), debug: vi.fn(),
info: vi.fn(), info: vi.fn(),
@@ -42,6 +68,18 @@ describe('SponsorService', () => {
error: vi.fn(), error: vi.fn(),
} as unknown as Logger; } 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( service = new SponsorService(
getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase, getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase,
getSponsorsUseCase as unknown as GetSponsorsUseCase, getSponsorsUseCase as unknown as GetSponsorsUseCase,
@@ -52,28 +90,39 @@ describe('SponsorService', () => {
getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase, getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase,
acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase, acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase,
rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase, rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase,
getSponsorBillingUseCase as unknown as GetSponsorBillingUseCase,
logger, logger,
getEntitySponsorshipPricingPresenter,
getSponsorsPresenter,
createSponsorPresenter,
getSponsorDashboardPresenter,
getSponsorSponsorshipsPresenter,
getSponsorPresenter,
getPendingSponsorshipRequestsPresenter,
acceptSponsorshipRequestPresenter,
rejectSponsorshipRequestPresenter,
sponsorBillingPresenter,
); );
}); });
describe('getEntitySponsorshipPricing', () => { describe('getEntitySponsorshipPricing', () => {
it('returns presenter with pricing data on success', async () => { it('returns pricing data on success', async () => {
const outputPort = { const outputPort = {
entityType: 'season', entityType: 'season',
entityId: 'season-1', entityId: 'season-1',
pricing: [ tiers: [
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' }, { name: 'Gold', price: 500, benefits: ['Main slot'] },
], ],
}; };
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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', entityType: 'season',
entityId: 'season-1', entityId: 'season-1',
pricing: [ 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 () => { it('returns empty pricing on error', async () => {
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); 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', entityType: 'season',
entityId: '', entityId: '',
pricing: [], pricing: [],
@@ -92,82 +141,93 @@ describe('SponsorService', () => {
}); });
describe('getSponsors', () => { describe('getSponsors', () => {
it('returns sponsors in presenter on success', async () => { it('returns sponsors on success', async () => {
const outputPort = { sponsors: [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }] }; const sponsors = [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }];
const outputPort = { sponsors };
getSponsorsUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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 () => { it('returns empty list on error', async () => {
getSponsorsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); 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', () => { describe('createSponsor', () => {
it('returns created sponsor in presenter on success', async () => { it('returns created sponsor on success', async () => {
const input = { name: 'Test', contactEmail: 'test@example.com' }; const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
const outputPort = { const sponsor = {
sponsor: { id: 's1',
id: 's1', name: 'Test',
name: 'Test', contactEmail: 'test@example.com',
contactEmail: 'test@example.com', createdAt: new Date(),
createdAt: new Date(),
},
}; };
const outputPort = { sponsor };
createSponsorUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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 () => { 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( createSponsorUseCase.execute.mockResolvedValue(
Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Invalid' } }), 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', () => { describe('getSponsorDashboard', () => {
it('returns dashboard in presenter on success', async () => { it('returns dashboard on success', async () => {
const params = { sponsorId: 's1' }; const params: GetSponsorDashboardInput = { sponsorId: 's1' };
const outputPort = { const outputPort = {
sponsorId: 's1', sponsorId: 's1',
sponsorName: 'S1', sponsorName: 'S1',
metrics: {} as any, metrics: {
impressions: 0,
impressionsChange: 0,
uniqueViewers: 0,
viewersChange: 0,
races: 0,
drivers: 0,
exposure: 0,
exposureChange: 0,
},
sponsoredLeagues: [], sponsoredLeagues: [],
investment: {} as any, investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
}; };
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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 () => { it('throws on error', async () => {
const params = { sponsorId: 's1' }; const params: GetSponsorDashboardInput = { sponsorId: 's1' };
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); getSponsorDashboardUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getSponsorDashboard(params as any); await expect(service.getSponsorDashboard(params)).rejects.toThrow('Sponsor dashboard not found');
expect(presenter.viewModel).toBeNull();
}); });
}); });
describe('getSponsorSponsorships', () => { describe('getSponsorSponsorships', () => {
it('returns sponsorships in presenter on success', async () => { it('returns sponsorships on success', async () => {
const params = { sponsorId: 's1' }; const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
const outputPort = { const outputPort = {
sponsorId: 's1', sponsorId: 's1',
sponsorName: 'S1', sponsorName: 'S1',
@@ -182,46 +242,43 @@ describe('SponsorService', () => {
}; };
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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 () => { it('throws on error', async () => {
const params = { sponsorId: 's1' }; const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
getSponsorSponsorshipsUseCase.execute.mockResolvedValue( getSponsorSponsorshipsUseCase.execute.mockResolvedValue(
Result.err({ code: 'REPOSITORY_ERROR' }), Result.err({ code: 'REPOSITORY_ERROR' }),
); );
const presenter = await service.getSponsorSponsorships(params as any); await expect(service.getSponsorSponsorships(params)).rejects.toThrow('Sponsor sponsorships not found');
expect(presenter.viewModel).toBeNull();
}); });
}); });
describe('getSponsor', () => { describe('getSponsor', () => {
it('returns sponsor in presenter when found', async () => { it('returns sponsor when found', async () => {
const sponsorId = 's1'; 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)); 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'; const sponsorId = 's1';
getSponsorUseCase.execute.mockResolvedValue(Result.ok(null)); getSponsorUseCase.execute.mockResolvedValue(Result.ok(null));
const presenter = await service.getSponsor(sponsorId); await expect(service.getSponsor(sponsorId)).rejects.toThrow('Sponsor not found');
expect(presenter.viewModel).toBeNull();
}); });
}); });
describe('getPendingSponsorshipRequests', () => { 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 params = { entityType: 'season' as const, entityId: 'season-1' };
const outputPort = { const outputPort = {
entityType: 'season', entityType: 'season',
@@ -231,9 +288,9 @@ describe('SponsorService', () => {
}; };
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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 () => { it('returns empty result on error', async () => {
@@ -242,9 +299,9 @@ describe('SponsorService', () => {
Result.err({ code: 'REPOSITORY_ERROR' }), 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', entityType: 'season',
entityId: 'season-1', entityId: 'season-1',
requests: [], requests: [],
@@ -253,8 +310,8 @@ describe('SponsorService', () => {
}); });
}); });
describe('acceptSponsorshipRequest', () => { describe('SponsorshipRequest', () => {
it('returns accept result in presenter on success', async () => { it('returns accept result on success', async () => {
const requestId = 'r1'; const requestId = 'r1';
const respondedBy = 'u1'; const respondedBy = 'u1';
const outputPort = { const outputPort = {
@@ -267,100 +324,114 @@ describe('SponsorService', () => {
}; };
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(outputPort)); 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 requestId = 'r1';
const respondedBy = 'u1'; const respondedBy = 'u1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue( acceptSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }), Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
); );
const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy); await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Accept sponsorship request failed');
expect(presenter.viewModel).toBeNull();
}); });
}); });
describe('rejectSponsorshipRequest', () => { describe('rejectSponsorshipRequest', () => {
it('returns reject result in presenter on success', async () => { it('returns reject result on success', async () => {
const requestId = 'r1'; const requestId = 'r1';
const respondedBy = 'u1'; const respondedBy = 'u1';
const reason = 'Not interested'; const reason = 'Not interested';
const output = { const output = {
requestId, requestId,
status: 'rejected' as const, status: 'rejected' as const,
rejectedAt: new Date(), respondedAt: new Date(),
reason, rejectionReason: reason,
}; };
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output)); 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 requestId = 'r1';
const respondedBy = 'u1'; const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue( rejectSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }), Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
); );
const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy); await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Reject sponsorship request failed');
expect(presenter.viewModel).toBeNull();
}); });
}); });
describe('getSponsorBilling', () => { describe('getSponsorBilling', () => {
it('returns mock billing data in presenter', async () => { it('returns billing data', async () => {
const presenter = await service.getSponsorBilling('s1'); // 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(); const result = await service.getSponsorBilling('s1');
expect(presenter.viewModel?.paymentMethods).toBeInstanceOf(Array);
expect(presenter.viewModel?.invoices).toBeInstanceOf(Array); expect(result).not.toBeNull();
expect(presenter.viewModel?.stats).toBeDefined(); expect(result.paymentMethods).toBeInstanceOf(Array);
expect(result.invoices).toBeInstanceOf(Array);
expect(result.stats).toBeDefined();
}); });
}); });
describe('getAvailableLeagues', () => { describe('getAvailableLeagues', () => {
it('returns mock leagues in presenter', async () => { it('returns mock leagues', async () => {
const presenter = await service.getAvailableLeagues(); const result = await service.getAvailableLeagues();
expect(presenter.viewModel).not.toBeNull(); expect(result).not.toBeNull();
expect(presenter.viewModel?.length).toBeGreaterThan(0); expect(result.viewModel).not.toBeNull();
expect(result.viewModel?.length).toBeGreaterThan(0);
}); });
}); });
describe('getLeagueDetail', () => { describe('getLeagueDetail', () => {
it('returns league detail in presenter', async () => { it('returns league detail', async () => {
const presenter = await service.getLeagueDetail('league-1'); const result = await service.getLeagueDetail('league-1');
expect(presenter.viewModel).not.toBeNull(); expect(result).not.toBeNull();
expect(presenter.viewModel?.league.id).toBe('league-1'); expect(result.viewModel?.league.id).toBe('league-1');
}); });
}); });
describe('getSponsorSettings', () => { describe('getSponsorSettings', () => {
it('returns settings in presenter', async () => { it('returns settings', async () => {
const presenter = await service.getSponsorSettings('s1'); const result = await service.getSponsorSettings('s1');
expect(presenter.viewModel).not.toBeNull(); expect(result).not.toBeNull();
expect(presenter.viewModel?.profile).toBeDefined(); expect(result.viewModel?.profile).toBeDefined();
expect(presenter.viewModel?.notifications).toBeDefined(); expect(result.viewModel?.notifications).toBeDefined();
expect(presenter.viewModel?.privacy).toBeDefined(); expect(result.viewModel?.privacy).toBeDefined();
}); });
}); });
describe('updateSponsorSettings', () => { describe('updateSponsorSettings', () => {
it('returns success result in presenter', async () => { it('returns success result', async () => {
const presenter = await service.updateSponsorSettings('s1', {}); 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 { Injectable, Inject } from '@nestjs/common';
import { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO'; import { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
import { CreateSponsorOutputDTO } from './dtos/CreateSponsorOutputDTO';
import { GetSponsorDashboardQueryParamsDTO } from './dtos/GetSponsorDashboardQueryParamsDTO'; import { GetSponsorDashboardQueryParamsDTO } from './dtos/GetSponsorDashboardQueryParamsDTO';
import { SponsorDashboardDTO } from './dtos/SponsorDashboardDTO';
import { GetSponsorSponsorshipsQueryParamsDTO } from './dtos/GetSponsorSponsorshipsQueryParamsDTO'; import { GetSponsorSponsorshipsQueryParamsDTO } from './dtos/GetSponsorSponsorshipsQueryParamsDTO';
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO'; import { SponsorSponsorshipsDTO } from './dtos/SponsorSponsorshipsDTO';
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO'; import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
import { PaymentMethodDTO } from './dtos/PaymentMethodDTO'; import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
import { InvoiceDTO } from './dtos/InvoiceDTO'; import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshipPricingResultDTO';
import { BillingStatsDTO } from './dtos/BillingStatsDTO'; import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO';
import { AvailableLeagueDTO } from './dtos/AvailableLeagueDTO'; import { AvailableLeagueDTO } from './dtos/AvailableLeagueDTO';
import { LeagueDetailDTO } from './dtos/LeagueDetailDTO'; import { LeagueDetailDTO } from './dtos/LeagueDetailDTO';
import { DriverDTO } from './dtos/DriverDTO'; import { DriverDTO } from './dtos/DriverDTO';
@@ -14,6 +16,9 @@ import { RaceDTO } from './dtos/RaceDTO';
import { SponsorProfileDTO } from './dtos/SponsorProfileDTO'; import { SponsorProfileDTO } from './dtos/SponsorProfileDTO';
import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO'; import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO';
import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO'; import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO';
import { PaymentMethodDTO } from './dtos/PaymentMethodDTO';
import { InvoiceDTO } from './dtos/InvoiceDTO';
import { BillingStatsDTO } from './dtos/BillingStatsDTO';
// Use cases // Use cases
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; 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 { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import { import {
GetPendingSponsorshipRequestsUseCase, GetPendingSponsorshipRequestsUseCase,
GetPendingSponsorshipRequestsDTO, GetPendingSponsorshipRequestsInput,
} from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
@@ -48,6 +53,8 @@ import { AvailableLeaguesPresenter } from './presenters/AvailableLeaguesPresente
import { LeagueDetailPresenter } from './presenters/LeagueDetailPresenter'; import { LeagueDetailPresenter } from './presenters/LeagueDetailPresenter';
import { SponsorSettingsPresenter } from './presenters/SponsorSettingsPresenter'; import { SponsorSettingsPresenter } from './presenters/SponsorSettingsPresenter';
import { SponsorSettingsUpdatePresenter } from './presenters/SponsorSettingsUpdatePresenter'; import { SponsorSettingsUpdatePresenter } from './presenters/SponsorSettingsUpdatePresenter';
import { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter';
import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
// Tokens // Tokens
import { import {
@@ -61,6 +68,16 @@ import {
ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
LOGGER_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'; } from './SponsorProviders';
@Injectable() @Injectable()
@@ -88,194 +105,152 @@ export class SponsorService {
private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase, private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase,
@Inject(LOGGER_TOKEN) @Inject(LOGGER_TOKEN)
private readonly logger: Logger, 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.'); this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
await this.getSponsorshipPricingUseCase.execute({});
const presenter = new GetEntitySponsorshipPricingPresenter(); return this.getEntitySponsorshipPricingPresenter.viewModel;
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;
} }
async getSponsors(): Promise<GetSponsorsOutputDTO> { async getSponsors(): Promise<GetSponsorsOutputDTO> {
this.logger.debug('[SponsorService] Fetching sponsors.'); this.logger.debug('[SponsorService] Fetching sponsors.');
await this.getSponsorsUseCase.execute();
const presenter = new GetSponsorsPresenter(); return this.getSponsorsPresenter.responseModel;
const result = await this.getSponsorsUseCase.execute();
presenter.present(result);
return presenter.responseModel;
} }
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorPresenter> { async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> {
this.logger.debug('[SponsorService] Creating sponsor.', { input }); this.logger.debug('[SponsorService] Creating sponsor.', { input });
await this.createSponsorUseCase.execute(input);
const presenter = new CreateSponsorPresenter(); return this.createSponsorPresenter.viewModel;
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;
} }
async getSponsorDashboard( async getSponsorDashboard(
params: GetSponsorDashboardQueryParamsDTO, params: GetSponsorDashboardQueryParamsDTO,
): Promise<GetSponsorDashboardPresenter> { ): Promise<SponsorDashboardDTO> {
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params }); this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
await this.getSponsorDashboardUseCase.execute(params);
const presenter = new GetSponsorDashboardPresenter(); const result = this.getSponsorDashboardPresenter.viewModel;
const result = await this.getSponsorDashboardUseCase.execute(params); if (!result) {
throw new Error('Sponsor dashboard not found');
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor dashboard.', result.error);
presenter.present(null);
return presenter;
} }
return result;
presenter.present(result.value);
return presenter;
} }
async getSponsorSponsorships( async getSponsorSponsorships(
params: GetSponsorSponsorshipsQueryParamsDTO, params: GetSponsorSponsorshipsQueryParamsDTO,
): Promise<GetSponsorSponsorshipsPresenter> { ): Promise<SponsorSponsorshipsDTO> {
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params }); this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
await this.getSponsorSponsorshipsUseCase.execute(params);
const presenter = new GetSponsorSponsorshipsPresenter(); const result = this.getSponsorSponsorshipsPresenter.viewModel;
const result = await this.getSponsorSponsorshipsUseCase.execute(params); if (!result) {
throw new Error('Sponsor sponsorships not found');
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor sponsorships.', result.error);
presenter.present(null);
return presenter;
} }
return result;
presenter.present(result.value);
return presenter;
} }
async getSponsor(sponsorId: string): Promise<GetSponsorPresenter> { async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO> {
this.logger.debug('[SponsorService] Fetching sponsor.', { sponsorId }); this.logger.debug('[SponsorService] Fetching sponsor.', { sponsorId });
await this.getSponsorUseCase.execute({ sponsorId });
const presenter = new GetSponsorPresenter(); const result = this.getSponsorPresenter.viewModel;
const result = await this.getSponsorUseCase.execute({ sponsorId }); if (!result) {
throw new Error('Sponsor not found');
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to fetch sponsor.', result.error);
presenter.present(null);
return presenter;
} }
return result;
presenter.present(result.value);
return presenter;
} }
async getPendingSponsorshipRequests(params: { async getPendingSponsorshipRequests(params: {
entityType: SponsorableEntityType; entityType: SponsorableEntityType;
entityId: string; entityId: string;
}): Promise<GetPendingSponsorshipRequestsPresenter> { }): Promise<GetPendingSponsorshipRequestsOutputDTO> {
this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params }); this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params });
await this.getPendingSponsorshipRequestsUseCase.execute(
const presenter = new GetPendingSponsorshipRequestsPresenter(); params as GetPendingSponsorshipRequestsInput,
const result = await this.getPendingSponsorshipRequestsUseCase.execute(
params as GetPendingSponsorshipRequestsDTO,
); );
const result = this.getPendingSponsorshipRequestsPresenter.viewModel;
if (result.isErr()) { if (!result) {
this.logger.error('[SponsorService] Failed to fetch pending sponsorship requests.', result.error); throw new Error('Pending sponsorship requests not found');
presenter.present({
entityType: params.entityType,
entityId: params.entityId,
requests: [],
totalCount: 0,
});
return presenter;
} }
return result;
presenter.present(result.value);
return presenter;
} }
async acceptSponsorshipRequest( async acceptSponsorshipRequest(
requestId: string, requestId: string,
respondedBy: string, respondedBy: string,
): Promise<AcceptSponsorshipRequestPresenter> { ): Promise<AcceptSponsorshipRequestResultViewModel> {
this.logger.debug('[SponsorService] Accepting sponsorship request.', { this.logger.debug('[SponsorService] Accepting sponsorship request.', {
requestId, requestId,
respondedBy, respondedBy,
}); });
await this.acceptSponsorshipRequestUseCase.execute({
const presenter = new AcceptSponsorshipRequestPresenter();
const result = await this.acceptSponsorshipRequestUseCase.execute({
requestId, requestId,
respondedBy, respondedBy,
} as AcceptSponsorshipRequestInputDTO); });
const result = this.acceptSponsorshipRequestPresenter.viewModel;
if (result.isErr()) { if (!result) {
this.logger.error('[SponsorService] Failed to accept sponsorship request.', result.error); throw new Error('Accept sponsorship request failed');
presenter.present(null);
return presenter;
} }
return result;
presenter.present(result.value);
return presenter;
} }
async rejectSponsorshipRequest( async rejectSponsorshipRequest(
requestId: string, requestId: string,
respondedBy: string, respondedBy: string,
reason?: string, reason?: string,
): Promise<RejectSponsorshipRequestPresenter> { ): Promise<RejectSponsorshipRequestResult> {
this.logger.debug('[SponsorService] Rejecting sponsorship request.', { this.logger.debug('[SponsorService] Rejecting sponsorship request.', {
requestId, requestId,
respondedBy, respondedBy,
reason, reason,
}); });
const input: { requestId: string; respondedBy: string; reason?: string } = {
const presenter = new RejectSponsorshipRequestPresenter();
const result = await this.rejectSponsorshipRequestUseCase.execute({
requestId, requestId,
respondedBy, respondedBy,
reason, };
} as RejectSponsorshipRequestInputDTO); if (reason !== undefined) {
input.reason = reason;
if (result.isErr()) {
this.logger.error('[SponsorService] Failed to reject sponsorship request.', result.error);
presenter.present(null);
return presenter;
} }
await this.rejectSponsorshipRequestUseCase.execute(input);
presenter.present(result.value); const result = this.rejectSponsorshipRequestPresenter.viewModel;
return presenter; 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 }); this.logger.debug('[SponsorService] Fetching sponsor billing.', { sponsorId });
await this.getSponsorBillingUseCase.execute({ sponsorId });
const result = await this.getSponsorBillingUseCase.execute({ sponsorId }); const result = this.sponsorBillingPresenter.viewModel;
if (!result) {
if (result.isErr()) { throw new Error('Sponsor billing not found');
this.logger.error('[SponsorService] Failed to fetch sponsor billing.', result.error);
throw new Error(result.error.details?.message || 'Failed to fetch sponsor billing');
} }
return result;
const presenter = new GetSponsorBillingPresenter();
presenter.present(result.value);
return presenter;
} }
async getAvailableLeagues(): Promise<AvailableLeaguesPresenter> { async getAvailableLeagues(): Promise<AvailableLeaguesPresenter> {
@@ -426,7 +401,7 @@ export class SponsorService {
website: 'https://acme-racing.com', website: 'https://acme-racing.com',
description: description:
'Premium sim racing equipment and accessories for competitive drivers.', 'Premium sim racing equipment and accessories for competitive drivers.',
logoUrl: null, logoUrl: '',
industry: 'Racing Equipment', industry: 'Racing Equipment',
address: { address: {
street: '123 Racing Boulevard', street: '123 Racing Boulevard',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,68 +1,68 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { export class LeagueDetailDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id: string = '';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name: string = '';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
game: string; game: string = '';
@ApiProperty({ enum: ['premium', 'standard', 'starter'] }) @ApiProperty({ enum: ['premium', 'standard', 'starter'] })
@IsEnum(['premium', 'standard', 'starter']) @IsEnum(['premium', 'standard', 'starter'])
tier: 'premium' | 'standard' | 'starter'; tier: 'premium' | 'standard' | 'starter' = 'standard';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
season: string; season: string = '';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
description: string; description: string = '';
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
drivers: number; drivers: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
races: number; races: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
completedRaces: number; completedRaces: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
totalImpressions: number; totalImpressions: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
avgViewsPerRace: number; avgViewsPerRace: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
engagement: number; engagement: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
rating: number; rating: number = 0;
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] }) @ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
@IsEnum(['active', 'upcoming', 'completed']) @IsEnum(['active', 'upcoming', 'completed'])
seasonStatus: 'active' | 'upcoming' | 'completed'; seasonStatus: 'active' | 'upcoming' | 'completed' = 'active';
@ApiProperty({ type: Object }) @ApiProperty({ type: Object })
seasonDates: { seasonDates: {
start: string; start: string;
end: string; end: string;
}; } = { start: '', end: '' };
@ApiProperty({ type: Object, required: false }) @ApiProperty({ type: Object, required: false })
@IsOptional() @IsOptional()
@@ -84,5 +84,8 @@ export class LeagueDetailDTO {
price: number; price: number;
benefits: string[]; 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 { export class NotificationSettingsDTO {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
emailNewSponsorships: boolean; emailNewSponsorships: boolean = false;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
emailWeeklyReport: boolean; emailWeeklyReport: boolean = false;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
emailRaceAlerts: boolean; emailRaceAlerts: boolean = false;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
emailPaymentAlerts: boolean; emailPaymentAlerts: boolean = false;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
emailNewOpportunities: boolean; emailNewOpportunities: boolean = false;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
emailContractExpiry: boolean; emailContractExpiry: boolean = false;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,14 +5,14 @@ import { SponsorshipDetailDTO } from './SponsorshipDetailDTO';
export class SponsorSponsorshipsDTO { export class SponsorSponsorshipsDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
sponsorId: string; sponsorId: string = '';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
sponsorName: string; sponsorName: string = '';
@ApiProperty({ type: [SponsorshipDetailDTO] }) @ApiProperty({ type: [SponsorshipDetailDTO] })
sponsorships: SponsorshipDetailDTO[]; sponsorships: SponsorshipDetailDTO[] = [];
@ApiProperty() @ApiProperty()
summary: { summary: {
@@ -21,5 +21,11 @@ export class SponsorSponsorshipsDTO {
totalInvestment: number; totalInvestment: number;
totalPlatformFees: number; totalPlatformFees: number;
currency: string; 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 { export class SponsoredLeagueDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id: string = '';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
name: string; name: string = '';
@ApiProperty({ enum: ['main', 'secondary'] }) @ApiProperty({ enum: ['main', 'secondary'] })
@IsEnum(['main', 'secondary']) @IsEnum(['main', 'secondary'])
tier: 'main' | 'secondary'; tier: 'main' | 'secondary' = 'main';
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
drivers: number; drivers: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
races: number; races: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
impressions: number; impressions: number = 0;
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] }) @ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
@IsEnum(['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 { export class SponsorshipDTO {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
id: string; id: string = '';
@ApiProperty({ enum: ['leagues', 'teams', 'drivers', 'races', 'platform'] }) @ApiProperty({ enum: ['leagues', 'teams', 'drivers', 'races', 'platform'] })
@IsEnum(['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() @ApiProperty()
@IsString() @IsString()
entityId: string; entityId: string = '';
@ApiProperty() @ApiProperty()
@IsString() @IsString()
entityName: string; entityName: string = '';
@ApiProperty({ enum: ['main', 'secondary'], required: false }) @ApiProperty({ enum: ['main', 'secondary'], required: false })
@IsOptional() @IsOptional()
@@ -25,7 +25,7 @@ export class SponsorshipDTO {
@ApiProperty({ enum: ['active', 'pending_approval', 'approved', 'rejected', 'expired'] }) @ApiProperty({ enum: ['active', 'pending_approval', 'approved', 'rejected', 'expired'] })
@IsEnum(['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 }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -44,19 +44,19 @@ export class SponsorshipDTO {
@ApiProperty() @ApiProperty()
@IsDateString() @IsDateString()
startDate: string; startDate: string = '';
@ApiProperty() @ApiProperty()
@IsDateString() @IsDateString()
endDate: string; endDate: string = '';
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
price: number; price: number = 0;
@ApiProperty() @ApiProperty()
@IsNumber() @IsNumber()
impressions: number; impressions: number = 0;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()

View File

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

View File

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

View File

@@ -2,38 +2,38 @@ import { ApiProperty } from '@nestjs/swagger';
export class SponsorshipRequestDTO { export class SponsorshipRequestDTO {
@ApiProperty() @ApiProperty()
id: string; id: string = '';
@ApiProperty() @ApiProperty()
sponsorId: string; sponsorId: string = '';
@ApiProperty() @ApiProperty()
sponsorName: string; sponsorName: string = '';
@ApiProperty({ required: false }) @ApiProperty({ required: false })
sponsorLogo?: string; sponsorLogo?: string;
@ApiProperty() @ApiProperty()
tier: string; tier: string = '';
@ApiProperty() @ApiProperty()
offeredAmount: number; offeredAmount: number = 0;
@ApiProperty() @ApiProperty()
currency: string; currency: string = '';
@ApiProperty() @ApiProperty()
formattedAmount: string; formattedAmount: string = '';
@ApiProperty({ required: false }) @ApiProperty({ required: false })
message?: string; message?: string;
@ApiProperty() @ApiProperty()
createdAt: Date; createdAt: Date = new Date();
@ApiProperty() @ApiProperty()
platformFee: number; platformFee: number = 0;
@ApiProperty() @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 { export interface AcceptSponsorshipRequestResultViewModel {
requestId: string; requestId: string;
@@ -16,7 +16,7 @@ export class AcceptSponsorshipRequestPresenter {
this.result = null; this.result = null;
} }
present(output: AcceptSponsorshipOutputPort | null) { present(output: AcceptSponsorshipResult | null) {
if (!output) { if (!output) {
this.result = null; this.result = null;
return; return;

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { CreateSponsorPresenter } from './CreateSponsorPresenter'; import { CreateSponsorPresenter } from './CreateSponsorPresenter';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
describe('CreateSponsorPresenter', () => { describe('CreateSponsorPresenter', () => {
let presenter: CreateSponsorPresenter; let presenter: CreateSponsorPresenter;
@@ -10,9 +11,23 @@ describe('CreateSponsorPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result to null', () => { it('should reset the result to null', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; const mockSponsor = Sponsor.create({
presenter.present(mockPort); id: 'sponsor-1',
expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); 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(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +36,24 @@ describe('CreateSponsorPresenter', () => {
describe('present', () => { describe('present', () => {
it('should store the result', () => { 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', () => { it('should return the result when presented', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; const mockSponsor = Sponsor.create({
presenter.present(mockPort); 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', () => { it('should return the result when presented', () => {
const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; const mockSponsor = Sponsor.create({
presenter.present(mockPort); 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'; import type { CreateSponsorOutputDTO } from '../dtos/CreateSponsorOutputDTO';
export class CreateSponsorPresenter { export class CreateSponsorPresenter {
@@ -8,16 +8,24 @@ export class CreateSponsorPresenter {
this.result = null; 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 = { this.result = {
sponsor: { sponsor: sponsorData,
id: port.sponsor.id,
name: port.sponsor.name,
contactEmail: port.sponsor.contactEmail,
logoUrl: port.sponsor.logoUrl,
websiteUrl: port.sponsor.websiteUrl,
createdAt: port.sponsor.createdAt,
},
}; };
} }

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { GetEntitySponsorshipPricingPresenter } from './GetEntitySponsorshipPricingPresenter'; import { GetEntitySponsorshipPricingPresenter } from './GetEntitySponsorshipPricingPresenter';
import type { GetEntitySponsorshipPricingResult } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
describe('GetEntitySponsorshipPricingPresenter', () => { describe('GetEntitySponsorshipPricingPresenter', () => {
let presenter: GetEntitySponsorshipPricingPresenter; let presenter: GetEntitySponsorshipPricingPresenter;
@@ -10,9 +11,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result to null', () => { 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); presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +33,21 @@ describe('GetEntitySponsorshipPricingPresenter', () => {
describe('present', () => { describe('present', () => {
it('should store the result', () => { 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); 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', () => { 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); 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', () => { 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); 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'; import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetEntitySponsorshipPricingPresenter { export class GetEntitySponsorshipPricingPresenter {
@@ -8,7 +8,7 @@ export class GetEntitySponsorshipPricingPresenter {
this.result = null; this.result = null;
} }
present(output: GetSponsorshipPricingOutputPort | null) { present(output: GetEntitySponsorshipPricingResult | null) {
if (!output) { if (!output) {
this.result = { this.result = {
entityType: 'season', entityType: 'season',
@@ -21,11 +21,11 @@ export class GetEntitySponsorshipPricingPresenter {
this.result = { this.result = {
entityType: output.entityType, entityType: output.entityType,
entityId: output.entityId, entityId: output.entityId,
pricing: output.pricing.map(item => ({ pricing: output.tiers.map(item => ({
id: item.id, id: item.name,
level: item.level, level: item.name,
price: item.price, 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'; import { GetPendingSponsorshipRequestsOutputDTO } from '../dtos/GetPendingSponsorshipRequestsOutputDTO';
export class GetPendingSponsorshipRequestsPresenter { export class GetPendingSponsorshipRequestsPresenter {
@@ -8,7 +8,7 @@ export class GetPendingSponsorshipRequestsPresenter {
this.result = null; this.result = null;
} }
present(outputPort: PendingSponsorshipRequestsOutputPort | null) { present(outputPort: GetPendingSponsorshipRequestsResult | null) {
if (!outputPort) { if (!outputPort) {
this.result = null; this.result = null;
return; return;
@@ -17,7 +17,30 @@ export class GetPendingSponsorshipRequestsPresenter {
this.result = { this.result = {
entityType: outputPort.entityType, entityType: outputPort.entityType,
entityId: outputPort.entityId, 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, totalCount: outputPort.totalCount,
}; };
} }

View File

@@ -1,5 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorDashboardPresenter } from './GetSponsorDashboardPresenter'; 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', () => { describe('GetSponsorDashboardPresenter', () => {
let presenter: GetSponsorDashboardPresenter; let presenter: GetSponsorDashboardPresenter;
@@ -10,9 +12,58 @@ describe('GetSponsorDashboardPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result to null', () => { 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); 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(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +72,59 @@ describe('GetSponsorDashboardPresenter', () => {
describe('present', () => { describe('present', () => {
it('should store the result', () => { 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); 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', () => { 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); 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', () => { 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); 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'; import { SponsorDashboardDTO } from '../dtos/SponsorDashboardDTO';
export class GetSponsorDashboardPresenter { export class GetSponsorDashboardPresenter {
@@ -8,8 +8,40 @@ export class GetSponsorDashboardPresenter {
this.result = null; this.result = null;
} }
present(outputPort: SponsorDashboardOutputPort | null) { present(outputPort: GetSponsorDashboardResult | null) {
this.result = outputPort ?? 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 { getViewModel(): SponsorDashboardDTO | null {

View File

@@ -1,5 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorSponsorshipsPresenter } from './GetSponsorSponsorshipsPresenter'; 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', () => { describe('GetSponsorSponsorshipsPresenter', () => {
let presenter: GetSponsorSponsorshipsPresenter; let presenter: GetSponsorSponsorshipsPresenter;
@@ -10,9 +13,38 @@ describe('GetSponsorSponsorshipsPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result to null', () => { 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); 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(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +53,39 @@ describe('GetSponsorSponsorshipsPresenter', () => {
describe('present', () => { describe('present', () => {
it('should store the result', () => { 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); 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', () => { 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); 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', () => { 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); 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'; import { SponsorSponsorshipsDTO } from '../dtos/SponsorSponsorshipsDTO';
export class GetSponsorSponsorshipsPresenter { export class GetSponsorSponsorshipsPresenter {
@@ -8,8 +8,58 @@ export class GetSponsorSponsorshipsPresenter {
this.result = null; this.result = null;
} }
present(outputPort: SponsorSponsorshipsOutputPort | null) { present(outputPort: GetSponsorSponsorshipsResult | null) {
this.result = outputPort ?? 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 { getViewModel(): SponsorSponsorshipsDTO | null {

View File

@@ -1,11 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest'; 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 { GetSponsorsPresenter } from './GetSponsorsPresenter';
import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
describe('GetSponsorsPresenter', () => { describe('GetSponsorsPresenter', () => {
let presenter: GetSponsorsPresenter; let presenter: GetSponsorsPresenter;
@@ -16,8 +11,8 @@ describe('GetSponsorsPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the model to null and cause responseModel to throw', () => { it('should reset the model to null and cause responseModel to throw', () => {
const result = Result.ok<GetSponsorsResult, never>({ sponsors: [] }); const sponsors: Sponsor[] = [];
presenter.present(result); presenter.present(sponsors);
expect(presenter.responseModel).toEqual({ sponsors: [] }); expect(presenter.responseModel).toEqual({ sponsors: [] });
presenter.reset(); presenter.reset();
@@ -26,29 +21,24 @@ describe('GetSponsorsPresenter', () => {
}); });
describe('present', () => { describe('present', () => {
it('should map Result.ok sponsors to DTO responseModel', () => { it('should map sponsors to DTO responseModel', () => {
const result = Result.ok<GetSponsorsResult, never>({ const sponsors: Sponsor[] = [
sponsors: [ Sponsor.create({
{ id: 'sponsor-1',
id: 'sponsor-1', name: 'Sponsor One',
name: 'Sponsor One', contactEmail: 's1@example.com',
contactEmail: 's1@example.com', logoUrl: 'logo1.png',
logoUrl: 'logo1.png', websiteUrl: 'https://one.example.com',
websiteUrl: 'https://one.example.com', createdAt: new Date('2024-01-01T00:00:00Z'),
createdAt: new Date('2024-01-01T00:00:00Z'), }),
}, Sponsor.create({
{ id: 'sponsor-2',
id: 'sponsor-2', name: 'Sponsor Two',
name: 'Sponsor Two', contactEmail: 's2@example.com',
contactEmail: 's2@example.com', }),
logoUrl: undefined, ];
websiteUrl: undefined,
createdAt: undefined,
},
],
});
presenter.present(result); presenter.present(sponsors);
expect(presenter.responseModel).toEqual({ expect(presenter.responseModel).toEqual({
sponsors: [ sponsors: [
@@ -64,9 +54,7 @@ describe('GetSponsorsPresenter', () => {
id: 'sponsor-2', id: 'sponsor-2',
name: 'Sponsor Two', name: 'Sponsor Two',
contactEmail: 's2@example.com', contactEmail: 's2@example.com',
logoUrl: undefined, createdAt: expect.any(Date),
websiteUrl: undefined,
createdAt: undefined,
}, },
], ],
}); });
@@ -79,8 +67,8 @@ describe('GetSponsorsPresenter', () => {
}); });
it('should return the model when presented', () => { it('should return the model when presented', () => {
const result = Result.ok<GetSponsorsResult, never>({ sponsors: [] }); const sponsors: Sponsor[] = [];
presenter.present(result); presenter.present(sponsors);
expect(presenter.getResponseModel()).toEqual({ sponsors: [] }); expect(presenter.getResponseModel()).toEqual({ sponsors: [] });
}); });
@@ -91,16 +79,26 @@ describe('GetSponsorsPresenter', () => {
expect(() => presenter.responseModel).toThrow('Presenter not presented'); expect(() => presenter.responseModel).toThrow('Presenter not presented');
}); });
it('should fallback to empty sponsors list on error', () => { it('should return the model when presented', () => {
const error = { const sponsors: Sponsor[] = [
code: 'REPOSITORY_ERROR' as GetSponsorsErrorCode, Sponsor.create({
details: { message: 'DB error' }, id: 'sponsor-1',
} satisfies ApplicationErrorCode<GetSponsorsErrorCode, { message: string }>; name: 'Sponsor One',
const result = Result.err<GetSponsorsResult, typeof error>(error); 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 { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type {
GetSponsorsResult,
GetSponsorsErrorCode,
} from '@core/racing/application/use-cases/GetSponsorsUseCase';
import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO'; import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO';
import type { SponsorDTO } from '../dtos/SponsorDTO'; import type { SponsorDTO } from '../dtos/SponsorDTO';
@@ -14,29 +9,26 @@ export class GetSponsorsPresenter {
this.model = null; this.model = null;
} }
present( present(sponsors: Sponsor[]): void {
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();
this.model = { this.model = {
sponsors: output.sponsors.map<SponsorDTO>((sponsor) => ({ sponsors: sponsors.map<SponsorDTO>((sponsor) => {
id: sponsor.id, const sponsorData: any = {
name: sponsor.name, id: sponsor.id.toString(),
contactEmail: sponsor.contactEmail, name: sponsor.name.toString(),
logoUrl: sponsor.logoUrl, contactEmail: sponsor.contactEmail.toString(),
websiteUrl: sponsor.websiteUrl, createdAt: sponsor.createdAt.toDate(),
createdAt: sponsor.createdAt, };
})),
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 { describe, it, expect, beforeEach } from 'vitest';
import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter'; import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter';
import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
describe('GetSponsorshipPricingPresenter', () => { describe('GetSponsorshipPricingPresenter', () => {
let presenter: GetSponsorshipPricingPresenter; let presenter: GetSponsorshipPricingPresenter;
@@ -10,9 +11,19 @@ describe('GetSponsorshipPricingPresenter', () => {
describe('reset', () => { describe('reset', () => {
it('should reset the result to null', () => { it('should reset the result to null', () => {
const mockResult = { tiers: [] }; const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult); presenter.present(mockResult);
expect(presenter.viewModel).toEqual(mockResult);
const expectedViewModel = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
expect(presenter.viewModel).toEqual(expectedViewModel);
presenter.reset(); presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented'); expect(() => presenter.viewModel).toThrow('Presenter not presented');
@@ -21,11 +32,20 @@ describe('GetSponsorshipPricingPresenter', () => {
describe('present', () => { describe('present', () => {
it('should store the result', () => { it('should store the result', () => {
const mockResult = { tiers: [] }; const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult); 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', () => { it('should return the result when presented', () => {
const mockResult = { tiers: [] }; const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult); 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', () => { it('should return the result when presented', () => {
const mockResult = { tiers: [] }; const mockResult: GetSponsorshipPricingResult = {
entityType: 'season',
entityId: 'season-1',
pricing: []
};
presenter.present(mockResult); 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'; import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO';
export class GetSponsorshipPricingPresenter { export class GetSponsorshipPricingPresenter {
present(outputPort: GetSponsorshipPricingOutputPort): GetEntitySponsorshipPricingResultDTO { private result: GetEntitySponsorshipPricingResultDTO | null = null;
return {
reset() {
this.result = null;
}
present(outputPort: GetSponsorshipPricingResult): void {
this.result = {
entityType: outputPort.entityType, entityType: outputPort.entityType,
entityId: outputPort.entityId, 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 { export class RejectSponsorshipRequestPresenter {
private result: RejectSponsorshipRequestResultDTO | null = null; private result: RejectSponsorshipRequestResult | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
present(output: RejectSponsorshipRequestResultDTO | null) { present(output: RejectSponsorshipRequestResult | null) {
this.result = output ?? null; this.result = output ?? null;
} }
getViewModel(): RejectSponsorshipRequestResultDTO | null { getViewModel(): RejectSponsorshipRequestResult | null {
return this.result; return this.result;
} }
get viewModel(): RejectSponsorshipRequestResultDTO | null { get viewModel(): RejectSponsorshipRequestResult | null {
return this.result; return this.result;
} }
} }

View File

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

View File

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

View File

@@ -3,10 +3,6 @@ import { TeamService } from './TeamService';
// Import core interfaces // Import core interfaces
import type { Logger } from '@core/shared/application/Logger'; 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 concrete in-memory implementations
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository'; 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 { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases // Use cases are imported and used directly in the service
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';
// Define injection tokens // Define injection tokens
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository'; export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
@@ -58,53 +46,5 @@ export const TeamProviders: Provider[] = [
provide: LOGGER_TOKEN, provide: LOGGER_TOKEN,
useClass: ConsoleLogger, useClass: ConsoleLogger,
}, },
// Use cases // Use cases are created directly in the service
{
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],
},
]; ];

View File

@@ -1,4 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { vi } from 'vitest';
import { TeamService } from './TeamService'; import { TeamService } from './TeamService';
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
@@ -11,20 +12,20 @@ import { DriverTeamViewModel } from './dtos/TeamDto';
describe('TeamService', () => { describe('TeamService', () => {
let service: TeamService; let service: TeamService;
let getAllTeamsUseCase: jest.Mocked<GetAllTeamsUseCase>; let getAllTeamsUseCase: ReturnType<typeof vi.mocked<GetAllTeamsUseCase>>;
let getDriverTeamUseCase: jest.Mocked<GetDriverTeamUseCase>; let getDriverTeamUseCase: ReturnType<typeof vi.mocked<GetDriverTeamUseCase>>;
beforeEach(async () => { beforeEach(async () => {
const mockGetAllTeamsUseCase = { const mockGetAllTeamsUseCase = {
execute: jest.fn(), execute: vi.fn(),
}; };
const mockGetDriverTeamUseCase = { const mockGetDriverTeamUseCase = {
execute: jest.fn(), execute: vi.fn(),
}; };
const mockLogger = { const mockLogger = {
debug: jest.fn(), debug: vi.fn(),
info: jest.fn(), info: vi.fn(),
error: jest.fn(), error: vi.fn(),
}; };
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@@ -61,11 +62,11 @@ describe('TeamService', () => {
getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any); getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = { const mockPresenter = {
present: jest.fn(), present: vi.fn(),
getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }), getViewModel: vi.fn().mockReturnValue({ teams: [], totalCount: 0 }),
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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(); const result = await service.getAll();
@@ -81,11 +82,11 @@ describe('TeamService', () => {
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any); getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
const mockPresenter = { const mockPresenter = {
present: jest.fn(), present: vi.fn(),
getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel), getViewModel: vi.fn().mockReturnValue({} as DriverTeamViewModel),
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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'); 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 { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
// Use cases // Use cases
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
@@ -38,7 +37,7 @@ import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter'; import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
// Tokens // 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() @Injectable()
export class TeamService { export class TeamService {
@@ -46,7 +45,6 @@ export class TeamService {
@Inject(TEAM_REPOSITORY_TOKEN) private readonly teamRepository: ITeamRepository, @Inject(TEAM_REPOSITORY_TOKEN) private readonly teamRepository: ITeamRepository,
@Inject(TEAM_MEMBERSHIP_REPOSITORY_TOKEN) private readonly membershipRepository: ITeamMembershipRepository, @Inject(TEAM_MEMBERSHIP_REPOSITORY_TOKEN) private readonly membershipRepository: ITeamMembershipRepository,
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository, @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository,
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort,
@Inject(LOGGER_TOKEN) private readonly logger: Logger, @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 useCase = new GetAllTeamsUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
const result = await useCase.execute(); const result = await useCase.execute();
if (result.isErr()) { 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 { teams: [], totalCount: 0 };
} }
return presenter.responseModel; return presenter.getResponseModel()!;
} }
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> { async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
@@ -75,14 +73,14 @@ export class TeamService {
return null; return null;
} }
return presenter.getResponseModel(); return presenter.getResponseModel()!;
} }
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> { async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`); this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
const presenter = new TeamMembersPresenter(); 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 }); const result = await useCase.execute({ teamId });
if (result.isErr()) { if (result.isErr()) {
this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`); 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 useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter);
const result = await useCase.execute({ teamId }); const result = await useCase.execute({ teamId });
if (result.isErr()) { 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 { return {
requests: [], requests: [],
pendingCount: 0, pendingCount: 0,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,31 +4,31 @@ export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
class TeamLeaderboardItemDTO { class TeamLeaderboardItemDTO {
@ApiProperty() @ApiProperty()
id: string; id!: string;
@ApiProperty() @ApiProperty()
name: string; name!: string;
@ApiProperty() @ApiProperty()
memberCount: number; memberCount!: number;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
rating: number | null; rating!: number | null;
@ApiProperty() @ApiProperty()
totalWins: number; totalWins!: number;
@ApiProperty() @ApiProperty()
totalRaces: number; totalRaces!: number;
@ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] }) @ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel: SkillLevel; performanceLevel!: SkillLevel;
@ApiProperty() @ApiProperty()
isRecruiting: boolean; isRecruiting!: boolean;
@ApiProperty() @ApiProperty()
createdAt: string; createdAt!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
description?: string; description?: string;
@@ -45,14 +45,14 @@ class TeamLeaderboardItemDTO {
export class GetTeamsLeaderboardOutputDTO { export class GetTeamsLeaderboardOutputDTO {
@ApiProperty({ type: [TeamLeaderboardItemDTO] }) @ApiProperty({ type: [TeamLeaderboardItemDTO] })
teams: TeamLeaderboardItemDTO[]; teams!: TeamLeaderboardItemDTO[];
@ApiProperty() @ApiProperty()
recruitingCount: number; recruitingCount!: number;
@ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } }) @ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } })
groupsBySkillLevel: Record<SkillLevel, TeamLeaderboardItemDTO[]>; groupsBySkillLevel!: Record<SkillLevel, TeamLeaderboardItemDTO[]>;
@ApiProperty({ type: [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 { export class TeamListItemViewModel {
@ApiProperty() @ApiProperty()
id: string; id!: string;
@ApiProperty() @ApiProperty()
name: string; name!: string;
@ApiProperty() @ApiProperty()
tag: string; tag!: string;
@ApiProperty() @ApiProperty()
description: string; description!: string;
@ApiProperty() @ApiProperty()
memberCount: number; memberCount!: number;
@ApiProperty({ type: [String] }) @ApiProperty({ type: [String] })
leagues: string[]; leagues!: string[];
@ApiProperty({ required: false }) @ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed'; specialization?: 'endurance' | 'sprint' | 'mixed';
@@ -32,30 +32,30 @@ export class TeamListItemViewModel {
export class AllTeamsViewModel { export class AllTeamsViewModel {
@ApiProperty({ type: [TeamListItemViewModel] }) @ApiProperty({ type: [TeamListItemViewModel] })
teams: TeamListItemViewModel[]; teams!: TeamListItemViewModel[];
@ApiProperty() @ApiProperty()
totalCount: number; totalCount!: number;
} }
export class TeamViewModel { export class TeamViewModel {
@ApiProperty() @ApiProperty()
id: string; id!: string;
@ApiProperty() @ApiProperty()
name: string; name!: string;
@ApiProperty() @ApiProperty()
tag: string; tag!: string;
@ApiProperty() @ApiProperty()
description: string; description!: string;
@ApiProperty() @ApiProperty()
ownerId: string; ownerId!: string;
@ApiProperty({ type: [String] }) @ApiProperty({ type: [String] })
leagues: string[]; leagues!: string[];
@ApiProperty({ required: false }) @ApiProperty({ required: false })
createdAt?: string; createdAt?: string;
@@ -85,131 +85,131 @@ export enum MembershipStatus {
export class MembershipViewModel { export class MembershipViewModel {
@ApiProperty() @ApiProperty()
role: 'owner' | 'manager' | 'member'; role!: 'owner' | 'manager' | 'member';
@ApiProperty() @ApiProperty()
joinedAt: string; joinedAt!: string;
@ApiProperty() @ApiProperty()
isActive: boolean; isActive!: boolean;
} }
export class DriverTeamViewModel { export class DriverTeamViewModel {
@ApiProperty({ type: TeamViewModel }) @ApiProperty({ type: TeamViewModel })
team: TeamViewModel; team!: TeamViewModel;
@ApiProperty({ type: MembershipViewModel }) @ApiProperty({ type: MembershipViewModel })
membership: MembershipViewModel; membership!: MembershipViewModel;
@ApiProperty() @ApiProperty()
isOwner: boolean; isOwner!: boolean;
@ApiProperty() @ApiProperty()
canManage: boolean; canManage!: boolean;
} }
export class GetDriverTeamQuery { export class GetDriverTeamQuery {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
teamId: string; teamId!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
driverId: string; driverId!: string;
} }
export class TeamDetailsViewModel { export class TeamDetailsViewModel {
@ApiProperty({ type: TeamViewModel }) @ApiProperty({ type: TeamViewModel })
team: TeamViewModel; team!: TeamViewModel;
@ApiProperty({ type: MembershipViewModel, nullable: true }) @ApiProperty({ type: MembershipViewModel, nullable: true })
membership: MembershipViewModel | null; membership!: MembershipViewModel | null;
@ApiProperty() @ApiProperty()
canManage: boolean; canManage!: boolean;
} }
export class TeamMemberViewModel { export class TeamMemberViewModel {
@ApiProperty() @ApiProperty()
driverId: string; driverId!: string;
@ApiProperty() @ApiProperty()
driverName: string; driverName!: string;
@ApiProperty() @ApiProperty()
role: 'owner' | 'manager' | 'member'; role!: 'owner' | 'manager' | 'member';
@ApiProperty() @ApiProperty()
joinedAt: string; joinedAt!: string;
@ApiProperty() @ApiProperty()
isActive: boolean; isActive!: boolean;
@ApiProperty() @ApiProperty()
avatarUrl: string; avatarUrl!: string;
} }
export class TeamMembersViewModel { export class TeamMembersViewModel {
@ApiProperty({ type: [TeamMemberViewModel] }) @ApiProperty({ type: [TeamMemberViewModel] })
members: TeamMemberViewModel[]; members!: TeamMemberViewModel[];
@ApiProperty() @ApiProperty()
totalCount: number; totalCount!: number;
@ApiProperty() @ApiProperty()
ownerCount: number; ownerCount!: number;
@ApiProperty() @ApiProperty()
managerCount: number; managerCount!: number;
@ApiProperty() @ApiProperty()
memberCount: number; memberCount!: number;
} }
export class TeamJoinRequestViewModel { export class TeamJoinRequestViewModel {
@ApiProperty() @ApiProperty()
requestId: string; requestId!: string;
@ApiProperty() @ApiProperty()
driverId: string; driverId!: string;
@ApiProperty() @ApiProperty()
driverName: string; driverName!: string;
@ApiProperty() @ApiProperty()
teamId: string; teamId!: string;
@ApiProperty() @ApiProperty()
status: 'pending' | 'approved' | 'rejected'; status!: 'pending' | 'approved' | 'rejected';
@ApiProperty() @ApiProperty()
requestedAt: string; requestedAt!: string;
@ApiProperty() @ApiProperty()
avatarUrl: string; avatarUrl!: string;
} }
export class TeamJoinRequestsViewModel { export class TeamJoinRequestsViewModel {
@ApiProperty({ type: [TeamJoinRequestViewModel] }) @ApiProperty({ type: [TeamJoinRequestViewModel] })
requests: TeamJoinRequestViewModel[]; requests!: TeamJoinRequestViewModel[];
@ApiProperty() @ApiProperty()
pendingCount: number; pendingCount!: number;
@ApiProperty() @ApiProperty()
totalCount: number; totalCount!: number;
} }
export class CreateTeamInput { export class CreateTeamInput {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name: string; name!: string;
@ApiProperty() @ApiProperty()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
tag: string; tag!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -218,17 +218,17 @@ export class CreateTeamInput {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
ownerId: string; ownerId!: string;
} }
export class CreateTeamOutput { export class CreateTeamOutput {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
teamId: string; teamId!: string;
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
} }
export class UpdateTeamInput { export class UpdateTeamInput {
@@ -254,19 +254,19 @@ export class UpdateTeamInput {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
updatedBy: string; updatedBy!: string;
} }
export class UpdateTeamOutput { export class UpdateTeamOutput {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
} }
export class ApproveTeamJoinRequestInput { export class ApproveTeamJoinRequestInput {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
requestId: string; requestId!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -277,13 +277,13 @@ export class ApproveTeamJoinRequestInput {
export class ApproveTeamJoinRequestOutput { export class ApproveTeamJoinRequestOutput {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
} }
export class RejectTeamJoinRequestInput { export class RejectTeamJoinRequestInput {
@ApiProperty() @ApiProperty()
@IsString() @IsString()
requestId: string; requestId!: string;
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@@ -294,5 +294,5 @@ export class RejectTeamJoinRequestInput {
export class RejectTeamJoinRequestOutput { export class RejectTeamJoinRequestOutput {
@ApiProperty() @ApiProperty()
@IsBoolean() @IsBoolean()
success: boolean; success!: boolean;
} }

View File

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

View File

@@ -16,4 +16,11 @@ export class TeamMembershipPresenter implements UseCaseOutputPort<GetTeamMembers
getResponseModel(): GetTeamMembershipOutputDTO | null { getResponseModel(): GetTeamMembershipOutputDTO | null {
return this.result; 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'; import type { GetTeamsLeaderboardOutputDTO } from '../dtos/GetTeamsLeaderboardOutputDTO';
export class TeamsLeaderboardPresenter { export class TeamsLeaderboardPresenter implements UseCaseOutputPort<GetTeamsLeaderboardResult> {
private result: GetTeamsLeaderboardOutputDTO | null = null; private result: GetTeamsLeaderboardOutputDTO | null = null;
reset() { reset() {
this.result = null; this.result = null;
} }
async present(outputPort: TeamsLeaderboardOutputPort): Promise<void> { present(result: GetTeamsLeaderboardResult): void {
this.result = { this.result = {
teams: outputPort.teams.map(team => ({ teams: result.items.map(item => ({
id: team.id, id: item.team.id,
name: team.name, name: item.team.name.toString(),
memberCount: team.memberCount, memberCount: item.memberCount,
rating: team.rating, rating: item.rating,
totalWins: team.totalWins, totalWins: item.totalWins,
totalRaces: team.totalRaces, totalRaces: item.totalRaces,
performanceLevel: team.performanceLevel, performanceLevel: item.performanceLevel,
isRecruiting: team.isRecruiting, isRecruiting: item.isRecruiting,
createdAt: team.createdAt.toISOString(), createdAt: item.createdAt.toISOString(),
description: team.description, description: item.team.description?.toString() || '',
specialization: team.specialization,
region: team.region,
languages: team.languages,
})), })),
recruitingCount: outputPort.recruitingCount, recruitingCount: result.recruitingCount,
groupsBySkillLevel: { groupsBySkillLevel: {
beginner: outputPort.groupsBySkillLevel.beginner.map(team => ({ beginner: result.groupsBySkillLevel.beginner.map(item => ({
id: team.id, id: item.team.id,
name: team.name, name: item.team.name.toString(),
memberCount: team.memberCount, memberCount: item.memberCount,
rating: team.rating, rating: item.rating,
totalWins: team.totalWins, totalWins: item.totalWins,
totalRaces: team.totalRaces, totalRaces: item.totalRaces,
performanceLevel: team.performanceLevel, performanceLevel: item.performanceLevel,
isRecruiting: team.isRecruiting, isRecruiting: item.isRecruiting,
createdAt: team.createdAt.toISOString(), createdAt: item.createdAt.toISOString(),
description: team.description, description: item.team.description?.toString() || '',
specialization: team.specialization,
region: team.region,
languages: team.languages,
})), })),
intermediate: outputPort.groupsBySkillLevel.intermediate.map(team => ({ intermediate: result.groupsBySkillLevel.intermediate.map(item => ({
id: team.id, id: item.team.id,
name: team.name, name: item.team.name.toString(),
memberCount: team.memberCount, memberCount: item.memberCount,
rating: team.rating, rating: item.rating,
totalWins: team.totalWins, totalWins: item.totalWins,
totalRaces: team.totalRaces, totalRaces: item.totalRaces,
performanceLevel: team.performanceLevel, performanceLevel: item.performanceLevel,
isRecruiting: team.isRecruiting, isRecruiting: item.isRecruiting,
createdAt: team.createdAt.toISOString(), createdAt: item.createdAt.toISOString(),
description: team.description, description: item.team.description?.toString() || '',
specialization: team.specialization,
region: team.region,
languages: team.languages,
})), })),
advanced: outputPort.groupsBySkillLevel.advanced.map(team => ({ advanced: result.groupsBySkillLevel.advanced.map(item => ({
id: team.id, id: item.team.id,
name: team.name, name: item.team.name.toString(),
memberCount: team.memberCount, memberCount: item.memberCount,
rating: team.rating, rating: item.rating,
totalWins: team.totalWins, totalWins: item.totalWins,
totalRaces: team.totalRaces, totalRaces: item.totalRaces,
performanceLevel: team.performanceLevel, performanceLevel: item.performanceLevel,
isRecruiting: team.isRecruiting, isRecruiting: item.isRecruiting,
createdAt: team.createdAt.toISOString(), createdAt: item.createdAt.toISOString(),
description: team.description, description: item.team.description?.toString() || '',
specialization: team.specialization,
region: team.region,
languages: team.languages,
})), })),
pro: outputPort.groupsBySkillLevel.pro.map(team => ({ pro: result.groupsBySkillLevel.pro.map(item => ({
id: team.id, id: item.team.id,
name: team.name, name: item.team.name.toString(),
memberCount: team.memberCount, memberCount: item.memberCount,
rating: team.rating, rating: item.rating,
totalWins: team.totalWins, totalWins: item.totalWins,
totalRaces: team.totalRaces, totalRaces: item.totalRaces,
performanceLevel: team.performanceLevel, performanceLevel: item.performanceLevel,
isRecruiting: team.isRecruiting, isRecruiting: item.isRecruiting,
createdAt: team.createdAt.toISOString(), createdAt: item.createdAt.toISOString(),
description: team.description, description: item.team.description?.toString() || '',
specialization: team.specialization,
region: team.region,
languages: team.languages,
})), })),
}, },
topTeams: outputPort.topTeams.map(team => ({ topTeams: result.topItems.map(item => ({
id: team.id, id: item.team.id,
name: team.name, name: item.team.name.toString(),
memberCount: team.memberCount, memberCount: item.memberCount,
rating: team.rating, rating: item.rating,
totalWins: team.totalWins, totalWins: item.totalWins,
totalRaces: team.totalRaces, totalRaces: item.totalRaces,
performanceLevel: team.performanceLevel, performanceLevel: item.performanceLevel,
isRecruiting: team.isRecruiting, isRecruiting: item.isRecruiting,
createdAt: team.createdAt.toISOString(), createdAt: item.createdAt.toISOString(),
description: team.description, description: item.team.description?.toString() || '',
specialization: team.specialization,
region: team.region,
languages: team.languages,
})), })),
}; };
} }

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": { "compilerOptions": {
"target": "es2017", "baseUrl": ".",
"module": "commonjs", "declaration": true,
"lib": ["es2022", "dom"], "declarationMap": true,
"moduleResolution": "node",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"incremental": true,
"lib": [
"es2022",
"dom"
],
"module": "commonjs",
"moduleResolution": "node",
"noEmit": false, "noEmit": false,
"noEmitOnError": true, "noEmitOnError": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"declaration": true, "outDir": "./dist",
"declarationMap": true, "paths": {
"@/*": [
"./*"
],
"@adapters/*": [
"../../adapters/*"
],
"@core/*": [
"../../core/*"
],
"@testing/*": [
"../../testing/*"
]
},
"removeComments": true, "removeComments": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "strict": true,
"incremental": true, "strictNullChecks": true,
"baseUrl": ".", "target": "es2017",
"types": ["node", "express", "vitest/globals"], "types": [
"paths": { "node",
"@/*": ["./*"], "express",
"@core/*": ["../../core/*"], "vitest/globals"
"@adapters/*": ["../../adapters/*"], ]
"@testing/*": ["../../testing/*"]
}
}, },
"include": ["src/**/*", "../../adapters/bootstrap/EnsureInitialData.ts"], "exclude": [
"exclude": ["node_modules", "dist", "**/*.mock.ts"] "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 { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application'; import type { UseCaseOutputPort } from '@core/shared/application';
import type { Driver } from '../../domain/entities/Driver'; import type { Driver } from '../../domain/entities/Driver';
import type { Penalty } from '../../domain/entities/penalty/Penalty';
export type GetRacePenaltiesInput = { export type GetRacePenaltiesInput = {
raceId: string; raceId: string;
}; };
export type GetRacePenaltiesResult = { export type GetRacePenaltiesResult = {
penalties: unknown[]; penalties: Penalty[];
drivers: Driver[]; drivers: Driver[];
}; };