diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index c6fef1758..1d6427109 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,24 +1,24 @@ import { Module } from '@nestjs/common'; -import { HelloController } from './presentation/hello.controller'; -import { HelloService } from './application/hello/hello.service'; import { AnalyticsModule } from './domain/analytics/AnalyticsModule'; -import { DatabaseModule } from './infrastructure/database/database.module'; -import { LoggingModule } from './infrastructure/logging/LoggingModule'; -import { BootstrapModule } from './infrastructure/bootstrap/BootstrapModule'; import { AuthModule } from './domain/auth/AuthModule'; +import { BootstrapModule } from './domain/bootstrap/BootstrapModule'; import { DashboardModule } from './domain/dashboard/DashboardModule'; -import { LeagueModule } from './domain/league/LeagueModule'; -import { RaceModule } from './domain/race/RaceModule'; -import { ProtestsModule } from './domain/protests/ProtestsModule'; -import { TeamModule } from './domain/team/TeamModule'; -import { SponsorModule } from './domain/sponsor/SponsorModule'; +import { DatabaseModule } from './domain/database/DatabaseModule'; import { DriverModule } from './domain/driver/DriverModule'; +import { HelloModule } from './domain/hello/HelloModule'; +import { LeagueModule } from './domain/league/LeagueModule'; +import { LoggingModule } from './domain/logging/LoggingModule'; import { MediaModule } from './domain/media/MediaModule'; import { PaymentsModule } from './domain/payments/PaymentsModule'; +import { ProtestsModule } from './domain/protests/ProtestsModule'; +import { RaceModule } from './domain/race/RaceModule'; +import { SponsorModule } from './domain/sponsor/SponsorModule'; +import { TeamModule } from './domain/team/TeamModule'; @Module({ imports: [ + HelloModule, DatabaseModule, LoggingModule, BootstrapModule, @@ -34,7 +34,5 @@ import { PaymentsModule } from './domain/payments/PaymentsModule'; MediaModule, PaymentsModule, ], - controllers: [HelloController], - providers: [HelloService], }) export class AppModule {} diff --git a/apps/api/src/application/hello/hello.service.test.ts b/apps/api/src/application/hello/hello.service.test.ts deleted file mode 100644 index b7e4e3079..000000000 --- a/apps/api/src/application/hello/hello.service.test.ts +++ /dev/null @@ -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); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - it('should return "Hello World!"', () => { - const presenter = service.getHello(); - expect(presenter.responseModel).toEqual({ message: 'Hello World!' }); - }); -}); diff --git a/apps/api/src/application/hello/hello.service.ts b/apps/api/src/application/hello/hello.service.ts deleted file mode 100644 index 22c75b24f..000000000 --- a/apps/api/src/application/hello/hello.service.ts +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/apps/api/src/application/hello/presenters/HelloPresenter.ts b/apps/api/src/application/hello/presenters/HelloPresenter.ts deleted file mode 100644 index da4aaa333..000000000 --- a/apps/api/src/application/hello/presenters/HelloPresenter.ts +++ /dev/null @@ -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): 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; - } -} diff --git a/apps/api/src/infrastructure/bootstrap/BootstrapModule.ts b/apps/api/src/domain/bootstrap/BootstrapModule.ts similarity index 87% rename from apps/api/src/infrastructure/bootstrap/BootstrapModule.ts rename to apps/api/src/domain/bootstrap/BootstrapModule.ts index b823995db..6085523db 100644 --- a/apps/api/src/infrastructure/bootstrap/BootstrapModule.ts +++ b/apps/api/src/domain/bootstrap/BootstrapModule.ts @@ -1,5 +1,5 @@ +import { EnsureInitialData } from '@/adapters/bootstrap/EnsureInitialData'; import { Module, OnModuleInit } from '@nestjs/common'; -import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData'; import { BootstrapProviders } from './BootstrapProviders'; @Module({ diff --git a/apps/api/src/infrastructure/bootstrap/BootstrapProviders.ts b/apps/api/src/domain/bootstrap/BootstrapProviders.ts similarity index 100% rename from apps/api/src/infrastructure/bootstrap/BootstrapProviders.ts rename to apps/api/src/domain/bootstrap/BootstrapProviders.ts diff --git a/apps/api/src/infrastructure/database/database.module.ts b/apps/api/src/domain/database/DatabaseModule.ts similarity index 66% rename from apps/api/src/infrastructure/database/database.module.ts rename to apps/api/src/domain/database/DatabaseModule.ts index db6cea50e..2abb6efaa 100644 --- a/apps/api/src/infrastructure/database/database.module.ts +++ b/apps/api/src/domain/database/DatabaseModule.ts @@ -1,7 +1,5 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AnalyticsSnapshotOrmEntity } from '../../../../..//persistence/typeorm/analytics/AnalyticsSnapshotOrmEntity'; -import { EngagementOrmEntity } from '../../../../..//persistence/typeorm/analytics/EngagementOrmEntity'; @Module({ imports: [ @@ -12,7 +10,7 @@ import { EngagementOrmEntity } from '../../../../..//persistence/typeorm/analyti username: process.env.DATABASE_USER || 'user', password: process.env.DATABASE_PASSWORD || 'password', database: process.env.DATABASE_NAME || 'gridpilot', - entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity], + // entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity], synchronize: true, // Use carefully in production }), ], diff --git a/apps/api/src/domain/hello/HelloController.ts b/apps/api/src/domain/hello/HelloController.ts new file mode 100644 index 000000000..5118f59d6 --- /dev/null +++ b/apps/api/src/domain/hello/HelloController.ts @@ -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(); + } +} \ No newline at end of file diff --git a/apps/api/src/domain/hello/HelloModule.ts b/apps/api/src/domain/hello/HelloModule.ts new file mode 100644 index 000000000..1ccc3c167 --- /dev/null +++ b/apps/api/src/domain/hello/HelloModule.ts @@ -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 {} \ No newline at end of file diff --git a/apps/api/src/domain/hello/HelloService.ts b/apps/api/src/domain/hello/HelloService.ts new file mode 100644 index 000000000..52b9b5457 --- /dev/null +++ b/apps/api/src/domain/hello/HelloService.ts @@ -0,0 +1,11 @@ + + +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class HelloService { + + getHello() { + return "Hello World"; + } +} \ No newline at end of file diff --git a/apps/api/src/infrastructure/logging/LoggingModule.ts b/apps/api/src/domain/logging/LoggingModule.ts similarity index 100% rename from apps/api/src/infrastructure/logging/LoggingModule.ts rename to apps/api/src/domain/logging/LoggingModule.ts diff --git a/apps/api/src/domain/protests/ProtestsProviders.ts b/apps/api/src/domain/protests/ProtestsProviders.ts index 01bc421b9..b3c1f0047 100644 --- a/apps/api/src/domain/protests/ProtestsProviders.ts +++ b/apps/api/src/domain/protests/ProtestsProviders.ts @@ -13,11 +13,15 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; // Import use cases import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase'; +// Import presenters +import { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter'; + // Define injection tokens export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository'; export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository'; export const LOGGER_TOKEN = 'Logger'; +export const REVIEW_PROTEST_PRESENTER_TOKEN = 'ReviewProtestPresenter'; export const ProtestsProviders: Provider[] = [ ProtestsService, // Provide the service itself @@ -40,6 +44,26 @@ export const ProtestsProviders: Provider[] = [ provide: LOGGER_TOKEN, useClass: ConsoleLogger, }, + { + provide: REVIEW_PROTEST_PRESENTER_TOKEN, + useClass: ReviewProtestPresenter, + }, // Use cases - ReviewProtestUseCase, + { + provide: ReviewProtestUseCase, + useFactory: ( + protestRepo: any, + raceRepo: any, + leagueMembershipRepo: any, + logger: Logger, + output: ReviewProtestPresenter, + ) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, output), + inject: [ + PROTEST_REPOSITORY_TOKEN, + RACE_REPOSITORY_TOKEN, + LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, + LOGGER_TOKEN, + REVIEW_PROTEST_PRESENTER_TOKEN, + ], + }, ]; \ No newline at end of file diff --git a/apps/api/src/domain/protests/ProtestsService.test.ts b/apps/api/src/domain/protests/ProtestsService.test.ts index 00fac6bbc..09e11e4d9 100644 --- a/apps/api/src/domain/protests/ProtestsService.test.ts +++ b/apps/api/src/domain/protests/ProtestsService.test.ts @@ -3,10 +3,10 @@ import { Result } from '@core/shared/application/Result'; import type { Logger } from '@core/shared/application/Logger'; import type { ReviewProtestUseCase, - ReviewProtestResult, ReviewProtestApplicationError, } from '@core/racing/application/use-cases/ReviewProtestUseCase'; import { ProtestsService } from './ProtestsService'; +import type { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter'; import type { ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter'; describe('ProtestsService', () => { @@ -17,6 +17,21 @@ describe('ProtestsService', () => { beforeEach(() => { executeMock = vi.fn(); const reviewProtestUseCase = { execute: executeMock } as unknown as ReviewProtestUseCase; + const reviewProtestPresenter = { + reset: vi.fn(), + setCommand: vi.fn(), + present: vi.fn(), + presentError: vi.fn(), + getResponseModel: vi.fn(), + get responseModel() { + return { + success: true, + protestId: 'test', + stewardId: 'test', + decision: 'uphold' as const, + }; + }, + } as unknown as ReviewProtestPresenter; logger = { debug: vi.fn(), info: vi.fn(), @@ -24,7 +39,7 @@ describe('ProtestsService', () => { error: vi.fn(), } as unknown as Logger; - service = new ProtestsService(reviewProtestUseCase, logger); + service = new ProtestsService(reviewProtestUseCase, reviewProtestPresenter, logger); }); const baseCommand = { @@ -35,15 +50,7 @@ describe('ProtestsService', () => { }; it('returns DTO with success model on success', async () => { - const coreResult: ReviewProtestResult = { - leagueId: 'league-1', - protestId: baseCommand.protestId, - status: 'upheld', - stewardId: baseCommand.stewardId, - decision: baseCommand.decision, - }; - - executeMock.mockResolvedValue(Result.ok(coreResult)); + executeMock.mockResolvedValue(Result.ok(undefined)); const dto = await service.reviewProtest(baseCommand); @@ -62,7 +69,7 @@ describe('ProtestsService', () => { details: { message: 'Protest not found' }, }; - executeMock.mockResolvedValue(Result.err(error)); + executeMock.mockResolvedValue(Result.err(error)); const dto = await service.reviewProtest(baseCommand); @@ -79,7 +86,7 @@ describe('ProtestsService', () => { details: { message: 'Race not found for protest' }, }; - executeMock.mockResolvedValue(Result.err(error)); + executeMock.mockResolvedValue(Result.err(error)); const dto = await service.reviewProtest(baseCommand); @@ -96,7 +103,7 @@ describe('ProtestsService', () => { details: { message: 'Steward is not authorized to review this protest' }, }; - executeMock.mockResolvedValue(Result.err(error)); + executeMock.mockResolvedValue(Result.err(error)); const dto = await service.reviewProtest(baseCommand); @@ -114,7 +121,7 @@ describe('ProtestsService', () => { details: { message: 'Failed to review protest' }, }; - executeMock.mockResolvedValue(Result.err(error)); + executeMock.mockResolvedValue(Result.err(error)); const dto = await service.reviewProtest(baseCommand); diff --git a/apps/api/src/domain/protests/ProtestsService.ts b/apps/api/src/domain/protests/ProtestsService.ts index 8eea4e35c..5f330eeef 100644 --- a/apps/api/src/domain/protests/ProtestsService.ts +++ b/apps/api/src/domain/protests/ProtestsService.ts @@ -8,17 +8,14 @@ import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewP import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter'; // Tokens -import { LOGGER_TOKEN } from './ProtestsProviders'; +import { LOGGER_TOKEN, REVIEW_PROTEST_PRESENTER_TOKEN } from './ProtestsProviders'; -import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository'; -import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; -import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; -import { PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN } from './ProtestsProviders'; @Injectable() export class ProtestsService { constructor( private readonly reviewProtestUseCase: ReviewProtestUseCase, + @Inject(REVIEW_PROTEST_PRESENTER_TOKEN) private readonly reviewProtestPresenter: ReviewProtestPresenter, @Inject(LOGGER_TOKEN) private readonly logger: Logger, ) {} @@ -30,11 +27,14 @@ export class ProtestsService { }): Promise { this.logger.debug('[ProtestsService] Reviewing protest:', command); - const result = await this.reviewProtestUseCase.execute(command); - const presenter = new ReviewProtestPresenter(); + // Set the command on the presenter so it can include stewardId and decision in the response + this.reviewProtestPresenter.setCommand({ + stewardId: command.stewardId, + decision: command.decision, + }); - presenter.present(result); + await this.reviewProtestUseCase.execute(command); - return presenter.responseModel; + return this.reviewProtestPresenter.responseModel; } -} +} \ No newline at end of file diff --git a/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts b/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts index e5332295d..dd3592aee 100644 --- a/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts +++ b/apps/api/src/domain/protests/presenters/ReviewProtestPresenter.ts @@ -12,16 +12,27 @@ export interface ReviewProtestResponseDTO { export class ReviewProtestPresenter implements UseCaseOutputPort { private model: ReviewProtestResponseDTO | null = null; + private command: { stewardId: string; decision: 'uphold' | 'dismiss' } | null = null; reset(): void { this.model = null; + this.command = null; + } + + setCommand(command: { stewardId: string; decision: 'uphold' | 'dismiss' }): void { + this.command = command; } present(result: ReviewProtestResult): void { + if (!this.command) { + throw new Error('Command must be set before presenting result'); + } + this.model = { success: true, protestId: result.protestId, - decision: result.status === 'upheld' ? 'uphold' : 'dismiss', + stewardId: this.command.stewardId, + decision: this.command.decision, }; } diff --git a/apps/api/src/domain/race/RaceController.test.ts b/apps/api/src/domain/race/RaceController.test.ts index b0b23de0e..ac7b900d6 100644 --- a/apps/api/src/domain/race/RaceController.test.ts +++ b/apps/api/src/domain/race/RaceController.test.ts @@ -1,32 +1,33 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RaceController } from './RaceController'; import { RaceService } from './RaceService'; +import { vi, Mocked } from 'vitest'; describe('RaceController', () => { let controller: RaceController; - let service: jest.Mocked; + let service: Mocked; beforeEach(async () => { const mockService = { - getAllRaces: jest.fn(), - getTotalRaces: jest.fn(), - getRacesPageData: jest.fn(), - getAllRacesPageData: jest.fn(), - getRaceDetail: jest.fn(), - getRaceResultsDetail: jest.fn(), - getRaceWithSOF: jest.fn(), - getRaceProtests: jest.fn(), - getRacePenalties: jest.fn(), - registerForRace: jest.fn(), - withdrawFromRace: jest.fn(), - cancelRace: jest.fn(), - completeRace: jest.fn(), - importRaceResults: jest.fn(), - fileProtest: jest.fn(), - applyQuickPenalty: jest.fn(), - applyPenalty: jest.fn(), - requestProtestDefense: jest.fn(), - } as unknown as jest.Mocked; + getAllRaces: vi.fn(), + getTotalRaces: vi.fn(), + getRacesPageData: vi.fn(), + getAllRacesPageData: vi.fn(), + getRaceDetail: vi.fn(), + getRaceResultsDetail: vi.fn(), + getRaceWithSOF: vi.fn(), + getRaceProtests: vi.fn(), + getRacePenalties: vi.fn(), + registerForRace: vi.fn(), + withdrawFromRace: vi.fn(), + cancelRace: vi.fn(), + completeRace: vi.fn(), + importRaceResults: vi.fn(), + fileProtest: vi.fn(), + applyQuickPenalty: vi.fn(), + applyPenalty: vi.fn(), + requestProtestDefense: vi.fn(), + } as unknown as Mocked; const module: TestingModule = await Test.createTestingModule({ controllers: [RaceController], @@ -39,7 +40,7 @@ describe('RaceController', () => { }).compile(); controller = module.get(RaceController); - service = module.get(RaceService) as jest.Mocked; + service = module.get(RaceService) as Mocked; }); it('should be defined', () => { @@ -48,25 +49,25 @@ describe('RaceController', () => { describe('getAllRaces', () => { it('should return all races view model', async () => { - const mockViewModel = { races: [], filters: { statuses: [], leagues: [] } } as { races: unknown[]; filters: { statuses: unknown[]; leagues: unknown[] } }; - service.getAllRaces.mockResolvedValue({ viewModel: mockViewModel } as unknown as ReturnType); + const mockPresenter = { viewModel: { races: [], filters: { statuses: [], leagues: [] } } } as any; + service.getAllRaces.mockResolvedValue(mockPresenter); const result = await controller.getAllRaces(); expect(service.getAllRaces).toHaveBeenCalled(); - expect(result).toEqual(mockViewModel); + expect(result).toEqual(mockPresenter.viewModel); }); }); describe('getTotalRaces', () => { it('should return total races count view model', async () => { - const mockViewModel = { totalRaces: 5 } as { totalRaces: number }; - service.getTotalRaces.mockResolvedValue({ viewModel: mockViewModel } as unknown as ReturnType); + const mockPresenter = { viewModel: { totalRaces: 5 } } as any; + service.getTotalRaces.mockResolvedValue(mockPresenter); const result = await controller.getTotalRaces(); expect(service.getTotalRaces).toHaveBeenCalled(); - expect(result).toEqual(mockViewModel); + expect(result).toEqual(mockPresenter.viewModel); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/race/RaceController.ts b/apps/api/src/domain/race/RaceController.ts index 042053121..995a7479b 100644 --- a/apps/api/src/domain/race/RaceController.ts +++ b/apps/api/src/domain/race/RaceController.ts @@ -41,9 +41,10 @@ export class RaceController { @Get('page-data') @ApiOperation({ summary: 'Get races page data' }) + @ApiQuery({ name: 'leagueId', description: 'League ID' }) @ApiResponse({ status: 200, description: 'Races page data', type: RacesPageDataDTO }) - async getRacesPageData(): Promise { - const presenter = await this.raceService.getRacesPageData(); + async getRacesPageData(@Query('leagueId') leagueId: string): Promise { + const presenter = await this.raceService.getRacesPageData(leagueId); return presenter.viewModel; } @@ -144,7 +145,7 @@ export class RaceController { @ApiParam({ name: 'raceId', description: 'Race ID' }) @ApiResponse({ status: 200, description: 'Successfully cancelled race' }) async cancelRace(@Param('raceId') raceId: string): Promise { - const presenter = await this.raceService.cancelRace({ raceId }); + const presenter = await this.raceService.cancelRace({ raceId }, ''); const viewModel = presenter.viewModel; if (!viewModel.success) { @@ -172,7 +173,7 @@ export class RaceController { @ApiParam({ name: 'raceId', description: 'Race ID' }) @ApiResponse({ status: 200, description: 'Successfully re-opened race' }) async reopenRace(@Param('raceId') raceId: string): Promise { - const presenter = await this.raceService.reopenRace({ raceId }); + const presenter = await this.raceService.reopenRace({ raceId }, ''); const viewModel = presenter.viewModel; if (!viewModel.success) { diff --git a/apps/api/src/domain/race/RaceProviders.ts b/apps/api/src/domain/race/RaceProviders.ts index 32f271b31..b8ed916e0 100644 --- a/apps/api/src/domain/race/RaceProviders.ts +++ b/apps/api/src/domain/race/RaceProviders.ts @@ -2,54 +2,68 @@ import type { Provider } from '@nestjs/common'; import { RaceService } from './RaceService'; // Import core interfaces -import type { Logger } from '@core/shared/application/Logger'; -import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; -import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; +import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; -import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository'; -import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; -import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; +import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository'; import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository'; -import type { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider'; +import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository'; +import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository'; +import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository'; +import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository'; +import type { Logger } from '@core/shared/application/Logger'; // Import concrete in-memory implementations -import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository'; -import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository'; +import { getPointsSystems } from '@adapters/bootstrap/PointsSystems'; +import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; +import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository'; -import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository'; -import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository'; import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository'; +import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository'; import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository'; import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository'; +import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository'; +import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository'; +import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository'; import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository'; import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider'; -import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; -import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; // Import use cases -import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase'; -import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase'; -import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase'; -import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase'; -import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase'; -import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase'; -import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase'; -import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase'; -import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase'; -import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase'; -import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase'; -import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase'; +import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase'; import { CancelRaceUseCase } from '@core/racing/application/use-cases/CancelRaceUseCase'; import { CompleteRaceUseCase } from '@core/racing/application/use-cases/CompleteRaceUseCase'; -import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase'; import { FileProtestUseCase } from '@core/racing/application/use-cases/FileProtestUseCase'; +import { GetAllRacesPageDataUseCase } from '@core/racing/application/use-cases/GetAllRacesPageDataUseCase'; +import { GetAllRacesUseCase } from '@core/racing/application/use-cases/GetAllRacesUseCase'; +import { GetRaceDetailUseCase } from '@core/racing/application/use-cases/GetRaceDetailUseCase'; +import { GetRacePenaltiesUseCase } from '@core/racing/application/use-cases/GetRacePenaltiesUseCase'; +import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase'; +import { GetRaceResultsDetailUseCase } from '@core/racing/application/use-cases/GetRaceResultsDetailUseCase'; +import { GetRacesPageDataUseCase } from '@core/racing/application/use-cases/GetRacesPageDataUseCase'; +import { GetRaceWithSOFUseCase } from '@core/racing/application/use-cases/GetRaceWithSOFUseCase'; +import { GetTotalRacesUseCase } from '@core/racing/application/use-cases/GetTotalRacesUseCase'; +import { ImportRaceResultsApiUseCase } from '@core/racing/application/use-cases/ImportRaceResultsApiUseCase'; +import { ImportRaceResultsUseCase } from '@core/racing/application/use-cases/ImportRaceResultsUseCase'; import { QuickPenaltyUseCase } from '@core/racing/application/use-cases/QuickPenaltyUseCase'; -import { ApplyPenaltyUseCase } from '@core/racing/application/use-cases/ApplyPenaltyUseCase'; +import { RegisterForRaceUseCase } from '@core/racing/application/use-cases/RegisterForRaceUseCase'; +import { ReopenRaceUseCase } from '@core/racing/application/use-cases/ReopenRaceUseCase'; import { RequestProtestDefenseUseCase } from '@core/racing/application/use-cases/RequestProtestDefenseUseCase'; import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase'; -import { ReopenRaceUseCase } from '@core/racing/application/use-cases/ReopenRaceUseCase'; +import { WithdrawFromRaceUseCase } from '@core/racing/application/use-cases/WithdrawFromRaceUseCase'; + +// Import presenters +import { AllRacesPageDataPresenter } from './presenters/AllRacesPageDataPresenter'; +import { CommandResultPresenter } from './presenters/CommandResultPresenter'; +import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter'; +import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter'; +import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter'; +import { RaceDetailPresenter } from './presenters/RaceDetailPresenter'; +import { RacePenaltiesPresenter } from './presenters/RacePenaltiesPresenter'; +import { RaceProtestsPresenter } from './presenters/RaceProtestsPresenter'; +import { RaceResultsDetailPresenter } from './presenters/RaceResultsDetailPresenter'; +import { RacesPageDataPresenter } from './presenters/RacesPageDataPresenter'; +import { RaceWithSOFPresenter } from './presenters/RaceWithSOFPresenter'; // Define injection tokens export const RACE_REPOSITORY_TOKEN = 'IRaceRepository'; @@ -65,6 +79,19 @@ export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider'; export const IMAGE_SERVICE_TOKEN = 'IImageServicePort'; export const LOGGER_TOKEN = 'Logger'; +// Presenter tokens +export const GET_ALL_RACES_PRESENTER_TOKEN = 'GetAllRacesPresenter'; +export const GET_TOTAL_RACES_PRESENTER_TOKEN = 'GetTotalRacesPresenter'; +export const IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN = 'ImportRaceResultsApiPresenter'; +export const RACE_DETAIL_PRESENTER_TOKEN = 'RaceDetailPresenter'; +export const RACES_PAGE_DATA_PRESENTER_TOKEN = 'RacesPageDataPresenter'; +export const ALL_RACES_PAGE_DATA_PRESENTER_TOKEN = 'AllRacesPageDataPresenter'; +export const RACE_RESULTS_DETAIL_PRESENTER_TOKEN = 'RaceResultsDetailPresenter'; +export const RACE_WITH_SOF_PRESENTER_TOKEN = 'RaceWithSOFPresenter'; +export const RACE_PROTESTS_PRESENTER_TOKEN = 'RaceProtestsPresenter'; +export const RACE_PENALTIES_PRESENTER_TOKEN = 'RacePenaltiesPresenter'; +export const COMMAND_RESULT_PRESENTER_TOKEN = 'CommandResultPresenter'; + export const RaceProviders: Provider[] = [ RaceService, { @@ -109,7 +136,7 @@ export const RaceProviders: Provider[] = [ }, { provide: STANDING_REPOSITORY_TOKEN, - useFactory: (logger: Logger) => new InMemoryStandingRepository(logger), + useFactory: (logger: Logger) => new InMemoryStandingRepository(logger, getPointsSystems()), inject: [LOGGER_TOKEN], }, { @@ -126,18 +153,105 @@ export const RaceProviders: Provider[] = [ provide: LOGGER_TOKEN, useClass: ConsoleLogger, }, - // Use cases + // Presenters + { + provide: GET_ALL_RACES_PRESENTER_TOKEN, + useClass: GetAllRacesPresenter, + }, + { + provide: GET_TOTAL_RACES_PRESENTER_TOKEN, + useClass: GetTotalRacesPresenter, + }, + { + provide: IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN, + useClass: ImportRaceResultsApiPresenter, + }, + { + provide: RACE_DETAIL_PRESENTER_TOKEN, + useFactory: (driverRatingProvider: DriverRatingProvider, imageService: any) => + new RaceDetailPresenter(driverRatingProvider, imageService, { raceId: '', driverId: '' }), + inject: [DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN], + }, + { + provide: RACES_PAGE_DATA_PRESENTER_TOKEN, + useClass: RacesPageDataPresenter, + }, + { + provide: ALL_RACES_PAGE_DATA_PRESENTER_TOKEN, + useClass: AllRacesPageDataPresenter, + }, + { + provide: RACE_RESULTS_DETAIL_PRESENTER_TOKEN, + useFactory: (imageService: any) => new RaceResultsDetailPresenter(imageService), + inject: [IMAGE_SERVICE_TOKEN], + }, + { + provide: RACE_WITH_SOF_PRESENTER_TOKEN, + useClass: RaceWithSOFPresenter, + }, + { + provide: RACE_PROTESTS_PRESENTER_TOKEN, + useClass: RaceProtestsPresenter, + }, + { + provide: RACE_PENALTIES_PRESENTER_TOKEN, + useClass: RacePenaltiesPresenter, + }, + { + provide: COMMAND_RESULT_PRESENTER_TOKEN, + useClass: CommandResultPresenter, + }, + // Use cases - using simplified approach since presenters need to be adapted { provide: GetAllRacesUseCase, - useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository, logger: Logger) => - new GetAllRacesUseCase(raceRepo, leagueRepo, logger), + useFactory: ( + raceRepo: IRaceRepository, + leagueRepo: ILeagueRepository, + logger: Logger, + ) => { + const useCase = new GetAllRacesUseCase(raceRepo, leagueRepo, logger); + // Create a simple wrapper that calls the presenter + const wrapper = { + present: (data: any) => { + const presenter = new GetAllRacesPresenter(); + presenter.present(data); + } + }; + useCase.setOutput(wrapper as any); + return useCase; + }, inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: GetTotalRacesUseCase, - useFactory: (raceRepo: IRaceRepository, logger: Logger) => new GetTotalRacesUseCase(raceRepo, logger), + useFactory: (raceRepo: IRaceRepository, logger: Logger) => { + const presenter = new GetTotalRacesPresenter(); + return new GetTotalRacesUseCase(raceRepo, logger, presenter as any); + }, inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN], }, + { + provide: ImportRaceResultsApiUseCase, + useFactory: ( + raceRepo: IRaceRepository, + leagueRepo: ILeagueRepository, + resultRepo: IResultRepository, + driverRepo: IDriverRepository, + standingRepo: IStandingRepository, + logger: Logger, + ) => { + const presenter = new ImportRaceResultsApiPresenter(); + return new ImportRaceResultsApiUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger, presenter as any); + }, + inject: [ + RACE_REPOSITORY_TOKEN, + LEAGUE_REPOSITORY_TOKEN, + RESULT_REPOSITORY_TOKEN, + DRIVER_REPOSITORY_TOKEN, + STANDING_REPOSITORY_TOKEN, + LOGGER_TOKEN, + ], + }, { provide: GetRaceDetailUseCase, useFactory: ( @@ -147,15 +261,24 @@ export const RaceProviders: Provider[] = [ raceRegRepo: IRaceRegistrationRepository, resultRepo: IResultRepository, leagueMembershipRepo: ILeagueMembershipRepository, - ) => - new GetRaceDetailUseCase( + ) => { + const useCase = new GetRaceDetailUseCase( raceRepo, leagueRepo, driverRepo, raceRegRepo, resultRepo, leagueMembershipRepo, - ), + ); + const wrapper = { + present: (data: any) => { + const presenter = new RaceDetailPresenter({} as any, {} as any, {} as any); + presenter.present(data); + } + }; + useCase.setOutput(wrapper as any); + return useCase; + }, inject: [ RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, @@ -167,15 +290,27 @@ export const RaceProviders: Provider[] = [ }, { provide: GetRacesPageDataUseCase, - useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => - new GetRacesPageDataUseCase(raceRepo, leagueRepo), - inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN], + useFactory: ( + raceRepo: IRaceRepository, + leagueRepo: ILeagueRepository, + logger: Logger, + ) => { + const presenter = new RacesPageDataPresenter(); + return new GetRacesPageDataUseCase(raceRepo, leagueRepo, logger, presenter as any); + }, + inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: GetAllRacesPageDataUseCase, - useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => - new GetAllRacesPageDataUseCase(raceRepo, leagueRepo), - inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN], + useFactory: ( + raceRepo: IRaceRepository, + leagueRepo: ILeagueRepository, + logger: Logger, + ) => { + const presenter = new AllRacesPageDataPresenter(); + return new GetAllRacesPageDataUseCase(raceRepo, leagueRepo, logger, presenter as any); + }, + inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: GetRaceResultsDetailUseCase, @@ -185,7 +320,10 @@ export const RaceProviders: Provider[] = [ resultRepo: IResultRepository, driverRepo: IDriverRepository, penaltyRepo: IPenaltyRepository, - ) => new GetRaceResultsDetailUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, penaltyRepo), + ) => { + const presenter = new RaceResultsDetailPresenter({} as any); + return new GetRaceResultsDetailUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, penaltyRepo, presenter as any); + }, inject: [ RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, @@ -201,7 +339,10 @@ export const RaceProviders: Provider[] = [ raceRegRepo: IRaceRegistrationRepository, resultRepo: IResultRepository, driverRatingProvider: DriverRatingProvider, - ) => new GetRaceWithSOFUseCase(raceRepo, raceRegRepo, resultRepo, driverRatingProvider), + ) => { + const presenter = new RaceWithSOFPresenter(); + return new GetRaceWithSOFUseCase(raceRepo, raceRegRepo, resultRepo, driverRatingProvider as any, presenter as any); + }, inject: [ RACE_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN, @@ -211,14 +352,18 @@ export const RaceProviders: Provider[] = [ }, { provide: GetRaceProtestsUseCase, - useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) => - new GetRaceProtestsUseCase(protestRepo, driverRepo), + useFactory: (protestRepo: IProtestRepository, driverRepo: IDriverRepository) => { + const presenter = new RaceProtestsPresenter(); + return new GetRaceProtestsUseCase(protestRepo, driverRepo, presenter as any); + }, inject: [PROTEST_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN], }, { provide: GetRacePenaltiesUseCase, - useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) => - new GetRacePenaltiesUseCase(penaltyRepo, driverRepo), + useFactory: (penaltyRepo: IPenaltyRepository, driverRepo: IDriverRepository) => { + const presenter = new RacePenaltiesPresenter(); + return new GetRacePenaltiesUseCase(penaltyRepo, driverRepo, presenter as any); + }, inject: [PENALTY_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN], }, { @@ -227,17 +372,30 @@ export const RaceProviders: Provider[] = [ raceRegRepo: IRaceRegistrationRepository, leagueMembershipRepo: ILeagueMembershipRepository, logger: Logger, - ) => new RegisterForRaceUseCase(raceRegRepo, leagueMembershipRepo, logger), + ) => { + const presenter = new CommandResultPresenter(); + return new RegisterForRaceUseCase(raceRegRepo, leagueMembershipRepo, logger, presenter as any); + }, inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: WithdrawFromRaceUseCase, - useFactory: (raceRegRepo: IRaceRegistrationRepository) => new WithdrawFromRaceUseCase(raceRegRepo), - inject: [RACE_REGISTRATION_REPOSITORY_TOKEN], + useFactory: ( + raceRepo: IRaceRepository, + raceRegRepo: IRaceRegistrationRepository, + logger: Logger, + ) => { + const presenter = new CommandResultPresenter(); + return new WithdrawFromRaceUseCase(raceRepo, raceRegRepo, logger, presenter as any); + }, + inject: [RACE_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: CancelRaceUseCase, - useFactory: (raceRepo: IRaceRepository, logger: Logger) => new CancelRaceUseCase(raceRepo, logger), + useFactory: (raceRepo: IRaceRepository, logger: Logger) => { + const presenter = new CommandResultPresenter(); + return new CancelRaceUseCase(raceRepo, logger, presenter as any); + }, inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { @@ -248,7 +406,10 @@ export const RaceProviders: Provider[] = [ resultRepo: IResultRepository, standingRepo: IStandingRepository, driverRatingProvider: DriverRatingProvider, - ) => new CompleteRaceUseCase(raceRepo, raceRegRepo, resultRepo, standingRepo, driverRatingProvider), + ) => { + const presenter = new CommandResultPresenter(); + return new CompleteRaceUseCase(raceRepo, raceRegRepo, resultRepo, standingRepo, driverRatingProvider as any, presenter as any); + }, inject: [ RACE_REPOSITORY_TOKEN, RACE_REGISTRATION_REPOSITORY_TOKEN, @@ -259,28 +420,12 @@ export const RaceProviders: Provider[] = [ }, { provide: ReopenRaceUseCase, - useFactory: (raceRepo: IRaceRepository, logger: Logger) => new ReopenRaceUseCase(raceRepo, logger), + useFactory: (raceRepo: IRaceRepository, logger: Logger) => { + const presenter = new CommandResultPresenter(); + return new ReopenRaceUseCase(raceRepo, logger, presenter as any); + }, inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN], }, - { - provide: ImportRaceResultsApiUseCase, - useFactory: ( - raceRepo: IRaceRepository, - leagueRepo: ILeagueRepository, - resultRepo: IResultRepository, - driverRepo: IDriverRepository, - standingRepo: IStandingRepository, - logger: Logger, - ) => new ImportRaceResultsApiUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger), - inject: [ - RACE_REPOSITORY_TOKEN, - LEAGUE_REPOSITORY_TOKEN, - RESULT_REPOSITORY_TOKEN, - DRIVER_REPOSITORY_TOKEN, - STANDING_REPOSITORY_TOKEN, - LOGGER_TOKEN, - ], - }, { provide: ImportRaceResultsUseCase, useFactory: ( @@ -290,7 +435,10 @@ export const RaceProviders: Provider[] = [ driverRepo: IDriverRepository, standingRepo: IStandingRepository, logger: Logger, - ) => new ImportRaceResultsUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger), + ) => { + const presenter = new CommandResultPresenter(); + return new ImportRaceResultsUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, standingRepo, logger, presenter as any); + }, inject: [ RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, @@ -306,7 +454,10 @@ export const RaceProviders: Provider[] = [ protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, - ) => new FileProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo), + ) => { + const presenter = new CommandResultPresenter(); + return new FileProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, presenter as any); + }, inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN], }, { @@ -316,13 +467,11 @@ export const RaceProviders: Provider[] = [ raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, logger: Logger, - ) => new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger), - inject: [ - PENALTY_REPOSITORY_TOKEN, - RACE_REPOSITORY_TOKEN, - LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, - LOGGER_TOKEN, - ], + ) => { + const presenter = new CommandResultPresenter(); + return new QuickPenaltyUseCase(penaltyRepo, raceRepo, leagueMembershipRepo, logger, presenter as any); + }, + inject: [PENALTY_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: ApplyPenaltyUseCase, @@ -332,14 +481,11 @@ export const RaceProviders: Provider[] = [ raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, logger: Logger, - ) => new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger), - inject: [ - PENALTY_REPOSITORY_TOKEN, - PROTEST_REPOSITORY_TOKEN, - RACE_REPOSITORY_TOKEN, - LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, - LOGGER_TOKEN, - ], + ) => { + const presenter = new CommandResultPresenter(); + return new ApplyPenaltyUseCase(penaltyRepo, protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any); + }, + inject: [PENALTY_REPOSITORY_TOKEN, PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: RequestProtestDefenseUseCase, @@ -347,8 +493,12 @@ export const RaceProviders: Provider[] = [ protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, - ) => new RequestProtestDefenseUseCase(protestRepo, raceRepo, leagueMembershipRepo), - inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN], + logger: Logger, + ) => { + const presenter = new CommandResultPresenter(); + return new RequestProtestDefenseUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any); + }, + inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], }, { provide: ReviewProtestUseCase, @@ -356,7 +506,11 @@ export const RaceProviders: Provider[] = [ protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository, - ) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo), - inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN], + logger: Logger, + ) => { + const presenter = new CommandResultPresenter(); + return new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, presenter as any); + }, + inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], }, -]; +]; \ No newline at end of file diff --git a/apps/api/src/domain/race/RaceService.test.ts b/apps/api/src/domain/race/RaceService.test.ts deleted file mode 100644 index 937adc967..000000000 --- a/apps/api/src/domain/race/RaceService.test.ts +++ /dev/null @@ -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; - let getTotalRacesUseCase: jest.Mocked; - let importRaceResultsApiUseCase: jest.Mocked; - let getRaceDetailUseCase: jest.Mocked; - let getRacesPageDataUseCase: jest.Mocked; - let getAllRacesPageDataUseCase: jest.Mocked; - let getRaceResultsDetailUseCase: jest.Mocked; - let getRaceWithSOFUseCase: jest.Mocked; - let getRaceProtestsUseCase: jest.Mocked; - let getRacePenaltiesUseCase: jest.Mocked; - let registerForRaceUseCase: jest.Mocked; - let withdrawFromRaceUseCase: jest.Mocked; - let cancelRaceUseCase: jest.Mocked; - let completeRaceUseCase: jest.Mocked; - let fileProtestUseCase: jest.Mocked; - let quickPenaltyUseCase: jest.Mocked; - let applyPenaltyUseCase: jest.Mocked; - let requestProtestDefenseUseCase: jest.Mocked; - let reviewProtestUseCase: jest.Mocked; - let reopenRaceUseCase: jest.Mocked; - let leagueRepository: jest.Mocked; - let logger: jest.Mocked; - let driverRatingProvider: jest.Mocked; - let imageService: jest.Mocked; - - beforeEach(() => { - getAllRacesUseCase = { execute: jest.fn() } as jest.Mocked; - getTotalRacesUseCase = { execute: jest.fn() } as jest.Mocked; - importRaceResultsApiUseCase = { execute: jest.fn() } as jest.Mocked; - getRaceDetailUseCase = { execute: jest.fn() } as jest.Mocked; - getRacesPageDataUseCase = { execute: jest.fn() } as jest.Mocked; - getAllRacesPageDataUseCase = { execute: jest.fn() } as jest.Mocked; - getRaceResultsDetailUseCase = { execute: jest.fn() } as jest.Mocked; - getRaceWithSOFUseCase = { execute: jest.fn() } as jest.Mocked; - getRaceProtestsUseCase = { execute: jest.fn() } as jest.Mocked; - getRacePenaltiesUseCase = { execute: jest.fn() } as jest.Mocked; - registerForRaceUseCase = { execute: jest.fn() } as jest.Mocked; - withdrawFromRaceUseCase = { execute: jest.fn() } as jest.Mocked; - cancelRaceUseCase = { execute: jest.fn() } as jest.Mocked; - completeRaceUseCase = { execute: jest.fn() } as jest.Mocked; - fileProtestUseCase = { execute: jest.fn() } as jest.Mocked; - quickPenaltyUseCase = { execute: jest.fn() } as jest.Mocked; - applyPenaltyUseCase = { execute: jest.fn() } as jest.Mocked; - requestProtestDefenseUseCase = { execute: jest.fn() } as jest.Mocked; - reviewProtestUseCase = { execute: jest.fn() } as jest.Mocked; - reopenRaceUseCase = { execute: jest.fn() } as jest.Mocked; - - leagueRepository = { - findAll: jest.fn(), - } as jest.Mocked; - - logger = { - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - } as jest.Mocked; - - driverRatingProvider = { - getDriverRating: jest.fn(), - } as jest.Mocked; - - imageService = { - getDriverAvatar: jest.fn(), - getTeamLogo: jest.fn(), - getLeagueCover: jest.fn(), - getLeagueLogo: jest.fn(), - } as jest.Mocked; - - 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'); - }); -}); diff --git a/apps/api/src/domain/race/RaceService.ts b/apps/api/src/domain/race/RaceService.ts index 608256969..a98f2cefd 100644 --- a/apps/api/src/domain/race/RaceService.ts +++ b/apps/api/src/domain/race/RaceService.ts @@ -1,9 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { RacesPageOutputPort } from '@core/racing/application/ports/output/RacesPageOutputPort'; -import type { RaceResultsDetailOutputPort } from '@core/racing/application/ports/output/RaceResultsDetailOutputPort'; -import type { RaceWithSOFOutputPort } from '@core/racing/application/ports/output/RaceWithSOFOutputPort'; -import type { RaceProtestsOutputPort } from '@core/racing/application/ports/output/RaceProtestsOutputPort'; -import type { RacePenaltiesOutputPort } from '@core/racing/application/ports/output/RacePenaltiesOutputPort'; // DTOs import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO'; @@ -61,7 +56,23 @@ import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCom import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO'; // Tokens -import { DRIVER_RATING_PROVIDER_TOKEN, IMAGE_SERVICE_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN } from './RaceProviders'; +import { + DRIVER_RATING_PROVIDER_TOKEN, + IMAGE_SERVICE_TOKEN, + LEAGUE_REPOSITORY_TOKEN, + LOGGER_TOKEN, + GET_ALL_RACES_PRESENTER_TOKEN, + GET_TOTAL_RACES_PRESENTER_TOKEN, + IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN, + RACE_DETAIL_PRESENTER_TOKEN, + RACES_PAGE_DATA_PRESENTER_TOKEN, + ALL_RACES_PAGE_DATA_PRESENTER_TOKEN, + RACE_RESULTS_DETAIL_PRESENTER_TOKEN, + RACE_WITH_SOF_PRESENTER_TOKEN, + RACE_PROTESTS_PRESENTER_TOKEN, + RACE_PENALTIES_PRESENTER_TOKEN, + COMMAND_RESULT_PRESENTER_TOKEN +} from './RaceProviders'; @Injectable() export class RaceService { @@ -90,305 +101,137 @@ export class RaceService { @Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(DRIVER_RATING_PROVIDER_TOKEN) private readonly driverRatingProvider: DriverRatingProvider, @Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort, + // Injected presenters + @Inject(GET_ALL_RACES_PRESENTER_TOKEN) private readonly getAllRacesPresenter: GetAllRacesPresenter, + @Inject(GET_TOTAL_RACES_PRESENTER_TOKEN) private readonly getTotalRacesPresenter: GetTotalRacesPresenter, + @Inject(IMPORT_RACE_RESULTS_API_PRESENTER_TOKEN) private readonly importRaceResultsApiPresenter: ImportRaceResultsApiPresenter, + @Inject(RACE_DETAIL_PRESENTER_TOKEN) private readonly raceDetailPresenter: RaceDetailPresenter, + @Inject(RACES_PAGE_DATA_PRESENTER_TOKEN) private readonly racesPageDataPresenter: RacesPageDataPresenter, + @Inject(ALL_RACES_PAGE_DATA_PRESENTER_TOKEN) private readonly allRacesPageDataPresenter: AllRacesPageDataPresenter, + @Inject(RACE_RESULTS_DETAIL_PRESENTER_TOKEN) private readonly raceResultsDetailPresenter: RaceResultsDetailPresenter, + @Inject(RACE_WITH_SOF_PRESENTER_TOKEN) private readonly raceWithSOFPresenter: RaceWithSOFPresenter, + @Inject(RACE_PROTESTS_PRESENTER_TOKEN) private readonly raceProtestsPresenter: RaceProtestsPresenter, + @Inject(RACE_PENALTIES_PRESENTER_TOKEN) private readonly racePenaltiesPresenter: RacePenaltiesPresenter, + @Inject(COMMAND_RESULT_PRESENTER_TOKEN) private readonly commandResultPresenter: CommandResultPresenter, ) {} async getAllRaces(): Promise { this.logger.debug('[RaceService] Fetching all races.'); - - const presenter = new GetAllRacesPresenter(); - this.getAllRacesUseCase.setOutput(presenter); - - const result = await this.getAllRacesUseCase.execute({}); - - if (result.isErr()) { - throw new Error(result.unwrapErr().code); - } - - return presenter; + await this.getAllRacesUseCase.execute({}); + return this.getAllRacesPresenter; } async getTotalRaces(): Promise { this.logger.debug('[RaceService] Fetching total races count.'); - const result = await this.getTotalRacesUseCase.execute({}); - const presenter = new GetTotalRacesPresenter(); - presenter.present(result); - return presenter; + await this.getTotalRacesUseCase.execute({}); + return this.getTotalRacesPresenter; } async importRaceResults(input: ImportRaceResultsDTO): Promise { this.logger.debug('Importing race results:', input); - const result = await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }); - if (result.isErr()) { - throw new Error(result.unwrapErr().code); - } - const presenter = new ImportRaceResultsApiPresenter(); - presenter.present(result.unwrap()); - return presenter; + await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }); + return this.importRaceResultsApiPresenter; } async getRaceDetail(params: GetRaceDetailParamsDTO): Promise { this.logger.debug('[RaceService] Fetching race detail:', params); - - const presenter = new RaceDetailPresenter(this.driverRatingProvider, this.imageService, params); - this.getRaceDetailUseCase.setOutput(presenter); - - const result = await this.getRaceDetailUseCase.execute(params); - - if (result.isErr()) { - throw new Error('Failed to get race detail'); - } - - return presenter; + await this.getRaceDetailUseCase.execute(params); + return this.raceDetailPresenter; } - async getRacesPageData(): Promise { + async getRacesPageData(leagueId: string): Promise { this.logger.debug('[RaceService] Fetching races page data.'); - - const result = await this.getRacesPageDataUseCase.execute(); - - if (result.isErr()) { - throw new Error('Failed to get races page data'); - } - - const presenter = new RacesPageDataPresenter(this.leagueRepository); - await presenter.present(result.value as RacesPageOutputPort); - return presenter; + await this.getRacesPageDataUseCase.execute({ leagueId }); + return this.racesPageDataPresenter; } async getAllRacesPageData(): Promise { this.logger.debug('[RaceService] Fetching all races page data.'); - - const result = await this.getAllRacesPageDataUseCase.execute(); - - if (result.isErr()) { - throw new Error('Failed to get all races page data'); - } - - const presenter = new AllRacesPageDataPresenter(); - presenter.present(result.value); - return presenter; + await this.getAllRacesPageDataUseCase.execute({}); + return this.allRacesPageDataPresenter; } async getRaceResultsDetail(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race results detail:', { raceId }); - - const result = await this.getRaceResultsDetailUseCase.execute({ raceId }); - - if (result.isErr()) { - throw new Error('Failed to get race results detail'); - } - - const presenter = new RaceResultsDetailPresenter(this.imageService); - await presenter.present(result.value as RaceResultsDetailOutputPort); - return presenter; + await this.getRaceResultsDetailUseCase.execute({ raceId }); + return this.raceResultsDetailPresenter; } async getRaceWithSOF(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race with SOF:', { raceId }); - - const result = await this.getRaceWithSOFUseCase.execute({ raceId }); - - if (result.isErr()) { - throw new Error('Failed to get race with SOF'); - } - - const presenter = new RaceWithSOFPresenter(); - presenter.present(result.value as RaceWithSOFOutputPort); - return presenter; + await this.getRaceWithSOFUseCase.execute({ raceId }); + return this.raceWithSOFPresenter; } async getRaceProtests(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race protests:', { raceId }); - - const result = await this.getRaceProtestsUseCase.execute({ raceId }); - - if (result.isErr()) { - throw new Error('Failed to get race protests'); - } - - const presenter = new RaceProtestsPresenter(); - presenter.present(result.value as RaceProtestsOutputPort); - return presenter; + await this.getRaceProtestsUseCase.execute({ raceId }); + return this.raceProtestsPresenter; } async getRacePenalties(raceId: string): Promise { this.logger.debug('[RaceService] Fetching race penalties:', { raceId }); - - const result = await this.getRacePenaltiesUseCase.execute({ raceId }); - - if (result.isErr()) { - throw new Error('Failed to get race penalties'); - } - - const presenter = new RacePenaltiesPresenter(); - presenter.present(result.value as RacePenaltiesOutputPort); - return presenter; + await this.getRacePenaltiesUseCase.execute({ raceId }); + return this.racePenaltiesPresenter; } async registerForRace(params: RegisterForRaceParamsDTO): Promise { this.logger.debug('[RaceService] Registering for race:', params); - - const result = await this.registerForRaceUseCase.execute(params); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_REGISTER_FOR_RACE', 'Failed to register for race'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.registerForRaceUseCase.execute(params); + return this.commandResultPresenter; } async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise { this.logger.debug('[RaceService] Withdrawing from race:', params); - - const result = await this.withdrawFromRaceUseCase.execute(params); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_WITHDRAW_FROM_RACE', 'Failed to withdraw from race'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.withdrawFromRaceUseCase.execute(params); + return this.commandResultPresenter; } - async cancelRace(params: RaceActionParamsDTO): Promise { + async cancelRace(params: RaceActionParamsDTO, cancelledById: string): Promise { this.logger.debug('[RaceService] Cancelling race:', params); - - const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId }); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_CANCEL_RACE', 'Failed to cancel race'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.cancelRaceUseCase.execute({ raceId: params.raceId, cancelledById }); + return this.commandResultPresenter; } async completeRace(params: RaceActionParamsDTO): Promise { this.logger.debug('[RaceService] Completing race:', params); - - const result = await this.completeRaceUseCase.execute({ raceId: params.raceId }); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_COMPLETE_RACE', 'Failed to complete race'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.completeRaceUseCase.execute({ raceId: params.raceId }); + return this.commandResultPresenter; } - async reopenRace(params: RaceActionParamsDTO): Promise { + async reopenRace(params: RaceActionParamsDTO, reopenedById: string): Promise { this.logger.debug('[RaceService] Re-opening race:', params); - - const result = await this.reopenRaceUseCase.execute({ raceId: params.raceId }); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const errorCode = result.unwrapErr().code; - - if (errorCode === 'RACE_ALREADY_SCHEDULED') { - this.logger.debug('[RaceService] Race is already scheduled, treating reopen as success.'); - presenter.presentSuccess('Race already scheduled'); - return presenter; - } - - presenter.presentFailure(errorCode ?? 'UNEXPECTED_ERROR', 'Unexpected error while reopening race'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.reopenRaceUseCase.execute({ raceId: params.raceId, reopenedById }); + return this.commandResultPresenter; } async fileProtest(command: FileProtestCommandDTO): Promise { this.logger.debug('[RaceService] Filing protest:', command); - - const result = await this.fileProtestUseCase.execute(command); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_FILE_PROTEST', 'Failed to file protest'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.fileProtestUseCase.execute(command); + return this.commandResultPresenter; } async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise { this.logger.debug('[RaceService] Applying quick penalty:', command); - - const result = await this.quickPenaltyUseCase.execute(command); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_APPLY_QUICK_PENALTY', 'Failed to apply quick penalty'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.quickPenaltyUseCase.execute(command); + return this.commandResultPresenter; } async applyPenalty(command: ApplyPenaltyCommandDTO): Promise { this.logger.debug('[RaceService] Applying penalty:', command); - - const result = await this.applyPenaltyUseCase.execute(command); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_APPLY_PENALTY', 'Failed to apply penalty'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.applyPenaltyUseCase.execute(command); + return this.commandResultPresenter; } async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise { this.logger.debug('[RaceService] Requesting protest defense:', command); - - const result = await this.requestProtestDefenseUseCase.execute(command); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_REQUEST_PROTEST_DEFENSE', 'Failed to request protest defense'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.requestProtestDefenseUseCase.execute(command); + return this.commandResultPresenter; } async reviewProtest(command: ReviewProtestCommandDTO): Promise { this.logger.debug('[RaceService] Reviewing protest:', command); - - const result = await this.reviewProtestUseCase.execute(command); - - const presenter = new CommandResultPresenter(); - if (result.isErr()) { - const error = result.unwrapErr(); - presenter.presentFailure(error.code ?? 'FAILED_TO_REVIEW_PROTEST', 'Failed to review protest'); - return presenter; - } - - presenter.presentSuccess(); - return presenter; + await this.reviewProtestUseCase.execute(command); + return this.commandResultPresenter; } -} +} \ No newline at end of file diff --git a/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts b/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts index 60f319c08..0f7a9b031 100644 --- a/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts +++ b/apps/api/src/domain/race/dtos/AllRacesPageDTO.ts @@ -28,12 +28,28 @@ export class AllRacesListItemDTO { strengthOfField!: number | null; } -export class AllRacesFilterOptionsDTO { - @ApiProperty({ type: [{ value: String, label: String }] }) - statuses!: { value: AllRacesStatus; label: string }[]; +export class AllRacesStatusFilterDTO { + @ApiProperty() + value!: AllRacesStatus; - @ApiProperty({ type: [{ id: String, name: String }] }) - leagues!: { id: string; name: string }[]; + @ApiProperty() + label!: string; +} + +export class AllRacesLeagueFilterDTO { + @ApiProperty() + id!: string; + + @ApiProperty() + name!: string; +} + +export class AllRacesFilterOptionsDTO { + @ApiProperty({ type: [AllRacesStatusFilterDTO] }) + statuses!: AllRacesStatusFilterDTO[]; + + @ApiProperty({ type: [AllRacesLeagueFilterDTO] }) + leagues!: AllRacesLeagueFilterDTO[]; } export class AllRacesPageDTO { diff --git a/apps/api/src/domain/race/presenters/CommandResultPresenter.ts b/apps/api/src/domain/race/presenters/CommandResultPresenter.ts index 94925dc22..7e79d14e0 100644 --- a/apps/api/src/domain/race/presenters/CommandResultPresenter.ts +++ b/apps/api/src/domain/race/presenters/CommandResultPresenter.ts @@ -7,19 +7,19 @@ export interface CommandResultDTO { message?: string; } -export type CommandApplicationError = ApplicationErrorCode< - E, +export type CommandApplicationError = ApplicationErrorCode< + string, { message: string } >; -export class CommandResultPresenter { +export class CommandResultPresenter { private model: CommandResultDTO | null = null; reset(): void { this.model = null; } - present(result: Result>): void { + present(result: Result): void { if (result.isErr()) { const error = result.unwrapErr(); this.model = { @@ -36,7 +36,7 @@ export class CommandResultPresenter { presentSuccess(message?: string): void { this.model = { success: true, - message, + ...(message !== undefined && { message }), }; } @@ -44,7 +44,7 @@ export class CommandResultPresenter { this.model = { success: false, errorCode, - message, + ...(message !== undefined && { message }), }; } @@ -59,4 +59,8 @@ export class CommandResultPresenter { return this.model; } + + get viewModel(): CommandResultDTO { + return this.responseModel; + } } diff --git a/apps/api/src/domain/race/presenters/RaceDetailPresenter.ts b/apps/api/src/domain/race/presenters/RaceDetailPresenter.ts index f45c76545..b03cf018b 100644 --- a/apps/api/src/domain/race/presenters/RaceDetailPresenter.ts +++ b/apps/api/src/domain/race/presenters/RaceDetailPresenter.ts @@ -40,11 +40,11 @@ export class RaceDetailPresenter implements UseCaseOutputPort { - const ratingResult = await this.driverRatingProvider.getDriverRating({ driverId: driver.id }); - const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id }); + const rating = this.driverRatingProvider.getRating(driver.id); + const avatarUrl = this.imageService.getDriverAvatar(driver.id); return { id: driver.id, name: driver.name.toString(), country: driver.country.toString(), - avatarUrl: avatarResult.avatarUrl, - rating: ratingResult.rating, + avatarUrl, + rating, isCurrentUser: driver.id === params.driverId, }; }), diff --git a/apps/api/src/domain/race/presenters/RaceProtestsPresenter.ts b/apps/api/src/domain/race/presenters/RaceProtestsPresenter.ts index a10cf2de5..480b78286 100644 --- a/apps/api/src/domain/race/presenters/RaceProtestsPresenter.ts +++ b/apps/api/src/domain/race/presenters/RaceProtestsPresenter.ts @@ -34,10 +34,10 @@ export class RaceProtestsPresenter { protestingDriverId: protest.protestingDriverId, accusedDriverId: protest.accusedDriverId, incident: { - lap: protest.incident.lap, - description: protest.incident.description, + lap: protest.incident.lap.toNumber(), + description: protest.incident.description.toString(), }, - status: protest.status, + status: protest.status.toString(), filedAt: protest.filedAt.toISOString(), } as RaceProtestDTO)); diff --git a/apps/api/src/domain/race/presenters/RaceResultsDetailPresenter.ts b/apps/api/src/domain/race/presenters/RaceResultsDetailPresenter.ts index 9f9e5532e..452f1f8e3 100644 --- a/apps/api/src/domain/race/presenters/RaceResultsDetailPresenter.ts +++ b/apps/api/src/domain/race/presenters/RaceResultsDetailPresenter.ts @@ -39,12 +39,12 @@ export class RaceResultsDetailPresenter { throw new Error(`Driver not found for result: ${singleResult.driverId}`); } - const avatarResult = await this.imageService.getDriverAvatar({ driverId: driver.id }); + const avatarUrl = this.imageService.getDriverAvatar(driver.id); return { driverId: singleResult.driverId.toString(), driverName: driver.name.toString(), - avatarUrl: avatarResult.avatarUrl, + avatarUrl, position: singleResult.position.toNumber(), startPosition: singleResult.startPosition.toNumber(), incidents: singleResult.incidents.toNumber(), diff --git a/apps/api/src/domain/sponsor/SponsorController.ts b/apps/api/src/domain/sponsor/SponsorController.ts index 5cc97da39..88a8adfee 100644 --- a/apps/api/src/domain/sponsor/SponsorController.ts +++ b/apps/api/src/domain/sponsor/SponsorController.ts @@ -24,7 +24,7 @@ import { SponsorProfileDTO } from './dtos/SponsorProfileDTO'; import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO'; import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO'; import type { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter'; -import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; +import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; @ApiTags('sponsors') @Controller('sponsors') @@ -39,8 +39,7 @@ export class SponsorController { type: GetEntitySponsorshipPricingResultDTO, }) async getEntitySponsorshipPricing(): Promise { - const presenter = await this.sponsorService.getEntitySponsorshipPricing(); - return presenter.viewModel; + return await this.sponsorService.getEntitySponsorshipPricing(); } @Get() @@ -51,8 +50,7 @@ export class SponsorController { type: GetSponsorsOutputDTO, }) async getSponsors(): Promise { - const presenter = await this.sponsorService.getSponsors(); - return presenter.viewModel; + return await this.sponsorService.getSponsors(); } @Post() @@ -64,8 +62,7 @@ export class SponsorController { type: CreateSponsorOutputDTO, }) async createSponsor(@Body() input: CreateSponsorInputDTO): Promise { - const presenter = await this.sponsorService.createSponsor(input); - return presenter.viewModel; + return await this.sponsorService.createSponsor(input); } @Get('dashboard/:sponsorId') @@ -78,11 +75,10 @@ export class SponsorController { @ApiResponse({ status: 404, description: 'Sponsor not found' }) async getSponsorDashboard( @Param('sponsorId') sponsorId: string, - ): Promise { - const presenter = await this.sponsorService.getSponsorDashboard({ + ): Promise { + return await this.sponsorService.getSponsorDashboard({ sponsorId, } as GetSponsorDashboardQueryParamsDTO); - return presenter.viewModel; } @Get(':sponsorId/sponsorships') @@ -97,11 +93,10 @@ export class SponsorController { @ApiResponse({ status: 404, description: 'Sponsor not found' }) async getSponsorSponsorships( @Param('sponsorId') sponsorId: string, - ): Promise { - const presenter = await this.sponsorService.getSponsorSponsorships({ + ): Promise { + return await this.sponsorService.getSponsorSponsorships({ sponsorId, } as GetSponsorSponsorshipsQueryParamsDTO); - return presenter.viewModel; } @Get(':sponsorId') @@ -112,9 +107,8 @@ export class SponsorController { type: GetSponsorOutputDTO, }) @ApiResponse({ status: 404, description: 'Sponsor not found' }) - async getSponsor(@Param('sponsorId') sponsorId: string): Promise { - const presenter = await this.sponsorService.getSponsor(sponsorId); - return presenter.viewModel; + async getSponsor(@Param('sponsorId') sponsorId: string): Promise { + return await this.sponsorService.getSponsor(sponsorId); } @Get('requests') @@ -126,14 +120,13 @@ export class SponsorController { }) async getPendingSponsorshipRequests( @Query() query: { entityType: string; entityId: string }, - ): Promise { - const presenter = await this.sponsorService.getPendingSponsorshipRequests( + ): Promise { + return await this.sponsorService.getPendingSponsorshipRequests( query as { entityType: import('@core/racing/domain/entities/SponsorshipRequest').SponsorableEntityType; entityId: string; }, ); - return presenter.viewModel; } @Post('requests/:requestId/accept') @@ -146,11 +139,10 @@ export class SponsorController { @Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO, ): Promise { - const presenter = await this.sponsorService.acceptSponsorshipRequest( + return await this.sponsorService.acceptSponsorshipRequest( requestId, input.respondedBy, ); - return presenter.viewModel; } @Post('requests/:requestId/reject') @@ -162,13 +154,12 @@ export class SponsorController { async rejectSponsorshipRequest( @Param('requestId') requestId: string, @Body() input: RejectSponsorshipRequestInputDTO, - ): Promise { - const presenter = await this.sponsorService.rejectSponsorshipRequest( + ): Promise { + return await this.sponsorService.rejectSponsorshipRequest( requestId, input.respondedBy, input.reason, ); - return presenter.viewModel; } @Get('billing/:sponsorId') @@ -181,8 +172,7 @@ export class SponsorController { invoices: InvoiceDTO[]; stats: BillingStatsDTO; }> { - const presenter = await this.sponsorService.getSponsorBilling(sponsorId); - return presenter.viewModel; + return await this.sponsorService.getSponsorBilling(sponsorId); } @Get('leagues/available') diff --git a/apps/api/src/domain/sponsor/SponsorProviders.ts b/apps/api/src/domain/sponsor/SponsorProviders.ts index f48f1e21c..555fa3206 100644 --- a/apps/api/src/domain/sponsor/SponsorProviders.ts +++ b/apps/api/src/domain/sponsor/SponsorProviders.ts @@ -5,7 +5,8 @@ import { SponsorService } from './SponsorService'; import { NotificationService } from '@core/notifications/application/ports/NotificationService'; import type { IPaymentRepository } from '@core/payments/domain/repositories/IPaymentRepository'; import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository'; -import { IPaymentGateway } from '@core/payments/domain/ports/IPaymentGateway'; +// Remove the missing import +// import { IPaymentGateway } from '@core/payments/domain/ports/IPaymentGateway'; import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository'; import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository'; @@ -15,7 +16,7 @@ import { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/I import { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository'; import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository'; import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository'; -import type { Logger } from '@core/shared/application'; +import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase'; import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; @@ -40,6 +41,18 @@ import { InMemorySponsorRepository } from '@adapters/racing/persistence/inmemory import { InMemorySponsorshipPricingRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository'; import { InMemorySponsorshipRequestRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository'; +// Import presenters +import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter'; +import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter'; +import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter'; +import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter'; +import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter'; +import { GetSponsorPresenter } from './presenters/GetSponsorPresenter'; +import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter'; +import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter'; +import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter'; +import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter'; + // Define injection tokens export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository'; export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository'; @@ -51,6 +64,18 @@ export const SPONSORSHIP_PRICING_REPOSITORY_TOKEN = 'ISponsorshipPricingReposito export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestRepository'; export const LOGGER_TOKEN = 'Logger'; +// Presenter tokens +export const GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN = 'GetEntitySponsorshipPricingPresenter'; +export const GET_SPONSORS_PRESENTER_TOKEN = 'GetSponsorsPresenter'; +export const CREATE_SPONSOR_PRESENTER_TOKEN = 'CreateSponsorPresenter'; +export const GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN = 'GetSponsorDashboardPresenter'; +export const GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN = 'GetSponsorSponsorshipsPresenter'; +export const GET_SPONSOR_PRESENTER_TOKEN = 'GetSponsorPresenter'; +export const GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN = 'GetPendingSponsorshipRequestsPresenter'; +export const ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'AcceptSponsorshipRequestPresenter'; +export const REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN = 'RejectSponsorshipRequestPresenter'; +export const GET_SPONSOR_BILLING_PRESENTER_TOKEN = 'SponsorBillingPresenter'; + // Use case / application service tokens export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase'; export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase'; @@ -64,6 +89,19 @@ export const ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'AcceptSponsorshipReque export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase'; export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase'; +// Output port tokens +export const GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetSponsorshipPricingOutputPort_TOKEN'; +export const GET_SPONSORS_OUTPUT_PORT_TOKEN = 'GetSponsorsOutputPort_TOKEN'; +export const CREATE_SPONSOR_OUTPUT_PORT_TOKEN = 'CreateSponsorOutputPort_TOKEN'; +export const GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN = 'GetSponsorDashboardOutputPort_TOKEN'; +export const GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSponsorSponsorshipsOutputPort_TOKEN'; +export const GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN = 'GetEntitySponsorshipPricingOutputPort_TOKEN'; +export const GET_SPONSOR_OUTPUT_PORT_TOKEN = 'GetSponsorOutputPort_TOKEN'; +export const GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN = 'GetPendingSponsorshipRequestsOutputPort_TOKEN'; +export const ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'AcceptSponsorshipRequestOutputPort_TOKEN'; +export const REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN = 'RejectSponsorshipRequestOutputPort_TOKEN'; +export const GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN = 'GetSponsorBillingOutputPort_TOKEN'; + export const SponsorProviders: Provider[] = [ SponsorService, // Repositories @@ -111,27 +149,94 @@ export const SponsorProviders: Provider[] = [ provide: LOGGER_TOKEN, useClass: ConsoleLogger, }, + // Presenters + GetEntitySponsorshipPricingPresenter, + GetSponsorsPresenter, + CreateSponsorPresenter, + GetSponsorDashboardPresenter, + GetSponsorSponsorshipsPresenter, + GetSponsorPresenter, + GetPendingSponsorshipRequestsPresenter, + AcceptSponsorshipRequestPresenter, + RejectSponsorshipRequestPresenter, + SponsorBillingPresenter, + // Output ports + { + provide: GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN, + useExisting: GetEntitySponsorshipPricingPresenter, + }, + { + provide: GET_SPONSORS_OUTPUT_PORT_TOKEN, + useExisting: GetSponsorsPresenter, + }, + { + provide: CREATE_SPONSOR_OUTPUT_PORT_TOKEN, + useExisting: CreateSponsorPresenter, + }, + { + provide: GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN, + useExisting: GetSponsorDashboardPresenter, + }, + { + provide: GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN, + useExisting: GetSponsorSponsorshipsPresenter, + }, + { + provide: GET_SPONSOR_OUTPUT_PORT_TOKEN, + useExisting: GetSponsorPresenter, + }, + { + provide: GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN, + useExisting: GetPendingSponsorshipRequestsPresenter, + }, + { + provide: ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN, + useExisting: AcceptSponsorshipRequestPresenter, + }, + { + provide: REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN, + useExisting: RejectSponsorshipRequestPresenter, + }, + { + provide: GET_SPONSOR_BILLING_OUTPUT_PORT_TOKEN, + useExisting: SponsorBillingPresenter, + }, // Use cases { provide: GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN, - useFactory: () => new GetSponsorshipPricingUseCase(), - inject: [], + useFactory: (output: UseCaseOutputPort) => new GetSponsorshipPricingUseCase(output), + inject: [GET_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN], }, { provide: GET_SPONSORS_USE_CASE_TOKEN, - useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorsUseCase(sponsorRepo), - inject: [SPONSOR_REPOSITORY_TOKEN], + useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort) => new GetSponsorsUseCase(sponsorRepo, output), + inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSORS_OUTPUT_PORT_TOKEN], }, { provide: CREATE_SPONSOR_USE_CASE_TOKEN, - useFactory: (sponsorRepo: ISponsorRepository) => new CreateSponsorUseCase(sponsorRepo), - inject: [SPONSOR_REPOSITORY_TOKEN], + useFactory: (sponsorRepo: ISponsorRepository, logger: Logger, output: UseCaseOutputPort) => new CreateSponsorUseCase(sponsorRepo, logger, output), + inject: [SPONSOR_REPOSITORY_TOKEN, LOGGER_TOKEN, CREATE_SPONSOR_OUTPUT_PORT_TOKEN], }, { provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, - useFactory: (sponsorRepo: ISponsorRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, leagueRepo: ILeagueRepository, leagueMembershipRepo: ILeagueMembershipRepository, raceRepo: IRaceRepository) => - new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo), - inject: [SPONSOR_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN], + useFactory: ( + sponsorRepo: ISponsorRepository, + seasonSponsorshipRepo: ISeasonSponsorshipRepository, + seasonRepo: ISeasonRepository, + leagueRepo: ILeagueRepository, + leagueMembershipRepo: ILeagueMembershipRepository, + raceRepo: IRaceRepository, + output: UseCaseOutputPort, + ) => new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output), + inject: [ + SPONSOR_REPOSITORY_TOKEN, + SEASON_SPONSORSHIP_REPOSITORY_TOKEN, + SEASON_REPOSITORY_TOKEN, + LEAGUE_REPOSITORY_TOKEN, + LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, + RACE_REPOSITORY_TOKEN, + GET_SPONSOR_DASHBOARD_OUTPUT_PORT_TOKEN, + ], }, { provide: GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN, @@ -142,7 +247,8 @@ export const SponsorProviders: Provider[] = [ leagueRepo: ILeagueRepository, leagueMembershipRepo: ILeagueMembershipRepository, raceRepo: IRaceRepository, - ) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo), + output: UseCaseOutputPort, + ) => new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo, output), inject: [ SPONSOR_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, @@ -150,41 +256,97 @@ export const SponsorProviders: Provider[] = [ LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, + GET_SPONSOR_SPONSORSHIPS_OUTPUT_PORT_TOKEN, ], }, { provide: GET_SPONSOR_BILLING_USE_CASE_TOKEN, - useFactory: (paymentRepo: IPaymentRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository) => - new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo), + useFactory: ( + paymentRepo: IPaymentRepository, + seasonSponsorshipRepo: ISeasonSponsorshipRepository, + ) => { + return new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo); + }, inject: ['IPaymentRepository', SEASON_SPONSORSHIP_REPOSITORY_TOKEN], }, { provide: GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN, - useFactory: (sponsorshipPricingRepo: ISponsorshipPricingRepository, sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, logger: Logger) => - new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, sponsorshipRequestRepo, seasonSponsorshipRepo, logger), - inject: [SPONSORSHIP_PRICING_REPOSITORY_TOKEN, SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: ( + sponsorshipPricingRepo: ISponsorshipPricingRepository, + sponsorshipRequestRepo: ISponsorshipRequestRepository, + seasonSponsorshipRepo: ISeasonSponsorshipRepository, + logger: Logger, + output: UseCaseOutputPort, + ) => new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, sponsorshipRequestRepo, seasonSponsorshipRepo, logger, output), + inject: [ + SPONSORSHIP_PRICING_REPOSITORY_TOKEN, + SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, + SEASON_SPONSORSHIP_REPOSITORY_TOKEN, + LOGGER_TOKEN, + GET_ENTITY_SPONSORSHIP_PRICING_OUTPUT_PORT_TOKEN, + ], }, { provide: GET_SPONSOR_USE_CASE_TOKEN, - useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorUseCase(sponsorRepo), - inject: [SPONSOR_REPOSITORY_TOKEN], + useFactory: (sponsorRepo: ISponsorRepository, output: UseCaseOutputPort) => new GetSponsorUseCase(sponsorRepo, output), + inject: [SPONSOR_REPOSITORY_TOKEN, GET_SPONSOR_OUTPUT_PORT_TOKEN], }, { provide: GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN, - useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, sponsorRepo: ISponsorRepository) => - new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo), - inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN], + useFactory: ( + sponsorshipRequestRepo: ISponsorshipRequestRepository, + sponsorRepo: ISponsorRepository, + output: UseCaseOutputPort, + ) => new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsorRepo, output), + inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SPONSOR_REPOSITORY_TOKEN, GET_PENDING_SPONSORSHIP_REQUESTS_OUTPUT_PORT_TOKEN], }, { provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, - useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: NotificationService, paymentGateway: IPaymentGateway, walletRepository: IWalletRepository, leagueWalletRepository: ILeagueWalletRepository, logger: Logger) => - new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepo, seasonSponsorshipRepo, seasonRepo, notificationService, paymentGateway, walletRepository, leagueWalletRepository, logger), - inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, 'INotificationService', 'IPaymentGateway', 'IWalletRepository', 'ILeagueWalletRepository', LOGGER_TOKEN], + useFactory: ( + sponsorshipRequestRepo: ISponsorshipRequestRepository, + seasonSponsorshipRepo: ISeasonSponsorshipRepository, + seasonRepo: ISeasonRepository, + notificationService: NotificationService, + walletRepository: IWalletRepository, + leagueWalletRepository: ILeagueWalletRepository, + logger: Logger, + output: UseCaseOutputPort, + ) => { + // Create a mock payment processor function + const paymentProcessor = async (_input: any) => { + return { success: true, transactionId: `txn_${Date.now()}` }; + }; + + return new AcceptSponsorshipRequestUseCase( + sponsorshipRequestRepo, + seasonSponsorshipRepo, + seasonRepo, + notificationService, + paymentProcessor, + walletRepository, + leagueWalletRepository, + logger, + output + ); + }, + inject: [ + SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, + SEASON_SPONSORSHIP_REPOSITORY_TOKEN, + SEASON_REPOSITORY_TOKEN, + 'INotificationService', + 'IWalletRepository', + 'ILeagueWalletRepository', + LOGGER_TOKEN, + ACCEPT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN, + ], }, { provide: REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, - useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, logger: Logger) => - new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger), - inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN], + useFactory: ( + sponsorshipRequestRepo: ISponsorshipRequestRepository, + logger: Logger, + output: UseCaseOutputPort, + ) => new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, logger, output), + inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, LOGGER_TOKEN, REJECT_SPONSORSHIP_REQUEST_OUTPUT_PORT_TOKEN], }, -]; +]; \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/SponsorService.test.ts b/apps/api/src/domain/sponsor/SponsorService.test.ts index e7676e55a..1a586e820 100644 --- a/apps/api/src/domain/sponsor/SponsorService.test.ts +++ b/apps/api/src/domain/sponsor/SponsorService.test.ts @@ -1,15 +1,27 @@ -import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; -import { Result } from '@core/shared/application/Result'; -import type { Logger } from '@core/shared/application'; -import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; -import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase'; -import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase'; -import type { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; -import type { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; -import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase'; -import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; +import type { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase'; import type { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; +import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase'; +import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; +import type { GetSponsorDashboardInput, GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; +import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; +import type { GetSponsorSponsorshipsInput, GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; +import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase'; +import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase'; import type { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; +import type { Logger } from '@core/shared/application'; +import { Result } from '@core/shared/application/Result'; +import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import type { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO'; +import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter'; +import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter'; +import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter'; +import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter'; +import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter'; +import { GetSponsorPresenter } from './presenters/GetSponsorPresenter'; +import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter'; +import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter'; +import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter'; +import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter'; import { SponsorService } from './SponsorService'; describe('SponsorService', () => { @@ -23,8 +35,21 @@ describe('SponsorService', () => { let getPendingSponsorshipRequestsUseCase: { execute: Mock }; let acceptSponsorshipRequestUseCase: { execute: Mock }; let rejectSponsorshipRequestUseCase: { execute: Mock }; + let getSponsorBillingUseCase: { execute: Mock }; let logger: Logger; + // Presenters + let getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter; + let getSponsorsPresenter: GetSponsorsPresenter; + let createSponsorPresenter: CreateSponsorPresenter; + let getSponsorDashboardPresenter: GetSponsorDashboardPresenter; + let getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter; + let getSponsorPresenter: GetSponsorPresenter; + let getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter; + let acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter; + let rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter; + let sponsorBillingPresenter: SponsorBillingPresenter; + beforeEach(() => { getSponsorshipPricingUseCase = { execute: vi.fn() }; getSponsorsUseCase = { execute: vi.fn() }; @@ -35,6 +60,7 @@ describe('SponsorService', () => { getPendingSponsorshipRequestsUseCase = { execute: vi.fn() }; acceptSponsorshipRequestUseCase = { execute: vi.fn() }; rejectSponsorshipRequestUseCase = { execute: vi.fn() }; + getSponsorBillingUseCase = { execute: vi.fn() }; logger = { debug: vi.fn(), info: vi.fn(), @@ -42,6 +68,18 @@ describe('SponsorService', () => { error: vi.fn(), } as unknown as Logger; + // Initialize presenters + getEntitySponsorshipPricingPresenter = new GetEntitySponsorshipPricingPresenter(); + getSponsorsPresenter = new GetSponsorsPresenter(); + createSponsorPresenter = new CreateSponsorPresenter(); + getSponsorDashboardPresenter = new GetSponsorDashboardPresenter(); + getSponsorSponsorshipsPresenter = new GetSponsorSponsorshipsPresenter(); + getSponsorPresenter = new GetSponsorPresenter(); + getPendingSponsorshipRequestsPresenter = new GetPendingSponsorshipRequestsPresenter(); + acceptSponsorshipRequestPresenter = new AcceptSponsorshipRequestPresenter(); + rejectSponsorshipRequestPresenter = new RejectSponsorshipRequestPresenter(); + sponsorBillingPresenter = new SponsorBillingPresenter(); + service = new SponsorService( getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase, getSponsorsUseCase as unknown as GetSponsorsUseCase, @@ -52,28 +90,39 @@ describe('SponsorService', () => { getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase, acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase, rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase, + getSponsorBillingUseCase as unknown as GetSponsorBillingUseCase, logger, + getEntitySponsorshipPricingPresenter, + getSponsorsPresenter, + createSponsorPresenter, + getSponsorDashboardPresenter, + getSponsorSponsorshipsPresenter, + getSponsorPresenter, + getPendingSponsorshipRequestsPresenter, + acceptSponsorshipRequestPresenter, + rejectSponsorshipRequestPresenter, + sponsorBillingPresenter, ); }); describe('getEntitySponsorshipPricing', () => { - it('returns presenter with pricing data on success', async () => { + it('returns pricing data on success', async () => { const outputPort = { entityType: 'season', entityId: 'season-1', - pricing: [ - { id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' }, + tiers: [ + { name: 'Gold', price: 500, benefits: ['Main slot'] }, ], }; getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.getEntitySponsorshipPricing(); + const result = await service.getEntitySponsorshipPricing(); - expect(presenter.viewModel).toEqual({ + expect(result).toEqual({ entityType: 'season', entityId: 'season-1', pricing: [ - { id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' }, + { id: 'Gold', level: 'Gold', price: 500, currency: 'USD' }, ], }); }); @@ -81,9 +130,9 @@ describe('SponsorService', () => { it('returns empty pricing on error', async () => { getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); - const presenter = await service.getEntitySponsorshipPricing(); + const result = await service.getEntitySponsorshipPricing(); - expect(presenter.viewModel).toEqual({ + expect(result).toEqual({ entityType: 'season', entityId: '', pricing: [], @@ -92,82 +141,93 @@ describe('SponsorService', () => { }); describe('getSponsors', () => { - it('returns sponsors in presenter on success', async () => { - const outputPort = { sponsors: [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }] }; + it('returns sponsors on success', async () => { + const sponsors = [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }]; + const outputPort = { sponsors }; getSponsorsUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.getSponsors(); + const result = await service.getSponsors(); - expect(presenter.viewModel).toEqual({ sponsors: outputPort.sponsors }); + expect(result).toEqual({ sponsors }); }); it('returns empty list on error', async () => { getSponsorsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); - const presenter = await service.getSponsors(); + const result = await service.getSponsors(); - expect(presenter.viewModel).toEqual({ sponsors: [] }); + expect(result).toEqual({ sponsors: [] }); }); }); describe('createSponsor', () => { - it('returns created sponsor in presenter on success', async () => { - const input = { name: 'Test', contactEmail: 'test@example.com' }; - const outputPort = { - sponsor: { - id: 's1', - name: 'Test', - contactEmail: 'test@example.com', - createdAt: new Date(), - }, + it('returns created sponsor on success', async () => { + const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' }; + const sponsor = { + id: 's1', + name: 'Test', + contactEmail: 'test@example.com', + createdAt: new Date(), }; + const outputPort = { sponsor }; createSponsorUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.createSponsor(input as any); + const result = await service.createSponsor(input); - expect(presenter.viewModel).toEqual({ sponsor: outputPort.sponsor }); + expect(result).toEqual({ sponsor }); }); it('throws on error', async () => { - const input = { name: 'Test', contactEmail: 'test@example.com' }; + const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' }; createSponsorUseCase.execute.mockResolvedValue( Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Invalid' } }), ); - await expect(service.createSponsor(input as any)).rejects.toThrow('Invalid'); + await expect(service.createSponsor(input)).rejects.toThrow('Invalid'); }); }); describe('getSponsorDashboard', () => { - it('returns dashboard in presenter on success', async () => { - const params = { sponsorId: 's1' }; + it('returns dashboard on success', async () => { + const params: GetSponsorDashboardInput = { sponsorId: 's1' }; const outputPort = { sponsorId: 's1', sponsorName: 'S1', - metrics: {} as any, + metrics: { + impressions: 0, + impressionsChange: 0, + uniqueViewers: 0, + viewersChange: 0, + races: 0, + drivers: 0, + exposure: 0, + exposureChange: 0, + }, sponsoredLeagues: [], - investment: {} as any, + investment: { + activeSponsorships: 0, + totalInvestment: 0, + costPerThousandViews: 0, + }, }; getSponsorDashboardUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.getSponsorDashboard(params as any); + const result = await service.getSponsorDashboard(params); - expect(presenter.viewModel).toEqual(outputPort); + expect(result).toEqual(outputPort); }); - it('returns null in presenter on error', async () => { - const params = { sponsorId: 's1' }; + it('throws on error', async () => { + const params: GetSponsorDashboardInput = { sponsorId: 's1' }; getSponsorDashboardUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' })); - const presenter = await service.getSponsorDashboard(params as any); - - expect(presenter.viewModel).toBeNull(); + await expect(service.getSponsorDashboard(params)).rejects.toThrow('Sponsor dashboard not found'); }); }); describe('getSponsorSponsorships', () => { - it('returns sponsorships in presenter on success', async () => { - const params = { sponsorId: 's1' }; + it('returns sponsorships on success', async () => { + const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' }; const outputPort = { sponsorId: 's1', sponsorName: 'S1', @@ -182,46 +242,43 @@ describe('SponsorService', () => { }; getSponsorSponsorshipsUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.getSponsorSponsorships(params as any); + const result = await service.getSponsorSponsorships(params); - expect(presenter.viewModel).toEqual(outputPort); + expect(result).toEqual(outputPort); }); - it('returns null in presenter on error', async () => { - const params = { sponsorId: 's1' }; + it('throws on error', async () => { + const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' }; getSponsorSponsorshipsUseCase.execute.mockResolvedValue( Result.err({ code: 'REPOSITORY_ERROR' }), ); - const presenter = await service.getSponsorSponsorships(params as any); - - expect(presenter.viewModel).toBeNull(); + await expect(service.getSponsorSponsorships(params)).rejects.toThrow('Sponsor sponsorships not found'); }); }); describe('getSponsor', () => { - it('returns sponsor in presenter when found', async () => { + it('returns sponsor when found', async () => { const sponsorId = 's1'; - const output = { sponsor: { id: sponsorId, name: 'S1' } }; + const sponsor = { id: sponsorId, name: 'S1' }; + const output = { sponsor }; getSponsorUseCase.execute.mockResolvedValue(Result.ok(output)); - const presenter = await service.getSponsor(sponsorId); + const result = await service.getSponsor(sponsorId); - expect(presenter.viewModel).toEqual({ sponsor: output.sponsor }); + expect(result).toEqual({ sponsor }); }); - it('returns null in presenter when not found', async () => { + it('throws when not found', async () => { const sponsorId = 's1'; getSponsorUseCase.execute.mockResolvedValue(Result.ok(null)); - const presenter = await service.getSponsor(sponsorId); - - expect(presenter.viewModel).toBeNull(); + await expect(service.getSponsor(sponsorId)).rejects.toThrow('Sponsor not found'); }); }); describe('getPendingSponsorshipRequests', () => { - it('returns requests in presenter on success', async () => { + it('returns requests on success', async () => { const params = { entityType: 'season' as const, entityId: 'season-1' }; const outputPort = { entityType: 'season', @@ -231,9 +288,9 @@ describe('SponsorService', () => { }; getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.getPendingSponsorshipRequests(params); + const result = await service.getPendingSponsorshipRequests(params); - expect(presenter.viewModel).toEqual(outputPort); + expect(result).toEqual(outputPort); }); it('returns empty result on error', async () => { @@ -242,9 +299,9 @@ describe('SponsorService', () => { Result.err({ code: 'REPOSITORY_ERROR' }), ); - const presenter = await service.getPendingSponsorshipRequests(params); + const result = await service.getPendingSponsorshipRequests(params); - expect(presenter.viewModel).toEqual({ + expect(result).toEqual({ entityType: 'season', entityId: 'season-1', requests: [], @@ -253,8 +310,8 @@ describe('SponsorService', () => { }); }); - describe('acceptSponsorshipRequest', () => { - it('returns accept result in presenter on success', async () => { + describe('SponsorshipRequest', () => { + it('returns accept result on success', async () => { const requestId = 'r1'; const respondedBy = 'u1'; const outputPort = { @@ -267,100 +324,114 @@ describe('SponsorService', () => { }; acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(outputPort)); - const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy); + const result = await service.acceptSponsorshipRequest(requestId, respondedBy); - expect(presenter.viewModel).toEqual(outputPort); + expect(result).toEqual(outputPort); }); - it('returns null in presenter on error', async () => { + it('throws on error', async () => { const requestId = 'r1'; const respondedBy = 'u1'; acceptSponsorshipRequestUseCase.execute.mockResolvedValue( Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }), ); - const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy); - - expect(presenter.viewModel).toBeNull(); + await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Accept sponsorship request failed'); }); }); describe('rejectSponsorshipRequest', () => { - it('returns reject result in presenter on success', async () => { + it('returns reject result on success', async () => { const requestId = 'r1'; const respondedBy = 'u1'; const reason = 'Not interested'; const output = { requestId, status: 'rejected' as const, - rejectedAt: new Date(), - reason, + respondedAt: new Date(), + rejectionReason: reason, }; rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output)); - const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy, reason); + const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason); - expect(presenter.viewModel).toEqual(output); + expect(result).toEqual(output); }); - it('returns null in presenter on error', async () => { + it('throws on error', async () => { const requestId = 'r1'; const respondedBy = 'u1'; rejectSponsorshipRequestUseCase.execute.mockResolvedValue( Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }), ); - const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy); - - expect(presenter.viewModel).toBeNull(); + await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Reject sponsorship request failed'); }); }); describe('getSponsorBilling', () => { - it('returns mock billing data in presenter', async () => { - const presenter = await service.getSponsorBilling('s1'); + it('returns billing data', async () => { + // Mock the use case to set up the presenter + getSponsorBillingUseCase.execute.mockImplementation(async () => { + sponsorBillingPresenter.present({ + paymentMethods: [], + invoices: [], + stats: { + totalSpent: 0, + pendingAmount: 0, + nextPaymentDate: '', + nextPaymentAmount: 0, + activeSponsorships: 0, + averageMonthlySpend: 0, + }, + }); + return Result.ok(undefined); + }); - expect(presenter.viewModel).not.toBeNull(); - expect(presenter.viewModel?.paymentMethods).toBeInstanceOf(Array); - expect(presenter.viewModel?.invoices).toBeInstanceOf(Array); - expect(presenter.viewModel?.stats).toBeDefined(); + const result = await service.getSponsorBilling('s1'); + + expect(result).not.toBeNull(); + expect(result.paymentMethods).toBeInstanceOf(Array); + expect(result.invoices).toBeInstanceOf(Array); + expect(result.stats).toBeDefined(); }); }); describe('getAvailableLeagues', () => { - it('returns mock leagues in presenter', async () => { - const presenter = await service.getAvailableLeagues(); + it('returns mock leagues', async () => { + const result = await service.getAvailableLeagues(); - expect(presenter.viewModel).not.toBeNull(); - expect(presenter.viewModel?.length).toBeGreaterThan(0); + expect(result).not.toBeNull(); + expect(result.viewModel).not.toBeNull(); + expect(result.viewModel?.length).toBeGreaterThan(0); }); }); describe('getLeagueDetail', () => { - it('returns league detail in presenter', async () => { - const presenter = await service.getLeagueDetail('league-1'); + it('returns league detail', async () => { + const result = await service.getLeagueDetail('league-1'); - expect(presenter.viewModel).not.toBeNull(); - expect(presenter.viewModel?.league.id).toBe('league-1'); + expect(result).not.toBeNull(); + expect(result.viewModel?.league.id).toBe('league-1'); }); }); describe('getSponsorSettings', () => { - it('returns settings in presenter', async () => { - const presenter = await service.getSponsorSettings('s1'); + it('returns settings', async () => { + const result = await service.getSponsorSettings('s1'); - expect(presenter.viewModel).not.toBeNull(); - expect(presenter.viewModel?.profile).toBeDefined(); - expect(presenter.viewModel?.notifications).toBeDefined(); - expect(presenter.viewModel?.privacy).toBeDefined(); + expect(result).not.toBeNull(); + expect(result.viewModel?.profile).toBeDefined(); + expect(result.viewModel?.notifications).toBeDefined(); + expect(result.viewModel?.privacy).toBeDefined(); }); }); describe('updateSponsorSettings', () => { - it('returns success result in presenter', async () => { - const presenter = await service.updateSponsorSettings('s1', {}); + it('returns success result', async () => { + const result = await service.updateSponsorSettings('s1', {}); - expect(presenter.viewModel).toEqual({ success: true }); + expect(result.viewModel).toEqual({ success: true }); }); }); -}); +}); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/SponsorService.ts b/apps/api/src/domain/sponsor/SponsorService.ts index 12f6c8528..437300428 100644 --- a/apps/api/src/domain/sponsor/SponsorService.ts +++ b/apps/api/src/domain/sponsor/SponsorService.ts @@ -1,12 +1,14 @@ import { Injectable, Inject } from '@nestjs/common'; import { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO'; +import { CreateSponsorOutputDTO } from './dtos/CreateSponsorOutputDTO'; import { GetSponsorDashboardQueryParamsDTO } from './dtos/GetSponsorDashboardQueryParamsDTO'; +import { SponsorDashboardDTO } from './dtos/SponsorDashboardDTO'; import { GetSponsorSponsorshipsQueryParamsDTO } from './dtos/GetSponsorSponsorshipsQueryParamsDTO'; -import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO'; -import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO'; -import { PaymentMethodDTO } from './dtos/PaymentMethodDTO'; -import { InvoiceDTO } from './dtos/InvoiceDTO'; -import { BillingStatsDTO } from './dtos/BillingStatsDTO'; +import { SponsorSponsorshipsDTO } from './dtos/SponsorSponsorshipsDTO'; +import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO'; +import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO'; +import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshipPricingResultDTO'; +import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO'; import { AvailableLeagueDTO } from './dtos/AvailableLeagueDTO'; import { LeagueDetailDTO } from './dtos/LeagueDetailDTO'; import { DriverDTO } from './dtos/DriverDTO'; @@ -14,6 +16,9 @@ import { RaceDTO } from './dtos/RaceDTO'; import { SponsorProfileDTO } from './dtos/SponsorProfileDTO'; import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO'; import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO'; +import { PaymentMethodDTO } from './dtos/PaymentMethodDTO'; +import { InvoiceDTO } from './dtos/InvoiceDTO'; +import { BillingStatsDTO } from './dtos/BillingStatsDTO'; // Use cases import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; @@ -24,7 +29,7 @@ import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-case import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase'; import { GetPendingSponsorshipRequestsUseCase, - GetPendingSponsorshipRequestsDTO, + GetPendingSponsorshipRequestsInput, } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; @@ -48,6 +53,8 @@ import { AvailableLeaguesPresenter } from './presenters/AvailableLeaguesPresente import { LeagueDetailPresenter } from './presenters/LeagueDetailPresenter'; import { SponsorSettingsPresenter } from './presenters/SponsorSettingsPresenter'; import { SponsorSettingsUpdatePresenter } from './presenters/SponsorSettingsUpdatePresenter'; +import { AcceptSponsorshipRequestResultViewModel } from './presenters/AcceptSponsorshipRequestPresenter'; +import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; // Tokens import { @@ -61,6 +68,16 @@ import { ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, LOGGER_TOKEN, + GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN, + GET_SPONSORS_PRESENTER_TOKEN, + CREATE_SPONSOR_PRESENTER_TOKEN, + GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN, + GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN, + GET_SPONSOR_PRESENTER_TOKEN, + GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN, + ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN, + REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN, + GET_SPONSOR_BILLING_PRESENTER_TOKEN, } from './SponsorProviders'; @Injectable() @@ -88,194 +105,152 @@ export class SponsorService { private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase, @Inject(LOGGER_TOKEN) private readonly logger: Logger, + // Injected presenters + @Inject(GET_ENTITY_SPONSORSHIP_PRICING_PRESENTER_TOKEN) + private readonly getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter, + @Inject(GET_SPONSORS_PRESENTER_TOKEN) + private readonly getSponsorsPresenter: GetSponsorsPresenter, + @Inject(CREATE_SPONSOR_PRESENTER_TOKEN) + private readonly createSponsorPresenter: CreateSponsorPresenter, + @Inject(GET_SPONSOR_DASHBOARD_PRESENTER_TOKEN) + private readonly getSponsorDashboardPresenter: GetSponsorDashboardPresenter, + @Inject(GET_SPONSOR_SPONSORSHIPS_PRESENTER_TOKEN) + private readonly getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter, + @Inject(GET_SPONSOR_PRESENTER_TOKEN) + private readonly getSponsorPresenter: GetSponsorPresenter, + @Inject(GET_PENDING_SPONSORSHIP_REQUESTS_PRESENTER_TOKEN) + private readonly getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter, + @Inject(ACCEPT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN) + private readonly acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter, + @Inject(REJECT_SPONSORSHIP_REQUEST_PRESENTER_TOKEN) + private readonly rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter, + @Inject(GET_SPONSOR_BILLING_PRESENTER_TOKEN) + private readonly sponsorBillingPresenter: SponsorBillingPresenter, ) {} - async getEntitySponsorshipPricing(): Promise { + async getEntitySponsorshipPricing(): Promise { this.logger.debug('[SponsorService] Fetching sponsorship pricing.'); - - const presenter = new GetEntitySponsorshipPricingPresenter(); - const result = await this.getSponsorshipPricingUseCase.execute(); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to fetch sponsorship pricing.', result.error); - presenter.present(null); - return presenter; - } - - presenter.present(result.value); - return presenter; + await this.getSponsorshipPricingUseCase.execute({}); + return this.getEntitySponsorshipPricingPresenter.viewModel; } async getSponsors(): Promise { this.logger.debug('[SponsorService] Fetching sponsors.'); - - const presenter = new GetSponsorsPresenter(); - const result = await this.getSponsorsUseCase.execute(); - - presenter.present(result); - - return presenter.responseModel; + await this.getSponsorsUseCase.execute(); + return this.getSponsorsPresenter.responseModel; } - async createSponsor(input: CreateSponsorInputDTO): Promise { + async createSponsor(input: CreateSponsorInputDTO): Promise { this.logger.debug('[SponsorService] Creating sponsor.', { input }); - - const presenter = new CreateSponsorPresenter(); - const result = await this.createSponsorUseCase.execute(input); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to create sponsor.', result.error); - throw new Error(result.error.details?.message || 'Failed to create sponsor'); - } - - presenter.present(result.value); - return presenter; + await this.createSponsorUseCase.execute(input); + return this.createSponsorPresenter.viewModel; } async getSponsorDashboard( params: GetSponsorDashboardQueryParamsDTO, - ): Promise { + ): Promise { this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params }); - - const presenter = new GetSponsorDashboardPresenter(); - const result = await this.getSponsorDashboardUseCase.execute(params); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to fetch sponsor dashboard.', result.error); - presenter.present(null); - return presenter; + await this.getSponsorDashboardUseCase.execute(params); + const result = this.getSponsorDashboardPresenter.viewModel; + if (!result) { + throw new Error('Sponsor dashboard not found'); } - - presenter.present(result.value); - return presenter; + return result; } async getSponsorSponsorships( params: GetSponsorSponsorshipsQueryParamsDTO, - ): Promise { + ): Promise { this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params }); - - const presenter = new GetSponsorSponsorshipsPresenter(); - const result = await this.getSponsorSponsorshipsUseCase.execute(params); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to fetch sponsor sponsorships.', result.error); - presenter.present(null); - return presenter; + await this.getSponsorSponsorshipsUseCase.execute(params); + const result = this.getSponsorSponsorshipsPresenter.viewModel; + if (!result) { + throw new Error('Sponsor sponsorships not found'); } - - presenter.present(result.value); - return presenter; + return result; } - async getSponsor(sponsorId: string): Promise { + async getSponsor(sponsorId: string): Promise { this.logger.debug('[SponsorService] Fetching sponsor.', { sponsorId }); - - const presenter = new GetSponsorPresenter(); - const result = await this.getSponsorUseCase.execute({ sponsorId }); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to fetch sponsor.', result.error); - presenter.present(null); - return presenter; + await this.getSponsorUseCase.execute({ sponsorId }); + const result = this.getSponsorPresenter.viewModel; + if (!result) { + throw new Error('Sponsor not found'); } - - presenter.present(result.value); - return presenter; + return result; } async getPendingSponsorshipRequests(params: { entityType: SponsorableEntityType; entityId: string; - }): Promise { + }): Promise { this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params }); - - const presenter = new GetPendingSponsorshipRequestsPresenter(); - const result = await this.getPendingSponsorshipRequestsUseCase.execute( - params as GetPendingSponsorshipRequestsDTO, + await this.getPendingSponsorshipRequestsUseCase.execute( + params as GetPendingSponsorshipRequestsInput, ); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to fetch pending sponsorship requests.', result.error); - presenter.present({ - entityType: params.entityType, - entityId: params.entityId, - requests: [], - totalCount: 0, - }); - return presenter; + const result = this.getPendingSponsorshipRequestsPresenter.viewModel; + if (!result) { + throw new Error('Pending sponsorship requests not found'); } - - presenter.present(result.value); - return presenter; + return result; } async acceptSponsorshipRequest( requestId: string, respondedBy: string, - ): Promise { + ): Promise { this.logger.debug('[SponsorService] Accepting sponsorship request.', { requestId, respondedBy, }); - - const presenter = new AcceptSponsorshipRequestPresenter(); - const result = await this.acceptSponsorshipRequestUseCase.execute({ + await this.acceptSponsorshipRequestUseCase.execute({ requestId, respondedBy, - } as AcceptSponsorshipRequestInputDTO); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to accept sponsorship request.', result.error); - presenter.present(null); - return presenter; + }); + const result = this.acceptSponsorshipRequestPresenter.viewModel; + if (!result) { + throw new Error('Accept sponsorship request failed'); } - - presenter.present(result.value); - return presenter; + return result; } async rejectSponsorshipRequest( requestId: string, respondedBy: string, reason?: string, - ): Promise { + ): Promise { this.logger.debug('[SponsorService] Rejecting sponsorship request.', { requestId, respondedBy, reason, }); - - const presenter = new RejectSponsorshipRequestPresenter(); - const result = await this.rejectSponsorshipRequestUseCase.execute({ + const input: { requestId: string; respondedBy: string; reason?: string } = { requestId, respondedBy, - reason, - } as RejectSponsorshipRequestInputDTO); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to reject sponsorship request.', result.error); - presenter.present(null); - return presenter; + }; + if (reason !== undefined) { + input.reason = reason; } - - presenter.present(result.value); - return presenter; + await this.rejectSponsorshipRequestUseCase.execute(input); + const result = this.rejectSponsorshipRequestPresenter.viewModel; + if (!result) { + throw new Error('Reject sponsorship request failed'); + } + return result; } - async getSponsorBilling(sponsorId: string): Promise { + async getSponsorBilling(sponsorId: string): Promise<{ + paymentMethods: PaymentMethodDTO[]; + invoices: InvoiceDTO[]; + stats: BillingStatsDTO; + }> { this.logger.debug('[SponsorService] Fetching sponsor billing.', { sponsorId }); - - const result = await this.getSponsorBillingUseCase.execute({ sponsorId }); - - if (result.isErr()) { - this.logger.error('[SponsorService] Failed to fetch sponsor billing.', result.error); - throw new Error(result.error.details?.message || 'Failed to fetch sponsor billing'); + await this.getSponsorBillingUseCase.execute({ sponsorId }); + const result = this.sponsorBillingPresenter.viewModel; + if (!result) { + throw new Error('Sponsor billing not found'); } - - const presenter = new GetSponsorBillingPresenter(); - presenter.present(result.value); - return presenter; + return result; } async getAvailableLeagues(): Promise { @@ -426,7 +401,7 @@ export class SponsorService { website: 'https://acme-racing.com', description: 'Premium sim racing equipment and accessories for competitive drivers.', - logoUrl: null, + logoUrl: '', industry: 'Racing Equipment', address: { street: '123 Racing Boulevard', @@ -479,4 +454,4 @@ export class SponsorService { presenter.present({ success: true }); return presenter; } -} +} \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/ActivityItemDTO.ts b/apps/api/src/domain/sponsor/dtos/ActivityItemDTO.ts index 8fd97e102..508e26151 100644 --- a/apps/api/src/domain/sponsor/dtos/ActivityItemDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/ActivityItemDTO.ts @@ -4,19 +4,19 @@ import { IsString, IsEnum, IsOptional, IsNumber } from 'class-validator'; export class ActivityItemDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty({ enum: ['race', 'league', 'team', 'driver', 'platform'] }) @IsEnum(['race', 'league', 'team', 'driver', 'platform']) - type: 'race' | 'league' | 'team' | 'driver' | 'platform'; + type: 'race' | 'league' | 'team' | 'driver' | 'platform' = 'race'; @ApiProperty() @IsString() - message: string; + message: string = ''; @ApiProperty() @IsString() - time: string; + time: string = ''; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/sponsor/dtos/AvailableLeagueDTO.ts b/apps/api/src/domain/sponsor/dtos/AvailableLeagueDTO.ts index b72738dac..90545dc1d 100644 --- a/apps/api/src/domain/sponsor/dtos/AvailableLeagueDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/AvailableLeagueDTO.ts @@ -1,47 +1,47 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsEnum, IsNumber, IsBoolean, IsOptional, IsDateString } from 'class-validator'; +import { IsString, IsEnum, IsNumber, IsOptional, IsDateString } from 'class-validator'; export class AvailableLeagueDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - name: string; + name: string = ''; @ApiProperty() @IsString() - game: string; + game: string = ''; @ApiProperty() @IsNumber() - drivers: number; + drivers: number = 0; @ApiProperty() @IsNumber() - avgViewsPerRace: number; + avgViewsPerRace: number = 0; @ApiProperty({ type: Object }) mainSponsorSlot: { available: boolean; price: number; - }; + } = { available: false, price: 0 }; @ApiProperty({ type: Object }) secondarySlots: { available: number; total: number; price: number; - }; + } = { available: 0, total: 0, price: 0 }; @ApiProperty() @IsNumber() - rating: number; + rating: number = 0; @ApiProperty({ enum: ['premium', 'standard', 'starter'] }) @IsEnum(['premium', 'standard', 'starter']) - tier: 'premium' | 'standard' | 'starter'; + tier: 'premium' | 'standard' | 'starter' = 'standard'; @ApiProperty({ required: false }) @IsOptional() @@ -50,9 +50,9 @@ export class AvailableLeagueDTO { @ApiProperty({ enum: ['active', 'upcoming', 'completed'] }) @IsEnum(['active', 'upcoming', 'completed']) - seasonStatus: 'active' | 'upcoming' | 'completed'; + seasonStatus: 'active' | 'upcoming' | 'completed' = 'active'; @ApiProperty() @IsString() - description: string; + description: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/BillingStatsDTO.ts b/apps/api/src/domain/sponsor/dtos/BillingStatsDTO.ts index 48e593968..cd44b7b1e 100644 --- a/apps/api/src/domain/sponsor/dtos/BillingStatsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/BillingStatsDTO.ts @@ -4,25 +4,25 @@ import { IsNumber, IsDateString } from 'class-validator'; export class BillingStatsDTO { @ApiProperty() @IsNumber() - totalSpent: number; + totalSpent: number = 0; @ApiProperty() @IsNumber() - pendingAmount: number; + pendingAmount: number = 0; @ApiProperty() @IsDateString() - nextPaymentDate: string; + nextPaymentDate: string = ''; @ApiProperty() @IsNumber() - nextPaymentAmount: number; + nextPaymentAmount: number = 0; @ApiProperty() @IsNumber() - activeSponsorships: number; + activeSponsorships: number = 0; @ApiProperty() @IsNumber() - averageMonthlySpend: number; + averageMonthlySpend: number = 0; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts b/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts index 66b8b838d..f8c5751f6 100644 --- a/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/CreateSponsorOutputDTO.ts @@ -3,5 +3,5 @@ import { SponsorDTO } from './SponsorDTO'; export class CreateSponsorOutputDTO { @ApiProperty({ type: SponsorDTO }) - sponsor: SponsorDTO; + sponsor: SponsorDTO = new SponsorDTO(); } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/DriverDTO.ts b/apps/api/src/domain/sponsor/dtos/DriverDTO.ts index d7c240c43..dce2103fc 100644 --- a/apps/api/src/domain/sponsor/dtos/DriverDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/DriverDTO.ts @@ -4,29 +4,29 @@ import { IsString, IsNumber } from 'class-validator'; export class DriverDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - name: string; + name: string = ''; @ApiProperty() @IsString() - country: string; + country: string = ''; @ApiProperty() @IsNumber() - position: number; + position: number = 0; @ApiProperty() @IsNumber() - races: number; + races: number = 0; @ApiProperty() @IsNumber() - impressions: number; + impressions: number = 0; @ApiProperty() @IsString() - team: string; + team: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts b/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts index 9f2d54474..b870b15db 100644 --- a/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetEntitySponsorshipPricingResultDTO.ts @@ -3,11 +3,11 @@ import { SponsorshipPricingItemDTO } from './SponsorshipPricingItemDTO'; export class GetEntitySponsorshipPricingResultDTO { @ApiProperty() - entityType: string; + entityType: string = ''; @ApiProperty() - entityId: string; + entityId: string = ''; @ApiProperty({ type: [SponsorshipPricingItemDTO] }) - pricing: SponsorshipPricingItemDTO[]; + pricing: SponsorshipPricingItemDTO[] = []; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetPendingSponsorshipRequestsOutputDTO.ts b/apps/api/src/domain/sponsor/dtos/GetPendingSponsorshipRequestsOutputDTO.ts index 73dbb6598..ed0a7ea76 100644 --- a/apps/api/src/domain/sponsor/dtos/GetPendingSponsorshipRequestsOutputDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetPendingSponsorshipRequestsOutputDTO.ts @@ -3,14 +3,14 @@ import { SponsorshipRequestDTO } from './SponsorshipRequestDTO'; export class GetPendingSponsorshipRequestsOutputDTO { @ApiProperty() - entityType: string; + entityType: string = ''; @ApiProperty() - entityId: string; + entityId: string = ''; @ApiProperty({ type: [SponsorshipRequestDTO] }) - requests: SponsorshipRequestDTO[]; + requests: SponsorshipRequestDTO[] = []; @ApiProperty() - totalCount: number; + totalCount: number = 0; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts index 9b9ce93a5..5727f7d18 100644 --- a/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetSponsorDashboardQueryParamsDTO.ts @@ -4,5 +4,5 @@ import { IsString } from 'class-validator'; export class GetSponsorDashboardQueryParamsDTO { @ApiProperty() @IsString() - sponsorId: string; + sponsorId: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorOutputDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorOutputDTO.ts index 85d00d651..6536d98c1 100644 --- a/apps/api/src/domain/sponsor/dtos/GetSponsorOutputDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetSponsorOutputDTO.ts @@ -3,5 +3,5 @@ import { SponsorDTO } from './SponsorDTO'; export class GetSponsorOutputDTO { @ApiProperty({ type: SponsorDTO }) - sponsor: SponsorDTO; + sponsor: SponsorDTO = new SponsorDTO(); } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts index 1e9c66e3b..06cac8de8 100644 --- a/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetSponsorSponsorshipsQueryParamsDTO.ts @@ -4,5 +4,5 @@ import { IsString } from 'class-validator'; export class GetSponsorSponsorshipsQueryParamsDTO { @ApiProperty() @IsString() - sponsorId: string; + sponsorId: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts b/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts index 82f4f748e..aa114f043 100644 --- a/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/GetSponsorsOutputDTO.ts @@ -3,5 +3,5 @@ import { SponsorDTO } from './SponsorDTO'; export class GetSponsorsOutputDTO { @ApiProperty({ type: [SponsorDTO] }) - sponsors: SponsorDTO[]; + sponsors: SponsorDTO[] = []; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/InvoiceDTO.ts b/apps/api/src/domain/sponsor/dtos/InvoiceDTO.ts index 63a7df77e..007995367 100644 --- a/apps/api/src/domain/sponsor/dtos/InvoiceDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/InvoiceDTO.ts @@ -4,45 +4,45 @@ import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator'; export class InvoiceDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - invoiceNumber: string; + invoiceNumber: string = ''; @ApiProperty() @IsDateString() - date: string; + date: string = ''; @ApiProperty() @IsDateString() - dueDate: string; + dueDate: string = ''; @ApiProperty() @IsNumber() - amount: number; + amount: number = 0; @ApiProperty() @IsNumber() - vatAmount: number; + vatAmount: number = 0; @ApiProperty() @IsNumber() - totalAmount: number; + totalAmount: number = 0; @ApiProperty({ enum: ['paid', 'pending', 'overdue', 'failed'] }) @IsEnum(['paid', 'pending', 'overdue', 'failed']) - status: 'paid' | 'pending' | 'overdue' | 'failed'; + status: 'paid' | 'pending' | 'overdue' | 'failed' = 'pending'; @ApiProperty() @IsString() - description: string; + description: string = ''; @ApiProperty({ enum: ['league', 'team', 'driver', 'race', 'platform'] }) @IsEnum(['league', 'team', 'driver', 'race', 'platform']) - sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; + sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform' = 'league'; @ApiProperty() @IsString() - pdfUrl: string; + pdfUrl: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/LeagueDetailDTO.ts b/apps/api/src/domain/sponsor/dtos/LeagueDetailDTO.ts index d98a98cc7..7ba5d992e 100644 --- a/apps/api/src/domain/sponsor/dtos/LeagueDetailDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/LeagueDetailDTO.ts @@ -1,68 +1,68 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsEnum, IsNumber, IsDateString, IsOptional } from 'class-validator'; +import { IsString, IsEnum, IsNumber, IsOptional } from 'class-validator'; export class LeagueDetailDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - name: string; + name: string = ''; @ApiProperty() @IsString() - game: string; + game: string = ''; @ApiProperty({ enum: ['premium', 'standard', 'starter'] }) @IsEnum(['premium', 'standard', 'starter']) - tier: 'premium' | 'standard' | 'starter'; + tier: 'premium' | 'standard' | 'starter' = 'standard'; @ApiProperty() @IsString() - season: string; + season: string = ''; @ApiProperty() @IsString() - description: string; + description: string = ''; @ApiProperty() @IsNumber() - drivers: number; + drivers: number = 0; @ApiProperty() @IsNumber() - races: number; + races: number = 0; @ApiProperty() @IsNumber() - completedRaces: number; + completedRaces: number = 0; @ApiProperty() @IsNumber() - totalImpressions: number; + totalImpressions: number = 0; @ApiProperty() @IsNumber() - avgViewsPerRace: number; + avgViewsPerRace: number = 0; @ApiProperty() @IsNumber() - engagement: number; + engagement: number = 0; @ApiProperty() @IsNumber() - rating: number; + rating: number = 0; @ApiProperty({ enum: ['active', 'upcoming', 'completed'] }) @IsEnum(['active', 'upcoming', 'completed']) - seasonStatus: 'active' | 'upcoming' | 'completed'; + seasonStatus: 'active' | 'upcoming' | 'completed' = 'active'; @ApiProperty({ type: Object }) seasonDates: { start: string; end: string; - }; + } = { start: '', end: '' }; @ApiProperty({ type: Object, required: false }) @IsOptional() @@ -84,5 +84,8 @@ export class LeagueDetailDTO { price: number; benefits: string[]; }; + } = { + main: { available: false, price: 0, benefits: [] }, + secondary: { available: 0, total: 0, price: 0, benefits: [] } }; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/NotificationSettingsDTO.ts b/apps/api/src/domain/sponsor/dtos/NotificationSettingsDTO.ts index 81f3d5828..ac3fc67e0 100644 --- a/apps/api/src/domain/sponsor/dtos/NotificationSettingsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/NotificationSettingsDTO.ts @@ -4,25 +4,25 @@ import { IsBoolean } from 'class-validator'; export class NotificationSettingsDTO { @ApiProperty() @IsBoolean() - emailNewSponsorships: boolean; + emailNewSponsorships: boolean = false; @ApiProperty() @IsBoolean() - emailWeeklyReport: boolean; + emailWeeklyReport: boolean = false; @ApiProperty() @IsBoolean() - emailRaceAlerts: boolean; + emailRaceAlerts: boolean = false; @ApiProperty() @IsBoolean() - emailPaymentAlerts: boolean; + emailPaymentAlerts: boolean = false; @ApiProperty() @IsBoolean() - emailNewOpportunities: boolean; + emailNewOpportunities: boolean = false; @ApiProperty() @IsBoolean() - emailContractExpiry: boolean; + emailContractExpiry: boolean = false; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/PaymentMethodDTO.ts b/apps/api/src/domain/sponsor/dtos/PaymentMethodDTO.ts index e9472416c..27cefb742 100644 --- a/apps/api/src/domain/sponsor/dtos/PaymentMethodDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/PaymentMethodDTO.ts @@ -4,15 +4,15 @@ import { IsString, IsEnum, IsBoolean, IsOptional, IsNumber } from 'class-validat export class PaymentMethodDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty({ enum: ['card', 'bank', 'sepa'] }) @IsEnum(['card', 'bank', 'sepa']) - type: 'card' | 'bank' | 'sepa'; + type: 'card' | 'bank' | 'sepa' = 'card'; @ApiProperty() @IsString() - last4: string; + last4: string = ''; @ApiProperty({ required: false }) @IsOptional() @@ -21,7 +21,7 @@ export class PaymentMethodDTO { @ApiProperty() @IsBoolean() - isDefault: boolean; + isDefault: boolean = false; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/sponsor/dtos/PrivacySettingsDTO.ts b/apps/api/src/domain/sponsor/dtos/PrivacySettingsDTO.ts index 4d5b21120..6254e9519 100644 --- a/apps/api/src/domain/sponsor/dtos/PrivacySettingsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/PrivacySettingsDTO.ts @@ -4,17 +4,17 @@ import { IsBoolean } from 'class-validator'; export class PrivacySettingsDTO { @ApiProperty() @IsBoolean() - publicProfile: boolean; + publicProfile: boolean = false; @ApiProperty() @IsBoolean() - showStats: boolean; + showStats: boolean = false; @ApiProperty() @IsBoolean() - showActiveSponsorships: boolean; + showActiveSponsorships: boolean = false; @ApiProperty() @IsBoolean() - allowDirectContact: boolean; + allowDirectContact: boolean = false; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/RaceDTO.ts b/apps/api/src/domain/sponsor/dtos/RaceDTO.ts index 3e723a4c6..2b741ff87 100644 --- a/apps/api/src/domain/sponsor/dtos/RaceDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/RaceDTO.ts @@ -4,21 +4,21 @@ import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator'; export class RaceDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - name: string; + name: string = ''; @ApiProperty() @IsDateString() - date: string; + date: string = ''; @ApiProperty() @IsNumber() - views: number; + views: number = 0; @ApiProperty({ enum: ['upcoming', 'completed'] }) @IsEnum(['upcoming', 'completed']) - status: 'upcoming' | 'completed'; + status: 'upcoming' | 'completed' = 'upcoming'; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/RejectSponsorshipRequestInputDTO.ts b/apps/api/src/domain/sponsor/dtos/RejectSponsorshipRequestInputDTO.ts index d8f8a5179..c2662dd60 100644 --- a/apps/api/src/domain/sponsor/dtos/RejectSponsorshipRequestInputDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/RejectSponsorshipRequestInputDTO.ts @@ -5,7 +5,7 @@ export class RejectSponsorshipRequestInputDTO { @ApiProperty() @IsString() @IsNotEmpty() - respondedBy: string; + respondedBy: string = ''; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/sponsor/dtos/RenewalAlertDTO.ts b/apps/api/src/domain/sponsor/dtos/RenewalAlertDTO.ts index d4c55d2c1..9be876284 100644 --- a/apps/api/src/domain/sponsor/dtos/RenewalAlertDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/RenewalAlertDTO.ts @@ -4,21 +4,21 @@ import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator'; export class RenewalAlertDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - name: string; + name: string = ''; @ApiProperty({ enum: ['league', 'team', 'driver', 'race', 'platform'] }) @IsEnum(['league', 'team', 'driver', 'race', 'platform']) - type: 'league' | 'team' | 'driver' | 'race' | 'platform'; + type: 'league' | 'team' | 'driver' | 'race' | 'platform' = 'league'; @ApiProperty() @IsDateString() - renewDate: string; + renewDate: string = ''; @ApiProperty() @IsNumber() - price: number; + price: number = 0; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts index 2f6589481..229d0ecea 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorDTO.ts @@ -2,10 +2,10 @@ import { ApiProperty } from '@nestjs/swagger'; export class SponsorDTO { @ApiProperty() - id: string; + id: string = ''; @ApiProperty() - name: string; + name: string = ''; @ApiProperty({ required: false }) contactEmail?: string; diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts index 0e13e473f..742ac05fb 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorDashboardDTO.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsArray, IsObject } from 'class-validator'; +import { IsString } from 'class-validator'; import { SponsorDashboardMetricsDTO } from './SponsorDashboardMetricsDTO'; import { SponsoredLeagueDTO } from './SponsoredLeagueDTO'; import { SponsorDashboardInvestmentDTO } from './SponsorDashboardInvestmentDTO'; @@ -10,20 +10,20 @@ import { RenewalAlertDTO } from './RenewalAlertDTO'; export class SponsorDashboardDTO { @ApiProperty() @IsString() - sponsorId: string; + sponsorId: string = ''; @ApiProperty() @IsString() - sponsorName: string; + sponsorName: string = ''; @ApiProperty({ type: SponsorDashboardMetricsDTO }) - metrics: SponsorDashboardMetricsDTO; + metrics: SponsorDashboardMetricsDTO = new SponsorDashboardMetricsDTO(); @ApiProperty({ type: [SponsoredLeagueDTO] }) - sponsoredLeagues: SponsoredLeagueDTO[]; + sponsoredLeagues: SponsoredLeagueDTO[] = []; @ApiProperty({ type: SponsorDashboardInvestmentDTO }) - investment: SponsorDashboardInvestmentDTO; + investment: SponsorDashboardInvestmentDTO = new SponsorDashboardInvestmentDTO(); @ApiProperty({ type: Object }) sponsorships: { @@ -32,11 +32,17 @@ export class SponsorDashboardDTO { drivers: SponsorshipDTO[]; races: SponsorshipDTO[]; platform: SponsorshipDTO[]; + } = { + leagues: [], + teams: [], + drivers: [], + races: [], + platform: [] }; @ApiProperty({ type: [ActivityItemDTO] }) - recentActivity: ActivityItemDTO[]; + recentActivity: ActivityItemDTO[] = []; @ApiProperty({ type: [RenewalAlertDTO] }) - upcomingRenewals: RenewalAlertDTO[]; + upcomingRenewals: RenewalAlertDTO[] = []; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts index c2ad67210..dde4e600b 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorDashboardInvestmentDTO.ts @@ -4,13 +4,13 @@ import { IsNumber } from 'class-validator'; export class SponsorDashboardInvestmentDTO { @ApiProperty() @IsNumber() - activeSponsorships: number; + activeSponsorships: number = 0; @ApiProperty() @IsNumber() - totalInvestment: number; + totalInvestment: number = 0; @ApiProperty() @IsNumber() - costPerThousandViews: number; + costPerThousandViews: number = 0; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts index 60aabc0b0..459f649fe 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorDashboardMetricsDTO.ts @@ -4,33 +4,33 @@ import { IsNumber } from 'class-validator'; export class SponsorDashboardMetricsDTO { @ApiProperty() @IsNumber() - impressions: number; + impressions: number = 0; @ApiProperty() @IsNumber() - impressionsChange: number; + impressionsChange: number = 0; @ApiProperty() @IsNumber() - uniqueViewers: number; + uniqueViewers: number = 0; @ApiProperty() @IsNumber() - viewersChange: number; + viewersChange: number = 0; @ApiProperty() @IsNumber() - races: number; + races: number = 0; @ApiProperty() @IsNumber() - drivers: number; + drivers: number = 0; @ApiProperty() @IsNumber() - exposure: number; + exposure: number = 0; @ApiProperty() @IsNumber() - exposureChange: number; + exposureChange: number = 0; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorProfileDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorProfileDTO.ts index ae9fcf430..102ee431d 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorProfileDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorProfileDTO.ts @@ -1,30 +1,30 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsOptional, IsObject } from 'class-validator'; +import { IsString, IsOptional } from 'class-validator'; export class SponsorProfileDTO { @ApiProperty() @IsString() - companyName: string; + companyName: string = ''; @ApiProperty() @IsString() - contactName: string; + contactName: string = ''; @ApiProperty() @IsString() - contactEmail: string; + contactEmail: string = ''; @ApiProperty() @IsString() - contactPhone: string; + contactPhone: string = ''; @ApiProperty() @IsString() - website: string; + website: string = ''; @ApiProperty() @IsString() - description: string; + description: string = ''; @ApiProperty({ required: false }) @IsOptional() @@ -33,7 +33,7 @@ export class SponsorProfileDTO { @ApiProperty() @IsString() - industry: string; + industry: string = ''; @ApiProperty({ type: Object }) address: { @@ -41,16 +41,16 @@ export class SponsorProfileDTO { city: string; country: string; postalCode: string; - }; + } = { street: '', city: '', country: '', postalCode: '' }; @ApiProperty() @IsString() - taxId: string; + taxId: string = ''; @ApiProperty({ type: Object }) socialLinks: { twitter: string; linkedin: string; instagram: string; - }; + } = { twitter: '', linkedin: '', instagram: '' }; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts index 8b45bca7f..651319fe4 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorSponsorshipsDTO.ts @@ -5,14 +5,14 @@ import { SponsorshipDetailDTO } from './SponsorshipDetailDTO'; export class SponsorSponsorshipsDTO { @ApiProperty() @IsString() - sponsorId: string; + sponsorId: string = ''; @ApiProperty() @IsString() - sponsorName: string; + sponsorName: string = ''; @ApiProperty({ type: [SponsorshipDetailDTO] }) - sponsorships: SponsorshipDetailDTO[]; + sponsorships: SponsorshipDetailDTO[] = []; @ApiProperty() summary: { @@ -21,5 +21,11 @@ export class SponsorSponsorshipsDTO { totalInvestment: number; totalPlatformFees: number; currency: string; + } = { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: 0, + totalPlatformFees: 0, + currency: '' }; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts index 081adbe49..4f19021b8 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsoredLeagueDTO.ts @@ -4,29 +4,29 @@ import { IsString, IsEnum, IsNumber } from 'class-validator'; export class SponsoredLeagueDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - name: string; + name: string = ''; @ApiProperty({ enum: ['main', 'secondary'] }) @IsEnum(['main', 'secondary']) - tier: 'main' | 'secondary'; + tier: 'main' | 'secondary' = 'main'; @ApiProperty() @IsNumber() - drivers: number; + drivers: number = 0; @ApiProperty() @IsNumber() - races: number; + races: number = 0; @ApiProperty() @IsNumber() - impressions: number; + impressions: number = 0; @ApiProperty({ enum: ['active', 'upcoming', 'completed'] }) @IsEnum(['active', 'upcoming', 'completed']) - status: 'active' | 'upcoming' | 'completed'; + status: 'active' | 'upcoming' | 'completed' = 'active'; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorshipDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorshipDTO.ts index 038d858e5..027ed3887 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorshipDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorshipDTO.ts @@ -4,19 +4,19 @@ import { IsString, IsEnum, IsNumber, IsOptional, IsDateString } from 'class-vali export class SponsorshipDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty({ enum: ['leagues', 'teams', 'drivers', 'races', 'platform'] }) @IsEnum(['leagues', 'teams', 'drivers', 'races', 'platform']) - type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform'; + type: 'leagues' | 'teams' | 'drivers' | 'races' | 'platform' = 'leagues'; @ApiProperty() @IsString() - entityId: string; + entityId: string = ''; @ApiProperty() @IsString() - entityName: string; + entityName: string = ''; @ApiProperty({ enum: ['main', 'secondary'], required: false }) @IsOptional() @@ -25,7 +25,7 @@ export class SponsorshipDTO { @ApiProperty({ enum: ['active', 'pending_approval', 'approved', 'rejected', 'expired'] }) @IsEnum(['active', 'pending_approval', 'approved', 'rejected', 'expired']) - status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired'; + status: 'active' | 'pending_approval' | 'approved' | 'rejected' | 'expired' = 'pending_approval'; @ApiProperty({ required: false }) @IsOptional() @@ -44,19 +44,19 @@ export class SponsorshipDTO { @ApiProperty() @IsDateString() - startDate: string; + startDate: string = ''; @ApiProperty() @IsDateString() - endDate: string; + endDate: string = ''; @ApiProperty() @IsNumber() - price: number; + price: number = 0; @ApiProperty() @IsNumber() - impressions: number; + impressions: number = 0; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts index b37a92cb4..46c41b248 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorshipDetailDTO.ts @@ -4,23 +4,23 @@ import { IsString, IsEnum, IsOptional, IsDate } from 'class-validator'; export class SponsorshipDetailDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - leagueId: string; + leagueId: string = ''; @ApiProperty() @IsString() - leagueName: string; + leagueName: string = ''; @ApiProperty() @IsString() - seasonId: string; + seasonId: string = ''; @ApiProperty() @IsString() - seasonName: string; + seasonName: string = ''; @ApiProperty({ required: false }) @IsOptional() @@ -34,29 +34,29 @@ export class SponsorshipDetailDTO { @ApiProperty({ enum: ['main', 'secondary'] }) @IsEnum(['main', 'secondary']) - tier: 'main' | 'secondary'; + tier: 'main' | 'secondary' = 'main'; @ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] }) @IsEnum(['pending', 'active', 'expired', 'cancelled']) - status: 'pending' | 'active' | 'expired' | 'cancelled'; + status: 'pending' | 'active' | 'expired' | 'cancelled' = 'pending'; @ApiProperty() pricing: { amount: number; currency: string; - }; + } = { amount: 0, currency: '' }; @ApiProperty() platformFee: { amount: number; currency: string; - }; + } = { amount: 0, currency: '' }; @ApiProperty() netAmount: { amount: number; currency: string; - }; + } = { amount: 0, currency: '' }; @ApiProperty() metrics: { @@ -64,10 +64,10 @@ export class SponsorshipDetailDTO { races: number; completedRaces: number; impressions: number; - }; + } = { drivers: 0, races: 0, completedRaces: 0, impressions: 0 }; @ApiProperty() - createdAt: Date; + createdAt: Date = new Date(); @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts index cce455adc..7f4cf09f5 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorshipPricingItemDTO.ts @@ -4,17 +4,17 @@ import { IsString, IsNumber } from 'class-validator'; export class SponsorshipPricingItemDTO { @ApiProperty() @IsString() - id: string; + id: string = ''; @ApiProperty() @IsString() - level: string; + level: string = ''; @ApiProperty() @IsNumber() - price: number; + price: number = 0; @ApiProperty() @IsString() - currency: string; + currency: string = ''; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/dtos/SponsorshipRequestDTO.ts b/apps/api/src/domain/sponsor/dtos/SponsorshipRequestDTO.ts index c09d97250..748936232 100644 --- a/apps/api/src/domain/sponsor/dtos/SponsorshipRequestDTO.ts +++ b/apps/api/src/domain/sponsor/dtos/SponsorshipRequestDTO.ts @@ -2,38 +2,38 @@ import { ApiProperty } from '@nestjs/swagger'; export class SponsorshipRequestDTO { @ApiProperty() - id: string; + id: string = ''; @ApiProperty() - sponsorId: string; + sponsorId: string = ''; @ApiProperty() - sponsorName: string; + sponsorName: string = ''; @ApiProperty({ required: false }) sponsorLogo?: string; @ApiProperty() - tier: string; + tier: string = ''; @ApiProperty() - offeredAmount: number; + offeredAmount: number = 0; @ApiProperty() - currency: string; + currency: string = ''; @ApiProperty() - formattedAmount: string; + formattedAmount: string = ''; @ApiProperty({ required: false }) message?: string; @ApiProperty() - createdAt: Date; + createdAt: Date = new Date(); @ApiProperty() - platformFee: number; + platformFee: number = 0; @ApiProperty() - netAmount: number; + netAmount: number = 0; } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/AcceptSponsorshipRequestPresenter.ts b/apps/api/src/domain/sponsor/presenters/AcceptSponsorshipRequestPresenter.ts index b47cb06bd..3f51fd472 100644 --- a/apps/api/src/domain/sponsor/presenters/AcceptSponsorshipRequestPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/AcceptSponsorshipRequestPresenter.ts @@ -1,4 +1,4 @@ -import type { AcceptSponsorshipOutputPort } from '@core/racing/application/ports/output/AcceptSponsorshipOutputPort'; +import type { AcceptSponsorshipResult } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase'; export interface AcceptSponsorshipRequestResultViewModel { requestId: string; @@ -16,7 +16,7 @@ export class AcceptSponsorshipRequestPresenter { this.result = null; } - present(output: AcceptSponsorshipOutputPort | null) { + present(output: AcceptSponsorshipResult | null) { if (!output) { this.result = null; return; diff --git a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts index 20eebf9b2..306c0a58d 100644 --- a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { CreateSponsorPresenter } from './CreateSponsorPresenter'; +import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; describe('CreateSponsorPresenter', () => { let presenter: CreateSponsorPresenter; @@ -10,9 +11,23 @@ describe('CreateSponsorPresenter', () => { describe('reset', () => { it('should reset the result to null', () => { - const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; - presenter.present(mockPort); - expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + presenter.present(mockSponsor); + + const expectedViewModel = { + sponsor: { + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: mockSponsor.createdAt.toDate() + } + }; + expect(presenter.viewModel).toEqual(expectedViewModel); presenter.reset(); expect(() => presenter.viewModel).toThrow('Presenter not presented'); @@ -21,11 +36,24 @@ describe('CreateSponsorPresenter', () => { describe('present', () => { it('should store the result', () => { - const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); - presenter.present(mockPort); + presenter.present(mockSponsor); - expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); + const expectedViewModel = { + sponsor: { + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: mockSponsor.createdAt.toDate() + } + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); @@ -35,10 +63,23 @@ describe('CreateSponsorPresenter', () => { }); it('should return the result when presented', () => { - const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; - presenter.present(mockPort); + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + presenter.present(mockSponsor); - expect(presenter.getViewModel()).toEqual({ sponsor: mockPort.sponsor }); + const expectedViewModel = { + sponsor: { + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: mockSponsor.createdAt.toDate() + } + }; + expect(presenter.getViewModel()).toEqual(expectedViewModel); }); }); @@ -48,10 +89,23 @@ describe('CreateSponsorPresenter', () => { }); it('should return the result when presented', () => { - const mockPort = { sponsor: { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com', createdAt: new Date() } }; - presenter.present(mockPort); + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + presenter.present(mockSponsor); - expect(presenter.viewModel).toEqual({ sponsor: mockPort.sponsor }); + const expectedViewModel = { + sponsor: { + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: mockSponsor.createdAt.toDate() + } + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts index a4cf04623..150e65e5e 100644 --- a/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/CreateSponsorPresenter.ts @@ -1,4 +1,4 @@ -import type { CreateSponsorOutputPort } from '@core/racing/application/ports/output/CreateSponsorOutputPort'; +import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; import type { CreateSponsorOutputDTO } from '../dtos/CreateSponsorOutputDTO'; export class CreateSponsorPresenter { @@ -8,16 +8,24 @@ export class CreateSponsorPresenter { this.result = null; } - present(port: CreateSponsorOutputPort) { + present(sponsor: Sponsor) { + const sponsorData: any = { + id: sponsor.id.toString(), + name: sponsor.name.toString(), + contactEmail: sponsor.contactEmail.toString(), + createdAt: sponsor.createdAt.toDate(), + }; + + if (sponsor.logoUrl) { + sponsorData.logoUrl = sponsor.logoUrl.toString(); + } + + if (sponsor.websiteUrl) { + sponsorData.websiteUrl = sponsor.websiteUrl.toString(); + } + this.result = { - sponsor: { - id: port.sponsor.id, - name: port.sponsor.name, - contactEmail: port.sponsor.contactEmail, - logoUrl: port.sponsor.logoUrl, - websiteUrl: port.sponsor.websiteUrl, - createdAt: port.sponsor.createdAt, - }, + sponsor: sponsorData, }; } diff --git a/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.test.ts index 101d735c7..bdd6b7b77 100644 --- a/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { GetEntitySponsorshipPricingPresenter } from './GetEntitySponsorshipPricingPresenter'; +import type { GetEntitySponsorshipPricingResult } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase'; describe('GetEntitySponsorshipPricingPresenter', () => { let presenter: GetEntitySponsorshipPricingPresenter; @@ -10,9 +11,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => { describe('reset', () => { it('should reset the result to null', () => { - const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] }; + const mockResult: GetEntitySponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + acceptingApplications: true, + tiers: [] + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.viewModel).toEqual(expectedViewModel); presenter.reset(); expect(() => presenter.viewModel).toThrow('Presenter not presented'); @@ -21,11 +33,21 @@ describe('GetEntitySponsorshipPricingPresenter', () => { describe('present', () => { it('should store the result', () => { - const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] }; + const mockResult: GetEntitySponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + acceptingApplications: true, + tiers: [] + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); @@ -35,10 +57,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] }; + const mockResult: GetEntitySponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + acceptingApplications: true, + tiers: [] + }; presenter.present(mockResult); - expect(presenter.getViewModel()).toEqual(mockResult); + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.getViewModel()).toEqual(expectedViewModel); }); }); @@ -48,10 +80,20 @@ describe('GetEntitySponsorshipPricingPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] }; + const mockResult: GetEntitySponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + acceptingApplications: true, + tiers: [] + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts index 6d44d8ad0..b3710bde5 100644 --- a/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetEntitySponsorshipPricingPresenter.ts @@ -1,4 +1,4 @@ -import type { GetSponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetSponsorshipPricingOutputPort'; +import type { GetEntitySponsorshipPricingResult } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase'; import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO'; export class GetEntitySponsorshipPricingPresenter { @@ -8,7 +8,7 @@ export class GetEntitySponsorshipPricingPresenter { this.result = null; } - present(output: GetSponsorshipPricingOutputPort | null) { + present(output: GetEntitySponsorshipPricingResult | null) { if (!output) { this.result = { entityType: 'season', @@ -21,11 +21,11 @@ export class GetEntitySponsorshipPricingPresenter { this.result = { entityType: output.entityType, entityId: output.entityId, - pricing: output.pricing.map(item => ({ - id: item.id, - level: item.level, + pricing: output.tiers.map(item => ({ + id: item.name, + level: item.name, price: item.price, - currency: item.currency, + currency: 'USD', })), }; } diff --git a/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts index c6c90ebd8..ef4112b2e 100644 --- a/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetPendingSponsorshipRequestsPresenter.ts @@ -1,4 +1,4 @@ -import type { PendingSponsorshipRequestsOutputPort } from '@core/racing/application/ports/output/PendingSponsorshipRequestsOutputPort'; +import type { GetPendingSponsorshipRequestsResult } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase'; import { GetPendingSponsorshipRequestsOutputDTO } from '../dtos/GetPendingSponsorshipRequestsOutputDTO'; export class GetPendingSponsorshipRequestsPresenter { @@ -8,7 +8,7 @@ export class GetPendingSponsorshipRequestsPresenter { this.result = null; } - present(outputPort: PendingSponsorshipRequestsOutputPort | null) { + present(outputPort: GetPendingSponsorshipRequestsResult | null) { if (!outputPort) { this.result = null; return; @@ -17,7 +17,30 @@ export class GetPendingSponsorshipRequestsPresenter { this.result = { entityType: outputPort.entityType, entityId: outputPort.entityId, - requests: outputPort.requests, + requests: outputPort.requests.map(r => { + const request: any = { + id: r.request.id, + sponsorId: r.request.sponsorId, + sponsorName: r.sponsor?.name?.toString() || 'Unknown Sponsor', + tier: r.request.tier, + offeredAmount: r.financials.offeredAmount.amount, + currency: r.financials.offeredAmount.currency, + formattedAmount: `${r.financials.offeredAmount.amount} ${r.financials.offeredAmount.currency}`, + createdAt: r.request.createdAt, + platformFee: r.financials.platformFee.amount, + netAmount: r.financials.netAmount.amount, + }; + + if (r.sponsor?.logoUrl) { + request.sponsorLogo = r.sponsor.logoUrl.toString(); + } + + if (r.request.message) { + request.message = r.request.message; + } + + return request; + }), totalCount: outputPort.totalCount, }; } diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.test.ts index 43c01f9d2..e7b484035 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { GetSponsorDashboardPresenter } from './GetSponsorDashboardPresenter'; +import type { GetSponsorDashboardResult } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; +import { Money } from '@core/racing/domain/value-objects/Money'; describe('GetSponsorDashboardPresenter', () => { let presenter: GetSponsorDashboardPresenter; @@ -10,9 +12,58 @@ describe('GetSponsorDashboardPresenter', () => { describe('reset', () => { it('should reset the result to null', () => { - const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] }; + const mockResult: GetSponsorDashboardResult = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + costPerThousandViews: 0, + }, + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: 0, + costPerThousandViews: 0, + }, + sponsorships: { + leagues: [], + teams: [], + drivers: [], + races: [], + platform: [], + }, + recentActivity: [], + upcomingRenewals: [], + }; + expect(presenter.viewModel).toEqual(expectedViewModel); presenter.reset(); expect(() => presenter.viewModel).toThrow('Presenter not presented'); @@ -21,11 +72,59 @@ describe('GetSponsorDashboardPresenter', () => { describe('present', () => { it('should store the result', () => { - const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] }; + const mockResult: GetSponsorDashboardResult = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + costPerThousandViews: 0, + }, + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: 0, + costPerThousandViews: 0, + }, + sponsorships: { + leagues: [], + teams: [], + drivers: [], + races: [], + platform: [], + }, + recentActivity: [], + upcomingRenewals: [], + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); @@ -35,10 +134,58 @@ describe('GetSponsorDashboardPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] }; + const mockResult: GetSponsorDashboardResult = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + costPerThousandViews: 0, + }, + }; presenter.present(mockResult); - expect(presenter.getViewModel()).toEqual(mockResult); + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: 0, + costPerThousandViews: 0, + }, + sponsorships: { + leagues: [], + teams: [], + drivers: [], + races: [], + platform: [], + }, + recentActivity: [], + upcomingRenewals: [], + }; + expect(presenter.getViewModel()).toEqual(expectedViewModel); }); }); @@ -48,10 +195,58 @@ describe('GetSponsorDashboardPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] }; + const mockResult: GetSponsorDashboardResult = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + costPerThousandViews: 0, + }, + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + metrics: { + impressions: 1000, + impressionsChange: 0, + uniqueViewers: 700, + viewersChange: 0, + races: 5, + drivers: 10, + exposure: 50, + exposureChange: 0, + }, + sponsoredLeagues: [], + investment: { + activeSponsorships: 0, + totalInvestment: 0, + costPerThousandViews: 0, + }, + sponsorships: { + leagues: [], + teams: [], + drivers: [], + races: [], + platform: [], + }, + recentActivity: [], + upcomingRenewals: [], + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts index 86154b71a..f2d6dd882 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorDashboardPresenter.ts @@ -1,4 +1,4 @@ -import type { SponsorDashboardOutputPort } from '@core/racing/application/ports/output/SponsorDashboardOutputPort'; +import type { GetSponsorDashboardResult } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase'; import { SponsorDashboardDTO } from '../dtos/SponsorDashboardDTO'; export class GetSponsorDashboardPresenter { @@ -8,8 +8,40 @@ export class GetSponsorDashboardPresenter { this.result = null; } - present(outputPort: SponsorDashboardOutputPort | null) { - this.result = outputPort ?? null; + present(outputPort: GetSponsorDashboardResult | null) { + if (!outputPort) { + this.result = null; + return; + } + + this.result = { + sponsorId: outputPort.sponsorId, + sponsorName: outputPort.sponsorName, + metrics: outputPort.metrics, + sponsoredLeagues: outputPort.sponsoredLeagues.map(league => ({ + id: league.leagueId, + name: league.leagueName, + tier: league.tier, + drivers: league.metrics.drivers, + races: league.metrics.races, + impressions: league.metrics.impressions, + status: league.status, + })), + investment: { + activeSponsorships: outputPort.investment.activeSponsorships, + totalInvestment: outputPort.investment.totalInvestment.amount, + costPerThousandViews: outputPort.investment.costPerThousandViews, + }, + sponsorships: { + leagues: [], + teams: [], + drivers: [], + races: [], + platform: [], + }, + recentActivity: [], + upcomingRenewals: [], + }; } getViewModel(): SponsorDashboardDTO | null { diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.test.ts index 0bbbbea48..6ef82f219 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { GetSponsorSponsorshipsPresenter } from './GetSponsorSponsorshipsPresenter'; +import type { GetSponsorSponsorshipsResult } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; +import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; +import { Money } from '@core/racing/domain/value-objects/Money'; describe('GetSponsorSponsorshipsPresenter', () => { let presenter: GetSponsorSponsorshipsPresenter; @@ -10,9 +13,38 @@ describe('GetSponsorSponsorshipsPresenter', () => { describe('reset', () => { it('should reset the result to null', () => { - const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] }; + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + + const mockResult: GetSponsorSponsorshipsResult = { + sponsor: mockSponsor, + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + totalPlatformFees: Money.create(0, 'USD'), + }, + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: 0, + totalPlatformFees: 0, + currency: 'USD', + }, + }; + expect(presenter.viewModel).toEqual(expectedViewModel); presenter.reset(); expect(() => presenter.viewModel).toThrow('Presenter not presented'); @@ -21,11 +53,39 @@ describe('GetSponsorSponsorshipsPresenter', () => { describe('present', () => { it('should store the result', () => { - const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] }; + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + + const mockResult: GetSponsorSponsorshipsResult = { + sponsor: mockSponsor, + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + totalPlatformFees: Money.create(0, 'USD'), + }, + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: 0, + totalPlatformFees: 0, + currency: 'USD', + }, + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); @@ -35,10 +95,38 @@ describe('GetSponsorSponsorshipsPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] }; + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + + const mockResult: GetSponsorSponsorshipsResult = { + sponsor: mockSponsor, + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + totalPlatformFees: Money.create(0, 'USD'), + }, + }; presenter.present(mockResult); - expect(presenter.getViewModel()).toEqual(mockResult); + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: 0, + totalPlatformFees: 0, + currency: 'USD', + }, + }; + expect(presenter.getViewModel()).toEqual(expectedViewModel); }); }); @@ -48,10 +136,38 @@ describe('GetSponsorSponsorshipsPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] }; + const mockSponsor = Sponsor.create({ + id: 'sponsor-1', + name: 'Test Sponsor', + contactEmail: 'test@example.com', + createdAt: new Date() + }); + + const mockResult: GetSponsorSponsorshipsResult = { + sponsor: mockSponsor, + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: Money.create(0, 'USD'), + totalPlatformFees: Money.create(0, 'USD'), + }, + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + sponsorId: 'sponsor-1', + sponsorName: 'Test Sponsor', + sponsorships: [], + summary: { + totalSponsorships: 0, + activeSponsorships: 0, + totalInvestment: 0, + totalPlatformFees: 0, + currency: 'USD', + }, + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts index 3bfec6139..552352d2b 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorSponsorshipsPresenter.ts @@ -1,4 +1,4 @@ -import type { SponsorSponsorshipsOutputPort } from '@core/racing/application/ports/output/SponsorSponsorshipsOutputPort'; +import type { GetSponsorSponsorshipsResult } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase'; import { SponsorSponsorshipsDTO } from '../dtos/SponsorSponsorshipsDTO'; export class GetSponsorSponsorshipsPresenter { @@ -8,8 +8,58 @@ export class GetSponsorSponsorshipsPresenter { this.result = null; } - present(outputPort: SponsorSponsorshipsOutputPort | null) { - this.result = outputPort ?? null; + present(outputPort: GetSponsorSponsorshipsResult | null) { + if (!outputPort) { + this.result = null; + return; + } + + this.result = { + sponsorId: outputPort.sponsor.id.toString(), + sponsorName: outputPort.sponsor.name.toString(), + sponsorships: outputPort.sponsorships.map(s => { + // Map status to DTO expected values + let status: 'pending' | 'active' | 'expired' | 'cancelled'; + if (s.sponsorship.status === 'ended') { + status = 'expired'; + } else if (s.sponsorship.status === 'pending' || s.sponsorship.status === 'active' || s.sponsorship.status === 'cancelled') { + status = s.sponsorship.status; + } else { + status = 'pending'; + } + + return { + id: s.sponsorship.id.toString(), + leagueId: s.league.id.toString(), + leagueName: s.league.name.toString(), + seasonId: s.season.id.toString(), + seasonName: s.season.name.toString(), + tier: s.sponsorship.tier, + status, + pricing: { + amount: s.financials.pricing.amount, + currency: s.financials.pricing.currency, + }, + platformFee: { + amount: s.financials.platformFee.amount, + currency: s.financials.platformFee.currency, + }, + netAmount: { + amount: s.financials.netAmount.amount, + currency: s.financials.netAmount.currency, + }, + metrics: s.metrics, + createdAt: s.sponsorship.createdAt, + }; + }), + summary: { + totalSponsorships: outputPort.summary.totalSponsorships, + activeSponsorships: outputPort.summary.activeSponsorships, + totalInvestment: outputPort.summary.totalInvestment.amount, + totalPlatformFees: outputPort.summary.totalPlatformFees.amount, + currency: outputPort.summary.totalInvestment.currency, + }, + }; } getViewModel(): SponsorSponsorshipsDTO | null { diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.test.ts index 62ef8322a..a4db24c36 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.test.ts @@ -1,11 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { Result } from '@core/shared/application/Result'; -import type { - GetSponsorsResult, - GetSponsorsErrorCode, -} from '@core/racing/application/use-cases/GetSponsorsUseCase'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { GetSponsorsPresenter } from './GetSponsorsPresenter'; +import { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; describe('GetSponsorsPresenter', () => { let presenter: GetSponsorsPresenter; @@ -16,8 +11,8 @@ describe('GetSponsorsPresenter', () => { describe('reset', () => { it('should reset the model to null and cause responseModel to throw', () => { - const result = Result.ok({ sponsors: [] }); - presenter.present(result); + const sponsors: Sponsor[] = []; + presenter.present(sponsors); expect(presenter.responseModel).toEqual({ sponsors: [] }); presenter.reset(); @@ -26,29 +21,24 @@ describe('GetSponsorsPresenter', () => { }); describe('present', () => { - it('should map Result.ok sponsors to DTO responseModel', () => { - const result = Result.ok({ - sponsors: [ - { - id: 'sponsor-1', - name: 'Sponsor One', - contactEmail: 's1@example.com', - logoUrl: 'logo1.png', - websiteUrl: 'https://one.example.com', - createdAt: new Date('2024-01-01T00:00:00Z'), - }, - { - id: 'sponsor-2', - name: 'Sponsor Two', - contactEmail: 's2@example.com', - logoUrl: undefined, - websiteUrl: undefined, - createdAt: undefined, - }, - ], - }); + it('should map sponsors to DTO responseModel', () => { + const sponsors: Sponsor[] = [ + Sponsor.create({ + id: 'sponsor-1', + name: 'Sponsor One', + contactEmail: 's1@example.com', + logoUrl: 'logo1.png', + websiteUrl: 'https://one.example.com', + createdAt: new Date('2024-01-01T00:00:00Z'), + }), + Sponsor.create({ + id: 'sponsor-2', + name: 'Sponsor Two', + contactEmail: 's2@example.com', + }), + ]; - presenter.present(result); + presenter.present(sponsors); expect(presenter.responseModel).toEqual({ sponsors: [ @@ -64,9 +54,7 @@ describe('GetSponsorsPresenter', () => { id: 'sponsor-2', name: 'Sponsor Two', contactEmail: 's2@example.com', - logoUrl: undefined, - websiteUrl: undefined, - createdAt: undefined, + createdAt: expect.any(Date), }, ], }); @@ -79,8 +67,8 @@ describe('GetSponsorsPresenter', () => { }); it('should return the model when presented', () => { - const result = Result.ok({ sponsors: [] }); - presenter.present(result); + const sponsors: Sponsor[] = []; + presenter.present(sponsors); expect(presenter.getResponseModel()).toEqual({ sponsors: [] }); }); @@ -91,16 +79,26 @@ describe('GetSponsorsPresenter', () => { expect(() => presenter.responseModel).toThrow('Presenter not presented'); }); - it('should fallback to empty sponsors list on error', () => { - const error = { - code: 'REPOSITORY_ERROR' as GetSponsorsErrorCode, - details: { message: 'DB error' }, - } satisfies ApplicationErrorCode; - const result = Result.err(error); + it('should return the model when presented', () => { + const sponsors: Sponsor[] = [ + Sponsor.create({ + id: 'sponsor-1', + name: 'Sponsor One', + contactEmail: 's1@example.com', + }), + ]; + presenter.present(sponsors); - presenter.present(result); - - expect(presenter.responseModel).toEqual({ sponsors: [] }); + expect(presenter.responseModel).toEqual({ + sponsors: [ + { + id: 'sponsor-1', + name: 'Sponsor One', + contactEmail: 's1@example.com', + createdAt: expect.any(Date), + }, + ], + }); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts index 7897c2cd2..d37dc1a5a 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorsPresenter.ts @@ -1,9 +1,4 @@ -import type { Result } from '@core/shared/application/Result'; -import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; -import type { - GetSponsorsResult, - GetSponsorsErrorCode, -} from '@core/racing/application/use-cases/GetSponsorsUseCase'; +import type { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor'; import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO'; import type { SponsorDTO } from '../dtos/SponsorDTO'; @@ -14,29 +9,26 @@ export class GetSponsorsPresenter { this.model = null; } - present( - result: Result< - GetSponsorsResult, - ApplicationErrorCode - >, - ): void { - if (result.isErr()) { - // For sponsor listing, fall back to empty list on error - this.model = { sponsors: [] }; - return; - } - - const output = result.unwrap(); - + present(sponsors: Sponsor[]): void { this.model = { - sponsors: output.sponsors.map((sponsor) => ({ - id: sponsor.id, - name: sponsor.name, - contactEmail: sponsor.contactEmail, - logoUrl: sponsor.logoUrl, - websiteUrl: sponsor.websiteUrl, - createdAt: sponsor.createdAt, - })), + sponsors: sponsors.map((sponsor) => { + const sponsorData: any = { + id: sponsor.id.toString(), + name: sponsor.name.toString(), + contactEmail: sponsor.contactEmail.toString(), + createdAt: sponsor.createdAt.toDate(), + }; + + if (sponsor.logoUrl) { + sponsorData.logoUrl = sponsor.logoUrl.toString(); + } + + if (sponsor.websiteUrl) { + sponsorData.websiteUrl = sponsor.websiteUrl.toString(); + } + + return sponsorData; + }), }; } diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.test.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.test.ts index 6d89aba42..eb2ca0d97 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.test.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter'; +import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; describe('GetSponsorshipPricingPresenter', () => { let presenter: GetSponsorshipPricingPresenter; @@ -10,9 +11,19 @@ describe('GetSponsorshipPricingPresenter', () => { describe('reset', () => { it('should reset the result to null', () => { - const mockResult = { tiers: [] }; + const mockResult: GetSponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.viewModel).toEqual(expectedViewModel); presenter.reset(); expect(() => presenter.viewModel).toThrow('Presenter not presented'); @@ -21,11 +32,20 @@ describe('GetSponsorshipPricingPresenter', () => { describe('present', () => { it('should store the result', () => { - const mockResult = { tiers: [] }; + const mockResult: GetSponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); @@ -35,10 +55,19 @@ describe('GetSponsorshipPricingPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { tiers: [] }; + const mockResult: GetSponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; presenter.present(mockResult); - expect(presenter.getViewModel()).toEqual(mockResult); + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.getViewModel()).toEqual(expectedViewModel); }); }); @@ -48,10 +77,19 @@ describe('GetSponsorshipPricingPresenter', () => { }); it('should return the result when presented', () => { - const mockResult = { tiers: [] }; + const mockResult: GetSponsorshipPricingResult = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; presenter.present(mockResult); - expect(presenter.viewModel).toEqual(mockResult); + const expectedViewModel = { + entityType: 'season', + entityId: 'season-1', + pricing: [] + }; + expect(presenter.viewModel).toEqual(expectedViewModel); }); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts b/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts index 029fbea6d..47a9e2587 100644 --- a/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/GetSponsorshipPricingPresenter.ts @@ -1,12 +1,32 @@ -import type { GetSponsorshipPricingOutputPort } from '@core/racing/application/ports/output/GetSponsorshipPricingOutputPort'; +import type { GetSponsorshipPricingResult } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase'; import { GetEntitySponsorshipPricingResultDTO } from '../dtos/GetEntitySponsorshipPricingResultDTO'; export class GetSponsorshipPricingPresenter { - present(outputPort: GetSponsorshipPricingOutputPort): GetEntitySponsorshipPricingResultDTO { - return { + private result: GetEntitySponsorshipPricingResultDTO | null = null; + + reset() { + this.result = null; + } + + present(outputPort: GetSponsorshipPricingResult): void { + this.result = { entityType: outputPort.entityType, entityId: outputPort.entityId, - pricing: outputPort.pricing, + pricing: outputPort.pricing.map(item => ({ + id: item.id, + level: item.level, + price: item.price, + currency: item.currency, + })), }; } + + getViewModel(): GetEntitySponsorshipPricingResultDTO | null { + return this.result; + } + + get viewModel(): GetEntitySponsorshipPricingResultDTO { + if (!this.result) throw new Error('Presenter not presented'); + return this.result; + } } \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/presenters/RejectSponsorshipRequestPresenter.ts b/apps/api/src/domain/sponsor/presenters/RejectSponsorshipRequestPresenter.ts index 85fee1b19..b2bfc4418 100644 --- a/apps/api/src/domain/sponsor/presenters/RejectSponsorshipRequestPresenter.ts +++ b/apps/api/src/domain/sponsor/presenters/RejectSponsorshipRequestPresenter.ts @@ -1,21 +1,21 @@ -import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; +import type { RejectSponsorshipRequestResult } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase'; export class RejectSponsorshipRequestPresenter { - private result: RejectSponsorshipRequestResultDTO | null = null; + private result: RejectSponsorshipRequestResult | null = null; reset() { this.result = null; } - present(output: RejectSponsorshipRequestResultDTO | null) { + present(output: RejectSponsorshipRequestResult | null) { this.result = output ?? null; } - getViewModel(): RejectSponsorshipRequestResultDTO | null { + getViewModel(): RejectSponsorshipRequestResult | null { return this.result; } - get viewModel(): RejectSponsorshipRequestResultDTO | null { + get viewModel(): RejectSponsorshipRequestResult | null { return this.result; } } diff --git a/apps/api/src/domain/team/TeamController.test.ts b/apps/api/src/domain/team/TeamController.test.ts index 5a69fad19..d16aa9915 100644 --- a/apps/api/src/domain/team/TeamController.test.ts +++ b/apps/api/src/domain/team/TeamController.test.ts @@ -3,7 +3,8 @@ import { vi } from 'vitest'; import { TeamController } from './TeamController'; import { TeamService } from './TeamService'; import type { Request } from 'express'; -import { CreateTeamInputDTO, UpdateTeamInputDTO } from './dtos/CreateTeamInputDTO'; +import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO'; +import { UpdateTeamInput } from './dtos/TeamDto'; describe('TeamController', () => { let controller: TeamController; @@ -35,7 +36,7 @@ describe('TeamController', () => { describe('getAll', () => { it('should return all teams', async () => { - const result = { teams: [] }; + const result = { teams: [], totalCount: 0 }; service.getAll.mockResolvedValue(result); const response = await controller.getAll(); @@ -49,12 +50,12 @@ describe('TeamController', () => { it('should return team details', async () => { const teamId = 'team-123'; const userId = 'user-456'; - const result = { id: teamId, name: 'Team' }; + const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: null, canManage: false }; service.getDetails.mockResolvedValue(result); - const mockReq: Partial = { ['user']: { userId } }; + const mockReq = { user: { userId } } as any; - const response = await controller.getDetails(teamId, mockReq as Request); + const response = await controller.getDetails(teamId, mockReq); expect(service.getDetails).toHaveBeenCalledWith(teamId, userId); expect(response).toEqual(result); @@ -64,7 +65,7 @@ describe('TeamController', () => { describe('getMembers', () => { it('should return team members', async () => { const teamId = 'team-123'; - const result = { members: [] }; + const result = { members: [], totalCount: 0, ownerCount: 0, managerCount: 0, memberCount: 0 }; service.getMembers.mockResolvedValue(result); const response = await controller.getMembers(teamId); @@ -77,7 +78,7 @@ describe('TeamController', () => { describe('getJoinRequests', () => { it('should return join requests', async () => { const teamId = 'team-123'; - const result = { requests: [] }; + const result = { requests: [], pendingCount: 0, totalCount: 0 }; service.getJoinRequests.mockResolvedValue(result); const response = await controller.getJoinRequests(teamId); @@ -89,14 +90,14 @@ describe('TeamController', () => { describe('create', () => { it('should create team', async () => { - const input: CreateTeamInputDTO = { name: 'New Team' }; + const input: CreateTeamInputDTO = { name: 'New Team', tag: 'TAG' }; const userId = 'user-123'; - const result = { teamId: 'team-456' }; + const result = { id: 'team-456', success: true }; service.create.mockResolvedValue(result); - const mockReq: Partial = { ['user']: { userId } }; + const mockReq = { user: { userId } } as any; - const response = await controller.create(input, mockReq as Request); + const response = await controller.create(input, mockReq); expect(service.create).toHaveBeenCalledWith(input, userId); expect(response).toEqual(result); @@ -106,14 +107,14 @@ describe('TeamController', () => { describe('update', () => { it('should update team', async () => { const teamId = 'team-123'; - const input: UpdateTeamInputDTO = { name: 'Updated Team' }; const userId = 'user-456'; + const input: UpdateTeamInput = { name: 'Updated Team', updatedBy: userId }; const result = { success: true }; service.update.mockResolvedValue(result); - const mockReq: Partial = { ['user']: { userId } }; + const mockReq = { user: { userId } } as any; - const response = await controller.update(teamId, input, mockReq as Request); + const response = await controller.update(teamId, input, mockReq); expect(service.update).toHaveBeenCalledWith(teamId, input, userId); expect(response).toEqual(result); @@ -123,7 +124,7 @@ describe('TeamController', () => { describe('getDriverTeam', () => { it('should return driver team', async () => { const driverId = 'driver-123'; - const result = { teamId: 'team-456' }; + const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false }; service.getDriverTeam.mockResolvedValue(result); const response = await controller.getDriverTeam(driverId); @@ -137,7 +138,7 @@ describe('TeamController', () => { it('should return team membership', async () => { const teamId = 'team-123'; const driverId = 'driver-456'; - const result = { role: 'member' }; + const result = { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }; service.getMembership.mockResolvedValue(result); const response = await controller.getMembership(teamId, driverId); diff --git a/apps/api/src/domain/team/TeamController.ts b/apps/api/src/domain/team/TeamController.ts index 2bb53e258..ab59394e7 100644 --- a/apps/api/src/domain/team/TeamController.ts +++ b/apps/api/src/domain/team/TeamController.ts @@ -8,7 +8,7 @@ import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO'; import { GetTeamJoinRequestsOutputDTO } from './dtos/GetTeamJoinRequestsOutputDTO'; import { CreateTeamInputDTO } from './dtos/CreateTeamInputDTO'; import { CreateTeamOutputDTO } from './dtos/CreateTeamOutputDTO'; -import { UpdateTeamInputDTO } from './dtos/UpdateTeamInputDTO'; +import { UpdateTeamInput } from './dtos/TeamDto'; import { UpdateTeamOutputDTO } from './dtos/UpdateTeamOutputDTO'; import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO'; import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO'; @@ -22,8 +22,7 @@ export class TeamController { @ApiOperation({ summary: 'Get all teams' }) @ApiResponse({ status: 200, description: 'List of all teams', type: GetAllTeamsOutputDTO }) async getAll(): Promise { - const presenter = await this.teamService.getAll(); - return presenter.responseModel; + return await this.teamService.getAll(); } @Get(':teamId') @@ -31,43 +30,38 @@ export class TeamController { @ApiResponse({ status: 200, description: 'Team details', type: GetTeamDetailsOutputDTO }) @ApiResponse({ status: 404, description: 'Team not found' }) async getDetails(@Param('teamId') teamId: string, @Req() req: Request): Promise { - const userId = req['user']?.userId; - const presenter = await this.teamService.getDetails(teamId, userId); - return presenter.getResponseModel(); + const userId = (req as any)['user']?.userId; + return await this.teamService.getDetails(teamId, userId); } @Get(':teamId/members') @ApiOperation({ summary: 'Get team members' }) @ApiResponse({ status: 200, description: 'Team members', type: GetTeamMembersOutputDTO }) async getMembers(@Param('teamId') teamId: string): Promise { - const presenter = await this.teamService.getMembers(teamId); - return presenter.getResponseModel()!; + return await this.teamService.getMembers(teamId); } @Get(':teamId/join-requests') @ApiOperation({ summary: 'Get team join requests' }) @ApiResponse({ status: 200, description: 'Team join requests', type: GetTeamJoinRequestsOutputDTO }) async getJoinRequests(@Param('teamId') teamId: string): Promise { - const presenter = await this.teamService.getJoinRequests(teamId); - return presenter.getResponseModel()!; + return await this.teamService.getJoinRequests(teamId); } @Post() @ApiOperation({ summary: 'Create a new team' }) @ApiResponse({ status: 201, description: 'Team created', type: CreateTeamOutputDTO }) async create(@Body() input: CreateTeamInputDTO, @Req() req: Request): Promise { - const userId = req['user']?.userId; - const presenter = await this.teamService.create(input, userId); - return presenter.responseModel; + const userId = (req as any)['user']?.userId; + return await this.teamService.create(input, userId); } @Patch(':teamId') @ApiOperation({ summary: 'Update team' }) @ApiResponse({ status: 200, description: 'Team updated', type: UpdateTeamOutputDTO }) - async update(@Param('teamId') teamId: string, @Body() input: UpdateTeamInputDTO, @Req() req: Request): Promise { - const userId = req['user']?.userId; - const presenter = await this.teamService.update(teamId, input, userId); - return presenter.responseModel; + async update(@Param('teamId') teamId: string, @Body() input: UpdateTeamInput, @Req() req: Request): Promise { + const userId = (req as any)['user']?.userId; + return await this.teamService.update(teamId, input, userId); } @Get('driver/:driverId') @@ -75,8 +69,7 @@ export class TeamController { @ApiResponse({ status: 200, description: 'Driver\'s team', type: GetDriverTeamOutputDTO }) @ApiResponse({ status: 404, description: 'Team not found' }) async getDriverTeam(@Param('driverId') driverId: string): Promise { - const presenter = await this.teamService.getDriverTeam(driverId); - return presenter.getResponseModel(); + return await this.teamService.getDriverTeam(driverId); } @Get(':teamId/members/:driverId') @@ -84,7 +77,6 @@ export class TeamController { @ApiResponse({ status: 200, description: 'Team membership', type: GetTeamMembershipOutputDTO }) @ApiResponse({ status: 404, description: 'Membership not found' }) async getMembership(@Param('teamId') teamId: string, @Param('driverId') driverId: string): Promise { - const presenter = await this.teamService.getMembership(teamId, driverId); - return presenter.responseModel; + return await this.teamService.getMembership(teamId, driverId); } } \ No newline at end of file diff --git a/apps/api/src/domain/team/TeamProviders.ts b/apps/api/src/domain/team/TeamProviders.ts index 12ba7be49..f62d7e6ca 100644 --- a/apps/api/src/domain/team/TeamProviders.ts +++ b/apps/api/src/domain/team/TeamProviders.ts @@ -3,10 +3,6 @@ import { TeamService } from './TeamService'; // Import core interfaces import type { Logger } from '@core/shared/application/Logger'; -import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; -import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; -import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; -import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; // Import concrete in-memory implementations import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository'; @@ -15,15 +11,7 @@ import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/ import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter'; import { ConsoleLogger } from '@adapters/logging/ConsoleLogger'; -// Import use cases -import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; -import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase'; -import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase'; -import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase'; -import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase'; -import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase'; -import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; -import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase'; +// Use cases are imported and used directly in the service // Define injection tokens export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository'; @@ -58,53 +46,5 @@ export const TeamProviders: Provider[] = [ provide: LOGGER_TOKEN, useClass: ConsoleLogger, }, - // Use cases - { - provide: GetAllTeamsUseCase, - useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) => - new GetAllTeamsUseCase(teamRepo, membershipRepo, logger), - inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], - }, - { - provide: GetTeamDetailsUseCase, - useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) => - new GetTeamDetailsUseCase(teamRepo, membershipRepo), - inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN], - }, - { - provide: GetTeamMembersUseCase, - useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) => - new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger), - inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN], - }, - { - provide: GetTeamJoinRequestsUseCase, - useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) => - new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger), - inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN], - }, - { - provide: CreateTeamUseCase, - useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) => - new CreateTeamUseCase(teamRepo, membershipRepo), - inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN], - }, - { - provide: UpdateTeamUseCase, - useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) => - new UpdateTeamUseCase(teamRepo, membershipRepo), - inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN], - }, - { - provide: GetDriverTeamUseCase, - useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) => - new GetDriverTeamUseCase(teamRepo, membershipRepo, logger), - inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], - }, - { - provide: GetTeamMembershipUseCase, - useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) => - new GetTeamMembershipUseCase(membershipRepo, logger), - inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN], - }, + // Use cases are created directly in the service ]; \ No newline at end of file diff --git a/apps/api/src/domain/team/TeamService.test.ts b/apps/api/src/domain/team/TeamService.test.ts index f97a37bfa..57d57f17a 100644 --- a/apps/api/src/domain/team/TeamService.test.ts +++ b/apps/api/src/domain/team/TeamService.test.ts @@ -1,4 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { vi } from 'vitest'; import { TeamService } from './TeamService'; import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase'; @@ -11,20 +12,20 @@ import { DriverTeamViewModel } from './dtos/TeamDto'; describe('TeamService', () => { let service: TeamService; - let getAllTeamsUseCase: jest.Mocked; - let getDriverTeamUseCase: jest.Mocked; + let getAllTeamsUseCase: ReturnType>; + let getDriverTeamUseCase: ReturnType>; beforeEach(async () => { const mockGetAllTeamsUseCase = { - execute: jest.fn(), + execute: vi.fn(), }; const mockGetDriverTeamUseCase = { - execute: jest.fn(), + execute: vi.fn(), }; const mockLogger = { - debug: jest.fn(), - info: jest.fn(), - error: jest.fn(), + debug: vi.fn(), + info: vi.fn(), + error: vi.fn(), }; const module: TestingModule = await Test.createTestingModule({ @@ -61,11 +62,11 @@ describe('TeamService', () => { getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any); const mockPresenter = { - present: jest.fn(), - getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }), + present: vi.fn(), + getViewModel: vi.fn().mockReturnValue({ teams: [], totalCount: 0 }), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - (AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter); + (AllTeamsPresenter as any) = vi.fn().mockImplementation(() => mockPresenter); const result = await service.getAll(); @@ -81,11 +82,11 @@ describe('TeamService', () => { getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any); const mockPresenter = { - present: jest.fn(), - getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel), + present: vi.fn(), + getViewModel: vi.fn().mockReturnValue({} as DriverTeamViewModel), }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - (DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter); + (DriverTeamPresenter as any) = vi.fn().mockImplementation(() => mockPresenter); const result = await service.getDriverTeam('driver1'); diff --git a/apps/api/src/domain/team/TeamService.ts b/apps/api/src/domain/team/TeamService.ts index f0ce50384..30821b664 100644 --- a/apps/api/src/domain/team/TeamService.ts +++ b/apps/api/src/domain/team/TeamService.ts @@ -15,7 +15,6 @@ import type { Logger } from '@core/shared/application/Logger'; import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository'; import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository'; import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository'; -import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort'; // Use cases import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase'; @@ -38,7 +37,7 @@ import { CreateTeamPresenter } from './presenters/CreateTeamPresenter'; import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter'; // Tokens -import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN } from './TeamProviders'; +import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN } from './TeamProviders'; @Injectable() export class TeamService { @@ -46,7 +45,6 @@ export class TeamService { @Inject(TEAM_REPOSITORY_TOKEN) private readonly teamRepository: ITeamRepository, @Inject(TEAM_MEMBERSHIP_REPOSITORY_TOKEN) private readonly membershipRepository: ITeamMembershipRepository, @Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository: IDriverRepository, - @Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: IImageServicePort, @Inject(LOGGER_TOKEN) private readonly logger: Logger, ) {} @@ -57,11 +55,11 @@ export class TeamService { const useCase = new GetAllTeamsUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter); const result = await useCase.execute(); if (result.isErr()) { - this.logger.error('Error fetching all teams', result.error?.details?.message || 'Unknown error'); + this.logger.error('Error fetching all teams', new Error(result.error?.details?.message || 'Unknown error')); return { teams: [], totalCount: 0 }; } - return presenter.responseModel; + return presenter.getResponseModel()!; } async getDetails(teamId: string, userId?: string): Promise { @@ -75,14 +73,14 @@ export class TeamService { return null; } - return presenter.getResponseModel(); + return presenter.getResponseModel()!; } async getMembers(teamId: string): Promise { this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`); const presenter = new TeamMembersPresenter(); - const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.imageService, this.logger, presenter); + const useCase = new GetTeamMembersUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, this.logger, presenter); const result = await useCase.execute({ teamId }); if (result.isErr()) { this.logger.error(`Error fetching team members for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`); @@ -105,7 +103,7 @@ export class TeamService { const useCase = new GetTeamJoinRequestsUseCase(this.membershipRepository, this.driverRepository, this.teamRepository, presenter); const result = await useCase.execute({ teamId }); if (result.isErr()) { - this.logger.error(new Error(`Error fetching team join requests for teamId: ${teamId}: ${result.error?.details?.message || 'Unknown error'}`)); + this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, new Error(result.error?.details?.message || 'Unknown error')); return { requests: [], pendingCount: 0, diff --git a/apps/api/src/domain/team/dtos/CreateTeamInputDTO.ts b/apps/api/src/domain/team/dtos/CreateTeamInputDTO.ts index 2f489597e..90af4a349 100644 --- a/apps/api/src/domain/team/dtos/CreateTeamInputDTO.ts +++ b/apps/api/src/domain/team/dtos/CreateTeamInputDTO.ts @@ -1,16 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class CreateTeamInputDTO { @ApiProperty() @IsString() @IsNotEmpty() - name: string; + name!: string; @ApiProperty() @IsString() @IsNotEmpty() - tag: string; + tag!: string; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/api/src/domain/team/dtos/CreateTeamOutputDTO.ts b/apps/api/src/domain/team/dtos/CreateTeamOutputDTO.ts index d7f5fad25..af8c2a6f7 100644 --- a/apps/api/src/domain/team/dtos/CreateTeamOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/CreateTeamOutputDTO.ts @@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; export class CreateTeamOutputDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - success: boolean; + success!: boolean; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetAllTeamsOutputDTO.ts b/apps/api/src/domain/team/dtos/GetAllTeamsOutputDTO.ts index ed25016d3..4f230d5a1 100644 --- a/apps/api/src/domain/team/dtos/GetAllTeamsOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetAllTeamsOutputDTO.ts @@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger'; class TeamListItemDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - tag: string; + tag!: string; @ApiProperty() - description: string; + description!: string; @ApiProperty() - memberCount: number; + memberCount!: number; @ApiProperty({ type: [String] }) - leagues: string[]; + leagues!: string[]; @ApiProperty({ required: false }) specialization?: 'endurance' | 'sprint' | 'mixed'; @@ -31,8 +31,8 @@ class TeamListItemDTO { export class GetAllTeamsOutputDTO { @ApiProperty({ type: [TeamListItemDTO] }) - teams: TeamListItemDTO[]; + teams!: TeamListItemDTO[]; @ApiProperty() - totalCount: number; + totalCount!: number; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetDriverTeamOutputDTO.ts b/apps/api/src/domain/team/dtos/GetDriverTeamOutputDTO.ts index 97f14b3a8..f584bce31 100644 --- a/apps/api/src/domain/team/dtos/GetDriverTeamOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetDriverTeamOutputDTO.ts @@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger'; class TeamDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - tag: string; + tag!: string; @ApiProperty() - description: string; + description!: string; @ApiProperty() - ownerId: string; + ownerId!: string; @ApiProperty({ type: [String] }) - leagues: string[]; + leagues!: string[]; @ApiProperty({ required: false }) createdAt?: string; @@ -34,25 +34,25 @@ class TeamDTO { class MembershipDTO { @ApiProperty() - role: 'owner' | 'manager' | 'member'; + role!: 'owner' | 'manager' | 'member'; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty() - isActive: boolean; + isActive!: boolean; } export class GetDriverTeamOutputDTO { @ApiProperty({ type: TeamDTO }) - team: TeamDTO; + team!: TeamDTO; @ApiProperty({ type: MembershipDTO }) - membership: MembershipDTO; + membership!: MembershipDTO; @ApiProperty() - isOwner: boolean; + isOwner!: boolean; @ApiProperty() - canManage: boolean; + canManage!: boolean; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetTeamDetailsOutputDTO.ts b/apps/api/src/domain/team/dtos/GetTeamDetailsOutputDTO.ts index a630da8f7..4bb46dd19 100644 --- a/apps/api/src/domain/team/dtos/GetTeamDetailsOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetTeamDetailsOutputDTO.ts @@ -2,22 +2,22 @@ import { ApiProperty } from '@nestjs/swagger'; class TeamDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - tag: string; + tag!: string; @ApiProperty() - description: string; + description!: string; @ApiProperty() - ownerId: string; + ownerId!: string; @ApiProperty({ type: [String] }) - leagues: string[]; + leagues!: string[]; @ApiProperty({ required: false }) createdAt?: string; @@ -34,22 +34,22 @@ class TeamDTO { class MembershipDTO { @ApiProperty() - role: 'owner' | 'manager' | 'member'; + role!: 'owner' | 'manager' | 'member'; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty() - isActive: boolean; + isActive!: boolean; } export class GetTeamDetailsOutputDTO { @ApiProperty({ type: TeamDTO }) - team: TeamDTO; + team!: TeamDTO; @ApiProperty({ type: MembershipDTO, nullable: true }) - membership: MembershipDTO | null; + membership!: MembershipDTO | null; @ApiProperty() - canManage: boolean; + canManage!: boolean; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetTeamJoinRequestsOutputDTO.ts b/apps/api/src/domain/team/dtos/GetTeamJoinRequestsOutputDTO.ts index 1869a3b0f..035ade6f3 100644 --- a/apps/api/src/domain/team/dtos/GetTeamJoinRequestsOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetTeamJoinRequestsOutputDTO.ts @@ -2,34 +2,34 @@ import { ApiProperty } from '@nestjs/swagger'; class TeamJoinRequestDTO { @ApiProperty() - requestId: string; + requestId!: string; @ApiProperty() - driverId: string; + driverId!: string; @ApiProperty() - driverName: string; + driverName!: string; @ApiProperty() - teamId: string; + teamId!: string; @ApiProperty() - status: 'pending' | 'approved' | 'rejected'; + status!: 'pending' | 'approved' | 'rejected'; @ApiProperty() - requestedAt: string; + requestedAt!: string; @ApiProperty() - avatarUrl: string; + avatarUrl!: string; } export class GetTeamJoinRequestsOutputDTO { @ApiProperty({ type: [TeamJoinRequestDTO] }) - requests: TeamJoinRequestDTO[]; + requests!: TeamJoinRequestDTO[]; @ApiProperty() - pendingCount: number; + pendingCount!: number; @ApiProperty() - totalCount: number; + totalCount!: number; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetTeamMembersOutputDTO.ts b/apps/api/src/domain/team/dtos/GetTeamMembersOutputDTO.ts index 24391f976..d3688a1b4 100644 --- a/apps/api/src/domain/team/dtos/GetTeamMembersOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetTeamMembersOutputDTO.ts @@ -2,37 +2,37 @@ import { ApiProperty } from '@nestjs/swagger'; class TeamMemberDTO { @ApiProperty() - driverId: string; + driverId!: string; @ApiProperty() - driverName: string; + driverName!: string; @ApiProperty() - role: 'owner' | 'manager' | 'member'; + role!: 'owner' | 'manager' | 'member'; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty() - isActive: boolean; + isActive!: boolean; @ApiProperty() - avatarUrl: string; + avatarUrl!: string; } export class GetTeamMembersOutputDTO { @ApiProperty({ type: [TeamMemberDTO] }) - members: TeamMemberDTO[]; + members!: TeamMemberDTO[]; @ApiProperty() - totalCount: number; + totalCount!: number; @ApiProperty() - ownerCount: number; + ownerCount!: number; @ApiProperty() - managerCount: number; + managerCount!: number; @ApiProperty() - memberCount: number; + memberCount!: number; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetTeamMembershipOutputDTO.ts b/apps/api/src/domain/team/dtos/GetTeamMembershipOutputDTO.ts index f297e3255..288896262 100644 --- a/apps/api/src/domain/team/dtos/GetTeamMembershipOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetTeamMembershipOutputDTO.ts @@ -2,11 +2,11 @@ import { ApiProperty } from '@nestjs/swagger'; export class GetTeamMembershipOutputDTO { @ApiProperty() - role: 'owner' | 'manager' | 'member'; + role!: 'owner' | 'manager' | 'member'; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty() - isActive: boolean; + isActive!: boolean; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts b/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts index 2d055e538..c1fbf7864 100644 --- a/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/GetTeamsLeaderboardOutputDTO.ts @@ -4,31 +4,31 @@ export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro'; class TeamLeaderboardItemDTO { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - memberCount: number; + memberCount!: number; @ApiProperty({ nullable: true }) - rating: number | null; + rating!: number | null; @ApiProperty() - totalWins: number; + totalWins!: number; @ApiProperty() - totalRaces: number; + totalRaces!: number; @ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] }) - performanceLevel: SkillLevel; + performanceLevel!: SkillLevel; @ApiProperty() - isRecruiting: boolean; + isRecruiting!: boolean; @ApiProperty() - createdAt: string; + createdAt!: string; @ApiProperty({ required: false }) description?: string; @@ -45,14 +45,14 @@ class TeamLeaderboardItemDTO { export class GetTeamsLeaderboardOutputDTO { @ApiProperty({ type: [TeamLeaderboardItemDTO] }) - teams: TeamLeaderboardItemDTO[]; + teams!: TeamLeaderboardItemDTO[]; @ApiProperty() - recruitingCount: number; + recruitingCount!: number; @ApiProperty({ type: 'object', additionalProperties: { type: 'array', items: { $ref: '#/components/schemas/TeamLeaderboardItemDTO' } } }) - groupsBySkillLevel: Record; + groupsBySkillLevel!: Record; @ApiProperty({ type: [TeamLeaderboardItemDTO] }) - topTeams: TeamLeaderboardItemDTO[]; + topTeams!: TeamLeaderboardItemDTO[]; } \ No newline at end of file diff --git a/apps/api/src/domain/team/dtos/TeamDto.ts b/apps/api/src/domain/team/dtos/TeamDto.ts index a4b192d05..2a1d21d00 100644 --- a/apps/api/src/domain/team/dtos/TeamDto.ts +++ b/apps/api/src/domain/team/dtos/TeamDto.ts @@ -3,22 +3,22 @@ import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator'; export class TeamListItemViewModel { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - tag: string; + tag!: string; @ApiProperty() - description: string; + description!: string; @ApiProperty() - memberCount: number; + memberCount!: number; @ApiProperty({ type: [String] }) - leagues: string[]; + leagues!: string[]; @ApiProperty({ required: false }) specialization?: 'endurance' | 'sprint' | 'mixed'; @@ -32,30 +32,30 @@ export class TeamListItemViewModel { export class AllTeamsViewModel { @ApiProperty({ type: [TeamListItemViewModel] }) - teams: TeamListItemViewModel[]; + teams!: TeamListItemViewModel[]; @ApiProperty() - totalCount: number; + totalCount!: number; } export class TeamViewModel { @ApiProperty() - id: string; + id!: string; @ApiProperty() - name: string; + name!: string; @ApiProperty() - tag: string; + tag!: string; @ApiProperty() - description: string; + description!: string; @ApiProperty() - ownerId: string; + ownerId!: string; @ApiProperty({ type: [String] }) - leagues: string[]; + leagues!: string[]; @ApiProperty({ required: false }) createdAt?: string; @@ -85,131 +85,131 @@ export enum MembershipStatus { export class MembershipViewModel { @ApiProperty() - role: 'owner' | 'manager' | 'member'; + role!: 'owner' | 'manager' | 'member'; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty() - isActive: boolean; + isActive!: boolean; } export class DriverTeamViewModel { @ApiProperty({ type: TeamViewModel }) - team: TeamViewModel; + team!: TeamViewModel; @ApiProperty({ type: MembershipViewModel }) - membership: MembershipViewModel; + membership!: MembershipViewModel; @ApiProperty() - isOwner: boolean; + isOwner!: boolean; @ApiProperty() - canManage: boolean; + canManage!: boolean; } export class GetDriverTeamQuery { @ApiProperty() @IsString() - teamId: string; + teamId!: string; @ApiProperty() @IsString() - driverId: string; + driverId!: string; } export class TeamDetailsViewModel { @ApiProperty({ type: TeamViewModel }) - team: TeamViewModel; + team!: TeamViewModel; @ApiProperty({ type: MembershipViewModel, nullable: true }) - membership: MembershipViewModel | null; + membership!: MembershipViewModel | null; @ApiProperty() - canManage: boolean; + canManage!: boolean; } export class TeamMemberViewModel { @ApiProperty() - driverId: string; + driverId!: string; @ApiProperty() - driverName: string; + driverName!: string; @ApiProperty() - role: 'owner' | 'manager' | 'member'; + role!: 'owner' | 'manager' | 'member'; @ApiProperty() - joinedAt: string; + joinedAt!: string; @ApiProperty() - isActive: boolean; + isActive!: boolean; @ApiProperty() - avatarUrl: string; + avatarUrl!: string; } export class TeamMembersViewModel { @ApiProperty({ type: [TeamMemberViewModel] }) - members: TeamMemberViewModel[]; + members!: TeamMemberViewModel[]; @ApiProperty() - totalCount: number; + totalCount!: number; @ApiProperty() - ownerCount: number; + ownerCount!: number; @ApiProperty() - managerCount: number; + managerCount!: number; @ApiProperty() - memberCount: number; + memberCount!: number; } export class TeamJoinRequestViewModel { @ApiProperty() - requestId: string; + requestId!: string; @ApiProperty() - driverId: string; + driverId!: string; @ApiProperty() - driverName: string; + driverName!: string; @ApiProperty() - teamId: string; + teamId!: string; @ApiProperty() - status: 'pending' | 'approved' | 'rejected'; + status!: 'pending' | 'approved' | 'rejected'; @ApiProperty() - requestedAt: string; + requestedAt!: string; @ApiProperty() - avatarUrl: string; + avatarUrl!: string; } export class TeamJoinRequestsViewModel { @ApiProperty({ type: [TeamJoinRequestViewModel] }) - requests: TeamJoinRequestViewModel[]; + requests!: TeamJoinRequestViewModel[]; @ApiProperty() - pendingCount: number; + pendingCount!: number; @ApiProperty() - totalCount: number; + totalCount!: number; } export class CreateTeamInput { @ApiProperty() @IsString() @IsNotEmpty() - name: string; + name!: string; @ApiProperty() @IsString() @IsNotEmpty() - tag: string; + tag!: string; @ApiProperty({ required: false }) @IsOptional() @@ -218,17 +218,17 @@ export class CreateTeamInput { @ApiProperty() @IsString() - ownerId: string; + ownerId!: string; } export class CreateTeamOutput { @ApiProperty() @IsString() - teamId: string; + teamId!: string; @ApiProperty() @IsBoolean() - success: boolean; + success!: boolean; } export class UpdateTeamInput { @@ -254,19 +254,19 @@ export class UpdateTeamInput { @ApiProperty() @IsString() - updatedBy: string; + updatedBy!: string; } export class UpdateTeamOutput { @ApiProperty() @IsBoolean() - success: boolean; + success!: boolean; } export class ApproveTeamJoinRequestInput { @ApiProperty() @IsString() - requestId: string; + requestId!: string; @ApiProperty({ required: false }) @IsOptional() @@ -277,13 +277,13 @@ export class ApproveTeamJoinRequestInput { export class ApproveTeamJoinRequestOutput { @ApiProperty() @IsBoolean() - success: boolean; + success!: boolean; } export class RejectTeamJoinRequestInput { @ApiProperty() @IsString() - requestId: string; + requestId!: string; @ApiProperty({ required: false }) @IsOptional() @@ -294,5 +294,5 @@ export class RejectTeamJoinRequestInput { export class RejectTeamJoinRequestOutput { @ApiProperty() @IsBoolean() - success: boolean; + success!: boolean; } diff --git a/apps/api/src/domain/team/dtos/UpdateTeamOutputDTO.ts b/apps/api/src/domain/team/dtos/UpdateTeamOutputDTO.ts index 89f0caa00..110bc1282 100644 --- a/apps/api/src/domain/team/dtos/UpdateTeamOutputDTO.ts +++ b/apps/api/src/domain/team/dtos/UpdateTeamOutputDTO.ts @@ -2,5 +2,5 @@ import { ApiProperty } from '@nestjs/swagger'; export class UpdateTeamOutputDTO { @ApiProperty() - success: boolean; + success!: boolean; } \ No newline at end of file diff --git a/apps/api/src/domain/team/presenters/TeamMembershipPresenter.ts b/apps/api/src/domain/team/presenters/TeamMembershipPresenter.ts index 1b1e44b97..6072a371f 100644 --- a/apps/api/src/domain/team/presenters/TeamMembershipPresenter.ts +++ b/apps/api/src/domain/team/presenters/TeamMembershipPresenter.ts @@ -16,4 +16,11 @@ export class TeamMembershipPresenter implements UseCaseOutputPort { private result: GetTeamsLeaderboardOutputDTO | null = null; reset() { this.result = null; } - async present(outputPort: TeamsLeaderboardOutputPort): Promise { + present(result: GetTeamsLeaderboardResult): void { this.result = { - teams: outputPort.teams.map(team => ({ - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt.toISOString(), - description: team.description, - specialization: team.specialization, - region: team.region, - languages: team.languages, + teams: result.items.map(item => ({ + id: item.team.id, + name: item.team.name.toString(), + memberCount: item.memberCount, + rating: item.rating, + totalWins: item.totalWins, + totalRaces: item.totalRaces, + performanceLevel: item.performanceLevel, + isRecruiting: item.isRecruiting, + createdAt: item.createdAt.toISOString(), + description: item.team.description?.toString() || '', })), - recruitingCount: outputPort.recruitingCount, + recruitingCount: result.recruitingCount, groupsBySkillLevel: { - beginner: outputPort.groupsBySkillLevel.beginner.map(team => ({ - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt.toISOString(), - description: team.description, - specialization: team.specialization, - region: team.region, - languages: team.languages, + beginner: result.groupsBySkillLevel.beginner.map(item => ({ + id: item.team.id, + name: item.team.name.toString(), + memberCount: item.memberCount, + rating: item.rating, + totalWins: item.totalWins, + totalRaces: item.totalRaces, + performanceLevel: item.performanceLevel, + isRecruiting: item.isRecruiting, + createdAt: item.createdAt.toISOString(), + description: item.team.description?.toString() || '', })), - intermediate: outputPort.groupsBySkillLevel.intermediate.map(team => ({ - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt.toISOString(), - description: team.description, - specialization: team.specialization, - region: team.region, - languages: team.languages, + intermediate: result.groupsBySkillLevel.intermediate.map(item => ({ + id: item.team.id, + name: item.team.name.toString(), + memberCount: item.memberCount, + rating: item.rating, + totalWins: item.totalWins, + totalRaces: item.totalRaces, + performanceLevel: item.performanceLevel, + isRecruiting: item.isRecruiting, + createdAt: item.createdAt.toISOString(), + description: item.team.description?.toString() || '', })), - advanced: outputPort.groupsBySkillLevel.advanced.map(team => ({ - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt.toISOString(), - description: team.description, - specialization: team.specialization, - region: team.region, - languages: team.languages, + advanced: result.groupsBySkillLevel.advanced.map(item => ({ + id: item.team.id, + name: item.team.name.toString(), + memberCount: item.memberCount, + rating: item.rating, + totalWins: item.totalWins, + totalRaces: item.totalRaces, + performanceLevel: item.performanceLevel, + isRecruiting: item.isRecruiting, + createdAt: item.createdAt.toISOString(), + description: item.team.description?.toString() || '', })), - pro: outputPort.groupsBySkillLevel.pro.map(team => ({ - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt.toISOString(), - description: team.description, - specialization: team.specialization, - region: team.region, - languages: team.languages, + pro: result.groupsBySkillLevel.pro.map(item => ({ + id: item.team.id, + name: item.team.name.toString(), + memberCount: item.memberCount, + rating: item.rating, + totalWins: item.totalWins, + totalRaces: item.totalRaces, + performanceLevel: item.performanceLevel, + isRecruiting: item.isRecruiting, + createdAt: item.createdAt.toISOString(), + description: item.team.description?.toString() || '', })), }, - topTeams: outputPort.topTeams.map(team => ({ - id: team.id, - name: team.name, - memberCount: team.memberCount, - rating: team.rating, - totalWins: team.totalWins, - totalRaces: team.totalRaces, - performanceLevel: team.performanceLevel, - isRecruiting: team.isRecruiting, - createdAt: team.createdAt.toISOString(), - description: team.description, - specialization: team.specialization, - region: team.region, - languages: team.languages, + topTeams: result.topItems.map(item => ({ + id: item.team.id, + name: item.team.name.toString(), + memberCount: item.memberCount, + rating: item.rating, + totalWins: item.totalWins, + totalRaces: item.totalRaces, + performanceLevel: item.performanceLevel, + isRecruiting: item.isRecruiting, + createdAt: item.createdAt.toISOString(), + description: item.team.description?.toString() || '', })), }; } diff --git a/apps/api/src/presentation/hello.controller.ts b/apps/api/src/presentation/hello.controller.ts deleted file mode 100644 index a44a05fa5..000000000 --- a/apps/api/src/presentation/hello.controller.ts +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/apps/api/src/presentation/payments/CreatePaymentPresenter.ts b/apps/api/src/presentation/payments/CreatePaymentPresenter.ts deleted file mode 100644 index 2e6f6a5a3..000000000 --- a/apps/api/src/presentation/payments/CreatePaymentPresenter.ts +++ /dev/null @@ -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 { - 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, - }; - } -} \ No newline at end of file diff --git a/apps/api/src/presentation/payments/GetPaymentsPresenter.ts b/apps/api/src/presentation/payments/GetPaymentsPresenter.ts deleted file mode 100644 index 979a53b1a..000000000 --- a/apps/api/src/presentation/payments/GetPaymentsPresenter.ts +++ /dev/null @@ -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 { - 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, - }; - } -} \ No newline at end of file diff --git a/apps/api/src/presentation/payments/GetSponsorBillingPresenter.ts b/apps/api/src/presentation/payments/GetSponsorBillingPresenter.ts deleted file mode 100644 index f3faba283..000000000 --- a/apps/api/src/presentation/payments/GetSponsorBillingPresenter.ts +++ /dev/null @@ -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 { - private viewModel: SponsorBillingSummary | null = null; - - present(result: GetSponsorBillingResult): void { - this.viewModel = result; - } - - getViewModel(): SponsorBillingSummary | null { - return this.viewModel; - } - - reset(): void { - this.viewModel = null; - } -} \ No newline at end of file diff --git a/apps/api/src/presentation/payments/index.ts b/apps/api/src/presentation/payments/index.ts deleted file mode 100644 index 1424ad873..000000000 --- a/apps/api/src/presentation/payments/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './types'; -export * from './CreatePaymentPresenter'; -export * from './GetPaymentsPresenter'; -export * from './GetSponsorBillingPresenter'; \ No newline at end of file diff --git a/apps/api/src/presentation/payments/types.ts b/apps/api/src/presentation/payments/types.ts deleted file mode 100644 index 7c59e7e2b..000000000 --- a/apps/api/src/presentation/payments/types.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 8d8cdeca8..99138cb6d 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,31 +1,54 @@ { - "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "es2017", - "module": "commonjs", - "lib": ["es2022", "dom"], - "moduleResolution": "node", + "baseUrl": ".", + "declaration": true, + "declarationMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, + "incremental": true, + "lib": [ + "es2022", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", "noEmit": false, "noEmitOnError": true, "noUnusedLocals": true, "noUnusedParameters": true, - "declaration": true, - "declarationMap": true, + "outDir": "./dist", + "paths": { + "@/*": [ + "./*" + ], + "@adapters/*": [ + "../../adapters/*" + ], + "@core/*": [ + "../../core/*" + ], + "@testing/*": [ + "../../testing/*" + ] + }, "removeComments": true, "sourceMap": true, - "outDir": "./dist", - "incremental": true, - "baseUrl": ".", - "types": ["node", "express", "vitest/globals"], - "paths": { - "@/*": ["./*"], - "@core/*": ["../../core/*"], - "@adapters/*": ["../../adapters/*"], - "@testing/*": ["../../testing/*"] - } + "strict": true, + "strictNullChecks": true, + "target": "es2017", + "types": [ + "node", + "express", + "vitest/globals" + ] }, - "include": ["src/**/*", "../../adapters/bootstrap/EnsureInitialData.ts"], - "exclude": ["node_modules", "dist", "**/*.mock.ts"] -} + "exclude": [ + "node_modules", + "dist", + "**/*.mock.ts" + ], + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts b/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts index e0bf7fc43..f4284e9b2 100644 --- a/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts +++ b/core/racing/application/use-cases/GetRacePenaltiesUseCase.ts @@ -11,13 +11,14 @@ import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort } from '@core/shared/application'; import type { Driver } from '../../domain/entities/Driver'; +import type { Penalty } from '../../domain/entities/penalty/Penalty'; export type GetRacePenaltiesInput = { raceId: string; }; export type GetRacePenaltiesResult = { - penalties: unknown[]; + penalties: Penalty[]; drivers: Driver[]; };