module creation
This commit is contained in:
@@ -2,13 +2,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HelloController } from './presentation/hello.controller';
|
||||
import { HelloService } from './application/hello/hello.service';
|
||||
import { AnalyticsModule } from './application/analytics/analytics.module';
|
||||
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
|
||||
import { DatabaseModule } from './infrastructure/database/database.module';
|
||||
import { AuthModule } from './modules/auth/AuthModule';
|
||||
import { LeagueModule } from './modules/league/LeagueModule';
|
||||
import { RaceModule } from './modules/race/RaceModule';
|
||||
import { TeamModule } from './modules/team/TeamModule';
|
||||
import { SponsorModule } from './modules/sponsor/SponsorModule';
|
||||
import { DriverModule } from './modules/driver/DriverModule';
|
||||
import { MediaModule } from './modules/media/MediaModule';
|
||||
import { PaymentsModule } from './modules/payments/PaymentsModule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
AnalyticsModule
|
||||
AnalyticsModule,
|
||||
AuthModule,
|
||||
LeagueModule,
|
||||
RaceModule,
|
||||
TeamModule,
|
||||
SponsorModule,
|
||||
DriverModule,
|
||||
MediaModule,
|
||||
PaymentsModule,
|
||||
],
|
||||
controllers: [HelloController],
|
||||
providers: [HelloService],
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
|
||||
import { InMemoryPageViewRepository } from '../../../../adapters/persistence/inmemory/analytics/InMemoryPageViewRepository';
|
||||
import { TypeOrmEngagementRepository } from '../../../../adapters/persistence/typeorm/analytics/TypeOrmEngagementRepository';
|
||||
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
|
||||
import { AnalyticsController } from '../../presentation/analytics.controller';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
{
|
||||
provide: ILogger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => new TypeOrmEngagementRepository(dataSource.manager),
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: RecordPageViewUseCase,
|
||||
useFactory: (repo: IPageViewRepository, logger: ILogger) => new RecordPageViewUseCase(repo, logger),
|
||||
inject: [IPAGE_VIEW_REPO_TOKEN, ILogger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RecordEngagementUseCase,
|
||||
useFactory: (repo: IEngagementRepository, logger: ILogger) => new RecordEngagementUseCase(repo, logger),
|
||||
inject: [IENGAGEMENT_REPO_TOKEN, ILogger_TOKEN],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
29
apps/api/src/modules/analytics/AnalyticsController.ts
Normal file
29
apps/api/src/modules/analytics/AnalyticsController.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { RecordPageViewInput, RecordPageViewOutput, RecordEngagementInput, RecordEngagementOutput } from './dto/AnalyticsDto';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
@Post('page-view')
|
||||
async recordPageView(
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.analyticsService.recordPageView(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
async recordEngagement(
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.analyticsService.recordEngagement(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
}
|
||||
43
apps/api/src/modules/analytics/AnalyticsModule.ts
Normal file
43
apps/api/src/modules/analytics/AnalyticsModule.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
|
||||
import { InMemoryPageViewRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
||||
import { InMemoryEngagementRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
AnalyticsService,
|
||||
{
|
||||
provide: ILogger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useExisting: InMemoryEngagementRepository, // Assuming TypeOrmEngagementRepository is not available
|
||||
},
|
||||
// No need for useExisting here if the original intent was to inject the concrete class when providing the TOKEN
|
||||
],
|
||||
exports: [
|
||||
AnalyticsService,
|
||||
ILogger_TOKEN,
|
||||
IPAGE_VIEW_REPO_TOKEN,
|
||||
IENGAGEMENT_REPO_TOKEN,
|
||||
],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
83
apps/api/src/modules/analytics/AnalyticsService.ts
Normal file
83
apps/api/src/modules/analytics/AnalyticsService.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { RecordEngagementInput, RecordEngagementOutput, RecordPageViewInput, RecordPageViewOutput } from './dto/AnalyticsDto';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { PageView } from '@gridpilot/analytics/domain/entities/PageView';
|
||||
import { EngagementEvent } from '@gridpilot/analytics/domain/entities/EngagementEvent';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
constructor(
|
||||
@Inject(IPAGE_VIEW_REPO_TOKEN) private readonly pageViewRepository: IPageViewRepository,
|
||||
@Inject(IENGAGEMENT_REPO_TOKEN) private readonly engagementRepository: IEngagementRepository,
|
||||
@Inject(ILogger_TOKEN) private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
this.logger.debug('Executing RecordPageViewUseCase', { input });
|
||||
try {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
visitorType: input.visitorType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
});
|
||||
|
||||
await this.pageViewRepository.save(pageView);
|
||||
this.logger.info('Page view recorded successfully', { pageViewId, input });
|
||||
return { pageViewId };
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording page view', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
this.logger.debug('Executing RecordEngagementUseCase', { input });
|
||||
try {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
action: input.action as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
});
|
||||
|
||||
await this.engagementRepository.save(event);
|
||||
this.logger.info('Engagement recorded successfully', { eventId, input });
|
||||
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording engagement', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
apps/api/src/modules/analytics/dto/AnalyticsDto.ts
Normal file
127
apps/api/src/modules/analytics/dto/AnalyticsDto.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsEnum, IsBoolean, IsNumber, IsObject } from 'class-validator';
|
||||
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum EntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum VisitorType {
|
||||
ANONYMOUS = 'anonymous',
|
||||
DRIVER = 'driver',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
export class RecordPageViewInput {
|
||||
@ApiProperty({ enum: EntityType })
|
||||
@IsEnum(EntityType)
|
||||
entityType: EntityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
visitorId?: string;
|
||||
|
||||
@ApiProperty({ enum: VisitorType })
|
||||
@IsEnum(VisitorType)
|
||||
visitorType: VisitorType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referrer?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userAgent?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export class RecordPageViewOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
pageViewId: string;
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementAction {
|
||||
CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
|
||||
CLICK_SPONSOR_URL = 'click_sponsor_url',
|
||||
DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
|
||||
JOIN_LEAGUE = 'join_league',
|
||||
REGISTER_RACE = 'register_race',
|
||||
VIEW_STANDINGS = 'view_standings',
|
||||
VIEW_SCHEDULE = 'view_schedule',
|
||||
SHARE_SOCIAL = 'share_social',
|
||||
CONTACT_SPONSOR = 'contact_sponsor',
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementEntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
}
|
||||
|
||||
export class RecordEngagementInput {
|
||||
@ApiProperty({ enum: EngagementAction })
|
||||
@IsEnum(EngagementAction)
|
||||
action: EngagementAction;
|
||||
|
||||
@ApiProperty({ enum: EngagementEntityType })
|
||||
@IsEnum(EngagementEntityType)
|
||||
entityType: EngagementEntityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
actorId?: string;
|
||||
|
||||
@ApiProperty({ enum: ['anonymous', 'driver', 'sponsor'] })
|
||||
@IsEnum(['anonymous', 'driver', 'sponsor'])
|
||||
actorType: 'anonymous' | 'driver' | 'sponsor';
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ required: false, type: 'object'/*, additionalProperties: { type: 'string' || 'number' || 'boolean' }*/ })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
export class RecordEngagementOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
eventId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
engagementWeight: number;
|
||||
}
|
||||
42
apps/api/src/modules/auth/AuthController.ts
Normal file
42
apps/api/src/modules/auth/AuthController.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParams, SignupParams, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('signup')
|
||||
async signup(@Body() params: SignupParams) {
|
||||
return this.authService.signupWithEmail(params);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() params: LoginParams) {
|
||||
return this.authService.loginWithEmail(params);
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
async getSession() {
|
||||
return this.authService.getCurrentSession();
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
async logout() {
|
||||
return this.authService.logout();
|
||||
}
|
||||
|
||||
@Get('iracing/start')
|
||||
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response) {
|
||||
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
|
||||
// In real application, you might want to store 'state' in a secure cookie or session.
|
||||
// For this example, we'll just redirect.
|
||||
res.redirect(HttpStatus.FOUND, redirectUrl);
|
||||
}
|
||||
|
||||
@Get('iracing/callback')
|
||||
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string) {
|
||||
return this.authService.loginWithIracingCallback({ code, state, returnTo });
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/auth/AuthModule.ts
Normal file
10
apps/api/src/modules/auth/AuthModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
import { AuthController } from './AuthController';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
64
apps/api/src/modules/auth/AuthProviders.ts
Normal file
64
apps/api/src/modules/auth/AuthProviders.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
|
||||
// Import interfaces and concrete implementations
|
||||
import { IAuthRepository } from '@gridpilot/core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@gridpilot/core/identity/domain/repositories/IUserRepository';
|
||||
import { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
import { ILogger } from '@gridpilot/core/shared/logging/ILogger';
|
||||
|
||||
import { InMemoryAuthRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
||||
import { InMemoryUserRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
import { InMemoryPasswordHashingService } from '../../../adapters/identity/services/InMemoryPasswordHashingService';
|
||||
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
|
||||
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort'; // Path from apps/api/src/modules/auth
|
||||
import { CookieIdentitySessionAdapter } from '../../../adapters/identity/session/CookieIdentitySessionAdapter';
|
||||
|
||||
// Define the tokens for dependency injection
|
||||
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
|
||||
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
|
||||
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
|
||||
export const LOGGER_TOKEN = 'ILogger';
|
||||
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
AuthService, // Provide the service itself
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: ILogger) => {
|
||||
// Seed initial users for InMemoryUserRepository
|
||||
const initialUsers: StoredUser[] = [
|
||||
// Example user (replace with actual test users as needed)
|
||||
{
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
passwordHash: 'demo_salt_moc.elpmaxe@tset', // "test@example.com" reversed
|
||||
displayName: 'Test User',
|
||||
salt: '', // Handled by hashing service
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
|
||||
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
|
||||
},
|
||||
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryUserRepository(logger), // Factory for InMemoryUserRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
useClass: InMemoryPasswordHashingService,
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IDENTITY_SESSION_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new CookieIdentitySessionAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
140
apps/api/src/modules/auth/AuthService.ts
Normal file
140
apps/api/src/modules/auth/AuthService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
// Core Use Cases
|
||||
import { LoginUseCase } from '../../../../core/identity/application/use-cases/LoginUseCase';
|
||||
import { SignupUseCase } from '../../../../core/identity/application/use-cases/SignupUseCase';
|
||||
import { GetCurrentSessionUseCase } from '../../../../core/identity/application/use-cases/GetCurrentSessionUseCase';
|
||||
import { LogoutUseCase } from '../../../../core/identity/application/use-cases/LogoutUseCase';
|
||||
import { StartIracingAuthRedirectUseCase } from '../../../../core/identity/application/use-cases/StartIracingAuthRedirectUseCase';
|
||||
import { LoginWithIracingCallbackUseCase } from '../../../../core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
|
||||
|
||||
// Core Interfaces and Tokens
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
import { IAuthRepository } from '../../../../core/identity/domain/repositories/IAuthRepository';
|
||||
import { IPasswordHashingService } from '../../../../core/identity/domain/services/PasswordHashingService';
|
||||
import { ILogger } from '../../../../core/shared/logging/ILogger';
|
||||
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort';
|
||||
import { UserId } from '../../../../core/identity/domain/value-objects/UserId';
|
||||
import { User } from '../../../../core/identity/domain/entities/User';
|
||||
import { IUserRepository } from '../../../../core/identity/domain/repositories/IUserRepository';
|
||||
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '../../../../core/identity/application/dto/AuthenticatedUserDTO';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly loginUseCase: LoginUseCase;
|
||||
private readonly signupUseCase: SignupUseCase;
|
||||
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
|
||||
private readonly logoutUseCase: LogoutUseCase;
|
||||
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
|
||||
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
|
||||
|
||||
constructor(
|
||||
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
|
||||
@Inject(PASSWORD_HASHING_SERVICE_TOKEN) private passwordHashingService: IPasswordHashingService,
|
||||
@Inject(LOGGER_TOKEN) private logger: ILogger,
|
||||
@Inject(IDENTITY_SESSION_PORT_TOKEN) private identitySessionPort: IdentitySessionPort,
|
||||
@Inject(USER_REPOSITORY_TOKEN) private userRepository: IUserRepository, // Inject IUserRepository here
|
||||
) {
|
||||
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
|
||||
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
|
||||
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
|
||||
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
|
||||
}
|
||||
|
||||
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
|
||||
return {
|
||||
userId: user.getId().value,
|
||||
email: user.getEmail(),
|
||||
displayName: user.getDisplayName(),
|
||||
// Map other fields as necessary
|
||||
iracingCustomerId: user.getIracingCustomerId() ?? undefined,
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
avatarUrl: user.getAvatarUrl() ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
this.logger.debug('[AuthService] Attempting to get current session.');
|
||||
const coreSession = await this.identitySessionPort.getCurrentSession();
|
||||
if (!coreSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(coreSession.user.id); // Use userRepository to fetch full user
|
||||
if (!user) {
|
||||
// If session exists but user doesn't in DB, perhaps clear session?
|
||||
this.logger.warn(`[AuthService] Session found for user ID ${coreSession.user.id}, but user not found in repository.`);
|
||||
await this.identitySessionPort.clearSession(); // Clear potentially stale session
|
||||
return null;
|
||||
}
|
||||
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(User.fromStored(user));
|
||||
|
||||
return {
|
||||
token: coreSession.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
|
||||
const user = await this.signupUseCase.execute(params.email, params.password, params.displayName);
|
||||
|
||||
// Create session after successful signup
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
|
||||
try {
|
||||
const user = await this.loginUseCase.execute(params.email, params.password);
|
||||
// Create session after successful login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`[AuthService] Login failed for email ${params.email}:`, error);
|
||||
throw new InternalServerErrorException('Login failed due to invalid credentials or server error.');
|
||||
}
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
|
||||
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
|
||||
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
|
||||
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
|
||||
return { redirectUrl: result.redirectUrl, state: result.state };
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
|
||||
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
|
||||
|
||||
// Create session after successful iRacing login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.logger.debug('[AuthService] Attempting logout.');
|
||||
await this.logoutUseCase.execute();
|
||||
}
|
||||
}
|
||||
55
apps/api/src/modules/auth/dto/AuthDto.ts
Normal file
55
apps/api/src/modules/auth/dto/AuthDto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthenticatedUserDTO {
|
||||
@ApiProperty()
|
||||
userId: string;
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export class AuthSessionDTO {
|
||||
@ApiProperty()
|
||||
token: string;
|
||||
@ApiProperty()
|
||||
user: AuthenticatedUserDTO;
|
||||
}
|
||||
|
||||
export class SignupParams {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
@ApiProperty({ required: false })
|
||||
iracingCustomerId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
primaryDriverId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class LoginParams {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class IracingAuthRedirectResult {
|
||||
@ApiProperty()
|
||||
redirectUrl: string;
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
}
|
||||
|
||||
export class LoginWithIracingCallbackParams {
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
@ApiProperty({ required: false })
|
||||
returnTo?: string;
|
||||
}
|
||||
49
apps/api/src/modules/driver/DriverController.ts
Normal file
49
apps/api/src/modules/driver/DriverController.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
|
||||
|
||||
@ApiTags('drivers')
|
||||
@Controller('drivers')
|
||||
export class DriverController {
|
||||
constructor(private readonly driverService: DriverService) {}
|
||||
|
||||
@Get('leaderboard')
|
||||
@ApiOperation({ summary: 'Get drivers leaderboard' })
|
||||
@ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardViewModel })
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
return this.driverService.getDriversLeaderboard();
|
||||
}
|
||||
|
||||
@Get('total-drivers')
|
||||
@ApiOperation({ summary: 'Get the total number of drivers' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDto })
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
return this.driverService.getTotalDrivers();
|
||||
}
|
||||
|
||||
@Post('complete-onboarding')
|
||||
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
|
||||
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
|
||||
async completeOnboarding(
|
||||
@Body() input: CompleteOnboardingInput,
|
||||
@Req() req: Request,
|
||||
): Promise<CompleteOnboardingOutput> {
|
||||
// Assuming userId is available from the request (e.g., via auth middleware)
|
||||
const userId = req['user'].userId; // Placeholder for actual user extraction
|
||||
return this.driverService.completeOnboarding(userId, input);
|
||||
}
|
||||
|
||||
@Get(':driverId/races/:raceId/registration-status')
|
||||
@ApiOperation({ summary: 'Get driver registration status for a specific race' })
|
||||
@ApiResponse({ status: 200, description: 'Driver registration status', type: DriverRegistrationStatusViewModel })
|
||||
async getDriverRegistrationStatus(
|
||||
@Param('driverId') driverId: string,
|
||||
@Param('raceId') raceId: string,
|
||||
): Promise<DriverRegistrationStatusViewModel> {
|
||||
return this.driverService.getDriverRegistrationStatus({ driverId, raceId });
|
||||
}
|
||||
|
||||
// Add other Driver endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/driver/DriverModule.ts
Normal file
10
apps/api/src/modules/driver/DriverModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriverController } from './DriverController';
|
||||
|
||||
@Module({
|
||||
controllers: [DriverController],
|
||||
providers: [DriverService],
|
||||
exports: [DriverService],
|
||||
})
|
||||
export class DriverModule {}
|
||||
75
apps/api/src/modules/driver/DriverProviders.ts
Normal file
75
apps/api/src/modules/driver/DriverProviders.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
|
||||
// Import core interfaces
|
||||
import { IDriverRepository } from '../../../../core/racing/domain/repositories/IDriverRepository';
|
||||
import { IRankingService } from '../../../../core/racing/domain/services/IRankingService';
|
||||
import { IDriverStatsService } from '../../../../core/racing/domain/services/IDriverStatsService';
|
||||
import { DriverRatingProvider } from '../../../../core/racing/application/ports/DriverRatingProvider';
|
||||
import { IImageServicePort } from '../../../../core/racing/application/ports/IImageServicePort';
|
||||
import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { ILogger } from '../../../../core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService';
|
||||
import { InMemoryDriverStatsService } from '../../../adapters/racing/services/InMemoryDriverStatsService';
|
||||
import { InMemoryDriverRatingProvider } from '../../../adapters/racing/ports/InMemoryDriverRatingProvider';
|
||||
import { InMemoryImageServiceAdapter } from '../../../adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { InMemoryRaceRegistrationRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '../../../adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const RANKING_SERVICE_TOKEN = 'IRankingService';
|
||||
export const DRIVER_STATS_SERVICE_TOKEN = 'IDriverStatsService';
|
||||
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
|
||||
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
|
||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
DriverService, // Provide the service itself
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RANKING_SERVICE_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRankingService(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_STATS_SERVICE_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverStatsService(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_RATING_PROVIDER_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverRatingProvider(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IMAGE_SERVICE_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryImageServiceAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRaceRegistrationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryNotificationPreferenceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
];
|
||||
46
apps/api/src/modules/driver/DriverService.ts
Normal file
46
apps/api/src/modules/driver/DriverService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto';
|
||||
|
||||
@Injectable()
|
||||
export class DriverService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
console.log('[DriverService] Returning mock driver leaderboard.');
|
||||
const drivers: DriverLeaderboardItemViewModel[] = [
|
||||
{ id: 'driver-1', name: 'Mock Driver 1', rating: 2500, skillLevel: 'Pro', nationality: 'DE', racesCompleted: 50, wins: 10, podiums: 20, isActive: true, rank: 1, avatarUrl: 'https://cdn.example.com/avatars/driver-1.png' },
|
||||
{ id: 'driver-2', name: 'Mock Driver 2', rating: 2400, skillLevel: 'Amateur', nationality: 'US', racesCompleted: 40, wins: 5, podiums: 15, isActive: true, rank: 2, avatarUrl: 'https://cdn.example.com/avatars/driver-2.png' },
|
||||
];
|
||||
return {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces: drivers.reduce((sum, item) => sum + (item.racesCompleted ?? 0), 0),
|
||||
totalWins: drivers.reduce((sum, item) => sum + (item.wins ?? 0), 0),
|
||||
activeCount: drivers.filter(d => d.isActive).length,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
console.log('[DriverService] Returning mock total drivers.');
|
||||
return {
|
||||
totalDrivers: 2,
|
||||
};
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
|
||||
console.log('Completing onboarding for user:', userId, input);
|
||||
return {
|
||||
success: true,
|
||||
driverId: `driver-${userId}-onboarded`,
|
||||
};
|
||||
}
|
||||
|
||||
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
|
||||
console.log('Checking driver registration status:', query);
|
||||
return {
|
||||
isRegistered: false, // Mock response
|
||||
raceId: query.raceId,
|
||||
driverId: query.driverId,
|
||||
};
|
||||
}
|
||||
}
|
||||
138
apps/api/src/modules/driver/dto/DriverDto.ts
Normal file
138
apps/api/src/modules/driver/dto/DriverDto.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
|
||||
|
||||
export class DriverLeaderboardItemViewModel {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
rating: number;
|
||||
|
||||
@ApiProperty()
|
||||
skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
|
||||
|
||||
@ApiProperty()
|
||||
nationality: string;
|
||||
|
||||
@ApiProperty()
|
||||
racesCompleted: number;
|
||||
|
||||
@ApiProperty()
|
||||
wins: number;
|
||||
|
||||
@ApiProperty()
|
||||
podiums: number;
|
||||
|
||||
@ApiProperty()
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
rank: number;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class DriversLeaderboardViewModel {
|
||||
@ApiProperty({ type: [DriverLeaderboardItemViewModel] })
|
||||
drivers: DriverLeaderboardItemViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalRaces: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalWins: number;
|
||||
|
||||
@ApiProperty()
|
||||
activeCount: number;
|
||||
}
|
||||
|
||||
export class DriverStatsDto {
|
||||
@ApiProperty()
|
||||
totalDrivers: number;
|
||||
}
|
||||
|
||||
export class CompleteOnboardingInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
displayName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
country: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timezone?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export class CompleteOnboardingOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
driverId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export class GetDriverRegistrationStatusQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class DriverRegistrationStatusViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isRegistered: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class DriverDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string; // Display name or full name
|
||||
}
|
||||
|
||||
// Add other DTOs for driver-related logic as needed
|
||||
136
apps/api/src/modules/league/LeagueController.ts
Normal file
136
apps/api/src/modules/league/LeagueController.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel } from './dto/LeagueDto';
|
||||
import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries
|
||||
|
||||
@ApiTags('leagues')
|
||||
@Controller('leagues')
|
||||
export class LeagueController {
|
||||
constructor(private readonly leagueService: LeagueService) {}
|
||||
|
||||
@Get('all-with-capacity')
|
||||
@ApiOperation({ summary: 'Get all leagues with their capacity information' })
|
||||
@ApiResponse({ status: 200, description: 'List of leagues with capacity', type: AllLeaguesWithCapacityViewModel })
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Get('total-leagues')
|
||||
@ApiOperation({ summary: 'Get the total number of leagues' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of leagues', type: LeagueStatsDto })
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
return this.leagueService.getTotalLeagues();
|
||||
}
|
||||
|
||||
@Get(':leagueId/join-requests')
|
||||
@ApiOperation({ summary: 'Get all outstanding join requests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of join requests', type: [LeagueJoinRequestViewModel] })
|
||||
async getJoinRequests(@Param('leagueId') leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
// No specific query DTO needed for GET, leagueId from param
|
||||
return this.leagueService.getLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
@Post(':leagueId/join-requests/approve')
|
||||
@ApiOperation({ summary: 'Approve a league join request' })
|
||||
@ApiBody({ type: ApproveJoinRequestInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Join request approved', type: ApproveJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async approveJoinRequest(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() input: ApproveJoinRequestInput,
|
||||
): Promise<ApproveJoinRequestOutput> {
|
||||
return this.leagueService.approveLeagueJoinRequest({ ...input, leagueId });
|
||||
}
|
||||
|
||||
@Post(':leagueId/join-requests/reject')
|
||||
@ApiOperation({ summary: 'Reject a league join request' })
|
||||
@ApiBody({ type: RejectJoinRequestInput })
|
||||
@ApiResponse({ status: 200, description: 'Join request rejected', type: RejectJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async rejectJoinRequest(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() input: RejectJoinRequestInput,
|
||||
): Promise<RejectJoinRequestOutput> {
|
||||
return this.leagueService.rejectLeagueJoinRequest({ ...input, leagueId });
|
||||
}
|
||||
|
||||
@Get(':leagueId/permissions/:performerDriverId')
|
||||
@ApiOperation({ summary: 'Get league admin permissions for a performer' })
|
||||
@ApiResponse({ status: 200, description: 'League admin permissions', type: LeagueAdminPermissionsViewModel })
|
||||
async getLeagueAdminPermissions(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
): Promise<LeagueAdminPermissionsViewModel> {
|
||||
// No specific input DTO needed for Get, parameters from path
|
||||
return this.leagueService.getLeagueAdminPermissions({ leagueId, performerDriverId });
|
||||
}
|
||||
|
||||
@Patch(':leagueId/members/:targetDriverId/remove')
|
||||
@ApiOperation({ summary: 'Remove a member from the league' })
|
||||
@ApiBody({ type: RemoveLeagueMemberInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Member removed successfully', type: RemoveLeagueMemberOutput })
|
||||
@ApiResponse({ status: 400, description: 'Cannot remove member' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async removeLeagueMember(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: RemoveLeagueMemberInput, // Body content for a patch often includes IDs
|
||||
): Promise<RemoveLeagueMemberOutput> {
|
||||
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
|
||||
}
|
||||
|
||||
@Patch(':leagueId/members/:targetDriverId/role')
|
||||
@ApiOperation({ summary: "Update a member's role in the league" })
|
||||
@ApiBody({ type: UpdateLeagueMemberRoleInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Member role updated successfully', type: UpdateLeagueMemberRoleOutput })
|
||||
@ApiResponse({ status: 400, description: 'Cannot update role' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async updateLeagueMemberRole(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: UpdateLeagueMemberRoleInput, // Body includes newRole, other for swagger
|
||||
): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
return this.leagueService.updateLeagueMemberRole({ leagueId, performerDriverId, targetDriverId, newRole: input.newRole });
|
||||
}
|
||||
|
||||
@Get(':leagueId/owner-summary/:ownerId')
|
||||
@ApiOperation({ summary: 'Get owner summary for a league' })
|
||||
@ApiResponse({ status: 200, description: 'League owner summary', type: LeagueOwnerSummaryViewModel })
|
||||
@ApiResponse({ status: 404, description: 'Owner or league not found' })
|
||||
async getLeagueOwnerSummary(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('ownerId') ownerId: string,
|
||||
): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId };
|
||||
return this.leagueService.getLeagueOwnerSummary(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/config')
|
||||
@ApiOperation({ summary: 'Get league full configuration' })
|
||||
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDto })
|
||||
async getLeagueFullConfig(
|
||||
@Param('leagueId') leagueId: string,
|
||||
): Promise<LeagueConfigFormModelDto | null> {
|
||||
const query: GetLeagueAdminConfigQuery = { leagueId };
|
||||
return this.leagueService.getLeagueFullConfig(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/protests')
|
||||
@ApiOperation({ summary: 'Get protests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsViewModel })
|
||||
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||
const query: GetLeagueProtestsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueProtests(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/seasons')
|
||||
@ApiOperation({ summary: 'Get seasons for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryViewModel] })
|
||||
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
const query: GetLeagueSeasonsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueSeasons(query);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/league/LeagueModule.ts
Normal file
10
apps/api/src/modules/league/LeagueModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeagueController } from './LeagueController';
|
||||
|
||||
@Module({
|
||||
controllers: [LeagueController],
|
||||
providers: [LeagueService],
|
||||
exports: [LeagueService],
|
||||
})
|
||||
export class LeagueModule {}
|
||||
83
apps/api/src/modules/league/LeagueProviders.ts
Normal file
83
apps/api/src/modules/league/LeagueProviders.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
|
||||
// Import core interfaces
|
||||
import { ILeagueRepository } from 'core/racing/domain/repositories/ILeagueRepository';
|
||||
import { ILeagueMembershipRepository } from 'core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { ILeagueStandingsRepository } from 'core/league/application/ports/ILeagueStandingsRepository';
|
||||
import { ISeasonRepository } from 'core/racing/domain/repositories/ISeasonRepository';
|
||||
import { ILeagueScoringConfigRepository } from 'core/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import { IGameRepository } from 'core/racing/domain/repositories/IGameRepository';
|
||||
import { IProtestRepository } from 'core/racing/domain/repositories/IProtestRepository';
|
||||
import { IRaceRepository } from 'core/racing/domain/repositories/IRaceRepository';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryLeagueStandingsRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||
import { InMemorySeasonRepository } from 'adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueScoringConfigRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||
import { InMemoryGameRepository } from 'adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||
import { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository';
|
||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
|
||||
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
|
||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
export const LeagueProviders: Provider[] = [
|
||||
LeagueService, // Provide the service itself
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueRepository(logger), // Factory for InMemoryLeagueRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueMembershipRepository(logger), // Factory for InMemoryLeagueMembershipRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SEASON_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GAME_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryGameRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PROTEST_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryProtestRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
];
|
||||
125
apps/api/src/modules/league/LeagueService.ts
Normal file
125
apps/api/src/modules/league/LeagueService.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto';
|
||||
import { DriverDto } from '../driver/dto/DriverDto'; // Using the local DTO for mock data
|
||||
import { RaceDto } from '../race/dto/RaceDto'; // Using the local DTO for mock data
|
||||
|
||||
const mockDriverData: Map<string, DriverDto> = new Map();
|
||||
mockDriverData.set('driver-owner-1', { id: 'driver-owner-1', name: 'Owner Driver' });
|
||||
mockDriverData.set('driver-1', { id: 'driver-1', name: 'Demo Driver 1' });
|
||||
mockDriverData.set('driver-2', { id: 'driver-2', name: 'Demo Driver 2' });
|
||||
|
||||
const mockRaceData: Map<string, RaceDto> = new Map();
|
||||
mockRaceData.set('race-1', { id: 'race-1', name: 'Test Race 1', date: new Date().toISOString() });
|
||||
mockRaceData.set('race-2', { id: 'race-2', name: 'Test Race 2', date: new Date().toISOString() });
|
||||
|
||||
@Injectable()
|
||||
export class LeagueService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
console.log('[LeagueService] Returning mock leagues with capacity.');
|
||||
return {
|
||||
leagues: [
|
||||
{ id: 'league-1', name: 'Global Racing', description: 'The premier league', ownerId: 'owner-1', settings: { maxDrivers: 100 }, createdAt: new Date().toISOString(), usedSlots: 50, socialLinks: { discordUrl: 'https://discord.gg/test' } },
|
||||
{ id: 'league-2', name: 'Amateur Series', description: 'Learn the ropes', ownerId: 'owner-2', settings: { maxDrivers: 50 }, createdAt: new Date().toISOString(), usedSlots: 20 },
|
||||
],
|
||||
totalCount: 2,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
console.log('[LeagueService] Returning mock total leagues.');
|
||||
return { totalLeagues: 2 };
|
||||
}
|
||||
|
||||
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
console.log(`[LeagueService] Returning mock join requests for league: ${leagueId}.`);
|
||||
return [
|
||||
{
|
||||
id: 'join-req-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: new Date(),
|
||||
message: 'I want to join!',
|
||||
driver: mockDriverData.get('driver-1'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
|
||||
console.log('Approving join request:', input);
|
||||
return { success: true, message: 'Join request approved.' };
|
||||
}
|
||||
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
|
||||
console.log('Rejecting join request:', input);
|
||||
return { success: true, message: 'Join request rejected.' };
|
||||
}
|
||||
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
|
||||
console.log('Getting league admin permissions:', query);
|
||||
return { canRemoveMember: true, canUpdateRoles: true };
|
||||
}
|
||||
|
||||
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
|
||||
console.log('Removing league member:', input.leagueId, input.targetDriverId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
console.log('Updating league member role:', input.leagueId, input.targetDriverId, input.newRole);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
console.log('Getting league owner summary:', query);
|
||||
return {
|
||||
driver: mockDriverData.get(query.ownerId)!,
|
||||
rating: 2000,
|
||||
rank: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
|
||||
console.log('Getting league full config:', query);
|
||||
return {
|
||||
leagueId: 'league-1',
|
||||
basics: { name: 'Demo League', description: 'A demo league', visibility: 'public' },
|
||||
structure: { mode: 'solo' },
|
||||
championships: [],
|
||||
scoring: { type: 'standard', points: 10 },
|
||||
dropPolicy: { strategy: 'none' },
|
||||
timings: { raceDayOfWeek: 'Sunday', raceTimeHour: 20, raceTimeMinute: 0 },
|
||||
stewarding: {
|
||||
decisionMode: 'single_steward',
|
||||
requireDefense: false,
|
||||
defenseTimeLimit: 24,
|
||||
voteTimeLimit: 24,
|
||||
protestDeadlineHours: 2,
|
||||
stewardingClosesHours: 24,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
|
||||
console.log('Getting league protests:', query);
|
||||
return {
|
||||
protests: [
|
||||
{ id: 'protest-1', raceId: 'race-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', submittedAt: new Date(), description: 'Bad driving!', status: 'pending' },
|
||||
],
|
||||
racesById: { 'race-1': mockRaceData.get('race-1')! },
|
||||
driversById: { 'driver-1': mockDriverData.get('driver-1')!, 'driver-2': mockDriverData.get('driver-2')! },
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
console.log('Getting league seasons:', query);
|
||||
return [
|
||||
{ seasonId: 'season-1', name: 'Season 1', status: 'active', startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), isPrimary: true, isParallelActive: false },
|
||||
{ seasonId: 'season-2', name: 'Season 2', status: 'upcoming', startDate: new Date('2026-01-01'), endDate: new Date('2026-12-31'), isPrimary: false, isParallelActive: false },
|
||||
];
|
||||
}
|
||||
}
|
||||
561
apps/api/src/modules/league/dto/LeagueDto.ts
Normal file
561
apps/api/src/modules/league/dto/LeagueDto.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional, IsEnum, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { DriverDto } from '../../driver/dto/DriverDto';
|
||||
import { RaceDto } from '../../race/dto/RaceDto';
|
||||
|
||||
|
||||
export class LeagueSettingsDto {
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
maxDrivers?: number;
|
||||
// Add other league settings properties as needed
|
||||
}
|
||||
|
||||
export class LeagueWithCapacityViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
// ... other properties of LeagueWithCapacityViewModel
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: () => LeagueSettingsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueSettingsDto)
|
||||
settings: LeagueSettingsDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
usedSlots: number;
|
||||
|
||||
@ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
|
||||
@IsOptional()
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AllLeaguesWithCapacityViewModel {
|
||||
@ApiProperty({ type: [LeagueWithCapacityViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueWithCapacityViewModel)
|
||||
leagues: LeagueWithCapacityViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class LeagueStatsDto {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalLeagues: number;
|
||||
}
|
||||
|
||||
export class ProtestDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
protestingDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
accusedDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
submittedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
|
||||
@IsEnum(['pending', 'accepted', 'rejected'])
|
||||
status: 'pending' | 'accepted' | 'rejected';
|
||||
}
|
||||
|
||||
export class SeasonDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty({ enum: ['planned', 'active', 'completed'] })
|
||||
@IsEnum(['planned', 'active', 'completed'])
|
||||
status: 'planned' | 'active' | 'completed';
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isPrimary: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonGroupId?: string;
|
||||
}
|
||||
|
||||
export class LeagueJoinRequestViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
requestedAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
message?: string;
|
||||
|
||||
@ApiProperty({ type: () => DriverDto, required: false })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driver?: DriverDto;
|
||||
}
|
||||
|
||||
export class GetLeagueJoinRequestsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class ApproveJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class ApproveJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class RejectJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class RejectJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminPermissionsInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
}
|
||||
|
||||
export class LeagueAdminPermissionsViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canRemoveMember: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canUpdateRoles: boolean;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
targetDriverId: string;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class UpdateLeagueMemberRoleInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
targetDriverId: string;
|
||||
|
||||
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
|
||||
@IsEnum(['owner', 'manager', 'member'])
|
||||
newRole: 'owner' | 'manager' | 'member';
|
||||
}
|
||||
|
||||
export class UpdateLeagueMemberRoleOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class GetLeagueOwnerSummaryQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueOwnerSummaryViewModel {
|
||||
@ApiProperty({ type: () => DriverDto })
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driver: DriverDto;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rating: number | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelBasicsDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ enum: ['public', 'private'] })
|
||||
@IsEnum(['public', 'private'])
|
||||
visibility: 'public' | 'private';
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelStructureDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsEnum(['solo', 'team'])
|
||||
mode: 'solo' | 'team';
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelScoringDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
points: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelDropPolicyDto {
|
||||
@ApiProperty({ enum: ['none', 'worst_n'] })
|
||||
@IsEnum(['none', 'worst_n'])
|
||||
strategy: 'none' | 'worst_n';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
n?: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelStewardingDto {
|
||||
@ApiProperty({ enum: ['single_steward', 'committee_vote'] })
|
||||
@IsEnum(['single_steward', 'committee_vote'])
|
||||
decisionMode: 'single_steward' | 'committee_vote';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
requiredVotes?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
requireDefense: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
defenseTimeLimit: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
voteTimeLimit: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
protestDeadlineHours: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
stewardingClosesHours: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
notifyAccusedOnProtest: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
notifyOnVoteRequired: boolean;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelTimingsDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceDayOfWeek: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
raceTimeHour: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
raceTimeMinute: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelBasicsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelBasicsDto)
|
||||
basics: LeagueConfigFormModelBasicsDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelStructureDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelStructureDto)
|
||||
structure: LeagueConfigFormModelStructureDto;
|
||||
|
||||
@ApiProperty({ type: [Object] })
|
||||
@IsArray()
|
||||
championships: any[];
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelScoringDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelScoringDto)
|
||||
scoring: LeagueConfigFormModelScoringDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelDropPolicyDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDropPolicyDto)
|
||||
dropPolicy: LeagueConfigFormModelDropPolicyDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelTimingsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelTimingsDto)
|
||||
timings: LeagueConfigFormModelTimingsDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelStewardingDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelStewardingDto)
|
||||
stewarding: LeagueConfigFormModelStewardingDto;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminConfigQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminConfigOutput {
|
||||
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDto)
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export class LeagueAdminConfigViewModel {
|
||||
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDto)
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export class GetLeagueProtestsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueAdminProtestsViewModel {
|
||||
@ApiProperty({ type: [ProtestDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProtestDto)
|
||||
protests: ProtestDto[];
|
||||
|
||||
@ApiProperty({ type: () => RaceDto })
|
||||
@ValidateNested()
|
||||
@Type(() => RaceDto)
|
||||
racesById: { [raceId: string]: RaceDto };
|
||||
|
||||
@ApiProperty({ type: () => DriverDto })
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driversById: { [driverId: string]: DriverDto };
|
||||
}
|
||||
|
||||
export class GetLeagueSeasonsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueSeasonSummaryViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
status: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isPrimary: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isParallelActive: boolean;
|
||||
}
|
||||
|
||||
export class LeagueAdminViewModel {
|
||||
@ApiProperty({ type: [LeagueJoinRequestViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueJoinRequestViewModel)
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
|
||||
@ApiProperty({ type: () => LeagueOwnerSummaryViewModel, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueOwnerSummaryViewModel)
|
||||
ownerSummary: LeagueOwnerSummaryViewModel | null;
|
||||
|
||||
@ApiProperty({ type: () => LeagueAdminConfigViewModel })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueAdminConfigViewModel)
|
||||
config: LeagueAdminConfigViewModel;
|
||||
|
||||
@ApiProperty({ type: () => LeagueAdminProtestsViewModel })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueAdminProtestsViewModel)
|
||||
protests: LeagueAdminProtestsViewModel;
|
||||
|
||||
@ApiProperty({ type: [LeagueSeasonSummaryViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueSeasonSummaryViewModel)
|
||||
seasons: LeagueSeasonSummaryViewModel[];
|
||||
}
|
||||
26
apps/api/src/modules/media/MediaController.ts
Normal file
26
apps/api/src/modules/media/MediaController.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller, Post, Body, HttpStatus, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { MediaService } from './MediaService';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
|
||||
@ApiTags('media')
|
||||
@Controller('media')
|
||||
export class MediaController {
|
||||
constructor(private readonly mediaService: MediaService) {}
|
||||
|
||||
@Post('avatar/generate')
|
||||
@ApiOperation({ summary: 'Request avatar generation' })
|
||||
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput })
|
||||
async requestAvatarGeneration(
|
||||
@Body() input: RequestAvatarGenerationInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const result = await this.mediaService.requestAvatarGeneration(input);
|
||||
if (result.success) {
|
||||
res.status(HttpStatus.CREATED).json(result);
|
||||
} else {
|
||||
res.status(HttpStatus.BAD_REQUEST).json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/media/MediaModule.ts
Normal file
10
apps/api/src/modules/media/MediaModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
import { MediaController } from './MediaController';
|
||||
|
||||
@Module({
|
||||
controllers: [MediaController],
|
||||
providers: [MediaService],
|
||||
exports: [MediaService],
|
||||
})
|
||||
export class MediaModule {}
|
||||
41
apps/api/src/modules/media/MediaProviders.ts
Normal file
41
apps/api/src/modules/media/MediaProviders.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
||||
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
||||
export const LOGGER_TOKEN = 'ILogger';
|
||||
|
||||
export const MediaProviders: Provider[] = [
|
||||
MediaService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryAvatarGenerationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: FACE_VALIDATION_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryFaceValidationAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
*/
|
||||
];
|
||||
20
apps/api/src/modules/media/MediaService.ts
Normal file
20
apps/api/src/modules/media/MediaService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
|
||||
console.log('[MediaService] Returning mock avatar generation request. Input:', input);
|
||||
return {
|
||||
success: true,
|
||||
requestId: `req-${Date.now()}`,
|
||||
avatarUrls: [
|
||||
'https://cdn.example.com/avatars/mock-avatar-1.png',
|
||||
'https://cdn.example.com/avatars/mock-avatar-2.png',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
40
apps/api/src/modules/media/dto/MediaDto.ts
Normal file
40
apps/api/src/modules/media/dto/MediaDto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsBoolean } from 'class-validator';
|
||||
|
||||
export class RequestAvatarGenerationInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
facePhotoData: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
suitColor: string;
|
||||
}
|
||||
|
||||
export class RequestAvatarGenerationOutput {
|
||||
@ApiProperty({ type: Boolean })
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
requestId?: string;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
avatarUrls?: string[];
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// Assuming FacePhotoData and SuitColor are simple string types for DTO purposes
|
||||
export type FacePhotoData = string;
|
||||
export type SuitColor = string;
|
||||
96
apps/api/src/modules/payments/PaymentsController.ts
Normal file
96
apps/api/src/modules/payments/PaymentsController.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Query, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, GetPaymentsQuery, GetPaymentsOutput, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput } from './dto/PaymentsDto';
|
||||
|
||||
@ApiTags('payments')
|
||||
@Controller('payments')
|
||||
export class PaymentsController {
|
||||
constructor(private readonly paymentsService: PaymentsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get payments based on filters' })
|
||||
@ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput })
|
||||
async getPayments(@Query() query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
return this.paymentsService.getPayments(query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new payment' })
|
||||
@ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput })
|
||||
async createPayment(@Body() input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
return this.paymentsService.createPayment(input);
|
||||
}
|
||||
|
||||
@Patch('status')
|
||||
@ApiOperation({ summary: 'Update the status of a payment' })
|
||||
@ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput })
|
||||
async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
return this.paymentsService.updatePaymentStatus(input);
|
||||
}
|
||||
|
||||
@Get('membership-fees')
|
||||
@ApiOperation({ summary: 'Get membership fees and member payments' })
|
||||
@ApiResponse({ status: 200, description: 'Membership fee configuration and member payments', type: GetMembershipFeesOutput })
|
||||
async getMembershipFees(@Query() query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
return this.paymentsService.getMembershipFees(query);
|
||||
}
|
||||
|
||||
@Post('membership-fees')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create or update membership fee configuration' })
|
||||
@ApiResponse({ status: 201, description: 'Membership fee configuration created or updated', type: UpsertMembershipFeeOutput })
|
||||
async upsertMembershipFee(@Body() input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
return this.paymentsService.upsertMembershipFee(input);
|
||||
}
|
||||
|
||||
@Patch('membership-fees/member-payment')
|
||||
@ApiOperation({ summary: 'Record or update a member payment' })
|
||||
@ApiResponse({ status: 200, description: 'Member payment recorded or updated', type: UpdateMemberPaymentOutput })
|
||||
async updateMemberPayment(@Body() input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
return this.paymentsService.updateMemberPayment(input);
|
||||
}
|
||||
@Get('prizes')
|
||||
@ApiOperation({ summary: 'Get prizes for a league or season' })
|
||||
@ApiResponse({ status: 200, description: 'List of prizes', type: GetPrizesOutput })
|
||||
async getPrizes(@Query() query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
return this.paymentsService.getPrizes(query);
|
||||
}
|
||||
|
||||
@Post('prizes')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new prize' })
|
||||
@ApiResponse({ status: 201, description: 'Prize created', type: CreatePrizeOutput })
|
||||
async createPrize(@Body() input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
return this.paymentsService.createPrize(input);
|
||||
}
|
||||
|
||||
@Patch('prizes/award')
|
||||
@ApiOperation({ summary: 'Award a prize to a driver' })
|
||||
@ApiResponse({ status: 200, description: 'Prize awarded', type: AwardPrizeOutput })
|
||||
async awardPrize(@Body() input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
return this.paymentsService.awardPrize(input);
|
||||
}
|
||||
|
||||
@Delete('prizes')
|
||||
@ApiOperation({ summary: 'Delete a prize' })
|
||||
@ApiResponse({ status: 200, description: 'Prize deleted', type: DeletePrizeOutput })
|
||||
async deletePrize(@Query() query: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
return this.paymentsService.deletePrize(query);
|
||||
}
|
||||
@Get('wallets')
|
||||
@ApiOperation({ summary: 'Get wallet information and transactions' })
|
||||
@ApiResponse({ status: 200, description: 'Wallet and transaction data', type: GetWalletOutput })
|
||||
async getWallet(@Query() query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
return this.paymentsService.getWallet(query);
|
||||
}
|
||||
|
||||
@Post('wallets/transactions')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Process a wallet transaction (deposit or withdrawal)' })
|
||||
@ApiResponse({ status: 201, description: 'Wallet transaction processed', type: ProcessWalletTransactionOutput })
|
||||
async processWalletTransaction(@Body() input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
return this.paymentsService.processWalletTransaction(input);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/payments/PaymentsModule.ts
Normal file
10
apps/api/src/modules/payments/PaymentsModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { PaymentsController } from './PaymentsController';
|
||||
|
||||
@Module({
|
||||
controllers: [PaymentsController],
|
||||
providers: [PaymentsService],
|
||||
exports: [PaymentsService],
|
||||
})
|
||||
export class PaymentsModule {}
|
||||
67
apps/api/src/modules/payments/PaymentsProviders.ts
Normal file
67
apps/api/src/modules/payments/PaymentsProviders.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
// Import core interfaces
|
||||
import { IPaymentRepository } from 'core/payments/domain/repositories/IPaymentRepository';
|
||||
import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMembershipFeeRepository';
|
||||
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
|
||||
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
|
||||
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { InMemoryMembershipFeeRepository } from 'adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryPrizeRepository } from 'adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||
import { InMemoryWalletRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { InMemoryPaymentGateway } from 'adapters/payments/ports/InMemoryPaymentGateway';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
|
||||
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
|
||||
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
|
||||
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
|
||||
export const PAYMENT_GATEWAY_TOKEN = 'IPaymentGateway';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in other Providers, but good to have here too
|
||||
|
||||
export const PaymentsProviders: Provider[] = [
|
||||
PaymentsService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPaymentRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: MEMBERSHIP_FEE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryMembershipFeeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PRIZE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPrizeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: WALLET_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryWalletRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_GATEWAY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPaymentGateway(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
*/
|
||||
];
|
||||
346
apps/api/src/modules/payments/PaymentsService.ts
Normal file
346
apps/api/src/modules/payments/PaymentsService.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, PaymentDto, GetPaymentsQuery, GetPaymentsOutput, PaymentStatus, MembershipFeeDto, MemberPaymentDto, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, MembershipFeeType, MemberPaymentStatus, PrizeDto, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, PrizeType, WalletDto, TransactionDto, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput, TransactionType, ReferenceType } from './dto/PaymentsDto';
|
||||
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
|
||||
|
||||
const payments: Map<string, PaymentDto> = new Map();
|
||||
const membershipFees: Map<string, MembershipFeeDto> = new Map();
|
||||
const memberPayments: Map<string, MemberPaymentDto> = new Map();
|
||||
const prizes: Map<string, PrizeDto> = new Map();
|
||||
const wallets: Map<string, WalletDto> = new Map();
|
||||
const transactions: Map<string, TransactionDto> = new Map();
|
||||
|
||||
const PLATFORM_FEE_RATE = 0.10;
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
let results = Array.from(payments.values());
|
||||
|
||||
if (query.leagueId) {
|
||||
results = results.filter(p => p.leagueId === query.leagueId);
|
||||
}
|
||||
if (query.payerId) {
|
||||
results = results.filter(p => p.payerId === query.payerId);
|
||||
}
|
||||
if (query.type) {
|
||||
results = results.filter(p => p.type === query.type);
|
||||
}
|
||||
|
||||
return { payments: results };
|
||||
}
|
||||
|
||||
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
|
||||
|
||||
const platformFee = amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = amount - platformFee;
|
||||
|
||||
const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const payment: PaymentDto = {
|
||||
id,
|
||||
type,
|
||||
amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
payerId,
|
||||
payerType,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
payments.set(id, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
const { paymentId, status } = input;
|
||||
|
||||
const payment = payments.get(paymentId);
|
||||
if (!payment) {
|
||||
throw new Error('Payment not found');
|
||||
}
|
||||
|
||||
payment.status = status;
|
||||
if (status === PaymentStatus.COMPLETED) {
|
||||
payment.completedAt = new Date();
|
||||
}
|
||||
|
||||
payments.set(paymentId, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
const { leagueId, driverId } = query;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('leagueId is required');
|
||||
}
|
||||
|
||||
const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
|
||||
|
||||
let payments: MemberPaymentDto[] = [];
|
||||
if (driverId) {
|
||||
payments = Array.from(memberPayments.values()).filter(
|
||||
p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId
|
||||
);
|
||||
}
|
||||
|
||||
return { fee, payments };
|
||||
}
|
||||
|
||||
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
const { leagueId, seasonId, type, amount } = input;
|
||||
|
||||
// Check for existing fee config
|
||||
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
|
||||
|
||||
if (existingFee) {
|
||||
// Update existing fee
|
||||
existingFee.type = type;
|
||||
existingFee.amount = amount;
|
||||
existingFee.seasonId = seasonId || existingFee.seasonId;
|
||||
existingFee.enabled = amount > 0;
|
||||
existingFee.updatedAt = new Date();
|
||||
membershipFees.set(existingFee.id, existingFee);
|
||||
return { fee: existingFee };
|
||||
}
|
||||
|
||||
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const fee: MembershipFeeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
type,
|
||||
amount,
|
||||
enabled: amount > 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
membershipFees.set(id, fee);
|
||||
|
||||
return { fee };
|
||||
}
|
||||
|
||||
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
const { feeId, driverId, status, paidAt } = input;
|
||||
|
||||
const fee = membershipFees.get(feeId);
|
||||
if (!fee) {
|
||||
throw new Error('Membership fee configuration not found');
|
||||
}
|
||||
|
||||
// Find or create payment record
|
||||
let payment = Array.from(memberPayments.values()).find(
|
||||
p => p.feeId === feeId && p.driverId === driverId
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
const platformFee = fee.amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = fee.amount - platformFee;
|
||||
|
||||
const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
payment = {
|
||||
id: paymentId,
|
||||
feeId,
|
||||
driverId,
|
||||
amount: fee.amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
status: MemberPaymentStatus.PENDING,
|
||||
dueDate: new Date(),
|
||||
};
|
||||
memberPayments.set(paymentId, payment);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
payment.status = status;
|
||||
}
|
||||
if (paidAt || status === MemberPaymentStatus.PAID) {
|
||||
payment.paidAt = paidAt ? new Date(paidAt) : new Date();
|
||||
}
|
||||
|
||||
memberPayments.set(payment.id, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
const { leagueId, seasonId } = query;
|
||||
|
||||
let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
|
||||
|
||||
if (seasonId) {
|
||||
results = results.filter(p => p.seasonId === seasonId);
|
||||
}
|
||||
|
||||
results.sort((a, b) => a.position - b.position);
|
||||
|
||||
return { prizes: results };
|
||||
}
|
||||
|
||||
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
const { leagueId, seasonId, position, name, amount, type, description } = input;
|
||||
|
||||
// Check for duplicate position
|
||||
const existingPrize = Array.from(prizes.values()).find(
|
||||
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
|
||||
);
|
||||
|
||||
if (existingPrize) {
|
||||
throw new Error(`Prize for position ${position} already exists`);
|
||||
}
|
||||
|
||||
const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const prize: PrizeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId,
|
||||
position,
|
||||
name,
|
||||
amount,
|
||||
type,
|
||||
description: description || undefined,
|
||||
awarded: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
prizes.set(id, prize);
|
||||
|
||||
return { prize };
|
||||
}
|
||||
|
||||
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
const { prizeId, driverId } = input;
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Prize has already been awarded');
|
||||
}
|
||||
|
||||
prize.awarded = true;
|
||||
prize.awardedTo = driverId;
|
||||
prize.awardedAt = new Date();
|
||||
|
||||
prizes.set(prizeId, prize);
|
||||
|
||||
return { prize };
|
||||
}
|
||||
|
||||
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
const { prizeId } = input;
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Cannot delete an awarded prize');
|
||||
}
|
||||
|
||||
prizes.delete(prizeId);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
const { leagueId } = query;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('LeagueId is required');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
const walletTransactions = Array.from(transactions.values())
|
||||
.filter(t => t.walletId === wallet!.id)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return { wallet, transactions: walletTransactions };
|
||||
}
|
||||
|
||||
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
const { leagueId, type, amount, description, referenceId, referenceType } = input;
|
||||
|
||||
if (!leagueId || !type || amount === undefined || !description) {
|
||||
throw new Error('Missing required fields: leagueId, type, amount, description');
|
||||
}
|
||||
|
||||
if (type !== TransactionType.DEPOSIT && type !== TransactionType.WITHDRAWAL) {
|
||||
throw new Error('Type must be "deposit" or "withdrawal"');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
if (type === TransactionType.WITHDRAWAL) {
|
||||
if (amount > wallet.balance) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
}
|
||||
|
||||
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const transaction: TransactionDto = {
|
||||
id: transactionId,
|
||||
walletId: wallet.id,
|
||||
type,
|
||||
amount,
|
||||
description,
|
||||
referenceId: referenceId || undefined,
|
||||
referenceType: referenceType || undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
transactions.set(transactionId, transaction);
|
||||
|
||||
if (type === TransactionType.DEPOSIT) {
|
||||
wallet.balance += amount;
|
||||
wallet.totalRevenue += amount;
|
||||
} else {
|
||||
wallet.balance -= amount;
|
||||
wallet.totalWithdrawn += amount;
|
||||
}
|
||||
|
||||
wallets.set(wallet.id, wallet);
|
||||
|
||||
return { wallet, transaction };
|
||||
}
|
||||
}
|
||||
566
apps/api/src/modules/payments/dto/PaymentsDto.ts
Normal file
566
apps/api/src/modules/payments/dto/PaymentsDto.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean } from 'class-validator';
|
||||
|
||||
export enum PaymentType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
}
|
||||
|
||||
export enum PayerType {
|
||||
SPONSOR = 'sponsor',
|
||||
DRIVER = 'driver',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'pending',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
|
||||
export class PaymentDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentType })
|
||||
@IsEnum(PaymentType)
|
||||
type: PaymentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
platformFee: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
netAmount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
payerId: string;
|
||||
|
||||
@ApiProperty({ enum: PayerType })
|
||||
@IsEnum(PayerType)
|
||||
payerType: PayerType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentStatus })
|
||||
@IsEnum(PaymentStatus)
|
||||
status: PaymentStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
export class CreatePaymentInput {
|
||||
@ApiProperty({ enum: PaymentType })
|
||||
@IsEnum(PaymentType)
|
||||
type: PaymentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
payerId: string;
|
||||
|
||||
@ApiProperty({ enum: PayerType })
|
||||
@IsEnum(PayerType)
|
||||
payerType: PayerType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class CreatePaymentOutput {
|
||||
@ApiProperty({ type: PaymentDto })
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
paymentId: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentStatus })
|
||||
@IsEnum(PaymentStatus)
|
||||
status: PaymentStatus;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusOutput {
|
||||
@ApiProperty({ type: PaymentDto })
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export enum MembershipFeeType {
|
||||
SEASON = 'season',
|
||||
MONTHLY = 'monthly',
|
||||
PER_RACE = 'per_race',
|
||||
}
|
||||
|
||||
export enum MemberPaymentStatus {
|
||||
PENDING = 'pending',
|
||||
PAID = 'paid',
|
||||
OVERDUE = 'overdue',
|
||||
}
|
||||
|
||||
export class MembershipFeeDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: MembershipFeeType })
|
||||
@IsEnum(MembershipFeeType)
|
||||
type: MembershipFeeType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
enabled: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export class MemberPaymentDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
feeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
platformFee: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
netAmount: number;
|
||||
|
||||
@ApiProperty({ enum: MemberPaymentStatus })
|
||||
@IsEnum(MemberPaymentStatus)
|
||||
status: MemberPaymentStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
dueDate: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
paidAt?: Date;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesOutput {
|
||||
@ApiProperty({ type: MembershipFeeDto, nullable: true })
|
||||
fee: MembershipFeeDto | null;
|
||||
|
||||
@ApiProperty({ type: [MemberPaymentDto] })
|
||||
payments: MemberPaymentDto[];
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: MembershipFeeType })
|
||||
@IsEnum(MembershipFeeType)
|
||||
type: MembershipFeeType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeOutput {
|
||||
@ApiProperty({ type: MembershipFeeDto })
|
||||
fee: MembershipFeeDto;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
feeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: MemberPaymentStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(MemberPaymentStatus)
|
||||
status?: MemberPaymentStatus;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
paidAt?: Date | string;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentOutput {
|
||||
@ApiProperty({ type: MemberPaymentDto })
|
||||
payment: MemberPaymentDto;
|
||||
}
|
||||
|
||||
export class GetPaymentsQuery {
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
payerId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: PaymentType })
|
||||
@IsOptional()
|
||||
@IsEnum(PaymentType)
|
||||
type?: PaymentType;
|
||||
}
|
||||
|
||||
export class GetPaymentsOutput {
|
||||
@ApiProperty({ type: [PaymentDto] })
|
||||
payments: PaymentDto[];
|
||||
}
|
||||
|
||||
export enum PrizeType {
|
||||
CASH = 'cash',
|
||||
MERCHANDISE = 'merchandise',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export class PrizeDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
position: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ enum: PrizeType })
|
||||
@IsEnum(PrizeType)
|
||||
type: PrizeType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
awarded: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awardedTo?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
awardedAt?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetPrizesQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class GetPrizesOutput {
|
||||
@ApiProperty({ type: [PrizeDto] })
|
||||
prizes: PrizeDto[];
|
||||
}
|
||||
|
||||
export class CreatePrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
position: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ enum: PrizeType })
|
||||
@IsEnum(PrizeType)
|
||||
type: PrizeType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreatePrizeOutput {
|
||||
@ApiProperty({ type: PrizeDto })
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export class AwardPrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
prizeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class AwardPrizeOutput {
|
||||
@ApiProperty({ type: PrizeDto })
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export class DeletePrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
prizeId: string;
|
||||
}
|
||||
|
||||
export class DeletePrizeOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export enum TransactionType {
|
||||
DEPOSIT = 'deposit',
|
||||
WITHDRAWAL = 'withdrawal',
|
||||
PLATFORM_FEE = 'platform_fee',
|
||||
}
|
||||
|
||||
export enum ReferenceType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
PRIZE = 'prize',
|
||||
}
|
||||
|
||||
export class WalletDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
balance: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalRevenue: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalPlatformFees: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalWithdrawn: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export class TransactionDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
walletId: string;
|
||||
|
||||
@ApiProperty({ enum: TransactionType })
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ReferenceType })
|
||||
@IsOptional()
|
||||
@IsEnum(ReferenceType)
|
||||
referenceType?: ReferenceType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetWalletQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
}
|
||||
|
||||
export class GetWalletOutput {
|
||||
@ApiProperty({ type: WalletDto })
|
||||
wallet: WalletDto;
|
||||
|
||||
@ApiProperty({ type: [TransactionDto] })
|
||||
transactions: TransactionDto[];
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ enum: TransactionType })
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ReferenceType })
|
||||
@IsOptional()
|
||||
@IsEnum(ReferenceType)
|
||||
referenceType?: ReferenceType;
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionOutput {
|
||||
@ApiProperty({ type: WalletDto })
|
||||
wallet: WalletDto;
|
||||
|
||||
@ApiProperty({ type: TransactionDto })
|
||||
transaction: TransactionDto;
|
||||
}
|
||||
|
||||
26
apps/api/src/modules/race/RaceController.ts
Normal file
26
apps/api/src/modules/race/RaceController.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { RaceService } from './RaceService';
|
||||
import { AllRacesPageViewModel, RaceStatsDto } from './dto/RaceDto';
|
||||
|
||||
@ApiTags('races')
|
||||
@Controller('races')
|
||||
export class RaceController {
|
||||
constructor(private readonly raceService: RaceService) {}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all races' })
|
||||
@ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageViewModel })
|
||||
async getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
return this.raceService.getAllRaces();
|
||||
}
|
||||
|
||||
@Get('total-races')
|
||||
@ApiOperation({ summary: 'Get the total number of races' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDto })
|
||||
async getTotalRaces(): Promise<RaceStatsDto> {
|
||||
return this.raceService.getTotalRaces();
|
||||
}
|
||||
|
||||
// Add other Race endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/race/RaceModule.ts
Normal file
10
apps/api/src/modules/race/RaceModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RaceController } from './RaceController';
|
||||
|
||||
@Module({
|
||||
controllers: [RaceController],
|
||||
providers: [RaceService],
|
||||
exports: [RaceService],
|
||||
})
|
||||
export class RaceModule {}
|
||||
18
apps/api/src/modules/race/RaceProviders.ts
Normal file
18
apps/api/src/modules/race/RaceProviders.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
|
||||
export const RaceProviders: Provider[] = [
|
||||
RaceService,
|
||||
// In a functional setup, other providers would be here, e.g.:
|
||||
/*
|
||||
{
|
||||
provide: 'ILogger',
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: 'IRaceRepository',
|
||||
useClass: InMemoryRaceRepository,
|
||||
},
|
||||
// ... other providers
|
||||
*/
|
||||
];
|
||||
37
apps/api/src/modules/race/RaceService.ts
Normal file
37
apps/api/src/modules/race/RaceService.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllRacesPageViewModel, RaceStatsDto, ImportRaceResultsInput, ImportRaceResultsSummaryViewModel } from './dto/RaceDto';
|
||||
|
||||
@Injectable()
|
||||
export class RaceService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
console.log('[RaceService] Returning mock all races.');
|
||||
return Promise.resolve({
|
||||
races: [
|
||||
{ id: 'race-1', name: 'Global Race 1', date: new Date().toISOString(), leagueName: 'Global Racing' },
|
||||
{ id: 'race-2', name: 'Amateur Race 1', date: new Date().toISOString(), leagueName: 'Amateur Series' },
|
||||
],
|
||||
totalCount: 2,
|
||||
});
|
||||
}
|
||||
|
||||
getTotalRaces(): Promise<RaceStatsDto> {
|
||||
console.log('[RaceService] Returning mock total races.');
|
||||
return Promise.resolve({
|
||||
totalRaces: 2, // Placeholder
|
||||
});
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
console.log('Importing race results:', input);
|
||||
return {
|
||||
success: true,
|
||||
raceId: input.raceId,
|
||||
driversProcessed: 10, // Mock data
|
||||
resultsRecorded: 10, // Mock data
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
78
apps/api/src/modules/race/dto/RaceDto.ts
Normal file
78
apps/api/src/modules/race/dto/RaceDto.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsBoolean, IsNumber } from 'class-validator';
|
||||
|
||||
export class RaceViewModel {
|
||||
@ApiProperty()
|
||||
id: string; // Assuming a race has an ID
|
||||
|
||||
@ApiProperty()
|
||||
name: string; // Assuming a race has a name
|
||||
|
||||
@ApiProperty()
|
||||
date: string; // Assuming a race has a date
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
leagueName?: string; // Assuming a race might belong to a league
|
||||
|
||||
// Add more race-related properties as needed based on the DTO from the application layer
|
||||
}
|
||||
|
||||
export class AllRacesPageViewModel {
|
||||
@ApiProperty({ type: [RaceViewModel] })
|
||||
races: RaceViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class RaceStatsDto {
|
||||
@ApiProperty()
|
||||
totalRaces: number;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
resultsFileContent: string;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsSummaryViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
driversProcessed: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
resultsRecorded: number;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export class RaceDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
date: string;
|
||||
}
|
||||
48
apps/api/src/modules/sponsor/SponsorController.ts
Normal file
48
apps/api/src/modules/sponsor/SponsorController.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Controller, Get, Post, Body, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO } from './dto/SponsorDto';
|
||||
|
||||
@ApiTags('sponsors')
|
||||
@Controller('sponsors')
|
||||
export class SponsorController {
|
||||
constructor(private readonly sponsorService: SponsorService) {}
|
||||
|
||||
@Get('pricing')
|
||||
@ApiOperation({ summary: 'Get sponsorship pricing for an entity' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship pricing', type: GetEntitySponsorshipPricingResultDto })
|
||||
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
return this.sponsorService.getEntitySponsorshipPricing();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all sponsors' })
|
||||
@ApiResponse({ status: 200, description: 'List of sponsors', type: GetSponsorsOutput })
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return this.sponsorService.getSponsors();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new sponsor' })
|
||||
@ApiResponse({ status: 201, description: 'Sponsor created', type: CreateSponsorOutput })
|
||||
async createSponsor(@Body() input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
return this.sponsorService.createSponsor(input);
|
||||
}
|
||||
|
||||
// Add other Sponsor endpoints here based on other presenters
|
||||
@Get('dashboard/:sponsorId')
|
||||
@ApiOperation({ summary: 'Get sponsor dashboard metrics and sponsored leagues' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsor dashboard data', type: SponsorDashboardDTO })
|
||||
@ApiResponse({ status: 404, description: 'Sponsor not found' })
|
||||
async getSponsorDashboard(@Param('sponsorId') sponsorId: string): Promise<SponsorDashboardDTO | null> {
|
||||
return this.sponsorService.getSponsorDashboard({ sponsorId });
|
||||
}
|
||||
@Get(':sponsorId/sponsorships')
|
||||
@ApiOperation({ summary: 'Get all sponsorships for a given sponsor' })
|
||||
@ApiResponse({ status: 200, description: 'List of sponsorships', type: SponsorSponsorshipsDTO })
|
||||
@ApiResponse({ status: 404, description: 'Sponsor not found' })
|
||||
async getSponsorSponsorships(@Param('sponsorId') sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
|
||||
return this.sponsorService.getSponsorSponsorships({ sponsorId });
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/sponsor/SponsorModule.ts
Normal file
10
apps/api/src/modules/sponsor/SponsorModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { SponsorController } from './SponsorController';
|
||||
|
||||
@Module({
|
||||
controllers: [SponsorController],
|
||||
providers: [SponsorService],
|
||||
exports: [SponsorService],
|
||||
})
|
||||
export class SponsorModule {}
|
||||
5
apps/api/src/modules/sponsor/SponsorProviders.ts
Normal file
5
apps/api/src/modules/sponsor/SponsorProviders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SponsorService } from './SponsorService';
|
||||
|
||||
export const SponsorProviders = [
|
||||
SponsorService,
|
||||
];
|
||||
162
apps/api/src/modules/sponsor/SponsorService.ts
Normal file
162
apps/api/src/modules/sponsor/SponsorService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetEntitySponsorshipPricingResultDto, SponsorDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorshipDetailDTO, SponsorSponsorshipsDTO, SponsoredLeagueDTO, SponsorDashboardMetricsDTO, SponsorDashboardInvestmentDTO } from './dto/SponsorDto';
|
||||
|
||||
const sponsors: Map<string, SponsorDto> = new Map();
|
||||
|
||||
@Injectable()
|
||||
export class SponsorService {
|
||||
|
||||
constructor() {
|
||||
// Seed some demo sponsors for dashboard if empty
|
||||
if (sponsors.size === 0) {
|
||||
const demoSponsor1: SponsorDto = {
|
||||
id: 'sponsor-demo-1',
|
||||
name: 'Demo Sponsor Co.',
|
||||
contactEmail: 'contact@demosponsor.com',
|
||||
websiteUrl: 'https://demosponsor.com',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/aaaaaa/ffffff?text=DemoCo',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const demoSponsor2: SponsorDto = {
|
||||
id: 'sponsor-demo-2',
|
||||
name: 'Second Brand',
|
||||
contactEmail: 'info@secondbrand.net',
|
||||
websiteUrl: 'https://secondbrand.net',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/cccccc/ffffff?text=Brand2',
|
||||
createdAt: new Date(Date.now() - 86400000 * 5),
|
||||
};
|
||||
sponsors.set(demoSponsor1.id, demoSponsor1);
|
||||
sponsors.set(demoSponsor2.id, demoSponsor2);
|
||||
}
|
||||
}
|
||||
|
||||
getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
// This logic relies on external factors (e.g., pricing configuration, entity type)
|
||||
// For now, return mock data
|
||||
return Promise.resolve({
|
||||
pricing: [
|
||||
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
|
||||
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
|
||||
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return { sponsors: Array.from(sponsors.values()) };
|
||||
}
|
||||
|
||||
async createSponsor(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newSponsor: SponsorDto = {
|
||||
id,
|
||||
name: input.name,
|
||||
contactEmail: input.contactEmail,
|
||||
websiteUrl: input.websiteUrl,
|
||||
logoUrl: input.logoUrl,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
sponsors.set(id, newSponsor);
|
||||
return { sponsor: newSponsor };
|
||||
}
|
||||
|
||||
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simplified mock data for dashboard metrics and sponsored leagues
|
||||
const metrics: SponsorDashboardMetricsDTO = {
|
||||
impressions: 10000,
|
||||
impressionsChange: 12.5,
|
||||
uniqueViewers: 7000,
|
||||
viewersChange: 8.3,
|
||||
races: 50,
|
||||
drivers: 100,
|
||||
exposure: 75,
|
||||
exposureChange: 5.2,
|
||||
};
|
||||
|
||||
const sponsoredLeagues: SponsoredLeagueDTO[] = [
|
||||
{ id: 'league-1', name: 'League 1', tier: 'main', drivers: 50, races: 10, impressions: 5000, status: 'active' },
|
||||
{ id: 'league-2', name: 'League 2', tier: 'secondary', drivers: 30, races: 5, impressions: 1500, status: 'upcoming' },
|
||||
];
|
||||
|
||||
const investment: SponsorDashboardInvestmentDTO = {
|
||||
activeSponsorships: 2,
|
||||
totalInvestment: 5000,
|
||||
costPerThousandViews: 0.5,
|
||||
};
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics,
|
||||
sponsoredLeagues,
|
||||
investment,
|
||||
};
|
||||
}
|
||||
|
||||
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const sponsorshipDetails: SponsorshipDetailDTO[] = [
|
||||
{
|
||||
id: 'sponsorship-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
seasonId: 'season-1',
|
||||
seasonName: 'Season 1',
|
||||
seasonStartDate: new Date('2025-01-01'),
|
||||
seasonEndDate: new Date('2025-12-31'),
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
pricing: { amount: 1000, currency: 'USD' },
|
||||
platformFee: { amount: 100, currency: 'USD' },
|
||||
netAmount: { amount: 900, currency: 'USD' },
|
||||
metrics: { drivers: 50, races: 10, completedRaces: 8, impressions: 5000 },
|
||||
createdAt: new Date('2024-12-01'),
|
||||
activatedAt: new Date('2025-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'sponsorship-2',
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'League 2',
|
||||
seasonId: 'season-2',
|
||||
seasonName: 'Season 2',
|
||||
tier: 'secondary',
|
||||
status: 'pending',
|
||||
pricing: { amount: 500, currency: 'USD' },
|
||||
platformFee: { amount: 50, currency: 'USD' },
|
||||
netAmount: { amount: 450, currency: 'USD' },
|
||||
metrics: { drivers: 30, races: 5, completedRaces: 0, impressions: 0 },
|
||||
createdAt: new Date('2025-03-15'),
|
||||
},
|
||||
];
|
||||
|
||||
const totalInvestment = sponsorshipDetails.reduce((sum, s) => sum + s.pricing.amount, 0);
|
||||
const totalPlatformFees = sponsorshipDetails.reduce((sum, s) => sum + s.platformFee.amount, 0);
|
||||
const activeSponsorships = sponsorshipDetails.filter(s => s.status === 'active').length;
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
summary: {
|
||||
totalSponsorships: sponsorshipDetails.length,
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
299
apps/api/src/modules/sponsor/dto/SponsorDto.ts
Normal file
299
apps/api/src/modules/sponsor/dto/SponsorDto.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean, IsUrl, IsEmail } from 'class-validator';
|
||||
|
||||
export class SponsorshipPricingItemDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
level: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
price: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingResultDto {
|
||||
@ApiProperty({ type: [SponsorshipPricingItemDto] })
|
||||
pricing: SponsorshipPricingItemDto[];
|
||||
}
|
||||
|
||||
export class SponsorDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetSponsorsOutput {
|
||||
@ApiProperty({ type: [SponsorDto] })
|
||||
sponsors: SponsorDto[];
|
||||
}
|
||||
|
||||
export class CreateSponsorInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class CreateSponsorOutput {
|
||||
@ApiProperty({ type: SponsorDto })
|
||||
sponsor: SponsorDto;
|
||||
}
|
||||
|
||||
export class GetSponsorDashboardQueryParams {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export class SponsoredLeagueDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ enum: ['main', 'secondary'] })
|
||||
@IsEnum(['main', 'secondary'])
|
||||
tier: 'main' | 'secondary';
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
drivers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
races: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressions: number;
|
||||
|
||||
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
|
||||
@IsEnum(['active', 'upcoming', 'completed'])
|
||||
status: 'active' | 'upcoming' | 'completed';
|
||||
}
|
||||
|
||||
export class SponsorDashboardMetricsDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressions: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressionsChange: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
uniqueViewers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
viewersChange: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
races: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
drivers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
exposure: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
exposureChange: number;
|
||||
}
|
||||
|
||||
export class SponsorDashboardInvestmentDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
activeSponsorships: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalInvestment: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
costPerThousandViews: number;
|
||||
}
|
||||
|
||||
export class SponsorDashboardDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorName: string;
|
||||
|
||||
@ApiProperty({ type: SponsorDashboardMetricsDTO })
|
||||
metrics: SponsorDashboardMetricsDTO;
|
||||
|
||||
@ApiProperty({ type: [SponsoredLeagueDTO] })
|
||||
sponsoredLeagues: SponsoredLeagueDTO[];
|
||||
|
||||
@ApiProperty({ type: SponsorDashboardInvestmentDTO })
|
||||
investment: SponsorDashboardInvestmentDTO;
|
||||
}
|
||||
|
||||
export class GetSponsorSponsorshipsQueryParams {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export class SponsorshipDetailDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonName: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
seasonStartDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
seasonEndDate?: Date;
|
||||
|
||||
@ApiProperty({ enum: ['main', 'secondary'] })
|
||||
@IsEnum(['main', 'secondary'])
|
||||
tier: 'main' | 'secondary';
|
||||
|
||||
@ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
|
||||
@IsEnum(['pending', 'active', 'expired', 'cancelled'])
|
||||
status: 'pending' | 'active' | 'expired' | 'cancelled';
|
||||
|
||||
@ApiProperty()
|
||||
pricing: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
platformFee: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
netAmount: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
metrics: {
|
||||
drivers: number;
|
||||
races: number;
|
||||
completedRaces: number;
|
||||
impressions: number;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
activatedAt?: Date;
|
||||
}
|
||||
|
||||
export class SponsorSponsorshipsDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorName: string;
|
||||
|
||||
@ApiProperty({ type: [SponsorshipDetailDTO] })
|
||||
sponsorships: SponsorshipDetailDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
summary: {
|
||||
totalSponsorships: number;
|
||||
activeSponsorships: number;
|
||||
totalInvestment: number;
|
||||
totalPlatformFees: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Add other DTOs for sponsor-related logic as needed
|
||||
19
apps/api/src/modules/team/TeamController.ts
Normal file
19
apps/api/src/modules/team/TeamController.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { TeamService } from './TeamService';
|
||||
import { AllTeamsViewModel } from './dto/TeamDto';
|
||||
|
||||
@ApiTags('teams')
|
||||
@Controller('teams')
|
||||
export class TeamController {
|
||||
constructor(private readonly teamService: TeamService) {}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all teams' })
|
||||
@ApiResponse({ status: 200, description: 'List of all teams', type: AllTeamsViewModel })
|
||||
async getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
return this.teamService.getAllTeams();
|
||||
}
|
||||
|
||||
// Add other Team endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/team/TeamModule.ts
Normal file
10
apps/api/src/modules/team/TeamModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamService } from './TeamService';
|
||||
import { TeamController } from './TeamController';
|
||||
|
||||
@Module({
|
||||
controllers: [TeamController],
|
||||
providers: [TeamService],
|
||||
exports: [TeamService],
|
||||
})
|
||||
export class TeamModule {}
|
||||
5
apps/api/src/modules/team/TeamProviders.ts
Normal file
5
apps/api/src/modules/team/TeamProviders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TeamService } from './TeamService';
|
||||
|
||||
export const TeamProviders = [
|
||||
TeamService,
|
||||
];
|
||||
43
apps/api/src/modules/team/TeamService.ts
Normal file
43
apps/api/src/modules/team/TeamService.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService {
|
||||
getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
// TODO: Implement actual logic to fetch all teams
|
||||
return Promise.resolve({
|
||||
teams: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private teams: Map<string, TeamDto> = new Map(); // In-memory store for teams
|
||||
|
||||
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
|
||||
const { teamId, driverId } = query;
|
||||
|
||||
const team = this.teams.get(teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mock membership and roles
|
||||
const membership: MembershipDto = {
|
||||
role: driverId === team.ownerId ? MembershipRole.OWNER : MembershipRole.MEMBER,
|
||||
joinedAt: new Date(Date.now() - 86400000 * 30), // Joined 30 days ago
|
||||
isActive: true, // Always active for mock
|
||||
};
|
||||
|
||||
const isOwner = team.ownerId === driverId;
|
||||
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
|
||||
|
||||
return {
|
||||
team: team,
|
||||
membership,
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
// Add other methods related to Team logic here based on other presenters
|
||||
}
|
||||
121
apps/api/src/modules/team/dto/TeamDto.ts
Normal file
121
apps/api/src/modules/team/dto/TeamDto.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class TeamLeagueDto {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class TeamListItemViewModel {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
tag?: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
memberCount: number;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export class AllTeamsViewModel {
|
||||
@ApiProperty({ type: [TeamListItemViewModel] })
|
||||
teams: TeamListItemViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator';
|
||||
|
||||
export class TeamDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
tag: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export enum MembershipRole {
|
||||
OWNER = 'owner',
|
||||
MANAGER = 'manager',
|
||||
MEMBER = 'member',
|
||||
}
|
||||
|
||||
export enum MembershipStatus {
|
||||
ACTIVE = 'active',
|
||||
PENDING = 'pending',
|
||||
INVITED = 'invited',
|
||||
INACTIVE = 'inactive',
|
||||
}
|
||||
|
||||
export class MembershipDto {
|
||||
@ApiProperty({ enum: MembershipRole })
|
||||
@IsEnum(MembershipRole)
|
||||
role: MembershipRole;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
joinedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class DriverTeamViewModel {
|
||||
@ApiProperty({ type: TeamDto })
|
||||
team: TeamDto;
|
||||
|
||||
@ApiProperty({ type: MembershipDto })
|
||||
membership: MembershipDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isOwner: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
teamId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
|
||||
import type { RecordPageViewInput, RecordPageViewOutput } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import type { RecordEngagementInput, RecordEngagementOutput } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly recordPageViewUseCase: RecordPageViewUseCase,
|
||||
private readonly recordEngagementUseCase: RecordEngagementUseCase,
|
||||
) {}
|
||||
|
||||
@Post('page-view')
|
||||
async recordPageView(
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.recordPageViewUseCase.execute(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
async recordEngagement(
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.recordEngagementUseCase.execute(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user