module creation
This commit is contained in:
6794
apps/api/package-lock.json
generated
Normal file
6794
apps/api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,15 +13,18 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@nestjs/testing": "^10.4.20",
|
||||
"@types/jest": "^30.0.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"@nestjs/testing": "^10.4.20"
|
||||
"ts-node-dev": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.20",
|
||||
"@nestjs/core": "^10.4.20",
|
||||
"@nestjs/platform-express": "^10.4.20",
|
||||
"@nestjs/swagger": "^7.4.2",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"pg": "^8.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -2,13 +2,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HelloController } from './presentation/hello.controller';
|
||||
import { HelloService } from './application/hello/hello.service';
|
||||
import { AnalyticsModule } from './application/analytics/analytics.module';
|
||||
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
|
||||
import { DatabaseModule } from './infrastructure/database/database.module';
|
||||
import { AuthModule } from './modules/auth/AuthModule';
|
||||
import { LeagueModule } from './modules/league/LeagueModule';
|
||||
import { RaceModule } from './modules/race/RaceModule';
|
||||
import { TeamModule } from './modules/team/TeamModule';
|
||||
import { SponsorModule } from './modules/sponsor/SponsorModule';
|
||||
import { DriverModule } from './modules/driver/DriverModule';
|
||||
import { MediaModule } from './modules/media/MediaModule';
|
||||
import { PaymentsModule } from './modules/payments/PaymentsModule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
AnalyticsModule
|
||||
AnalyticsModule,
|
||||
AuthModule,
|
||||
LeagueModule,
|
||||
RaceModule,
|
||||
TeamModule,
|
||||
SponsorModule,
|
||||
DriverModule,
|
||||
MediaModule,
|
||||
PaymentsModule,
|
||||
],
|
||||
controllers: [HelloController],
|
||||
providers: [HelloService],
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
|
||||
import { InMemoryPageViewRepository } from '../../../../adapters/persistence/inmemory/analytics/InMemoryPageViewRepository';
|
||||
import { TypeOrmEngagementRepository } from '../../../../adapters/persistence/typeorm/analytics/TypeOrmEngagementRepository';
|
||||
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
|
||||
import { AnalyticsController } from '../../presentation/analytics.controller';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
{
|
||||
provide: ILogger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => new TypeOrmEngagementRepository(dataSource.manager),
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: RecordPageViewUseCase,
|
||||
useFactory: (repo: IPageViewRepository, logger: ILogger) => new RecordPageViewUseCase(repo, logger),
|
||||
inject: [IPAGE_VIEW_REPO_TOKEN, ILogger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RecordEngagementUseCase,
|
||||
useFactory: (repo: IEngagementRepository, logger: ILogger) => new RecordEngagementUseCase(repo, logger),
|
||||
inject: [IENGAGEMENT_REPO_TOKEN, ILogger_TOKEN],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
29
apps/api/src/modules/analytics/AnalyticsController.ts
Normal file
29
apps/api/src/modules/analytics/AnalyticsController.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { RecordPageViewInput, RecordPageViewOutput, RecordEngagementInput, RecordEngagementOutput } from './dto/AnalyticsDto';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
@Post('page-view')
|
||||
async recordPageView(
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.analyticsService.recordPageView(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
async recordEngagement(
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.analyticsService.recordEngagement(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
}
|
||||
43
apps/api/src/modules/analytics/AnalyticsModule.ts
Normal file
43
apps/api/src/modules/analytics/AnalyticsModule.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
|
||||
import { InMemoryPageViewRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
||||
import { InMemoryEngagementRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
AnalyticsService,
|
||||
{
|
||||
provide: ILogger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useExisting: InMemoryEngagementRepository, // Assuming TypeOrmEngagementRepository is not available
|
||||
},
|
||||
// No need for useExisting here if the original intent was to inject the concrete class when providing the TOKEN
|
||||
],
|
||||
exports: [
|
||||
AnalyticsService,
|
||||
ILogger_TOKEN,
|
||||
IPAGE_VIEW_REPO_TOKEN,
|
||||
IENGAGEMENT_REPO_TOKEN,
|
||||
],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
83
apps/api/src/modules/analytics/AnalyticsService.ts
Normal file
83
apps/api/src/modules/analytics/AnalyticsService.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { RecordEngagementInput, RecordEngagementOutput, RecordPageViewInput, RecordPageViewOutput } from './dto/AnalyticsDto';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { PageView } from '@gridpilot/analytics/domain/entities/PageView';
|
||||
import { EngagementEvent } from '@gridpilot/analytics/domain/entities/EngagementEvent';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
constructor(
|
||||
@Inject(IPAGE_VIEW_REPO_TOKEN) private readonly pageViewRepository: IPageViewRepository,
|
||||
@Inject(IENGAGEMENT_REPO_TOKEN) private readonly engagementRepository: IEngagementRepository,
|
||||
@Inject(ILogger_TOKEN) private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
this.logger.debug('Executing RecordPageViewUseCase', { input });
|
||||
try {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
visitorType: input.visitorType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
});
|
||||
|
||||
await this.pageViewRepository.save(pageView);
|
||||
this.logger.info('Page view recorded successfully', { pageViewId, input });
|
||||
return { pageViewId };
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording page view', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
this.logger.debug('Executing RecordEngagementUseCase', { input });
|
||||
try {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
action: input.action as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
});
|
||||
|
||||
await this.engagementRepository.save(event);
|
||||
this.logger.info('Engagement recorded successfully', { eventId, input });
|
||||
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording engagement', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
apps/api/src/modules/analytics/dto/AnalyticsDto.ts
Normal file
127
apps/api/src/modules/analytics/dto/AnalyticsDto.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsEnum, IsBoolean, IsNumber, IsObject } from 'class-validator';
|
||||
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum EntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum VisitorType {
|
||||
ANONYMOUS = 'anonymous',
|
||||
DRIVER = 'driver',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
export class RecordPageViewInput {
|
||||
@ApiProperty({ enum: EntityType })
|
||||
@IsEnum(EntityType)
|
||||
entityType: EntityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
visitorId?: string;
|
||||
|
||||
@ApiProperty({ enum: VisitorType })
|
||||
@IsEnum(VisitorType)
|
||||
visitorType: VisitorType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referrer?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userAgent?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export class RecordPageViewOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
pageViewId: string;
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementAction {
|
||||
CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
|
||||
CLICK_SPONSOR_URL = 'click_sponsor_url',
|
||||
DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
|
||||
JOIN_LEAGUE = 'join_league',
|
||||
REGISTER_RACE = 'register_race',
|
||||
VIEW_STANDINGS = 'view_standings',
|
||||
VIEW_SCHEDULE = 'view_schedule',
|
||||
SHARE_SOCIAL = 'share_social',
|
||||
CONTACT_SPONSOR = 'contact_sponsor',
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementEntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
}
|
||||
|
||||
export class RecordEngagementInput {
|
||||
@ApiProperty({ enum: EngagementAction })
|
||||
@IsEnum(EngagementAction)
|
||||
action: EngagementAction;
|
||||
|
||||
@ApiProperty({ enum: EngagementEntityType })
|
||||
@IsEnum(EngagementEntityType)
|
||||
entityType: EngagementEntityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
actorId?: string;
|
||||
|
||||
@ApiProperty({ enum: ['anonymous', 'driver', 'sponsor'] })
|
||||
@IsEnum(['anonymous', 'driver', 'sponsor'])
|
||||
actorType: 'anonymous' | 'driver' | 'sponsor';
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ required: false, type: 'object'/*, additionalProperties: { type: 'string' || 'number' || 'boolean' }*/ })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
export class RecordEngagementOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
eventId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
engagementWeight: number;
|
||||
}
|
||||
42
apps/api/src/modules/auth/AuthController.ts
Normal file
42
apps/api/src/modules/auth/AuthController.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParams, SignupParams, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('signup')
|
||||
async signup(@Body() params: SignupParams) {
|
||||
return this.authService.signupWithEmail(params);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() params: LoginParams) {
|
||||
return this.authService.loginWithEmail(params);
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
async getSession() {
|
||||
return this.authService.getCurrentSession();
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
async logout() {
|
||||
return this.authService.logout();
|
||||
}
|
||||
|
||||
@Get('iracing/start')
|
||||
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response) {
|
||||
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
|
||||
// In real application, you might want to store 'state' in a secure cookie or session.
|
||||
// For this example, we'll just redirect.
|
||||
res.redirect(HttpStatus.FOUND, redirectUrl);
|
||||
}
|
||||
|
||||
@Get('iracing/callback')
|
||||
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string) {
|
||||
return this.authService.loginWithIracingCallback({ code, state, returnTo });
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/auth/AuthModule.ts
Normal file
10
apps/api/src/modules/auth/AuthModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
import { AuthController } from './AuthController';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
64
apps/api/src/modules/auth/AuthProviders.ts
Normal file
64
apps/api/src/modules/auth/AuthProviders.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
|
||||
// Import interfaces and concrete implementations
|
||||
import { IAuthRepository } from '@gridpilot/core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@gridpilot/core/identity/domain/repositories/IUserRepository';
|
||||
import { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
import { ILogger } from '@gridpilot/core/shared/logging/ILogger';
|
||||
|
||||
import { InMemoryAuthRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
||||
import { InMemoryUserRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
import { InMemoryPasswordHashingService } from '../../../adapters/identity/services/InMemoryPasswordHashingService';
|
||||
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
|
||||
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort'; // Path from apps/api/src/modules/auth
|
||||
import { CookieIdentitySessionAdapter } from '../../../adapters/identity/session/CookieIdentitySessionAdapter';
|
||||
|
||||
// Define the tokens for dependency injection
|
||||
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
|
||||
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
|
||||
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
|
||||
export const LOGGER_TOKEN = 'ILogger';
|
||||
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
AuthService, // Provide the service itself
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: ILogger) => {
|
||||
// Seed initial users for InMemoryUserRepository
|
||||
const initialUsers: StoredUser[] = [
|
||||
// Example user (replace with actual test users as needed)
|
||||
{
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
passwordHash: 'demo_salt_moc.elpmaxe@tset', // "test@example.com" reversed
|
||||
displayName: 'Test User',
|
||||
salt: '', // Handled by hashing service
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
|
||||
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
|
||||
},
|
||||
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryUserRepository(logger), // Factory for InMemoryUserRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
useClass: InMemoryPasswordHashingService,
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IDENTITY_SESSION_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new CookieIdentitySessionAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
140
apps/api/src/modules/auth/AuthService.ts
Normal file
140
apps/api/src/modules/auth/AuthService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
// Core Use Cases
|
||||
import { LoginUseCase } from '../../../../core/identity/application/use-cases/LoginUseCase';
|
||||
import { SignupUseCase } from '../../../../core/identity/application/use-cases/SignupUseCase';
|
||||
import { GetCurrentSessionUseCase } from '../../../../core/identity/application/use-cases/GetCurrentSessionUseCase';
|
||||
import { LogoutUseCase } from '../../../../core/identity/application/use-cases/LogoutUseCase';
|
||||
import { StartIracingAuthRedirectUseCase } from '../../../../core/identity/application/use-cases/StartIracingAuthRedirectUseCase';
|
||||
import { LoginWithIracingCallbackUseCase } from '../../../../core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
|
||||
|
||||
// Core Interfaces and Tokens
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
import { IAuthRepository } from '../../../../core/identity/domain/repositories/IAuthRepository';
|
||||
import { IPasswordHashingService } from '../../../../core/identity/domain/services/PasswordHashingService';
|
||||
import { ILogger } from '../../../../core/shared/logging/ILogger';
|
||||
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort';
|
||||
import { UserId } from '../../../../core/identity/domain/value-objects/UserId';
|
||||
import { User } from '../../../../core/identity/domain/entities/User';
|
||||
import { IUserRepository } from '../../../../core/identity/domain/repositories/IUserRepository';
|
||||
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '../../../../core/identity/application/dto/AuthenticatedUserDTO';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly loginUseCase: LoginUseCase;
|
||||
private readonly signupUseCase: SignupUseCase;
|
||||
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
|
||||
private readonly logoutUseCase: LogoutUseCase;
|
||||
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
|
||||
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
|
||||
|
||||
constructor(
|
||||
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
|
||||
@Inject(PASSWORD_HASHING_SERVICE_TOKEN) private passwordHashingService: IPasswordHashingService,
|
||||
@Inject(LOGGER_TOKEN) private logger: ILogger,
|
||||
@Inject(IDENTITY_SESSION_PORT_TOKEN) private identitySessionPort: IdentitySessionPort,
|
||||
@Inject(USER_REPOSITORY_TOKEN) private userRepository: IUserRepository, // Inject IUserRepository here
|
||||
) {
|
||||
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
|
||||
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
|
||||
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
|
||||
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
|
||||
}
|
||||
|
||||
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
|
||||
return {
|
||||
userId: user.getId().value,
|
||||
email: user.getEmail(),
|
||||
displayName: user.getDisplayName(),
|
||||
// Map other fields as necessary
|
||||
iracingCustomerId: user.getIracingCustomerId() ?? undefined,
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
avatarUrl: user.getAvatarUrl() ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
this.logger.debug('[AuthService] Attempting to get current session.');
|
||||
const coreSession = await this.identitySessionPort.getCurrentSession();
|
||||
if (!coreSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(coreSession.user.id); // Use userRepository to fetch full user
|
||||
if (!user) {
|
||||
// If session exists but user doesn't in DB, perhaps clear session?
|
||||
this.logger.warn(`[AuthService] Session found for user ID ${coreSession.user.id}, but user not found in repository.`);
|
||||
await this.identitySessionPort.clearSession(); // Clear potentially stale session
|
||||
return null;
|
||||
}
|
||||
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(User.fromStored(user));
|
||||
|
||||
return {
|
||||
token: coreSession.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
|
||||
const user = await this.signupUseCase.execute(params.email, params.password, params.displayName);
|
||||
|
||||
// Create session after successful signup
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
|
||||
try {
|
||||
const user = await this.loginUseCase.execute(params.email, params.password);
|
||||
// Create session after successful login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`[AuthService] Login failed for email ${params.email}:`, error);
|
||||
throw new InternalServerErrorException('Login failed due to invalid credentials or server error.');
|
||||
}
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
|
||||
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
|
||||
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
|
||||
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
|
||||
return { redirectUrl: result.redirectUrl, state: result.state };
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
|
||||
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
|
||||
|
||||
// Create session after successful iRacing login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.logger.debug('[AuthService] Attempting logout.');
|
||||
await this.logoutUseCase.execute();
|
||||
}
|
||||
}
|
||||
55
apps/api/src/modules/auth/dto/AuthDto.ts
Normal file
55
apps/api/src/modules/auth/dto/AuthDto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthenticatedUserDTO {
|
||||
@ApiProperty()
|
||||
userId: string;
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export class AuthSessionDTO {
|
||||
@ApiProperty()
|
||||
token: string;
|
||||
@ApiProperty()
|
||||
user: AuthenticatedUserDTO;
|
||||
}
|
||||
|
||||
export class SignupParams {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
@ApiProperty({ required: false })
|
||||
iracingCustomerId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
primaryDriverId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class LoginParams {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class IracingAuthRedirectResult {
|
||||
@ApiProperty()
|
||||
redirectUrl: string;
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
}
|
||||
|
||||
export class LoginWithIracingCallbackParams {
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
@ApiProperty({ required: false })
|
||||
returnTo?: string;
|
||||
}
|
||||
49
apps/api/src/modules/driver/DriverController.ts
Normal file
49
apps/api/src/modules/driver/DriverController.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
|
||||
|
||||
@ApiTags('drivers')
|
||||
@Controller('drivers')
|
||||
export class DriverController {
|
||||
constructor(private readonly driverService: DriverService) {}
|
||||
|
||||
@Get('leaderboard')
|
||||
@ApiOperation({ summary: 'Get drivers leaderboard' })
|
||||
@ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardViewModel })
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
return this.driverService.getDriversLeaderboard();
|
||||
}
|
||||
|
||||
@Get('total-drivers')
|
||||
@ApiOperation({ summary: 'Get the total number of drivers' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDto })
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
return this.driverService.getTotalDrivers();
|
||||
}
|
||||
|
||||
@Post('complete-onboarding')
|
||||
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
|
||||
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
|
||||
async completeOnboarding(
|
||||
@Body() input: CompleteOnboardingInput,
|
||||
@Req() req: Request,
|
||||
): Promise<CompleteOnboardingOutput> {
|
||||
// Assuming userId is available from the request (e.g., via auth middleware)
|
||||
const userId = req['user'].userId; // Placeholder for actual user extraction
|
||||
return this.driverService.completeOnboarding(userId, input);
|
||||
}
|
||||
|
||||
@Get(':driverId/races/:raceId/registration-status')
|
||||
@ApiOperation({ summary: 'Get driver registration status for a specific race' })
|
||||
@ApiResponse({ status: 200, description: 'Driver registration status', type: DriverRegistrationStatusViewModel })
|
||||
async getDriverRegistrationStatus(
|
||||
@Param('driverId') driverId: string,
|
||||
@Param('raceId') raceId: string,
|
||||
): Promise<DriverRegistrationStatusViewModel> {
|
||||
return this.driverService.getDriverRegistrationStatus({ driverId, raceId });
|
||||
}
|
||||
|
||||
// Add other Driver endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/driver/DriverModule.ts
Normal file
10
apps/api/src/modules/driver/DriverModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriverController } from './DriverController';
|
||||
|
||||
@Module({
|
||||
controllers: [DriverController],
|
||||
providers: [DriverService],
|
||||
exports: [DriverService],
|
||||
})
|
||||
export class DriverModule {}
|
||||
75
apps/api/src/modules/driver/DriverProviders.ts
Normal file
75
apps/api/src/modules/driver/DriverProviders.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
|
||||
// Import core interfaces
|
||||
import { IDriverRepository } from '../../../../core/racing/domain/repositories/IDriverRepository';
|
||||
import { IRankingService } from '../../../../core/racing/domain/services/IRankingService';
|
||||
import { IDriverStatsService } from '../../../../core/racing/domain/services/IDriverStatsService';
|
||||
import { DriverRatingProvider } from '../../../../core/racing/application/ports/DriverRatingProvider';
|
||||
import { IImageServicePort } from '../../../../core/racing/application/ports/IImageServicePort';
|
||||
import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { ILogger } from '../../../../core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService';
|
||||
import { InMemoryDriverStatsService } from '../../../adapters/racing/services/InMemoryDriverStatsService';
|
||||
import { InMemoryDriverRatingProvider } from '../../../adapters/racing/ports/InMemoryDriverRatingProvider';
|
||||
import { InMemoryImageServiceAdapter } from '../../../adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { InMemoryRaceRegistrationRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '../../../adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const RANKING_SERVICE_TOKEN = 'IRankingService';
|
||||
export const DRIVER_STATS_SERVICE_TOKEN = 'IDriverStatsService';
|
||||
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
|
||||
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
|
||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
DriverService, // Provide the service itself
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RANKING_SERVICE_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRankingService(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_STATS_SERVICE_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverStatsService(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_RATING_PROVIDER_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverRatingProvider(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IMAGE_SERVICE_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryImageServiceAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRaceRegistrationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryNotificationPreferenceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
];
|
||||
46
apps/api/src/modules/driver/DriverService.ts
Normal file
46
apps/api/src/modules/driver/DriverService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto';
|
||||
|
||||
@Injectable()
|
||||
export class DriverService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
console.log('[DriverService] Returning mock driver leaderboard.');
|
||||
const drivers: DriverLeaderboardItemViewModel[] = [
|
||||
{ id: 'driver-1', name: 'Mock Driver 1', rating: 2500, skillLevel: 'Pro', nationality: 'DE', racesCompleted: 50, wins: 10, podiums: 20, isActive: true, rank: 1, avatarUrl: 'https://cdn.example.com/avatars/driver-1.png' },
|
||||
{ id: 'driver-2', name: 'Mock Driver 2', rating: 2400, skillLevel: 'Amateur', nationality: 'US', racesCompleted: 40, wins: 5, podiums: 15, isActive: true, rank: 2, avatarUrl: 'https://cdn.example.com/avatars/driver-2.png' },
|
||||
];
|
||||
return {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces: drivers.reduce((sum, item) => sum + (item.racesCompleted ?? 0), 0),
|
||||
totalWins: drivers.reduce((sum, item) => sum + (item.wins ?? 0), 0),
|
||||
activeCount: drivers.filter(d => d.isActive).length,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
console.log('[DriverService] Returning mock total drivers.');
|
||||
return {
|
||||
totalDrivers: 2,
|
||||
};
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
|
||||
console.log('Completing onboarding for user:', userId, input);
|
||||
return {
|
||||
success: true,
|
||||
driverId: `driver-${userId}-onboarded`,
|
||||
};
|
||||
}
|
||||
|
||||
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
|
||||
console.log('Checking driver registration status:', query);
|
||||
return {
|
||||
isRegistered: false, // Mock response
|
||||
raceId: query.raceId,
|
||||
driverId: query.driverId,
|
||||
};
|
||||
}
|
||||
}
|
||||
138
apps/api/src/modules/driver/dto/DriverDto.ts
Normal file
138
apps/api/src/modules/driver/dto/DriverDto.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
|
||||
|
||||
export class DriverLeaderboardItemViewModel {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
rating: number;
|
||||
|
||||
@ApiProperty()
|
||||
skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
|
||||
|
||||
@ApiProperty()
|
||||
nationality: string;
|
||||
|
||||
@ApiProperty()
|
||||
racesCompleted: number;
|
||||
|
||||
@ApiProperty()
|
||||
wins: number;
|
||||
|
||||
@ApiProperty()
|
||||
podiums: number;
|
||||
|
||||
@ApiProperty()
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
rank: number;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class DriversLeaderboardViewModel {
|
||||
@ApiProperty({ type: [DriverLeaderboardItemViewModel] })
|
||||
drivers: DriverLeaderboardItemViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalRaces: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalWins: number;
|
||||
|
||||
@ApiProperty()
|
||||
activeCount: number;
|
||||
}
|
||||
|
||||
export class DriverStatsDto {
|
||||
@ApiProperty()
|
||||
totalDrivers: number;
|
||||
}
|
||||
|
||||
export class CompleteOnboardingInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
displayName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
country: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timezone?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export class CompleteOnboardingOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
driverId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export class GetDriverRegistrationStatusQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class DriverRegistrationStatusViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isRegistered: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class DriverDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string; // Display name or full name
|
||||
}
|
||||
|
||||
// Add other DTOs for driver-related logic as needed
|
||||
136
apps/api/src/modules/league/LeagueController.ts
Normal file
136
apps/api/src/modules/league/LeagueController.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel } from './dto/LeagueDto';
|
||||
import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries
|
||||
|
||||
@ApiTags('leagues')
|
||||
@Controller('leagues')
|
||||
export class LeagueController {
|
||||
constructor(private readonly leagueService: LeagueService) {}
|
||||
|
||||
@Get('all-with-capacity')
|
||||
@ApiOperation({ summary: 'Get all leagues with their capacity information' })
|
||||
@ApiResponse({ status: 200, description: 'List of leagues with capacity', type: AllLeaguesWithCapacityViewModel })
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Get('total-leagues')
|
||||
@ApiOperation({ summary: 'Get the total number of leagues' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of leagues', type: LeagueStatsDto })
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
return this.leagueService.getTotalLeagues();
|
||||
}
|
||||
|
||||
@Get(':leagueId/join-requests')
|
||||
@ApiOperation({ summary: 'Get all outstanding join requests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of join requests', type: [LeagueJoinRequestViewModel] })
|
||||
async getJoinRequests(@Param('leagueId') leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
// No specific query DTO needed for GET, leagueId from param
|
||||
return this.leagueService.getLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
@Post(':leagueId/join-requests/approve')
|
||||
@ApiOperation({ summary: 'Approve a league join request' })
|
||||
@ApiBody({ type: ApproveJoinRequestInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Join request approved', type: ApproveJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async approveJoinRequest(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() input: ApproveJoinRequestInput,
|
||||
): Promise<ApproveJoinRequestOutput> {
|
||||
return this.leagueService.approveLeagueJoinRequest({ ...input, leagueId });
|
||||
}
|
||||
|
||||
@Post(':leagueId/join-requests/reject')
|
||||
@ApiOperation({ summary: 'Reject a league join request' })
|
||||
@ApiBody({ type: RejectJoinRequestInput })
|
||||
@ApiResponse({ status: 200, description: 'Join request rejected', type: RejectJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async rejectJoinRequest(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() input: RejectJoinRequestInput,
|
||||
): Promise<RejectJoinRequestOutput> {
|
||||
return this.leagueService.rejectLeagueJoinRequest({ ...input, leagueId });
|
||||
}
|
||||
|
||||
@Get(':leagueId/permissions/:performerDriverId')
|
||||
@ApiOperation({ summary: 'Get league admin permissions for a performer' })
|
||||
@ApiResponse({ status: 200, description: 'League admin permissions', type: LeagueAdminPermissionsViewModel })
|
||||
async getLeagueAdminPermissions(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
): Promise<LeagueAdminPermissionsViewModel> {
|
||||
// No specific input DTO needed for Get, parameters from path
|
||||
return this.leagueService.getLeagueAdminPermissions({ leagueId, performerDriverId });
|
||||
}
|
||||
|
||||
@Patch(':leagueId/members/:targetDriverId/remove')
|
||||
@ApiOperation({ summary: 'Remove a member from the league' })
|
||||
@ApiBody({ type: RemoveLeagueMemberInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Member removed successfully', type: RemoveLeagueMemberOutput })
|
||||
@ApiResponse({ status: 400, description: 'Cannot remove member' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async removeLeagueMember(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: RemoveLeagueMemberInput, // Body content for a patch often includes IDs
|
||||
): Promise<RemoveLeagueMemberOutput> {
|
||||
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
|
||||
}
|
||||
|
||||
@Patch(':leagueId/members/:targetDriverId/role')
|
||||
@ApiOperation({ summary: "Update a member's role in the league" })
|
||||
@ApiBody({ type: UpdateLeagueMemberRoleInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Member role updated successfully', type: UpdateLeagueMemberRoleOutput })
|
||||
@ApiResponse({ status: 400, description: 'Cannot update role' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async updateLeagueMemberRole(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: UpdateLeagueMemberRoleInput, // Body includes newRole, other for swagger
|
||||
): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
return this.leagueService.updateLeagueMemberRole({ leagueId, performerDriverId, targetDriverId, newRole: input.newRole });
|
||||
}
|
||||
|
||||
@Get(':leagueId/owner-summary/:ownerId')
|
||||
@ApiOperation({ summary: 'Get owner summary for a league' })
|
||||
@ApiResponse({ status: 200, description: 'League owner summary', type: LeagueOwnerSummaryViewModel })
|
||||
@ApiResponse({ status: 404, description: 'Owner or league not found' })
|
||||
async getLeagueOwnerSummary(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('ownerId') ownerId: string,
|
||||
): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId };
|
||||
return this.leagueService.getLeagueOwnerSummary(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/config')
|
||||
@ApiOperation({ summary: 'Get league full configuration' })
|
||||
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDto })
|
||||
async getLeagueFullConfig(
|
||||
@Param('leagueId') leagueId: string,
|
||||
): Promise<LeagueConfigFormModelDto | null> {
|
||||
const query: GetLeagueAdminConfigQuery = { leagueId };
|
||||
return this.leagueService.getLeagueFullConfig(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/protests')
|
||||
@ApiOperation({ summary: 'Get protests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsViewModel })
|
||||
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||
const query: GetLeagueProtestsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueProtests(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/seasons')
|
||||
@ApiOperation({ summary: 'Get seasons for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryViewModel] })
|
||||
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
const query: GetLeagueSeasonsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueSeasons(query);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/league/LeagueModule.ts
Normal file
10
apps/api/src/modules/league/LeagueModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeagueController } from './LeagueController';
|
||||
|
||||
@Module({
|
||||
controllers: [LeagueController],
|
||||
providers: [LeagueService],
|
||||
exports: [LeagueService],
|
||||
})
|
||||
export class LeagueModule {}
|
||||
83
apps/api/src/modules/league/LeagueProviders.ts
Normal file
83
apps/api/src/modules/league/LeagueProviders.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
|
||||
// Import core interfaces
|
||||
import { ILeagueRepository } from 'core/racing/domain/repositories/ILeagueRepository';
|
||||
import { ILeagueMembershipRepository } from 'core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { ILeagueStandingsRepository } from 'core/league/application/ports/ILeagueStandingsRepository';
|
||||
import { ISeasonRepository } from 'core/racing/domain/repositories/ISeasonRepository';
|
||||
import { ILeagueScoringConfigRepository } from 'core/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import { IGameRepository } from 'core/racing/domain/repositories/IGameRepository';
|
||||
import { IProtestRepository } from 'core/racing/domain/repositories/IProtestRepository';
|
||||
import { IRaceRepository } from 'core/racing/domain/repositories/IRaceRepository';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryLeagueStandingsRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||
import { InMemorySeasonRepository } from 'adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueScoringConfigRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||
import { InMemoryGameRepository } from 'adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||
import { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository';
|
||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
|
||||
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
|
||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
export const LeagueProviders: Provider[] = [
|
||||
LeagueService, // Provide the service itself
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueRepository(logger), // Factory for InMemoryLeagueRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueMembershipRepository(logger), // Factory for InMemoryLeagueMembershipRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SEASON_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GAME_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryGameRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PROTEST_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryProtestRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
];
|
||||
125
apps/api/src/modules/league/LeagueService.ts
Normal file
125
apps/api/src/modules/league/LeagueService.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto';
|
||||
import { DriverDto } from '../driver/dto/DriverDto'; // Using the local DTO for mock data
|
||||
import { RaceDto } from '../race/dto/RaceDto'; // Using the local DTO for mock data
|
||||
|
||||
const mockDriverData: Map<string, DriverDto> = new Map();
|
||||
mockDriverData.set('driver-owner-1', { id: 'driver-owner-1', name: 'Owner Driver' });
|
||||
mockDriverData.set('driver-1', { id: 'driver-1', name: 'Demo Driver 1' });
|
||||
mockDriverData.set('driver-2', { id: 'driver-2', name: 'Demo Driver 2' });
|
||||
|
||||
const mockRaceData: Map<string, RaceDto> = new Map();
|
||||
mockRaceData.set('race-1', { id: 'race-1', name: 'Test Race 1', date: new Date().toISOString() });
|
||||
mockRaceData.set('race-2', { id: 'race-2', name: 'Test Race 2', date: new Date().toISOString() });
|
||||
|
||||
@Injectable()
|
||||
export class LeagueService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
console.log('[LeagueService] Returning mock leagues with capacity.');
|
||||
return {
|
||||
leagues: [
|
||||
{ id: 'league-1', name: 'Global Racing', description: 'The premier league', ownerId: 'owner-1', settings: { maxDrivers: 100 }, createdAt: new Date().toISOString(), usedSlots: 50, socialLinks: { discordUrl: 'https://discord.gg/test' } },
|
||||
{ id: 'league-2', name: 'Amateur Series', description: 'Learn the ropes', ownerId: 'owner-2', settings: { maxDrivers: 50 }, createdAt: new Date().toISOString(), usedSlots: 20 },
|
||||
],
|
||||
totalCount: 2,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
console.log('[LeagueService] Returning mock total leagues.');
|
||||
return { totalLeagues: 2 };
|
||||
}
|
||||
|
||||
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
console.log(`[LeagueService] Returning mock join requests for league: ${leagueId}.`);
|
||||
return [
|
||||
{
|
||||
id: 'join-req-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: new Date(),
|
||||
message: 'I want to join!',
|
||||
driver: mockDriverData.get('driver-1'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
|
||||
console.log('Approving join request:', input);
|
||||
return { success: true, message: 'Join request approved.' };
|
||||
}
|
||||
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
|
||||
console.log('Rejecting join request:', input);
|
||||
return { success: true, message: 'Join request rejected.' };
|
||||
}
|
||||
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
|
||||
console.log('Getting league admin permissions:', query);
|
||||
return { canRemoveMember: true, canUpdateRoles: true };
|
||||
}
|
||||
|
||||
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
|
||||
console.log('Removing league member:', input.leagueId, input.targetDriverId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
console.log('Updating league member role:', input.leagueId, input.targetDriverId, input.newRole);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
console.log('Getting league owner summary:', query);
|
||||
return {
|
||||
driver: mockDriverData.get(query.ownerId)!,
|
||||
rating: 2000,
|
||||
rank: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
|
||||
console.log('Getting league full config:', query);
|
||||
return {
|
||||
leagueId: 'league-1',
|
||||
basics: { name: 'Demo League', description: 'A demo league', visibility: 'public' },
|
||||
structure: { mode: 'solo' },
|
||||
championships: [],
|
||||
scoring: { type: 'standard', points: 10 },
|
||||
dropPolicy: { strategy: 'none' },
|
||||
timings: { raceDayOfWeek: 'Sunday', raceTimeHour: 20, raceTimeMinute: 0 },
|
||||
stewarding: {
|
||||
decisionMode: 'single_steward',
|
||||
requireDefense: false,
|
||||
defenseTimeLimit: 24,
|
||||
voteTimeLimit: 24,
|
||||
protestDeadlineHours: 2,
|
||||
stewardingClosesHours: 24,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
|
||||
console.log('Getting league protests:', query);
|
||||
return {
|
||||
protests: [
|
||||
{ id: 'protest-1', raceId: 'race-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', submittedAt: new Date(), description: 'Bad driving!', status: 'pending' },
|
||||
],
|
||||
racesById: { 'race-1': mockRaceData.get('race-1')! },
|
||||
driversById: { 'driver-1': mockDriverData.get('driver-1')!, 'driver-2': mockDriverData.get('driver-2')! },
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
console.log('Getting league seasons:', query);
|
||||
return [
|
||||
{ seasonId: 'season-1', name: 'Season 1', status: 'active', startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), isPrimary: true, isParallelActive: false },
|
||||
{ seasonId: 'season-2', name: 'Season 2', status: 'upcoming', startDate: new Date('2026-01-01'), endDate: new Date('2026-12-31'), isPrimary: false, isParallelActive: false },
|
||||
];
|
||||
}
|
||||
}
|
||||
561
apps/api/src/modules/league/dto/LeagueDto.ts
Normal file
561
apps/api/src/modules/league/dto/LeagueDto.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional, IsEnum, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { DriverDto } from '../../driver/dto/DriverDto';
|
||||
import { RaceDto } from '../../race/dto/RaceDto';
|
||||
|
||||
|
||||
export class LeagueSettingsDto {
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
maxDrivers?: number;
|
||||
// Add other league settings properties as needed
|
||||
}
|
||||
|
||||
export class LeagueWithCapacityViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
// ... other properties of LeagueWithCapacityViewModel
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: () => LeagueSettingsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueSettingsDto)
|
||||
settings: LeagueSettingsDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
usedSlots: number;
|
||||
|
||||
@ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
|
||||
@IsOptional()
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AllLeaguesWithCapacityViewModel {
|
||||
@ApiProperty({ type: [LeagueWithCapacityViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueWithCapacityViewModel)
|
||||
leagues: LeagueWithCapacityViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class LeagueStatsDto {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalLeagues: number;
|
||||
}
|
||||
|
||||
export class ProtestDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
protestingDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
accusedDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
submittedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
|
||||
@IsEnum(['pending', 'accepted', 'rejected'])
|
||||
status: 'pending' | 'accepted' | 'rejected';
|
||||
}
|
||||
|
||||
export class SeasonDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty({ enum: ['planned', 'active', 'completed'] })
|
||||
@IsEnum(['planned', 'active', 'completed'])
|
||||
status: 'planned' | 'active' | 'completed';
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isPrimary: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonGroupId?: string;
|
||||
}
|
||||
|
||||
export class LeagueJoinRequestViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
requestedAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
message?: string;
|
||||
|
||||
@ApiProperty({ type: () => DriverDto, required: false })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driver?: DriverDto;
|
||||
}
|
||||
|
||||
export class GetLeagueJoinRequestsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class ApproveJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class ApproveJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class RejectJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class RejectJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminPermissionsInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
}
|
||||
|
||||
export class LeagueAdminPermissionsViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canRemoveMember: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canUpdateRoles: boolean;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
targetDriverId: string;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class UpdateLeagueMemberRoleInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
targetDriverId: string;
|
||||
|
||||
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
|
||||
@IsEnum(['owner', 'manager', 'member'])
|
||||
newRole: 'owner' | 'manager' | 'member';
|
||||
}
|
||||
|
||||
export class UpdateLeagueMemberRoleOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class GetLeagueOwnerSummaryQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueOwnerSummaryViewModel {
|
||||
@ApiProperty({ type: () => DriverDto })
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driver: DriverDto;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rating: number | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelBasicsDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ enum: ['public', 'private'] })
|
||||
@IsEnum(['public', 'private'])
|
||||
visibility: 'public' | 'private';
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelStructureDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsEnum(['solo', 'team'])
|
||||
mode: 'solo' | 'team';
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelScoringDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
points: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelDropPolicyDto {
|
||||
@ApiProperty({ enum: ['none', 'worst_n'] })
|
||||
@IsEnum(['none', 'worst_n'])
|
||||
strategy: 'none' | 'worst_n';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
n?: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelStewardingDto {
|
||||
@ApiProperty({ enum: ['single_steward', 'committee_vote'] })
|
||||
@IsEnum(['single_steward', 'committee_vote'])
|
||||
decisionMode: 'single_steward' | 'committee_vote';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
requiredVotes?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
requireDefense: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
defenseTimeLimit: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
voteTimeLimit: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
protestDeadlineHours: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
stewardingClosesHours: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
notifyAccusedOnProtest: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
notifyOnVoteRequired: boolean;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelTimingsDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceDayOfWeek: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
raceTimeHour: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
raceTimeMinute: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelBasicsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelBasicsDto)
|
||||
basics: LeagueConfigFormModelBasicsDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelStructureDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelStructureDto)
|
||||
structure: LeagueConfigFormModelStructureDto;
|
||||
|
||||
@ApiProperty({ type: [Object] })
|
||||
@IsArray()
|
||||
championships: any[];
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelScoringDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelScoringDto)
|
||||
scoring: LeagueConfigFormModelScoringDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelDropPolicyDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDropPolicyDto)
|
||||
dropPolicy: LeagueConfigFormModelDropPolicyDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelTimingsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelTimingsDto)
|
||||
timings: LeagueConfigFormModelTimingsDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelStewardingDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelStewardingDto)
|
||||
stewarding: LeagueConfigFormModelStewardingDto;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminConfigQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminConfigOutput {
|
||||
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDto)
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export class LeagueAdminConfigViewModel {
|
||||
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDto)
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export class GetLeagueProtestsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueAdminProtestsViewModel {
|
||||
@ApiProperty({ type: [ProtestDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProtestDto)
|
||||
protests: ProtestDto[];
|
||||
|
||||
@ApiProperty({ type: () => RaceDto })
|
||||
@ValidateNested()
|
||||
@Type(() => RaceDto)
|
||||
racesById: { [raceId: string]: RaceDto };
|
||||
|
||||
@ApiProperty({ type: () => DriverDto })
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driversById: { [driverId: string]: DriverDto };
|
||||
}
|
||||
|
||||
export class GetLeagueSeasonsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueSeasonSummaryViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
status: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isPrimary: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isParallelActive: boolean;
|
||||
}
|
||||
|
||||
export class LeagueAdminViewModel {
|
||||
@ApiProperty({ type: [LeagueJoinRequestViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueJoinRequestViewModel)
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
|
||||
@ApiProperty({ type: () => LeagueOwnerSummaryViewModel, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueOwnerSummaryViewModel)
|
||||
ownerSummary: LeagueOwnerSummaryViewModel | null;
|
||||
|
||||
@ApiProperty({ type: () => LeagueAdminConfigViewModel })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueAdminConfigViewModel)
|
||||
config: LeagueAdminConfigViewModel;
|
||||
|
||||
@ApiProperty({ type: () => LeagueAdminProtestsViewModel })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueAdminProtestsViewModel)
|
||||
protests: LeagueAdminProtestsViewModel;
|
||||
|
||||
@ApiProperty({ type: [LeagueSeasonSummaryViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueSeasonSummaryViewModel)
|
||||
seasons: LeagueSeasonSummaryViewModel[];
|
||||
}
|
||||
26
apps/api/src/modules/media/MediaController.ts
Normal file
26
apps/api/src/modules/media/MediaController.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller, Post, Body, HttpStatus, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { MediaService } from './MediaService';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
|
||||
@ApiTags('media')
|
||||
@Controller('media')
|
||||
export class MediaController {
|
||||
constructor(private readonly mediaService: MediaService) {}
|
||||
|
||||
@Post('avatar/generate')
|
||||
@ApiOperation({ summary: 'Request avatar generation' })
|
||||
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput })
|
||||
async requestAvatarGeneration(
|
||||
@Body() input: RequestAvatarGenerationInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const result = await this.mediaService.requestAvatarGeneration(input);
|
||||
if (result.success) {
|
||||
res.status(HttpStatus.CREATED).json(result);
|
||||
} else {
|
||||
res.status(HttpStatus.BAD_REQUEST).json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/media/MediaModule.ts
Normal file
10
apps/api/src/modules/media/MediaModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
import { MediaController } from './MediaController';
|
||||
|
||||
@Module({
|
||||
controllers: [MediaController],
|
||||
providers: [MediaService],
|
||||
exports: [MediaService],
|
||||
})
|
||||
export class MediaModule {}
|
||||
41
apps/api/src/modules/media/MediaProviders.ts
Normal file
41
apps/api/src/modules/media/MediaProviders.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
||||
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
||||
export const LOGGER_TOKEN = 'ILogger';
|
||||
|
||||
export const MediaProviders: Provider[] = [
|
||||
MediaService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryAvatarGenerationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: FACE_VALIDATION_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryFaceValidationAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
*/
|
||||
];
|
||||
20
apps/api/src/modules/media/MediaService.ts
Normal file
20
apps/api/src/modules/media/MediaService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
|
||||
console.log('[MediaService] Returning mock avatar generation request. Input:', input);
|
||||
return {
|
||||
success: true,
|
||||
requestId: `req-${Date.now()}`,
|
||||
avatarUrls: [
|
||||
'https://cdn.example.com/avatars/mock-avatar-1.png',
|
||||
'https://cdn.example.com/avatars/mock-avatar-2.png',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
40
apps/api/src/modules/media/dto/MediaDto.ts
Normal file
40
apps/api/src/modules/media/dto/MediaDto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsBoolean } from 'class-validator';
|
||||
|
||||
export class RequestAvatarGenerationInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
facePhotoData: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
suitColor: string;
|
||||
}
|
||||
|
||||
export class RequestAvatarGenerationOutput {
|
||||
@ApiProperty({ type: Boolean })
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
requestId?: string;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
avatarUrls?: string[];
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// Assuming FacePhotoData and SuitColor are simple string types for DTO purposes
|
||||
export type FacePhotoData = string;
|
||||
export type SuitColor = string;
|
||||
96
apps/api/src/modules/payments/PaymentsController.ts
Normal file
96
apps/api/src/modules/payments/PaymentsController.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Query, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, GetPaymentsQuery, GetPaymentsOutput, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput } from './dto/PaymentsDto';
|
||||
|
||||
@ApiTags('payments')
|
||||
@Controller('payments')
|
||||
export class PaymentsController {
|
||||
constructor(private readonly paymentsService: PaymentsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get payments based on filters' })
|
||||
@ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput })
|
||||
async getPayments(@Query() query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
return this.paymentsService.getPayments(query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new payment' })
|
||||
@ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput })
|
||||
async createPayment(@Body() input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
return this.paymentsService.createPayment(input);
|
||||
}
|
||||
|
||||
@Patch('status')
|
||||
@ApiOperation({ summary: 'Update the status of a payment' })
|
||||
@ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput })
|
||||
async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
return this.paymentsService.updatePaymentStatus(input);
|
||||
}
|
||||
|
||||
@Get('membership-fees')
|
||||
@ApiOperation({ summary: 'Get membership fees and member payments' })
|
||||
@ApiResponse({ status: 200, description: 'Membership fee configuration and member payments', type: GetMembershipFeesOutput })
|
||||
async getMembershipFees(@Query() query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
return this.paymentsService.getMembershipFees(query);
|
||||
}
|
||||
|
||||
@Post('membership-fees')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create or update membership fee configuration' })
|
||||
@ApiResponse({ status: 201, description: 'Membership fee configuration created or updated', type: UpsertMembershipFeeOutput })
|
||||
async upsertMembershipFee(@Body() input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
return this.paymentsService.upsertMembershipFee(input);
|
||||
}
|
||||
|
||||
@Patch('membership-fees/member-payment')
|
||||
@ApiOperation({ summary: 'Record or update a member payment' })
|
||||
@ApiResponse({ status: 200, description: 'Member payment recorded or updated', type: UpdateMemberPaymentOutput })
|
||||
async updateMemberPayment(@Body() input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
return this.paymentsService.updateMemberPayment(input);
|
||||
}
|
||||
@Get('prizes')
|
||||
@ApiOperation({ summary: 'Get prizes for a league or season' })
|
||||
@ApiResponse({ status: 200, description: 'List of prizes', type: GetPrizesOutput })
|
||||
async getPrizes(@Query() query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
return this.paymentsService.getPrizes(query);
|
||||
}
|
||||
|
||||
@Post('prizes')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new prize' })
|
||||
@ApiResponse({ status: 201, description: 'Prize created', type: CreatePrizeOutput })
|
||||
async createPrize(@Body() input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
return this.paymentsService.createPrize(input);
|
||||
}
|
||||
|
||||
@Patch('prizes/award')
|
||||
@ApiOperation({ summary: 'Award a prize to a driver' })
|
||||
@ApiResponse({ status: 200, description: 'Prize awarded', type: AwardPrizeOutput })
|
||||
async awardPrize(@Body() input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
return this.paymentsService.awardPrize(input);
|
||||
}
|
||||
|
||||
@Delete('prizes')
|
||||
@ApiOperation({ summary: 'Delete a prize' })
|
||||
@ApiResponse({ status: 200, description: 'Prize deleted', type: DeletePrizeOutput })
|
||||
async deletePrize(@Query() query: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
return this.paymentsService.deletePrize(query);
|
||||
}
|
||||
@Get('wallets')
|
||||
@ApiOperation({ summary: 'Get wallet information and transactions' })
|
||||
@ApiResponse({ status: 200, description: 'Wallet and transaction data', type: GetWalletOutput })
|
||||
async getWallet(@Query() query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
return this.paymentsService.getWallet(query);
|
||||
}
|
||||
|
||||
@Post('wallets/transactions')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Process a wallet transaction (deposit or withdrawal)' })
|
||||
@ApiResponse({ status: 201, description: 'Wallet transaction processed', type: ProcessWalletTransactionOutput })
|
||||
async processWalletTransaction(@Body() input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
return this.paymentsService.processWalletTransaction(input);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/payments/PaymentsModule.ts
Normal file
10
apps/api/src/modules/payments/PaymentsModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { PaymentsController } from './PaymentsController';
|
||||
|
||||
@Module({
|
||||
controllers: [PaymentsController],
|
||||
providers: [PaymentsService],
|
||||
exports: [PaymentsService],
|
||||
})
|
||||
export class PaymentsModule {}
|
||||
67
apps/api/src/modules/payments/PaymentsProviders.ts
Normal file
67
apps/api/src/modules/payments/PaymentsProviders.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
// Import core interfaces
|
||||
import { IPaymentRepository } from 'core/payments/domain/repositories/IPaymentRepository';
|
||||
import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMembershipFeeRepository';
|
||||
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
|
||||
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
|
||||
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { InMemoryMembershipFeeRepository } from 'adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryPrizeRepository } from 'adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||
import { InMemoryWalletRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { InMemoryPaymentGateway } from 'adapters/payments/ports/InMemoryPaymentGateway';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
|
||||
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
|
||||
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
|
||||
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
|
||||
export const PAYMENT_GATEWAY_TOKEN = 'IPaymentGateway';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in other Providers, but good to have here too
|
||||
|
||||
export const PaymentsProviders: Provider[] = [
|
||||
PaymentsService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPaymentRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: MEMBERSHIP_FEE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryMembershipFeeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PRIZE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPrizeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: WALLET_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryWalletRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_GATEWAY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPaymentGateway(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
*/
|
||||
];
|
||||
346
apps/api/src/modules/payments/PaymentsService.ts
Normal file
346
apps/api/src/modules/payments/PaymentsService.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, PaymentDto, GetPaymentsQuery, GetPaymentsOutput, PaymentStatus, MembershipFeeDto, MemberPaymentDto, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, MembershipFeeType, MemberPaymentStatus, PrizeDto, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, PrizeType, WalletDto, TransactionDto, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput, TransactionType, ReferenceType } from './dto/PaymentsDto';
|
||||
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
|
||||
|
||||
const payments: Map<string, PaymentDto> = new Map();
|
||||
const membershipFees: Map<string, MembershipFeeDto> = new Map();
|
||||
const memberPayments: Map<string, MemberPaymentDto> = new Map();
|
||||
const prizes: Map<string, PrizeDto> = new Map();
|
||||
const wallets: Map<string, WalletDto> = new Map();
|
||||
const transactions: Map<string, TransactionDto> = new Map();
|
||||
|
||||
const PLATFORM_FEE_RATE = 0.10;
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
let results = Array.from(payments.values());
|
||||
|
||||
if (query.leagueId) {
|
||||
results = results.filter(p => p.leagueId === query.leagueId);
|
||||
}
|
||||
if (query.payerId) {
|
||||
results = results.filter(p => p.payerId === query.payerId);
|
||||
}
|
||||
if (query.type) {
|
||||
results = results.filter(p => p.type === query.type);
|
||||
}
|
||||
|
||||
return { payments: results };
|
||||
}
|
||||
|
||||
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
|
||||
|
||||
const platformFee = amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = amount - platformFee;
|
||||
|
||||
const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const payment: PaymentDto = {
|
||||
id,
|
||||
type,
|
||||
amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
payerId,
|
||||
payerType,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
payments.set(id, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
const { paymentId, status } = input;
|
||||
|
||||
const payment = payments.get(paymentId);
|
||||
if (!payment) {
|
||||
throw new Error('Payment not found');
|
||||
}
|
||||
|
||||
payment.status = status;
|
||||
if (status === PaymentStatus.COMPLETED) {
|
||||
payment.completedAt = new Date();
|
||||
}
|
||||
|
||||
payments.set(paymentId, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
const { leagueId, driverId } = query;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('leagueId is required');
|
||||
}
|
||||
|
||||
const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
|
||||
|
||||
let payments: MemberPaymentDto[] = [];
|
||||
if (driverId) {
|
||||
payments = Array.from(memberPayments.values()).filter(
|
||||
p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId
|
||||
);
|
||||
}
|
||||
|
||||
return { fee, payments };
|
||||
}
|
||||
|
||||
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
const { leagueId, seasonId, type, amount } = input;
|
||||
|
||||
// Check for existing fee config
|
||||
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
|
||||
|
||||
if (existingFee) {
|
||||
// Update existing fee
|
||||
existingFee.type = type;
|
||||
existingFee.amount = amount;
|
||||
existingFee.seasonId = seasonId || existingFee.seasonId;
|
||||
existingFee.enabled = amount > 0;
|
||||
existingFee.updatedAt = new Date();
|
||||
membershipFees.set(existingFee.id, existingFee);
|
||||
return { fee: existingFee };
|
||||
}
|
||||
|
||||
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const fee: MembershipFeeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
type,
|
||||
amount,
|
||||
enabled: amount > 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
membershipFees.set(id, fee);
|
||||
|
||||
return { fee };
|
||||
}
|
||||
|
||||
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
const { feeId, driverId, status, paidAt } = input;
|
||||
|
||||
const fee = membershipFees.get(feeId);
|
||||
if (!fee) {
|
||||
throw new Error('Membership fee configuration not found');
|
||||
}
|
||||
|
||||
// Find or create payment record
|
||||
let payment = Array.from(memberPayments.values()).find(
|
||||
p => p.feeId === feeId && p.driverId === driverId
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
const platformFee = fee.amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = fee.amount - platformFee;
|
||||
|
||||
const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
payment = {
|
||||
id: paymentId,
|
||||
feeId,
|
||||
driverId,
|
||||
amount: fee.amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
status: MemberPaymentStatus.PENDING,
|
||||
dueDate: new Date(),
|
||||
};
|
||||
memberPayments.set(paymentId, payment);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
payment.status = status;
|
||||
}
|
||||
if (paidAt || status === MemberPaymentStatus.PAID) {
|
||||
payment.paidAt = paidAt ? new Date(paidAt) : new Date();
|
||||
}
|
||||
|
||||
memberPayments.set(payment.id, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
const { leagueId, seasonId } = query;
|
||||
|
||||
let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
|
||||
|
||||
if (seasonId) {
|
||||
results = results.filter(p => p.seasonId === seasonId);
|
||||
}
|
||||
|
||||
results.sort((a, b) => a.position - b.position);
|
||||
|
||||
return { prizes: results };
|
||||
}
|
||||
|
||||
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
const { leagueId, seasonId, position, name, amount, type, description } = input;
|
||||
|
||||
// Check for duplicate position
|
||||
const existingPrize = Array.from(prizes.values()).find(
|
||||
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
|
||||
);
|
||||
|
||||
if (existingPrize) {
|
||||
throw new Error(`Prize for position ${position} already exists`);
|
||||
}
|
||||
|
||||
const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const prize: PrizeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId,
|
||||
position,
|
||||
name,
|
||||
amount,
|
||||
type,
|
||||
description: description || undefined,
|
||||
awarded: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
prizes.set(id, prize);
|
||||
|
||||
return { prize };
|
||||
}
|
||||
|
||||
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
const { prizeId, driverId } = input;
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Prize has already been awarded');
|
||||
}
|
||||
|
||||
prize.awarded = true;
|
||||
prize.awardedTo = driverId;
|
||||
prize.awardedAt = new Date();
|
||||
|
||||
prizes.set(prizeId, prize);
|
||||
|
||||
return { prize };
|
||||
}
|
||||
|
||||
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
const { prizeId } = input;
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Cannot delete an awarded prize');
|
||||
}
|
||||
|
||||
prizes.delete(prizeId);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
const { leagueId } = query;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('LeagueId is required');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
const walletTransactions = Array.from(transactions.values())
|
||||
.filter(t => t.walletId === wallet!.id)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return { wallet, transactions: walletTransactions };
|
||||
}
|
||||
|
||||
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
const { leagueId, type, amount, description, referenceId, referenceType } = input;
|
||||
|
||||
if (!leagueId || !type || amount === undefined || !description) {
|
||||
throw new Error('Missing required fields: leagueId, type, amount, description');
|
||||
}
|
||||
|
||||
if (type !== TransactionType.DEPOSIT && type !== TransactionType.WITHDRAWAL) {
|
||||
throw new Error('Type must be "deposit" or "withdrawal"');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
if (type === TransactionType.WITHDRAWAL) {
|
||||
if (amount > wallet.balance) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
}
|
||||
|
||||
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const transaction: TransactionDto = {
|
||||
id: transactionId,
|
||||
walletId: wallet.id,
|
||||
type,
|
||||
amount,
|
||||
description,
|
||||
referenceId: referenceId || undefined,
|
||||
referenceType: referenceType || undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
transactions.set(transactionId, transaction);
|
||||
|
||||
if (type === TransactionType.DEPOSIT) {
|
||||
wallet.balance += amount;
|
||||
wallet.totalRevenue += amount;
|
||||
} else {
|
||||
wallet.balance -= amount;
|
||||
wallet.totalWithdrawn += amount;
|
||||
}
|
||||
|
||||
wallets.set(wallet.id, wallet);
|
||||
|
||||
return { wallet, transaction };
|
||||
}
|
||||
}
|
||||
566
apps/api/src/modules/payments/dto/PaymentsDto.ts
Normal file
566
apps/api/src/modules/payments/dto/PaymentsDto.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean } from 'class-validator';
|
||||
|
||||
export enum PaymentType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
}
|
||||
|
||||
export enum PayerType {
|
||||
SPONSOR = 'sponsor',
|
||||
DRIVER = 'driver',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'pending',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
|
||||
export class PaymentDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentType })
|
||||
@IsEnum(PaymentType)
|
||||
type: PaymentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
platformFee: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
netAmount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
payerId: string;
|
||||
|
||||
@ApiProperty({ enum: PayerType })
|
||||
@IsEnum(PayerType)
|
||||
payerType: PayerType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentStatus })
|
||||
@IsEnum(PaymentStatus)
|
||||
status: PaymentStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
export class CreatePaymentInput {
|
||||
@ApiProperty({ enum: PaymentType })
|
||||
@IsEnum(PaymentType)
|
||||
type: PaymentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
payerId: string;
|
||||
|
||||
@ApiProperty({ enum: PayerType })
|
||||
@IsEnum(PayerType)
|
||||
payerType: PayerType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class CreatePaymentOutput {
|
||||
@ApiProperty({ type: PaymentDto })
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
paymentId: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentStatus })
|
||||
@IsEnum(PaymentStatus)
|
||||
status: PaymentStatus;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusOutput {
|
||||
@ApiProperty({ type: PaymentDto })
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export enum MembershipFeeType {
|
||||
SEASON = 'season',
|
||||
MONTHLY = 'monthly',
|
||||
PER_RACE = 'per_race',
|
||||
}
|
||||
|
||||
export enum MemberPaymentStatus {
|
||||
PENDING = 'pending',
|
||||
PAID = 'paid',
|
||||
OVERDUE = 'overdue',
|
||||
}
|
||||
|
||||
export class MembershipFeeDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: MembershipFeeType })
|
||||
@IsEnum(MembershipFeeType)
|
||||
type: MembershipFeeType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
enabled: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export class MemberPaymentDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
feeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
platformFee: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
netAmount: number;
|
||||
|
||||
@ApiProperty({ enum: MemberPaymentStatus })
|
||||
@IsEnum(MemberPaymentStatus)
|
||||
status: MemberPaymentStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
dueDate: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
paidAt?: Date;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesOutput {
|
||||
@ApiProperty({ type: MembershipFeeDto, nullable: true })
|
||||
fee: MembershipFeeDto | null;
|
||||
|
||||
@ApiProperty({ type: [MemberPaymentDto] })
|
||||
payments: MemberPaymentDto[];
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: MembershipFeeType })
|
||||
@IsEnum(MembershipFeeType)
|
||||
type: MembershipFeeType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeOutput {
|
||||
@ApiProperty({ type: MembershipFeeDto })
|
||||
fee: MembershipFeeDto;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
feeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: MemberPaymentStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(MemberPaymentStatus)
|
||||
status?: MemberPaymentStatus;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
paidAt?: Date | string;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentOutput {
|
||||
@ApiProperty({ type: MemberPaymentDto })
|
||||
payment: MemberPaymentDto;
|
||||
}
|
||||
|
||||
export class GetPaymentsQuery {
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
payerId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: PaymentType })
|
||||
@IsOptional()
|
||||
@IsEnum(PaymentType)
|
||||
type?: PaymentType;
|
||||
}
|
||||
|
||||
export class GetPaymentsOutput {
|
||||
@ApiProperty({ type: [PaymentDto] })
|
||||
payments: PaymentDto[];
|
||||
}
|
||||
|
||||
export enum PrizeType {
|
||||
CASH = 'cash',
|
||||
MERCHANDISE = 'merchandise',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export class PrizeDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
position: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ enum: PrizeType })
|
||||
@IsEnum(PrizeType)
|
||||
type: PrizeType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
awarded: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awardedTo?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
awardedAt?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetPrizesQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class GetPrizesOutput {
|
||||
@ApiProperty({ type: [PrizeDto] })
|
||||
prizes: PrizeDto[];
|
||||
}
|
||||
|
||||
export class CreatePrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
position: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ enum: PrizeType })
|
||||
@IsEnum(PrizeType)
|
||||
type: PrizeType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreatePrizeOutput {
|
||||
@ApiProperty({ type: PrizeDto })
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export class AwardPrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
prizeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class AwardPrizeOutput {
|
||||
@ApiProperty({ type: PrizeDto })
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export class DeletePrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
prizeId: string;
|
||||
}
|
||||
|
||||
export class DeletePrizeOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export enum TransactionType {
|
||||
DEPOSIT = 'deposit',
|
||||
WITHDRAWAL = 'withdrawal',
|
||||
PLATFORM_FEE = 'platform_fee',
|
||||
}
|
||||
|
||||
export enum ReferenceType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
PRIZE = 'prize',
|
||||
}
|
||||
|
||||
export class WalletDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
balance: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalRevenue: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalPlatformFees: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalWithdrawn: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export class TransactionDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
walletId: string;
|
||||
|
||||
@ApiProperty({ enum: TransactionType })
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ReferenceType })
|
||||
@IsOptional()
|
||||
@IsEnum(ReferenceType)
|
||||
referenceType?: ReferenceType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetWalletQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
}
|
||||
|
||||
export class GetWalletOutput {
|
||||
@ApiProperty({ type: WalletDto })
|
||||
wallet: WalletDto;
|
||||
|
||||
@ApiProperty({ type: [TransactionDto] })
|
||||
transactions: TransactionDto[];
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ enum: TransactionType })
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ReferenceType })
|
||||
@IsOptional()
|
||||
@IsEnum(ReferenceType)
|
||||
referenceType?: ReferenceType;
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionOutput {
|
||||
@ApiProperty({ type: WalletDto })
|
||||
wallet: WalletDto;
|
||||
|
||||
@ApiProperty({ type: TransactionDto })
|
||||
transaction: TransactionDto;
|
||||
}
|
||||
|
||||
26
apps/api/src/modules/race/RaceController.ts
Normal file
26
apps/api/src/modules/race/RaceController.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { RaceService } from './RaceService';
|
||||
import { AllRacesPageViewModel, RaceStatsDto } from './dto/RaceDto';
|
||||
|
||||
@ApiTags('races')
|
||||
@Controller('races')
|
||||
export class RaceController {
|
||||
constructor(private readonly raceService: RaceService) {}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all races' })
|
||||
@ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageViewModel })
|
||||
async getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
return this.raceService.getAllRaces();
|
||||
}
|
||||
|
||||
@Get('total-races')
|
||||
@ApiOperation({ summary: 'Get the total number of races' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDto })
|
||||
async getTotalRaces(): Promise<RaceStatsDto> {
|
||||
return this.raceService.getTotalRaces();
|
||||
}
|
||||
|
||||
// Add other Race endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/race/RaceModule.ts
Normal file
10
apps/api/src/modules/race/RaceModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RaceController } from './RaceController';
|
||||
|
||||
@Module({
|
||||
controllers: [RaceController],
|
||||
providers: [RaceService],
|
||||
exports: [RaceService],
|
||||
})
|
||||
export class RaceModule {}
|
||||
18
apps/api/src/modules/race/RaceProviders.ts
Normal file
18
apps/api/src/modules/race/RaceProviders.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
|
||||
export const RaceProviders: Provider[] = [
|
||||
RaceService,
|
||||
// In a functional setup, other providers would be here, e.g.:
|
||||
/*
|
||||
{
|
||||
provide: 'ILogger',
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: 'IRaceRepository',
|
||||
useClass: InMemoryRaceRepository,
|
||||
},
|
||||
// ... other providers
|
||||
*/
|
||||
];
|
||||
37
apps/api/src/modules/race/RaceService.ts
Normal file
37
apps/api/src/modules/race/RaceService.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllRacesPageViewModel, RaceStatsDto, ImportRaceResultsInput, ImportRaceResultsSummaryViewModel } from './dto/RaceDto';
|
||||
|
||||
@Injectable()
|
||||
export class RaceService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
console.log('[RaceService] Returning mock all races.');
|
||||
return Promise.resolve({
|
||||
races: [
|
||||
{ id: 'race-1', name: 'Global Race 1', date: new Date().toISOString(), leagueName: 'Global Racing' },
|
||||
{ id: 'race-2', name: 'Amateur Race 1', date: new Date().toISOString(), leagueName: 'Amateur Series' },
|
||||
],
|
||||
totalCount: 2,
|
||||
});
|
||||
}
|
||||
|
||||
getTotalRaces(): Promise<RaceStatsDto> {
|
||||
console.log('[RaceService] Returning mock total races.');
|
||||
return Promise.resolve({
|
||||
totalRaces: 2, // Placeholder
|
||||
});
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
console.log('Importing race results:', input);
|
||||
return {
|
||||
success: true,
|
||||
raceId: input.raceId,
|
||||
driversProcessed: 10, // Mock data
|
||||
resultsRecorded: 10, // Mock data
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
78
apps/api/src/modules/race/dto/RaceDto.ts
Normal file
78
apps/api/src/modules/race/dto/RaceDto.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsBoolean, IsNumber } from 'class-validator';
|
||||
|
||||
export class RaceViewModel {
|
||||
@ApiProperty()
|
||||
id: string; // Assuming a race has an ID
|
||||
|
||||
@ApiProperty()
|
||||
name: string; // Assuming a race has a name
|
||||
|
||||
@ApiProperty()
|
||||
date: string; // Assuming a race has a date
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
leagueName?: string; // Assuming a race might belong to a league
|
||||
|
||||
// Add more race-related properties as needed based on the DTO from the application layer
|
||||
}
|
||||
|
||||
export class AllRacesPageViewModel {
|
||||
@ApiProperty({ type: [RaceViewModel] })
|
||||
races: RaceViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class RaceStatsDto {
|
||||
@ApiProperty()
|
||||
totalRaces: number;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
resultsFileContent: string;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsSummaryViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
driversProcessed: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
resultsRecorded: number;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export class RaceDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
date: string;
|
||||
}
|
||||
48
apps/api/src/modules/sponsor/SponsorController.ts
Normal file
48
apps/api/src/modules/sponsor/SponsorController.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Controller, Get, Post, Body, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO } from './dto/SponsorDto';
|
||||
|
||||
@ApiTags('sponsors')
|
||||
@Controller('sponsors')
|
||||
export class SponsorController {
|
||||
constructor(private readonly sponsorService: SponsorService) {}
|
||||
|
||||
@Get('pricing')
|
||||
@ApiOperation({ summary: 'Get sponsorship pricing for an entity' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship pricing', type: GetEntitySponsorshipPricingResultDto })
|
||||
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
return this.sponsorService.getEntitySponsorshipPricing();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all sponsors' })
|
||||
@ApiResponse({ status: 200, description: 'List of sponsors', type: GetSponsorsOutput })
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return this.sponsorService.getSponsors();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new sponsor' })
|
||||
@ApiResponse({ status: 201, description: 'Sponsor created', type: CreateSponsorOutput })
|
||||
async createSponsor(@Body() input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
return this.sponsorService.createSponsor(input);
|
||||
}
|
||||
|
||||
// Add other Sponsor endpoints here based on other presenters
|
||||
@Get('dashboard/:sponsorId')
|
||||
@ApiOperation({ summary: 'Get sponsor dashboard metrics and sponsored leagues' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsor dashboard data', type: SponsorDashboardDTO })
|
||||
@ApiResponse({ status: 404, description: 'Sponsor not found' })
|
||||
async getSponsorDashboard(@Param('sponsorId') sponsorId: string): Promise<SponsorDashboardDTO | null> {
|
||||
return this.sponsorService.getSponsorDashboard({ sponsorId });
|
||||
}
|
||||
@Get(':sponsorId/sponsorships')
|
||||
@ApiOperation({ summary: 'Get all sponsorships for a given sponsor' })
|
||||
@ApiResponse({ status: 200, description: 'List of sponsorships', type: SponsorSponsorshipsDTO })
|
||||
@ApiResponse({ status: 404, description: 'Sponsor not found' })
|
||||
async getSponsorSponsorships(@Param('sponsorId') sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
|
||||
return this.sponsorService.getSponsorSponsorships({ sponsorId });
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/sponsor/SponsorModule.ts
Normal file
10
apps/api/src/modules/sponsor/SponsorModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { SponsorController } from './SponsorController';
|
||||
|
||||
@Module({
|
||||
controllers: [SponsorController],
|
||||
providers: [SponsorService],
|
||||
exports: [SponsorService],
|
||||
})
|
||||
export class SponsorModule {}
|
||||
5
apps/api/src/modules/sponsor/SponsorProviders.ts
Normal file
5
apps/api/src/modules/sponsor/SponsorProviders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SponsorService } from './SponsorService';
|
||||
|
||||
export const SponsorProviders = [
|
||||
SponsorService,
|
||||
];
|
||||
162
apps/api/src/modules/sponsor/SponsorService.ts
Normal file
162
apps/api/src/modules/sponsor/SponsorService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetEntitySponsorshipPricingResultDto, SponsorDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorshipDetailDTO, SponsorSponsorshipsDTO, SponsoredLeagueDTO, SponsorDashboardMetricsDTO, SponsorDashboardInvestmentDTO } from './dto/SponsorDto';
|
||||
|
||||
const sponsors: Map<string, SponsorDto> = new Map();
|
||||
|
||||
@Injectable()
|
||||
export class SponsorService {
|
||||
|
||||
constructor() {
|
||||
// Seed some demo sponsors for dashboard if empty
|
||||
if (sponsors.size === 0) {
|
||||
const demoSponsor1: SponsorDto = {
|
||||
id: 'sponsor-demo-1',
|
||||
name: 'Demo Sponsor Co.',
|
||||
contactEmail: 'contact@demosponsor.com',
|
||||
websiteUrl: 'https://demosponsor.com',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/aaaaaa/ffffff?text=DemoCo',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const demoSponsor2: SponsorDto = {
|
||||
id: 'sponsor-demo-2',
|
||||
name: 'Second Brand',
|
||||
contactEmail: 'info@secondbrand.net',
|
||||
websiteUrl: 'https://secondbrand.net',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/cccccc/ffffff?text=Brand2',
|
||||
createdAt: new Date(Date.now() - 86400000 * 5),
|
||||
};
|
||||
sponsors.set(demoSponsor1.id, demoSponsor1);
|
||||
sponsors.set(demoSponsor2.id, demoSponsor2);
|
||||
}
|
||||
}
|
||||
|
||||
getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
// This logic relies on external factors (e.g., pricing configuration, entity type)
|
||||
// For now, return mock data
|
||||
return Promise.resolve({
|
||||
pricing: [
|
||||
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
|
||||
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
|
||||
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return { sponsors: Array.from(sponsors.values()) };
|
||||
}
|
||||
|
||||
async createSponsor(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newSponsor: SponsorDto = {
|
||||
id,
|
||||
name: input.name,
|
||||
contactEmail: input.contactEmail,
|
||||
websiteUrl: input.websiteUrl,
|
||||
logoUrl: input.logoUrl,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
sponsors.set(id, newSponsor);
|
||||
return { sponsor: newSponsor };
|
||||
}
|
||||
|
||||
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simplified mock data for dashboard metrics and sponsored leagues
|
||||
const metrics: SponsorDashboardMetricsDTO = {
|
||||
impressions: 10000,
|
||||
impressionsChange: 12.5,
|
||||
uniqueViewers: 7000,
|
||||
viewersChange: 8.3,
|
||||
races: 50,
|
||||
drivers: 100,
|
||||
exposure: 75,
|
||||
exposureChange: 5.2,
|
||||
};
|
||||
|
||||
const sponsoredLeagues: SponsoredLeagueDTO[] = [
|
||||
{ id: 'league-1', name: 'League 1', tier: 'main', drivers: 50, races: 10, impressions: 5000, status: 'active' },
|
||||
{ id: 'league-2', name: 'League 2', tier: 'secondary', drivers: 30, races: 5, impressions: 1500, status: 'upcoming' },
|
||||
];
|
||||
|
||||
const investment: SponsorDashboardInvestmentDTO = {
|
||||
activeSponsorships: 2,
|
||||
totalInvestment: 5000,
|
||||
costPerThousandViews: 0.5,
|
||||
};
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics,
|
||||
sponsoredLeagues,
|
||||
investment,
|
||||
};
|
||||
}
|
||||
|
||||
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const sponsorshipDetails: SponsorshipDetailDTO[] = [
|
||||
{
|
||||
id: 'sponsorship-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
seasonId: 'season-1',
|
||||
seasonName: 'Season 1',
|
||||
seasonStartDate: new Date('2025-01-01'),
|
||||
seasonEndDate: new Date('2025-12-31'),
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
pricing: { amount: 1000, currency: 'USD' },
|
||||
platformFee: { amount: 100, currency: 'USD' },
|
||||
netAmount: { amount: 900, currency: 'USD' },
|
||||
metrics: { drivers: 50, races: 10, completedRaces: 8, impressions: 5000 },
|
||||
createdAt: new Date('2024-12-01'),
|
||||
activatedAt: new Date('2025-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'sponsorship-2',
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'League 2',
|
||||
seasonId: 'season-2',
|
||||
seasonName: 'Season 2',
|
||||
tier: 'secondary',
|
||||
status: 'pending',
|
||||
pricing: { amount: 500, currency: 'USD' },
|
||||
platformFee: { amount: 50, currency: 'USD' },
|
||||
netAmount: { amount: 450, currency: 'USD' },
|
||||
metrics: { drivers: 30, races: 5, completedRaces: 0, impressions: 0 },
|
||||
createdAt: new Date('2025-03-15'),
|
||||
},
|
||||
];
|
||||
|
||||
const totalInvestment = sponsorshipDetails.reduce((sum, s) => sum + s.pricing.amount, 0);
|
||||
const totalPlatformFees = sponsorshipDetails.reduce((sum, s) => sum + s.platformFee.amount, 0);
|
||||
const activeSponsorships = sponsorshipDetails.filter(s => s.status === 'active').length;
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
summary: {
|
||||
totalSponsorships: sponsorshipDetails.length,
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
299
apps/api/src/modules/sponsor/dto/SponsorDto.ts
Normal file
299
apps/api/src/modules/sponsor/dto/SponsorDto.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean, IsUrl, IsEmail } from 'class-validator';
|
||||
|
||||
export class SponsorshipPricingItemDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
level: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
price: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingResultDto {
|
||||
@ApiProperty({ type: [SponsorshipPricingItemDto] })
|
||||
pricing: SponsorshipPricingItemDto[];
|
||||
}
|
||||
|
||||
export class SponsorDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetSponsorsOutput {
|
||||
@ApiProperty({ type: [SponsorDto] })
|
||||
sponsors: SponsorDto[];
|
||||
}
|
||||
|
||||
export class CreateSponsorInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class CreateSponsorOutput {
|
||||
@ApiProperty({ type: SponsorDto })
|
||||
sponsor: SponsorDto;
|
||||
}
|
||||
|
||||
export class GetSponsorDashboardQueryParams {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export class SponsoredLeagueDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ enum: ['main', 'secondary'] })
|
||||
@IsEnum(['main', 'secondary'])
|
||||
tier: 'main' | 'secondary';
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
drivers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
races: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressions: number;
|
||||
|
||||
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
|
||||
@IsEnum(['active', 'upcoming', 'completed'])
|
||||
status: 'active' | 'upcoming' | 'completed';
|
||||
}
|
||||
|
||||
export class SponsorDashboardMetricsDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressions: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressionsChange: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
uniqueViewers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
viewersChange: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
races: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
drivers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
exposure: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
exposureChange: number;
|
||||
}
|
||||
|
||||
export class SponsorDashboardInvestmentDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
activeSponsorships: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalInvestment: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
costPerThousandViews: number;
|
||||
}
|
||||
|
||||
export class SponsorDashboardDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorName: string;
|
||||
|
||||
@ApiProperty({ type: SponsorDashboardMetricsDTO })
|
||||
metrics: SponsorDashboardMetricsDTO;
|
||||
|
||||
@ApiProperty({ type: [SponsoredLeagueDTO] })
|
||||
sponsoredLeagues: SponsoredLeagueDTO[];
|
||||
|
||||
@ApiProperty({ type: SponsorDashboardInvestmentDTO })
|
||||
investment: SponsorDashboardInvestmentDTO;
|
||||
}
|
||||
|
||||
export class GetSponsorSponsorshipsQueryParams {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export class SponsorshipDetailDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonName: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
seasonStartDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
seasonEndDate?: Date;
|
||||
|
||||
@ApiProperty({ enum: ['main', 'secondary'] })
|
||||
@IsEnum(['main', 'secondary'])
|
||||
tier: 'main' | 'secondary';
|
||||
|
||||
@ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
|
||||
@IsEnum(['pending', 'active', 'expired', 'cancelled'])
|
||||
status: 'pending' | 'active' | 'expired' | 'cancelled';
|
||||
|
||||
@ApiProperty()
|
||||
pricing: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
platformFee: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
netAmount: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
metrics: {
|
||||
drivers: number;
|
||||
races: number;
|
||||
completedRaces: number;
|
||||
impressions: number;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
activatedAt?: Date;
|
||||
}
|
||||
|
||||
export class SponsorSponsorshipsDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorName: string;
|
||||
|
||||
@ApiProperty({ type: [SponsorshipDetailDTO] })
|
||||
sponsorships: SponsorshipDetailDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
summary: {
|
||||
totalSponsorships: number;
|
||||
activeSponsorships: number;
|
||||
totalInvestment: number;
|
||||
totalPlatformFees: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Add other DTOs for sponsor-related logic as needed
|
||||
19
apps/api/src/modules/team/TeamController.ts
Normal file
19
apps/api/src/modules/team/TeamController.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { TeamService } from './TeamService';
|
||||
import { AllTeamsViewModel } from './dto/TeamDto';
|
||||
|
||||
@ApiTags('teams')
|
||||
@Controller('teams')
|
||||
export class TeamController {
|
||||
constructor(private readonly teamService: TeamService) {}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all teams' })
|
||||
@ApiResponse({ status: 200, description: 'List of all teams', type: AllTeamsViewModel })
|
||||
async getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
return this.teamService.getAllTeams();
|
||||
}
|
||||
|
||||
// Add other Team endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/team/TeamModule.ts
Normal file
10
apps/api/src/modules/team/TeamModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamService } from './TeamService';
|
||||
import { TeamController } from './TeamController';
|
||||
|
||||
@Module({
|
||||
controllers: [TeamController],
|
||||
providers: [TeamService],
|
||||
exports: [TeamService],
|
||||
})
|
||||
export class TeamModule {}
|
||||
5
apps/api/src/modules/team/TeamProviders.ts
Normal file
5
apps/api/src/modules/team/TeamProviders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TeamService } from './TeamService';
|
||||
|
||||
export const TeamProviders = [
|
||||
TeamService,
|
||||
];
|
||||
43
apps/api/src/modules/team/TeamService.ts
Normal file
43
apps/api/src/modules/team/TeamService.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService {
|
||||
getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
// TODO: Implement actual logic to fetch all teams
|
||||
return Promise.resolve({
|
||||
teams: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private teams: Map<string, TeamDto> = new Map(); // In-memory store for teams
|
||||
|
||||
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
|
||||
const { teamId, driverId } = query;
|
||||
|
||||
const team = this.teams.get(teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mock membership and roles
|
||||
const membership: MembershipDto = {
|
||||
role: driverId === team.ownerId ? MembershipRole.OWNER : MembershipRole.MEMBER,
|
||||
joinedAt: new Date(Date.now() - 86400000 * 30), // Joined 30 days ago
|
||||
isActive: true, // Always active for mock
|
||||
};
|
||||
|
||||
const isOwner = team.ownerId === driverId;
|
||||
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
|
||||
|
||||
return {
|
||||
team: team,
|
||||
membership,
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
// Add other methods related to Team logic here based on other presenters
|
||||
}
|
||||
121
apps/api/src/modules/team/dto/TeamDto.ts
Normal file
121
apps/api/src/modules/team/dto/TeamDto.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class TeamLeagueDto {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class TeamListItemViewModel {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
tag?: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
memberCount: number;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export class AllTeamsViewModel {
|
||||
@ApiProperty({ type: [TeamListItemViewModel] })
|
||||
teams: TeamListItemViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator';
|
||||
|
||||
export class TeamDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
tag: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export enum MembershipRole {
|
||||
OWNER = 'owner',
|
||||
MANAGER = 'manager',
|
||||
MEMBER = 'member',
|
||||
}
|
||||
|
||||
export enum MembershipStatus {
|
||||
ACTIVE = 'active',
|
||||
PENDING = 'pending',
|
||||
INVITED = 'invited',
|
||||
INACTIVE = 'inactive',
|
||||
}
|
||||
|
||||
export class MembershipDto {
|
||||
@ApiProperty({ enum: MembershipRole })
|
||||
@IsEnum(MembershipRole)
|
||||
role: MembershipRole;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
joinedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class DriverTeamViewModel {
|
||||
@ApiProperty({ type: TeamDto })
|
||||
team: TeamDto;
|
||||
|
||||
@ApiProperty({ type: MembershipDto })
|
||||
membership: MembershipDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isOwner: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
teamId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
|
||||
import type { RecordPageViewInput, RecordPageViewOutput } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import type { RecordEngagementInput, RecordEngagementOutput } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly recordPageViewUseCase: RecordPageViewUseCase,
|
||||
private readonly recordEngagementUseCase: RecordEngagementUseCase,
|
||||
) {}
|
||||
|
||||
@Post('page-view')
|
||||
async recordPageView(
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.recordPageViewUseCase.execute(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
async recordEngagement(
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.recordEngagementUseCase.execute(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,18 @@
|
||||
"@gridpilot/analytics/domain/entities/*": [
|
||||
"../../core/analytics/domain/entities/*"
|
||||
],
|
||||
"@gridpilot/core/identity/domain/repositories/*": [
|
||||
"../../core/identity/domain/repositories/*"
|
||||
],
|
||||
"@gridpilot/core/identity/domain/services/*": [
|
||||
"../../core/identity/domain/services/*"
|
||||
],
|
||||
"@gridpilot/core/identity/domain/entities/*": [
|
||||
"../../core/identity/domain/entities/*"
|
||||
],
|
||||
"@gridpilot/core/shared/logging/*": [
|
||||
"../../core/shared/logging/*"
|
||||
],
|
||||
"@nestjs/testing": [
|
||||
"./node_modules/@nestjs/testing"
|
||||
]
|
||||
|
||||
62
apps/website/lib/apiClient.ts
Normal file
62
apps/website/lib/apiClient.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, data?: object): Promise<T | void> {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const config: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (data) {
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, config);
|
||||
|
||||
if (!response.ok) {
|
||||
// Attempt to read error message from response body
|
||||
let errorData: any;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch (e) {
|
||||
errorData = { message: response.statusText };
|
||||
}
|
||||
throw new Error(errorData.message || `API request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : undefined;
|
||||
}
|
||||
|
||||
get<T>(path: string): Promise<T | void> {
|
||||
return this.request<T>('GET', path);
|
||||
}
|
||||
|
||||
post<T>(path: string, data: object): Promise<T | void> {
|
||||
return this.request<T>('POST', path, data);
|
||||
}
|
||||
|
||||
put<T>(path: string, data: object): Promise<T | void> {
|
||||
return this.request<T>('PUT', path, data);
|
||||
}
|
||||
|
||||
delete<T>(path: string): Promise<T | void> {
|
||||
return this.request<T>('DELETE', path);
|
||||
}
|
||||
|
||||
patch<T>(path: string, data: object): Promise<T | void> {
|
||||
return this.request<T>('PATCH', path, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the API client with your backend's base URL
|
||||
// You might want to get this from an environment variable
|
||||
export const api = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
|
||||
50
apps/website/lib/auth/AuthApiClient.ts
Normal file
50
apps/website/lib/auth/AuthApiClient.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { api } from '../apiClient';
|
||||
import type {
|
||||
AuthenticatedUserDTO,
|
||||
AuthSessionDTO,
|
||||
SignupParams,
|
||||
LoginParams,
|
||||
IracingAuthRedirectResult,
|
||||
LoginWithIracingCallbackParams,
|
||||
} from '../../../apps/api/src/modules/auth/dto/AuthDto'; // Using generated API DTOs
|
||||
|
||||
export class AuthApiClient {
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
try {
|
||||
return await api.get<AuthSessionDTO>('/auth/session');
|
||||
} catch (error) {
|
||||
// Handle error, e.g., if session is not found or API is down
|
||||
console.error('Error fetching current session:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
|
||||
return api.post<AuthSessionDTO>('/auth/signup', params);
|
||||
}
|
||||
|
||||
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
|
||||
return api.post<AuthSessionDTO>('/auth/login', params);
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
const query = returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : '';
|
||||
return api.get<IracingAuthRedirectResult>(`/auth/iracing/start${query}`);
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
const query = new URLSearchParams();
|
||||
query.append('code', params.code);
|
||||
query.append('state', params.state);
|
||||
if (params.returnTo) {
|
||||
query.append('returnTo', params.returnTo);
|
||||
}
|
||||
return await api.get<AuthSessionDTO>(`/auth/iracing/callback?${query.toString()}`);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
return api.post<void>('/auth/logout', {});
|
||||
}
|
||||
}
|
||||
|
||||
export const authApiClient = new AuthApiClient();
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { AuthenticatedUserDTO } from '@gridpilot/identity/application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '@gridpilot/identity/application/dto/AuthSessionDTO';
|
||||
|
||||
export type AuthUser = AuthenticatedUserDTO;
|
||||
export type AuthSession = AuthSessionDTO;
|
||||
|
||||
export interface SignupParams {
|
||||
email: string;
|
||||
password: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface LoginParams {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthService {
|
||||
getCurrentSession(): Promise<AuthSession | null>;
|
||||
|
||||
// Email/password authentication
|
||||
signupWithEmail(params: SignupParams): Promise<AuthSession>;
|
||||
loginWithEmail(params: LoginParams): Promise<AuthSession>;
|
||||
|
||||
// iRacing OAuth (demo)
|
||||
startIracingAuthRedirect(
|
||||
returnTo?: string,
|
||||
): Promise<{ redirectUrl: string; state: string }>;
|
||||
loginWithIracingCallback(params: {
|
||||
code: string;
|
||||
state: string;
|
||||
returnTo?: string;
|
||||
}): Promise<AuthSession>;
|
||||
|
||||
logout(): Promise<void>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { AuthService } from './AuthService';
|
||||
import { InMemoryAuthService } from './InMemoryAuthService';
|
||||
import { getDIContainer } from '../di-container';
|
||||
import { DI_TOKENS } from '../di-tokens';
|
||||
|
||||
export function getAuthService(): AuthService {
|
||||
const container = getDIContainer();
|
||||
if (!container.isRegistered(DI_TOKENS.AuthService)) {
|
||||
throw new Error(
|
||||
`${DI_TOKENS.AuthService.description} not registered in DI container.`,
|
||||
);
|
||||
}
|
||||
return container.resolve<AuthService>(DI_TOKENS.AuthService);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,982 +0,0 @@
|
||||
/**
|
||||
* Dependency Injection Container - TSyringe Facade
|
||||
*
|
||||
* Provides backward-compatible API for accessing dependencies managed by TSyringe.
|
||||
*/
|
||||
|
||||
import { configureDIContainer, getDIContainer } from './di-config';
|
||||
import { DI_TOKENS } from './di-tokens';
|
||||
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
|
||||
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/IStandingRepository';
|
||||
import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository';
|
||||
import type { IProtestRepository } from '@gridpilot/racing/domain/repositories/IProtestRepository';
|
||||
import type { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository';
|
||||
import type { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository';
|
||||
import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository';
|
||||
import type {
|
||||
ITeamRepository,
|
||||
ITeamMembershipRepository,
|
||||
IRaceRegistrationRepository,
|
||||
} from '@gridpilot/racing';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { LeagueMembership, JoinRequest } from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { ImageServicePort } from '@gridpilot/media';
|
||||
|
||||
// Notifications package imports
|
||||
import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application';
|
||||
import type {
|
||||
SendNotificationUseCase,
|
||||
MarkNotificationReadUseCase,
|
||||
GetUnreadNotificationsUseCase
|
||||
} from '@gridpilot/notifications/application';
|
||||
import type {
|
||||
JoinLeagueUseCase,
|
||||
RegisterForRaceUseCase,
|
||||
WithdrawFromRaceUseCase,
|
||||
CreateTeamUseCase,
|
||||
JoinTeamUseCase,
|
||||
LeaveTeamUseCase,
|
||||
ApproveTeamJoinRequestUseCase,
|
||||
RejectTeamJoinRequestUseCase,
|
||||
UpdateTeamUseCase,
|
||||
GetAllTeamsUseCase,
|
||||
GetTeamDetailsUseCase,
|
||||
GetTeamMembersUseCase,
|
||||
GetTeamJoinRequestsUseCase,
|
||||
GetDriverTeamUseCase,
|
||||
CreateLeagueWithSeasonAndScoringUseCase,
|
||||
FileProtestUseCase,
|
||||
ReviewProtestUseCase,
|
||||
ApplyPenaltyUseCase,
|
||||
QuickPenaltyUseCase,
|
||||
RequestProtestDefenseUseCase,
|
||||
SubmitProtestDefenseUseCase,
|
||||
GetSponsorDashboardUseCase,
|
||||
GetSponsorSponsorshipsUseCase,
|
||||
ApplyForSponsorshipUseCase,
|
||||
AcceptSponsorshipRequestUseCase,
|
||||
RejectSponsorshipRequestUseCase,
|
||||
GetPendingSponsorshipRequestsUseCase,
|
||||
GetEntitySponsorshipPricingUseCase,
|
||||
} from '@gridpilot/racing/application';
|
||||
import type { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import type { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsUseCase';
|
||||
import type { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFUseCase';
|
||||
import type { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
import type { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesUseCase';
|
||||
import type { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
|
||||
import type { GetRaceDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceDetailUseCase';
|
||||
import type { GetRaceResultsDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceResultsDetailUseCase';
|
||||
import type { GetAllRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetAllRacesPageDataUseCase';
|
||||
import type { GetProfileOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
import type { UpdateDriverProfileUseCase } from '@gridpilot/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import type { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import type { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import type { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import type { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase';
|
||||
import type { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import type { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
import type { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
|
||||
import type { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigUseCase';
|
||||
import type { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import type { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsUseCase';
|
||||
import type { CancelRaceUseCase } from '@gridpilot/racing/application/use-cases/CancelRaceUseCase';
|
||||
import type { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase';
|
||||
import type { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '@gridpilot/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISponsorshipPricingRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application';
|
||||
import type { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
|
||||
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase';
|
||||
import type { ListSeasonsForLeagueUseCase } from '@gridpilot/racing/application/use-cases/SeasonUseCases';
|
||||
import { createDemoDriverStats, getDemoLeagueRankings, type DriverStats } from '@gridpilot/testing-support';
|
||||
|
||||
/**
|
||||
* DI Container - TSyringe Facade
|
||||
* Provides singleton access to TSyringe container with lazy initialization
|
||||
*/
|
||||
class DIContainer {
|
||||
private static instance: DIContainer;
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {
|
||||
// Private constructor for singleton pattern
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure TSyringe container is configured
|
||||
*/
|
||||
private ensureInitialized(): void {
|
||||
if (this.initialized) return;
|
||||
configureDIContainer();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(): DIContainer {
|
||||
if (!DIContainer.instance) {
|
||||
DIContainer.instance = new DIContainer();
|
||||
}
|
||||
return DIContainer.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the container (useful for testing)
|
||||
*/
|
||||
static reset(): void {
|
||||
DIContainer.instance = new DIContainer();
|
||||
DIContainer.instance.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository getters - resolve from TSyringe container
|
||||
*/
|
||||
get driverRepository(): IDriverRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IDriverRepository>(DI_TOKENS.DriverRepository);
|
||||
}
|
||||
|
||||
get leagueRepository(): ILeagueRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ILeagueRepository>(DI_TOKENS.LeagueRepository);
|
||||
}
|
||||
|
||||
get raceRepository(): IRaceRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IRaceRepository>(DI_TOKENS.RaceRepository);
|
||||
}
|
||||
|
||||
get resultRepository(): IResultRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IResultRepository>(DI_TOKENS.ResultRepository);
|
||||
}
|
||||
|
||||
get standingRepository(): IStandingRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IStandingRepository>(DI_TOKENS.StandingRepository);
|
||||
}
|
||||
|
||||
get penaltyRepository(): IPenaltyRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IPenaltyRepository>(DI_TOKENS.PenaltyRepository);
|
||||
}
|
||||
|
||||
get protestRepository(): IProtestRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IProtestRepository>(DI_TOKENS.ProtestRepository);
|
||||
}
|
||||
|
||||
get raceRegistrationRepository(): IRaceRegistrationRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IRaceRegistrationRepository>(DI_TOKENS.RaceRegistrationRepository);
|
||||
}
|
||||
|
||||
get leagueMembershipRepository(): ILeagueMembershipRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ILeagueMembershipRepository>(DI_TOKENS.LeagueMembershipRepository);
|
||||
}
|
||||
|
||||
get gameRepository(): IGameRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IGameRepository>(DI_TOKENS.GameRepository);
|
||||
}
|
||||
|
||||
get seasonRepository(): ISeasonRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISeasonRepository>(DI_TOKENS.SeasonRepository);
|
||||
}
|
||||
|
||||
get leagueScoringConfigRepository(): ILeagueScoringConfigRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ILeagueScoringConfigRepository>(DI_TOKENS.LeagueScoringConfigRepository);
|
||||
}
|
||||
|
||||
get leagueScoringPresetProvider(): LeagueScoringPresetProvider {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<LeagueScoringPresetProvider>(DI_TOKENS.LeagueScoringPresetProvider);
|
||||
}
|
||||
|
||||
get joinLeagueUseCase(): JoinLeagueUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<JoinLeagueUseCase>(DI_TOKENS.JoinLeagueUseCase);
|
||||
}
|
||||
|
||||
get registerForRaceUseCase(): RegisterForRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RegisterForRaceUseCase>(DI_TOKENS.RegisterForRaceUseCase);
|
||||
}
|
||||
|
||||
get withdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<WithdrawFromRaceUseCase>(DI_TOKENS.WithdrawFromRaceUseCase);
|
||||
}
|
||||
|
||||
get isDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IsDriverRegisteredForRaceUseCase>(DI_TOKENS.IsDriverRegisteredForRaceUseCase);
|
||||
}
|
||||
|
||||
get getRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceRegistrationsUseCase>(DI_TOKENS.GetRaceRegistrationsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueStandingsUseCase>(DI_TOKENS.GetLeagueStandingsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueDriverSeasonStatsUseCase>(DI_TOKENS.GetLeagueDriverSeasonStatsUseCase);
|
||||
}
|
||||
|
||||
get getAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityUseCase);
|
||||
}
|
||||
|
||||
get getAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase);
|
||||
}
|
||||
|
||||
get listSeasonsForLeagueUseCase(): ListSeasonsForLeagueUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ListSeasonsForLeagueUseCase>(DI_TOKENS.ListSeasonsForLeagueUseCase);
|
||||
}
|
||||
|
||||
get listLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ListLeagueScoringPresetsUseCase>(DI_TOKENS.ListLeagueScoringPresetsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueScoringConfigUseCase>(DI_TOKENS.GetLeagueScoringConfigUseCase);
|
||||
}
|
||||
|
||||
get getLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueFullConfigUseCase>(DI_TOKENS.GetLeagueFullConfigUseCase);
|
||||
}
|
||||
|
||||
get previewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<PreviewLeagueScheduleUseCase>(DI_TOKENS.PreviewLeagueScheduleUseCase);
|
||||
}
|
||||
|
||||
get getRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceWithSOFUseCase>(DI_TOKENS.GetRaceWithSOFUseCase);
|
||||
}
|
||||
|
||||
get getLeagueStatsUseCase(): GetLeagueStatsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueStatsUseCase>(DI_TOKENS.GetLeagueStatsUseCase);
|
||||
}
|
||||
|
||||
get getRacesPageDataUseCase(): GetRacesPageDataUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRacesPageDataUseCase>(DI_TOKENS.GetRacesPageDataUseCase);
|
||||
}
|
||||
|
||||
get getAllRacesPageDataUseCase(): GetAllRacesPageDataUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllRacesPageDataUseCase>(DI_TOKENS.GetAllRacesPageDataUseCase);
|
||||
}
|
||||
|
||||
get getRaceDetailUseCase(): GetRaceDetailUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceDetailUseCase>(DI_TOKENS.GetRaceDetailUseCase);
|
||||
}
|
||||
|
||||
get getRaceResultsDetailUseCase(): GetRaceResultsDetailUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceResultsDetailUseCase>(DI_TOKENS.GetRaceResultsDetailUseCase);
|
||||
}
|
||||
|
||||
get getDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDriversLeaderboardUseCase>(DI_TOKENS.GetDriversLeaderboardUseCase);
|
||||
}
|
||||
|
||||
get getTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamsLeaderboardUseCase>(DI_TOKENS.GetTeamsLeaderboardUseCase);
|
||||
}
|
||||
|
||||
get getDashboardOverviewUseCase(): GetDashboardOverviewUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDashboardOverviewUseCase>(DI_TOKENS.GetDashboardOverviewUseCase);
|
||||
}
|
||||
|
||||
get getProfileOverviewUseCase(): GetProfileOverviewUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetProfileOverviewUseCase>(DI_TOKENS.GetProfileOverviewUseCase);
|
||||
}
|
||||
|
||||
get updateDriverProfileUseCase(): UpdateDriverProfileUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<UpdateDriverProfileUseCase>(DI_TOKENS.UpdateDriverProfileUseCase);
|
||||
}
|
||||
|
||||
get driverRatingProvider(): DriverRatingProvider {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<DriverRatingProvider>(DI_TOKENS.DriverRatingProvider);
|
||||
}
|
||||
|
||||
get cancelRaceUseCase(): CancelRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<CancelRaceUseCase>(DI_TOKENS.CancelRaceUseCase);
|
||||
}
|
||||
|
||||
get completeRaceUseCase(): import('@gridpilot/racing/application/use-cases/CompleteRaceUseCase').CompleteRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<import('@gridpilot/racing/application/use-cases/CompleteRaceUseCase').CompleteRaceUseCase>(DI_TOKENS.CompleteRaceUseCase);
|
||||
}
|
||||
|
||||
get importRaceResultsUseCase(): ImportRaceResultsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ImportRaceResultsUseCase>(DI_TOKENS.ImportRaceResultsUseCase);
|
||||
}
|
||||
|
||||
get createLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<CreateLeagueWithSeasonAndScoringUseCase>(DI_TOKENS.CreateLeagueWithSeasonAndScoringUseCase);
|
||||
}
|
||||
|
||||
get createTeamUseCase(): CreateTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<CreateTeamUseCase>(DI_TOKENS.CreateTeamUseCase);
|
||||
}
|
||||
|
||||
get joinTeamUseCase(): JoinTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<JoinTeamUseCase>(DI_TOKENS.JoinTeamUseCase);
|
||||
}
|
||||
|
||||
get leaveTeamUseCase(): LeaveTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<LeaveTeamUseCase>(DI_TOKENS.LeaveTeamUseCase);
|
||||
}
|
||||
|
||||
get approveTeamJoinRequestUseCase(): ApproveTeamJoinRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ApproveTeamJoinRequestUseCase>(DI_TOKENS.ApproveTeamJoinRequestUseCase);
|
||||
}
|
||||
|
||||
get rejectTeamJoinRequestUseCase(): RejectTeamJoinRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RejectTeamJoinRequestUseCase>(DI_TOKENS.RejectTeamJoinRequestUseCase);
|
||||
}
|
||||
|
||||
get updateTeamUseCase(): UpdateTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<UpdateTeamUseCase>(DI_TOKENS.UpdateTeamUseCase);
|
||||
}
|
||||
|
||||
get getAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllTeamsUseCase>(DI_TOKENS.GetAllTeamsUseCase);
|
||||
}
|
||||
|
||||
get getTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamDetailsUseCase>(DI_TOKENS.GetTeamDetailsUseCase);
|
||||
}
|
||||
|
||||
get getTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamMembersUseCase>(DI_TOKENS.GetTeamMembersUseCase);
|
||||
}
|
||||
|
||||
get getTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamJoinRequestsUseCase>(DI_TOKENS.GetTeamJoinRequestsUseCase);
|
||||
}
|
||||
|
||||
get getDriverTeamUseCase(): GetDriverTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDriverTeamUseCase>(DI_TOKENS.GetDriverTeamUseCase);
|
||||
}
|
||||
|
||||
get teamRepository(): ITeamRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ITeamRepository>(DI_TOKENS.TeamRepository);
|
||||
}
|
||||
|
||||
get teamMembershipRepository(): ITeamMembershipRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ITeamMembershipRepository>(DI_TOKENS.TeamMembershipRepository);
|
||||
}
|
||||
|
||||
get feedRepository(): IFeedRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IFeedRepository>(DI_TOKENS.FeedRepository);
|
||||
}
|
||||
|
||||
get socialRepository(): ISocialGraphRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISocialGraphRepository>(DI_TOKENS.SocialRepository);
|
||||
}
|
||||
|
||||
get imageService(): ImageServicePort {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ImageServicePort>(DI_TOKENS.ImageService);
|
||||
}
|
||||
|
||||
get trackRepository(): ITrackRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ITrackRepository>(DI_TOKENS.TrackRepository);
|
||||
}
|
||||
|
||||
get carRepository(): ICarRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ICarRepository>(DI_TOKENS.CarRepository);
|
||||
}
|
||||
|
||||
get notificationRepository(): INotificationRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<INotificationRepository>(DI_TOKENS.NotificationRepository);
|
||||
}
|
||||
|
||||
get notificationPreferenceRepository(): INotificationPreferenceRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<INotificationPreferenceRepository>(DI_TOKENS.NotificationPreferenceRepository);
|
||||
}
|
||||
|
||||
get sendNotificationUseCase(): SendNotificationUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<SendNotificationUseCase>(DI_TOKENS.SendNotificationUseCase);
|
||||
}
|
||||
|
||||
get markNotificationReadUseCase(): MarkNotificationReadUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<MarkNotificationReadUseCase>(DI_TOKENS.MarkNotificationReadUseCase);
|
||||
}
|
||||
|
||||
get getUnreadNotificationsUseCase(): GetUnreadNotificationsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetUnreadNotificationsUseCase>(DI_TOKENS.GetUnreadNotificationsUseCase);
|
||||
}
|
||||
|
||||
get fileProtestUseCase(): FileProtestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<FileProtestUseCase>(DI_TOKENS.FileProtestUseCase);
|
||||
}
|
||||
|
||||
get reviewProtestUseCase(): ReviewProtestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ReviewProtestUseCase>(DI_TOKENS.ReviewProtestUseCase);
|
||||
}
|
||||
|
||||
get applyPenaltyUseCase(): ApplyPenaltyUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ApplyPenaltyUseCase>(DI_TOKENS.ApplyPenaltyUseCase);
|
||||
}
|
||||
|
||||
get quickPenaltyUseCase(): QuickPenaltyUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<QuickPenaltyUseCase>(DI_TOKENS.QuickPenaltyUseCase);
|
||||
}
|
||||
|
||||
get getRaceProtestsUseCase(): GetRaceProtestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceProtestsUseCase>(DI_TOKENS.GetRaceProtestsUseCase);
|
||||
}
|
||||
|
||||
get getRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRacePenaltiesUseCase>(DI_TOKENS.GetRacePenaltiesUseCase);
|
||||
}
|
||||
|
||||
get requestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RequestProtestDefenseUseCase>(DI_TOKENS.RequestProtestDefenseUseCase);
|
||||
}
|
||||
|
||||
get submitProtestDefenseUseCase(): SubmitProtestDefenseUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<SubmitProtestDefenseUseCase>(DI_TOKENS.SubmitProtestDefenseUseCase);
|
||||
}
|
||||
|
||||
get transferLeagueOwnershipUseCase(): TransferLeagueOwnershipUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<TransferLeagueOwnershipUseCase>(DI_TOKENS.TransferLeagueOwnershipUseCase);
|
||||
}
|
||||
|
||||
get sponsorRepository(): ISponsorRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
|
||||
}
|
||||
|
||||
get seasonSponsorshipRepository(): ISeasonSponsorshipRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
|
||||
}
|
||||
|
||||
get getSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetSponsorDashboardUseCase>(DI_TOKENS.GetSponsorDashboardUseCase);
|
||||
}
|
||||
|
||||
get getSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetSponsorSponsorshipsUseCase>(DI_TOKENS.GetSponsorSponsorshipsUseCase);
|
||||
}
|
||||
|
||||
get sponsorshipRequestRepository(): ISponsorshipRequestRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISponsorshipRequestRepository>(DI_TOKENS.SponsorshipRequestRepository);
|
||||
}
|
||||
|
||||
get sponsorshipPricingRepository(): ISponsorshipPricingRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISponsorshipPricingRepository>(DI_TOKENS.SponsorshipPricingRepository);
|
||||
}
|
||||
|
||||
get applyForSponsorshipUseCase(): ApplyForSponsorshipUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ApplyForSponsorshipUseCase>(DI_TOKENS.ApplyForSponsorshipUseCase);
|
||||
}
|
||||
|
||||
get acceptSponsorshipRequestUseCase(): AcceptSponsorshipRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<AcceptSponsorshipRequestUseCase>(DI_TOKENS.AcceptSponsorshipRequestUseCase);
|
||||
}
|
||||
|
||||
get rejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RejectSponsorshipRequestUseCase>(DI_TOKENS.RejectSponsorshipRequestUseCase);
|
||||
}
|
||||
|
||||
get getPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetPendingSponsorshipRequestsUseCase>(DI_TOKENS.GetPendingSponsorshipRequestsUseCase);
|
||||
}
|
||||
|
||||
get getEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetEntitySponsorshipPricingUseCase>(DI_TOKENS.GetEntitySponsorshipPricingUseCase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported accessor functions
|
||||
*/
|
||||
export function getDriverRepository(): IDriverRepository {
|
||||
return DIContainer.getInstance().driverRepository;
|
||||
}
|
||||
|
||||
export function getLeagueRepository(): ILeagueRepository {
|
||||
return DIContainer.getInstance().leagueRepository;
|
||||
}
|
||||
|
||||
export function getRaceRepository(): IRaceRepository {
|
||||
return DIContainer.getInstance().raceRepository;
|
||||
}
|
||||
|
||||
export function getResultRepository(): IResultRepository {
|
||||
return DIContainer.getInstance().resultRepository;
|
||||
}
|
||||
|
||||
export function getStandingRepository(): IStandingRepository {
|
||||
return DIContainer.getInstance().standingRepository;
|
||||
}
|
||||
|
||||
export function getPenaltyRepository(): IPenaltyRepository {
|
||||
return DIContainer.getInstance().penaltyRepository;
|
||||
}
|
||||
|
||||
export function getProtestRepository(): IProtestRepository {
|
||||
return DIContainer.getInstance().protestRepository;
|
||||
}
|
||||
|
||||
export function getRaceRegistrationRepository(): IRaceRegistrationRepository {
|
||||
return DIContainer.getInstance().raceRegistrationRepository;
|
||||
}
|
||||
|
||||
export function getLeagueMembershipRepository(): ILeagueMembershipRepository {
|
||||
return DIContainer.getInstance().leagueMembershipRepository;
|
||||
}
|
||||
|
||||
export function getJoinLeagueUseCase(): JoinLeagueUseCase {
|
||||
return DIContainer.getInstance().joinLeagueUseCase;
|
||||
}
|
||||
|
||||
export function getRegisterForRaceUseCase(): RegisterForRaceUseCase {
|
||||
return DIContainer.getInstance().registerForRaceUseCase;
|
||||
}
|
||||
|
||||
export function getWithdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
|
||||
return DIContainer.getInstance().withdrawFromRaceUseCase;
|
||||
}
|
||||
|
||||
export function getIsDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
|
||||
return DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query facade for checking if a driver is registered for a race.
|
||||
*/
|
||||
export function getIsDriverRegisteredForRaceQuery(): {
|
||||
execute(input: { raceId: string; driverId: string }): Promise<boolean>;
|
||||
} {
|
||||
const useCase = DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
|
||||
return {
|
||||
async execute(input: { raceId: string; driverId: string }): Promise<boolean> {
|
||||
const result = await useCase.execute(input);
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getGetRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
|
||||
return DIContainer.getInstance().getRaceRegistrationsUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
|
||||
return DIContainer.getInstance().getLeagueStandingsUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
|
||||
return DIContainer.getInstance().getLeagueDriverSeasonStatsUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringUseCase;
|
||||
}
|
||||
|
||||
export function getListSeasonsForLeagueUseCase(): ListSeasonsForLeagueUseCase {
|
||||
return DIContainer.getInstance().listSeasonsForLeagueUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
|
||||
return DIContainer.getInstance().getLeagueScoringConfigUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
|
||||
return DIContainer.getInstance().getLeagueFullConfigUseCase;
|
||||
}
|
||||
|
||||
export function getPreviewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
|
||||
return DIContainer.getInstance().previewLeagueScheduleUseCase;
|
||||
}
|
||||
|
||||
export function getListLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
|
||||
return DIContainer.getInstance().listLeagueScoringPresetsUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight query facade for listing league scoring presets.
|
||||
* Returns an object with an execute() method for use in UI code.
|
||||
*/
|
||||
export function getListLeagueScoringPresetsQuery(): {
|
||||
execute(): Promise<LeagueScoringPresetDTO[]>;
|
||||
} {
|
||||
const useCase = DIContainer.getInstance().listLeagueScoringPresetsUseCase;
|
||||
return {
|
||||
async execute(): Promise<LeagueScoringPresetDTO[]> {
|
||||
const presenter = new LeagueScoringPresetsPresenter();
|
||||
await useCase.execute(undefined as void, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
return viewModel.presets;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getCreateLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
|
||||
return DIContainer.getInstance().createLeagueWithSeasonAndScoringUseCase;
|
||||
}
|
||||
|
||||
export function getCancelRaceUseCase(): CancelRaceUseCase {
|
||||
return DIContainer.getInstance().cancelRaceUseCase;
|
||||
}
|
||||
|
||||
export function getCompleteRaceUseCase(): import('@gridpilot/racing/application/use-cases/CompleteRaceUseCase').CompleteRaceUseCase {
|
||||
return DIContainer.getInstance().completeRaceUseCase;
|
||||
}
|
||||
|
||||
export function getImportRaceResultsUseCase(): ImportRaceResultsUseCase {
|
||||
return DIContainer.getInstance().importRaceResultsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
|
||||
return DIContainer.getInstance().getRaceWithSOFUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueStatsUseCase(): GetLeagueStatsUseCase {
|
||||
return DIContainer.getInstance().getLeagueStatsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRacesPageDataUseCase(): GetRacesPageDataUseCase {
|
||||
return DIContainer.getInstance().getRacesPageDataUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllRacesPageDataUseCase(): GetAllRacesPageDataUseCase {
|
||||
return DIContainer.getInstance().getAllRacesPageDataUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceDetailUseCase(): GetRaceDetailUseCase {
|
||||
return DIContainer.getInstance().getRaceDetailUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceResultsDetailUseCase(): GetRaceResultsDetailUseCase {
|
||||
return DIContainer.getInstance().getRaceResultsDetailUseCase;
|
||||
}
|
||||
|
||||
export function getGetDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
|
||||
return DIContainer.getInstance().getDriversLeaderboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
|
||||
return DIContainer.getInstance().getTeamsLeaderboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetDashboardOverviewUseCase(): GetDashboardOverviewUseCase {
|
||||
return DIContainer.getInstance().getDashboardOverviewUseCase;
|
||||
}
|
||||
|
||||
export function getGetProfileOverviewUseCase(): GetProfileOverviewUseCase {
|
||||
return DIContainer.getInstance().getProfileOverviewUseCase;
|
||||
}
|
||||
|
||||
export function getUpdateDriverProfileUseCase(): UpdateDriverProfileUseCase {
|
||||
return DIContainer.getInstance().updateDriverProfileUseCase;
|
||||
}
|
||||
|
||||
export function getDriverRatingProvider(): DriverRatingProvider {
|
||||
return DIContainer.getInstance().driverRatingProvider;
|
||||
}
|
||||
|
||||
export function getTeamRepository(): ITeamRepository {
|
||||
return DIContainer.getInstance().teamRepository;
|
||||
}
|
||||
|
||||
export function getTeamMembershipRepository(): ITeamMembershipRepository {
|
||||
return DIContainer.getInstance().teamMembershipRepository;
|
||||
}
|
||||
|
||||
export function getCreateTeamUseCase(): CreateTeamUseCase {
|
||||
return DIContainer.getInstance().createTeamUseCase;
|
||||
}
|
||||
|
||||
export function getJoinTeamUseCase(): JoinTeamUseCase {
|
||||
return DIContainer.getInstance().joinTeamUseCase;
|
||||
}
|
||||
|
||||
export function getLeaveTeamUseCase(): LeaveTeamUseCase {
|
||||
return DIContainer.getInstance().leaveTeamUseCase;
|
||||
}
|
||||
|
||||
export function getApproveTeamJoinRequestUseCase(): ApproveTeamJoinRequestUseCase {
|
||||
return DIContainer.getInstance().approveTeamJoinRequestUseCase;
|
||||
}
|
||||
|
||||
export function getRejectTeamJoinRequestUseCase(): RejectTeamJoinRequestUseCase {
|
||||
return DIContainer.getInstance().rejectTeamJoinRequestUseCase;
|
||||
}
|
||||
|
||||
export function getUpdateTeamUseCase(): UpdateTeamUseCase {
|
||||
return DIContainer.getInstance().updateTeamUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
return DIContainer.getInstance().getAllTeamsUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
return DIContainer.getInstance().getTeamDetailsUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
return DIContainer.getInstance().getTeamMembersUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
return DIContainer.getInstance().getTeamJoinRequestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetDriverTeamUseCase(): GetDriverTeamUseCase {
|
||||
return DIContainer.getInstance().getDriverTeamUseCase;
|
||||
}
|
||||
|
||||
export function getFeedRepository(): IFeedRepository {
|
||||
return DIContainer.getInstance().feedRepository;
|
||||
}
|
||||
|
||||
export function getSocialRepository(): ISocialGraphRepository {
|
||||
return DIContainer.getInstance().socialRepository;
|
||||
}
|
||||
|
||||
export function getImageService(): ImageServicePort {
|
||||
return DIContainer.getInstance().imageService;
|
||||
}
|
||||
|
||||
export function getTrackRepository(): ITrackRepository {
|
||||
return DIContainer.getInstance().trackRepository;
|
||||
}
|
||||
|
||||
export function getCarRepository(): ICarRepository {
|
||||
return DIContainer.getInstance().carRepository;
|
||||
}
|
||||
|
||||
export function getSeasonRepository(): ISeasonRepository {
|
||||
return DIContainer.getInstance().seasonRepository;
|
||||
}
|
||||
|
||||
export function getNotificationRepository(): INotificationRepository {
|
||||
return DIContainer.getInstance().notificationRepository;
|
||||
}
|
||||
|
||||
export function getNotificationPreferenceRepository(): INotificationPreferenceRepository {
|
||||
return DIContainer.getInstance().notificationPreferenceRepository;
|
||||
}
|
||||
|
||||
export function getSendNotificationUseCase(): SendNotificationUseCase {
|
||||
return DIContainer.getInstance().sendNotificationUseCase;
|
||||
}
|
||||
|
||||
export function getMarkNotificationReadUseCase(): MarkNotificationReadUseCase {
|
||||
return DIContainer.getInstance().markNotificationReadUseCase;
|
||||
}
|
||||
|
||||
export function getGetUnreadNotificationsUseCase(): GetUnreadNotificationsUseCase {
|
||||
return DIContainer.getInstance().getUnreadNotificationsUseCase;
|
||||
}
|
||||
|
||||
export function getFileProtestUseCase(): FileProtestUseCase {
|
||||
return DIContainer.getInstance().fileProtestUseCase;
|
||||
}
|
||||
|
||||
export function getReviewProtestUseCase(): ReviewProtestUseCase {
|
||||
return DIContainer.getInstance().reviewProtestUseCase;
|
||||
}
|
||||
|
||||
export function getApplyPenaltyUseCase(): ApplyPenaltyUseCase {
|
||||
return DIContainer.getInstance().applyPenaltyUseCase;
|
||||
}
|
||||
|
||||
export function getQuickPenaltyUseCase(): QuickPenaltyUseCase {
|
||||
return DIContainer.getInstance().quickPenaltyUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceProtestsUseCase(): GetRaceProtestsUseCase {
|
||||
return DIContainer.getInstance().getRaceProtestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
|
||||
return DIContainer.getInstance().getRacePenaltiesUseCase;
|
||||
}
|
||||
|
||||
export function getRequestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
|
||||
return DIContainer.getInstance().requestProtestDefenseUseCase;
|
||||
}
|
||||
|
||||
export function getSubmitProtestDefenseUseCase(): SubmitProtestDefenseUseCase {
|
||||
return DIContainer.getInstance().submitProtestDefenseUseCase;
|
||||
}
|
||||
|
||||
export function getTransferLeagueOwnershipUseCase(): TransferLeagueOwnershipUseCase {
|
||||
return DIContainer.getInstance().transferLeagueOwnershipUseCase;
|
||||
}
|
||||
|
||||
export function getSponsorRepository(): ISponsorRepository {
|
||||
return DIContainer.getInstance().sponsorRepository;
|
||||
}
|
||||
|
||||
export function getSeasonSponsorshipRepository(): ISeasonSponsorshipRepository {
|
||||
return DIContainer.getInstance().seasonSponsorshipRepository;
|
||||
}
|
||||
|
||||
export function getGetSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
|
||||
return DIContainer.getInstance().getSponsorDashboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
|
||||
return DIContainer.getInstance().getSponsorSponsorshipsUseCase;
|
||||
}
|
||||
|
||||
export function getSponsorshipRequestRepository(): ISponsorshipRequestRepository {
|
||||
return DIContainer.getInstance().sponsorshipRequestRepository;
|
||||
}
|
||||
|
||||
export function getSponsorshipPricingRepository(): ISponsorshipPricingRepository {
|
||||
return DIContainer.getInstance().sponsorshipPricingRepository;
|
||||
}
|
||||
|
||||
export function getApplyForSponsorshipUseCase(): ApplyForSponsorshipUseCase {
|
||||
return DIContainer.getInstance().applyForSponsorshipUseCase;
|
||||
}
|
||||
|
||||
export function getAcceptSponsorshipRequestUseCase(): AcceptSponsorshipRequestUseCase {
|
||||
return DIContainer.getInstance().acceptSponsorshipRequestUseCase;
|
||||
}
|
||||
|
||||
export function getRejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUseCase {
|
||||
return DIContainer.getInstance().rejectSponsorshipRequestUseCase;
|
||||
}
|
||||
|
||||
export function getGetPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
|
||||
return DIContainer.getInstance().getPendingSponsorshipRequestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
|
||||
return DIContainer.getInstance().getEntitySponsorshipPricingUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset function for testing
|
||||
*/
|
||||
export function resetContainer(): void {
|
||||
DIContainer.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export stats from testing-support for backward compatibility
|
||||
*/
|
||||
export type { DriverStats };
|
||||
|
||||
/**
|
||||
* Get driver statistics and rankings
|
||||
* These functions access the demo driver stats registered in the DI container
|
||||
*/
|
||||
export function getDriverStats(driverId: string): DriverStats | null {
|
||||
const container = DIContainer.getInstance();
|
||||
// Ensure container is initialized
|
||||
container['ensureInitialized']();
|
||||
const stats = getDIContainer().resolve<Record<string, DriverStats>>(DI_TOKENS.DriverStats);
|
||||
return stats[driverId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all driver rankings sorted by rating
|
||||
*/
|
||||
export function getAllDriverRankings(): DriverStats[] {
|
||||
const container = DIContainer.getInstance();
|
||||
// Ensure container is initialized
|
||||
container['ensureInitialized']();
|
||||
const stats = getDIContainer().resolve<Record<string, DriverStats>>(DI_TOKENS.DriverStats);
|
||||
return Object.values(stats).sort((a, b) => b.rating - a.rating);
|
||||
}
|
||||
export { getDemoLeagueRankings as getLeagueRankings };
|
||||
@@ -1,177 +0,0 @@
|
||||
/**
|
||||
* Dependency Injection tokens for TSyringe container (Website)
|
||||
*/
|
||||
|
||||
export const DI_TOKENS = {
|
||||
// Repositories
|
||||
DriverRepository: Symbol.for('IDriverRepository'),
|
||||
LeagueRepository: Symbol.for('ILeagueRepository'),
|
||||
RaceRepository: Symbol.for('IRaceRepository'),
|
||||
ResultRepository: Symbol.for('IResultRepository'),
|
||||
StandingRepository: Symbol.for('IStandingRepository'),
|
||||
LeagueStandingsRepository: Symbol.for('ILeagueStandingsRepository'),
|
||||
PenaltyRepository: Symbol.for('IPenaltyRepository'),
|
||||
ProtestRepository: Symbol.for('IProtestRepository'),
|
||||
TeamRepository: Symbol.for('ITeamRepository'),
|
||||
TeamMembershipRepository: Symbol.for('ITeamMembershipRepository'),
|
||||
RaceRegistrationRepository: Symbol.for('IRaceRegistrationRepository'),
|
||||
LeagueMembershipRepository: Symbol.for('ILeagueMembershipRepository'),
|
||||
GameRepository: Symbol.for('IGameRepository'),
|
||||
SeasonRepository: Symbol.for('ISeasonRepository'),
|
||||
LeagueScoringConfigRepository: Symbol.for('ILeagueScoringConfigRepository'),
|
||||
TrackRepository: Symbol.for('ITrackRepository'),
|
||||
CarRepository: Symbol.for('ICarRepository'),
|
||||
FeedRepository: Symbol.for('IFeedRepository'),
|
||||
SocialRepository: Symbol.for('ISocialGraphRepository'),
|
||||
NotificationRepository: Symbol.for('INotificationRepository'),
|
||||
NotificationPreferenceRepository: Symbol.for('INotificationPreferenceRepository'),
|
||||
SponsorRepository: Symbol.for('ISponsorRepository'),
|
||||
SeasonSponsorshipRepository: Symbol.for('ISeasonSponsorshipRepository'),
|
||||
SponsorshipRequestRepository: Symbol.for('ISponsorshipRequestRepository'),
|
||||
SponsorshipPricingRepository: Symbol.for('ISponsorshipPricingRepository'),
|
||||
PageViewRepository: Symbol.for('IPageViewRepository'),
|
||||
EngagementRepository: Symbol.for('IEngagementRepository'),
|
||||
UserRepository: Symbol.for('IUserRepository'),
|
||||
SponsorAccountRepository: Symbol.for('ISponsorAccountRepository'),
|
||||
LiveryRepository: Symbol.for('ILiveryRepository'),
|
||||
ChampionshipStandingRepository: Symbol.for('IChampionshipStandingRepository'),
|
||||
LeagueWalletRepository: Symbol.for('ILeagueWalletRepository'),
|
||||
TransactionRepository: Symbol.for('ITransactionRepository'),
|
||||
SessionRepository: Symbol.for('ISessionRepository'),
|
||||
AchievementRepository: Symbol.for('IAchievementRepository'),
|
||||
UserRatingRepository: Symbol.for('IUserRatingRepository'),
|
||||
|
||||
// Providers
|
||||
LeagueScoringPresetProvider: Symbol.for('LeagueScoringPresetProvider'),
|
||||
DriverRatingProvider: Symbol.for('DriverRatingProvider'),
|
||||
|
||||
// Services
|
||||
ImageService: Symbol.for('ImageServicePort'),
|
||||
NotificationGatewayRegistry: Symbol.for('NotificationGatewayRegistry'),
|
||||
Logger: Symbol.for('ILogger'),
|
||||
AuthService: Symbol.for('AuthService'),
|
||||
|
||||
// Auth dependencies
|
||||
IdentityProvider: Symbol.for('IdentityProvider'),
|
||||
SessionPort: Symbol.for('SessionPort'),
|
||||
|
||||
// Use Cases - Auth
|
||||
StartAuthUseCase: Symbol.for('StartAuthUseCase'),
|
||||
GetCurrentUserSessionUseCase: Symbol.for('GetCurrentUserSessionUseCase'),
|
||||
HandleAuthCallbackUseCase: Symbol.for('HandleAuthCallbackUseCase'),
|
||||
LogoutUseCase: Symbol.for('LogoutUseCase'),
|
||||
SignupWithEmailUseCase: Symbol.for('SignupWithEmailUseCase'),
|
||||
LoginWithEmailUseCase: Symbol.for('LoginWithEmailUseCase'),
|
||||
|
||||
// Use Cases - Analytics
|
||||
RecordPageViewUseCase: Symbol.for('RecordPageViewUseCase'),
|
||||
RecordEngagementUseCase: Symbol.for('RecordEngagementUseCase'),
|
||||
|
||||
// Use Cases - Racing
|
||||
JoinLeagueUseCase: Symbol.for('JoinLeagueUseCase'),
|
||||
RegisterForRaceUseCase: Symbol.for('RegisterForRaceUseCase'),
|
||||
WithdrawFromRaceUseCase: Symbol.for('WithdrawFromRaceUseCase'),
|
||||
CreateLeagueWithSeasonAndScoringUseCase: Symbol.for('CreateLeagueWithSeasonAndScoringUseCase'),
|
||||
TransferLeagueOwnershipUseCase: Symbol.for('TransferLeagueOwnershipUseCase'),
|
||||
CancelRaceUseCase: Symbol.for('CancelRaceUseCase'),
|
||||
CompleteRaceUseCase: Symbol.for('CompleteRaceUseCase'),
|
||||
ImportRaceResultsUseCase: Symbol.for('ImportRaceResultsUseCase'),
|
||||
|
||||
// Queries - Dashboard
|
||||
GetDashboardOverviewUseCase: Symbol.for('GetDashboardOverviewUseCase'),
|
||||
GetProfileOverviewUseCase: Symbol.for('GetProfileOverviewUseCase'),
|
||||
|
||||
// Use Cases - Teams
|
||||
CreateTeamUseCase: Symbol.for('CreateTeamUseCase'),
|
||||
JoinTeamUseCase: Symbol.for('JoinTeamUseCase'),
|
||||
LeaveTeamUseCase: Symbol.for('LeaveTeamUseCase'),
|
||||
ApproveTeamJoinRequestUseCase: Symbol.for('ApproveTeamJoinRequestUseCase'),
|
||||
RejectTeamJoinRequestUseCase: Symbol.for('RejectTeamJoinRequestUseCase'),
|
||||
UpdateTeamUseCase: Symbol.for('UpdateTeamUseCase'),
|
||||
|
||||
// Use Cases - Stewarding
|
||||
FileProtestUseCase: Symbol.for('FileProtestUseCase'),
|
||||
ReviewProtestUseCase: Symbol.for('ReviewProtestUseCase'),
|
||||
ApplyPenaltyUseCase: Symbol.for('ApplyPenaltyUseCase'),
|
||||
QuickPenaltyUseCase: Symbol.for('QuickPenaltyUseCase'),
|
||||
RequestProtestDefenseUseCase: Symbol.for('RequestProtestDefenseUseCase'),
|
||||
SubmitProtestDefenseUseCase: Symbol.for('SubmitProtestDefenseUseCase'),
|
||||
|
||||
// Use Cases - Notifications
|
||||
SendNotificationUseCase: Symbol.for('SendNotificationUseCase'),
|
||||
MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'),
|
||||
|
||||
// Queries - Racing
|
||||
IsDriverRegisteredForRaceUseCase: Symbol.for('IsDriverRegisteredForRaceUseCase'),
|
||||
GetRaceRegistrationsUseCase: Symbol.for('GetRaceRegistrationsUseCase'),
|
||||
GetLeagueStandingsUseCase: Symbol.for('GetLeagueStandingsUseCase'),
|
||||
GetLeagueDriverSeasonStatsUseCase: Symbol.for('GetLeagueDriverSeasonStatsUseCase'),
|
||||
GetAllLeaguesWithCapacityUseCase: Symbol.for('GetAllLeaguesWithCapacityUseCase'),
|
||||
GetAllLeaguesWithCapacityAndScoringUseCase: Symbol.for('GetAllLeaguesWithCapacityAndScoringUseCase'),
|
||||
ListLeagueScoringPresetsUseCase: Symbol.for('ListLeagueScoringPresetsUseCase'),
|
||||
GetLeagueScoringConfigUseCase: Symbol.for('GetLeagueScoringConfigUseCase'),
|
||||
GetLeagueFullConfigUseCase: Symbol.for('GetLeagueFullConfigUseCase'),
|
||||
PreviewLeagueScheduleUseCase: Symbol.for('PreviewLeagueScheduleUseCase'),
|
||||
GetRaceWithSOFUseCase: Symbol.for('GetRaceWithSOFUseCase'),
|
||||
GetLeagueStatsUseCase: Symbol.for('GetLeagueStatsUseCase'),
|
||||
ListSeasonsForLeagueUseCase: Symbol.for('ListSeasonsForLeagueUseCase'),
|
||||
GetRacesPageDataUseCase: Symbol.for('GetRacesPageDataUseCase'),
|
||||
GetAllRacesPageDataUseCase: Symbol.for('GetAllRacesPageDataUseCase'),
|
||||
GetRaceDetailUseCase: Symbol.for('GetRaceDetailUseCase'),
|
||||
GetRaceResultsDetailUseCase: Symbol.for('GetRaceResultsDetailUseCase'),
|
||||
GetDriversLeaderboardUseCase: Symbol.for('GetDriversLeaderboardUseCase'),
|
||||
GetTeamsLeaderboardUseCase: Symbol.for('GetTeamsLeaderboardUseCase'),
|
||||
|
||||
// Use Cases - Teams (Query-like)
|
||||
GetAllTeamsUseCase: Symbol.for('GetAllTeamsUseCase'),
|
||||
GetTeamDetailsUseCase: Symbol.for('GetTeamDetailsUseCase'),
|
||||
GetTeamMembersUseCase: Symbol.for('GetTeamMembersUseCase'),
|
||||
GetTeamJoinRequestsUseCase: Symbol.for('GetTeamJoinRequestsUseCase'),
|
||||
GetDriverTeamUseCase: Symbol.for('GetDriverTeamUseCase'),
|
||||
|
||||
// Queries - Stewarding
|
||||
GetRaceProtestsUseCase: Symbol.for('GetRaceProtestsUseCase'),
|
||||
GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
|
||||
|
||||
// Queries - Notifications
|
||||
GetUnreadNotificationsUseCase: Symbol.for('GetUnreadNotificationsUseCase'),
|
||||
|
||||
// Use Cases - Sponsors
|
||||
GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),
|
||||
GetSponsorSponsorshipsUseCase: Symbol.for('GetSponsorSponsorshipsUseCase'),
|
||||
GetPendingSponsorshipRequestsUseCase: Symbol.for('GetPendingSponsorshipRequestsUseCase'),
|
||||
GetEntitySponsorshipPricingUseCase: Symbol.for('GetEntitySponsorshipPricingUseCase'),
|
||||
|
||||
// Use Cases - Sponsorship
|
||||
ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'),
|
||||
AcceptSponsorshipRequestUseCase: Symbol.for('AcceptSponsorshipRequestUseCase'),
|
||||
RejectSponsorshipRequestUseCase: Symbol.for('RejectSponsorshipRequestUseCase'),
|
||||
|
||||
// Use Cases - Driver Profile
|
||||
UpdateDriverProfileUseCase: Symbol.for('UpdateDriverProfileUseCase'),
|
||||
|
||||
// Data
|
||||
DriverStats: Symbol.for('DriverStats'),
|
||||
|
||||
// Presenters - Racing
|
||||
LeagueStandingsPresenter: Symbol.for('ILeagueStandingsPresenter'),
|
||||
RaceWithSOFPresenter: Symbol.for('IRaceWithSOFPresenter'),
|
||||
RaceProtestsPresenter: Symbol.for('IRaceProtestsPresenter'),
|
||||
RacePenaltiesPresenter: Symbol.for('IRacePenaltiesPresenter'),
|
||||
RaceRegistrationsPresenter: Symbol.for('IRaceRegistrationsPresenter'),
|
||||
DriverRegistrationStatusPresenter: Symbol.for('IDriverRegistrationStatusPresenter'),
|
||||
RaceDetailPresenter: Symbol.for('IRaceDetailPresenter'),
|
||||
RaceResultsDetailPresenter: Symbol.for('IRaceResultsDetailPresenter'),
|
||||
ImportRaceResultsPresenter: Symbol.for('IImportRaceResultsPresenter'),
|
||||
DashboardOverviewPresenter: Symbol.for('IDashboardOverviewPresenter'),
|
||||
ProfileOverviewPresenter: Symbol.for('IProfileOverviewPresenter'),
|
||||
|
||||
// Presenters - Sponsors
|
||||
SponsorDashboardPresenter: Symbol.for('ISponsorDashboardPresenter'),
|
||||
SponsorSponsorshipsPresenter: Symbol.for('ISponsorSponsorshipsPresenter'),
|
||||
PendingSponsorshipRequestsPresenter: Symbol.for('IPendingSponsorshipRequestsPresenter'),
|
||||
EntitySponsorshipPricingPresenter: Symbol.for('IEntitySponsorshipPricingPresenter'),
|
||||
LeagueSchedulePreviewPresenter: Symbol.for('ILeagueSchedulePreviewPresenter'),
|
||||
} as const;
|
||||
|
||||
export type DITokens = typeof DI_TOKENS;
|
||||
@@ -30,7 +30,8 @@
|
||||
"@gridpilot/media/*": ["../../core/media/*"],
|
||||
"@gridpilot/shared/logging": ["../../core/shared/logging"],
|
||||
"@gridpilot/shared/*": ["../../core/shared/*"],
|
||||
"@gridpilot/core/*": ["../../core/*"]
|
||||
"@gridpilot/core/*": ["../../core/*"],
|
||||
"@gridpilot/api/*": ["../../apps/api/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
||||
Reference in New Issue
Block a user