module creation

This commit is contained in:
2025-12-15 21:44:06 +01:00
parent b834f88bbd
commit 7c7267da72
88 changed files with 12119 additions and 4241 deletions

View File

@@ -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],

View File

@@ -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 {}

View 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);
}
}

View 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 {}

View 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;
}
}
}

View 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;
}

View 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 });
}
}

View 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 {}

View 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],
},
];

View 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();
}
}

View 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;
}

View 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
}

View 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 {}

View 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,
},
];

View 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,
};
}
}

View 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

View 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);
}
}

View 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 {}

View 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,
},
];

View 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 },
];
}
}

View 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[];
}

View 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);
}
}
}

View 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 {}

View 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,
},
*/
];

View 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',
],
};
}
}

View 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;

View 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);
}
}

View 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 {}

View 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,
},
*/
];

View 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 };
}
}

View 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;
}

View 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
}

View 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 {}

View 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
*/
];

View 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: [],
};
}
}

View 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;
}

View 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 });
}
}

View 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 {}

View File

@@ -0,0 +1,5 @@
import { SponsorService } from './SponsorService';
export const SponsorProviders = [
SponsorService,
];

View 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',
},
};
}
}

View 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

View 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
}

View 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 {}

View File

@@ -0,0 +1,5 @@
import { TeamService } from './TeamService';
export const TeamProviders = [
TeamService,
];

View 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
}

View 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;
}

View File

@@ -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);
}
}