module creation
This commit is contained in:
129
adapters/identity/persistence/inmemory/InMemoryAuthRepository.ts
Normal file
129
adapters/identity/persistence/inmemory/InMemoryAuthRepository.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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 { User } from '@gridpilot/core/identity/domain/entities/User';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { EmailAddress } from '@gridpilot/core/identity/domain/value-objects/EmailAddress';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export class InMemoryAuthRepository implements IAuthRepository {
|
||||
constructor(
|
||||
private readonly userRepository: IUserRepository,
|
||||
private readonly passwordHashingService: IPasswordHashingService,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async findByEmail(email: EmailAddress): Promise<User | null> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Finding user by email: ${email.value}`);
|
||||
const storedUser = await this.userRepository.findByEmail(email.value);
|
||||
if (!storedUser) {
|
||||
return null;
|
||||
}
|
||||
return User.fromStored(storedUser);
|
||||
}
|
||||
|
||||
async save(user: User): Promise<void> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Saving user with email: ${user.getEmail()}`);
|
||||
|
||||
const userId = user.getId().value;
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
const userPasswordHash = user.getPasswordHash()?.value ?? '';
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: userPasswordHash,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // In-memory doesn't use actual salt, but Stub has to provide.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
// Check if user exists to decide between create or update
|
||||
const existingStoredUser = await this.userRepository.findById(userId);
|
||||
if (existingStoredUser) {
|
||||
// For update, ensure createdAt is preserved from existing user
|
||||
const updatedStoredUser: StoredUser = {
|
||||
...storedUser,
|
||||
createdAt: existingStoredUser.createdAt,
|
||||
};
|
||||
await this.userRepository.update(updatedStoredUser);
|
||||
} else {
|
||||
await this.userRepository.create(storedUser);
|
||||
}
|
||||
}
|
||||
|
||||
async create(user: User, passwordPlain: string): Promise<User> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Creating user with email: ${user.getEmail()}`);
|
||||
const passwordHashValue = await this.passwordHashingService.hash(passwordPlain);
|
||||
|
||||
// Ensure email is not undefined before using
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
const userId = user.getId().value;
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: passwordHashValue,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // Salt is handled by PasswordHashingService, but StoredUser requires it.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const createdStoredUser = await this.userRepository.create(storedUser);
|
||||
return User.fromStored(createdStoredUser);
|
||||
}
|
||||
|
||||
async update(user: User): Promise<User> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Updating user with email: ${user.getEmail()}`);
|
||||
// Ensure values are not undefined before using
|
||||
const userId = user.getId().value;
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userPasswordHash = user.getPasswordHash()?.value ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: userPasswordHash,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // Salt is handled by PasswordHashingService, but StoredUser requires it.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(), // Assuming creation date is maintained or updated within UserRepository
|
||||
};
|
||||
const updatedStoredUser = await this.userRepository.update(storedUser);
|
||||
return User.fromStored(updatedStoredUser);
|
||||
}
|
||||
|
||||
async delete(userId: string): Promise<boolean> {
|
||||
// Deletion needs to be implemented in InMemoryUserRepository and exposed via IUserRepository
|
||||
// For now, it's not supported.
|
||||
this.logger.warn(`[InMemoryAuthRepository] Delete operation not implemented for user: ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
async userExists(email: string): Promise<boolean> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Checking if user exists for email: ${email}`);
|
||||
return this.userRepository.emailExists(email);
|
||||
}
|
||||
|
||||
async verifyPassword(email: string, passwordPlain: string): Promise<User | null> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Verifying password for user with email: ${email}`);
|
||||
const user = await this.findByEmail(EmailAddress.create(email)); // Use EmailAddress value object
|
||||
if (!user) {
|
||||
this.logger.warn(`[InMemoryAuthRepository] User not found for email: ${email}`);
|
||||
return null;
|
||||
}
|
||||
const isPasswordValid = await this.passwordHashingService.verify(
|
||||
passwordPlain,
|
||||
user.getPasswordHash()?.value ?? '', // Access value and provide fallback
|
||||
);
|
||||
if (!isPasswordValid) {
|
||||
this.logger.warn(`[InMemoryAuthRepository] Invalid password for user with email: ${email}`);
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
22
adapters/identity/services/InMemoryPasswordHashingService.ts
Normal file
22
adapters/identity/services/InMemoryPasswordHashingService.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* In-Memory Password Hashing Service
|
||||
*
|
||||
* Provides basic password hashing and comparison for demo/development purposes.
|
||||
* NOT FOR PRODUCTION USE - uses a simple string reversal as "hashing".
|
||||
*/
|
||||
|
||||
import type { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
|
||||
export class InMemoryPasswordHashingService implements IPasswordHashingService {
|
||||
async hash(plain: string): Promise<string> {
|
||||
// In a real application, use a robust hashing library like bcrypt or Argon2.
|
||||
// For demo, we'll just reverse the password and add a salt-like prefix.
|
||||
const salt = 'demo_salt_';
|
||||
return Promise.resolve(salt + plain.split('').reverse().join(''));
|
||||
}
|
||||
|
||||
async verify(plain: string, hash: string): Promise<boolean> {
|
||||
const hashedPlain = await this.hash(plain);
|
||||
return Promise.resolve(hashedPlain === hash);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,47 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '../../application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '../../application/ports/IdentitySessionPort';
|
||||
/**
|
||||
* Adapter: CookieIdentitySessionAdapter
|
||||
*
|
||||
* Manages user session using cookies. This is a placeholder implementation.
|
||||
*/
|
||||
|
||||
const SESSION_COOKIE = 'gp_demo_session';
|
||||
|
||||
function parseCookieValue(raw: string | undefined): AuthSessionDTO | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as AuthSessionDTO;
|
||||
if (!parsed.expiresAt || Date.now() > parsed.expiresAt) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeSession(session: AuthSessionDTO): string {
|
||||
return JSON.stringify(session);
|
||||
}
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import type { AuthenticatedUserDTO } from '@gridpilot/core/identity/application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '@gridpilot/core/identity/application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '@gridpilot/core/identity/application/ports/IdentitySessionPort';
|
||||
|
||||
export class CookieIdentitySessionAdapter implements IdentitySessionPort {
|
||||
private currentSession: AuthSessionDTO | null = null;
|
||||
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('CookieIdentitySessionAdapter initialized.');
|
||||
// In a real application, you would load the session from a cookie here
|
||||
// For demo, we'll start with no session.
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
const store = await cookies();
|
||||
const raw = store.get(SESSION_COOKIE)?.value;
|
||||
return parseCookieValue(raw);
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Getting current session.');
|
||||
return Promise.resolve(this.currentSession);
|
||||
}
|
||||
|
||||
async createSession(user: AuthenticatedUserDTO): Promise<AuthSessionDTO> {
|
||||
const now = Date.now();
|
||||
const expiresAt = now + 24 * 60 * 60 * 1000;
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user,
|
||||
issuedAt: now,
|
||||
expiresAt,
|
||||
token: randomUUID(),
|
||||
this.logger.debug(`[CookieIdentitySessionAdapter] Creating session for user: ${user.id}`);
|
||||
const newSession: AuthSessionDTO = {
|
||||
user: user,
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 3600 * 1000, // 1 hour expiration
|
||||
token: `mock-token-${user.id}-${Date.now()}`,
|
||||
};
|
||||
|
||||
const store = await cookies();
|
||||
store.set(SESSION_COOKIE, serializeSession(session), {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
return session;
|
||||
this.currentSession = newSession;
|
||||
// In a real app, you'd set a secure, HTTP-only cookie here.
|
||||
this.logger.info(`[CookieIdentitySessionAdapter] Session created for user ${user.id}.`);
|
||||
return Promise.resolve(newSession);
|
||||
}
|
||||
|
||||
async clearSession(): Promise<void> {
|
||||
const store = await cookies();
|
||||
store.delete(SESSION_COOKIE);
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Clearing session.');
|
||||
this.currentSession = null;
|
||||
// In a real app, you'd clear the session cookie here.
|
||||
this.logger.info('[CookieIdentitySessionAdapter] Session cleared.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,85 @@
|
||||
import type {
|
||||
IAvatarGenerationRepository,
|
||||
} from '@gridpilot/media';
|
||||
import {
|
||||
AvatarGenerationRequest,
|
||||
type AvatarGenerationRequestProps,
|
||||
} from '@gridpilot/media';
|
||||
import { IAvatarGenerationRepository } from '@gridpilot/core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { AvatarGenerationRequest } from '@gridpilot/core/media/domain/entities/AvatarGenerationRequest';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
/**
|
||||
* In-memory implementation of IAvatarGenerationRepository.
|
||||
*
|
||||
* For demo/development purposes. In production, this would use a database.
|
||||
*/
|
||||
export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepository {
|
||||
private readonly requests = new Map<string, AvatarGenerationRequestProps>();
|
||||
private readonly logger: ILogger;
|
||||
private requests: Map<string, AvatarGenerationRequest> = new Map(); // Key: requestId
|
||||
private userRequests: Map<string, AvatarGenerationRequest[]> = new Map(); // Key: userId
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialRequests: AvatarGenerationRequest[] = []) {
|
||||
this.logger.info('InMemoryAvatarGenerationRepository initialized.');
|
||||
for (const req of initialRequests) {
|
||||
this.requests.set(req.id, req);
|
||||
if (!this.userRequests.has(req.userId)) {
|
||||
this.userRequests.set(req.userId, []);
|
||||
}
|
||||
this.userRequests.get(req.userId)?.push(req);
|
||||
this.logger.debug(`Seeded avatar generation request: ${req.id} for user ${req.userId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async save(request: AvatarGenerationRequest): Promise<void> {
|
||||
this.logger.debug(`Saving avatar generation request with ID: ${request.id}`);
|
||||
this.requests.set(request.id, request.toProps());
|
||||
this.logger.info(`Avatar generation request with ID: ${request.id} saved successfully.`);
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Saving avatar generation request: ${request.id} for user ${request.userId}.`);
|
||||
this.requests.set(request.id, request);
|
||||
if (!this.userRequests.has(request.userId)) {
|
||||
this.userRequests.set(request.userId, []);
|
||||
}
|
||||
const existingRequests = this.userRequests.get(request.userId)!;
|
||||
// Remove old version if exists and add new version (update-like behavior)
|
||||
const index = existingRequests.findIndex(r => r.id === request.id);
|
||||
if (index > -1) {
|
||||
existingRequests[index] = request;
|
||||
} else {
|
||||
existingRequests.push(request);
|
||||
}
|
||||
this.logger.info(`Avatar generation request ${request.id} for user ${request.userId} saved successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AvatarGenerationRequest | null> {
|
||||
this.logger.debug(`Finding avatar generation request by ID: ${id}`);
|
||||
const props = this.requests.get(id);
|
||||
if (!props) {
|
||||
this.logger.info(`Avatar generation request with ID: ${id} not found.`);
|
||||
return null;
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Finding request by ID: ${id}`);
|
||||
const request = this.requests.get(id) ?? null;
|
||||
if (request) {
|
||||
this.logger.info(`Found request by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Request with ID ${id} not found.`);
|
||||
}
|
||||
this.logger.info(`Avatar generation request with ID: ${id} found.`);
|
||||
return AvatarGenerationRequest.reconstitute(props);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<AvatarGenerationRequest[]> {
|
||||
this.logger.debug(`Finding avatar generation requests by user ID: ${userId}`);
|
||||
const results: AvatarGenerationRequest[] = [];
|
||||
for (const props of Array.from(this.requests.values())) {
|
||||
if (props.userId === userId) {
|
||||
results.push(AvatarGenerationRequest.reconstitute(props));
|
||||
}
|
||||
}
|
||||
this.logger.info(`${results.length} avatar generation requests found for user ID: ${userId}.`);
|
||||
return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Finding requests for user: ${userId}`);
|
||||
const requests = this.userRequests.get(userId) ?? [];
|
||||
this.logger.info(`Found ${requests.length} requests for user ${userId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> {
|
||||
this.logger.debug(`Finding latest avatar generation request for user ID: ${userId}`);
|
||||
const userRequests = await this.findByUserId(userId);
|
||||
if (userRequests.length === 0) {
|
||||
this.logger.info(`No avatar generation requests found for user ID: ${userId}.`);
|
||||
return null;
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Finding latest request for user: ${userId}`);
|
||||
const requests = (this.userRequests.get(userId) ?? [])
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); // Sort by most recent
|
||||
const latest = requests.length > 0 ? requests[0]! : null; // Use non-null assertion as `requests.length > 0` is checked
|
||||
if (latest) {
|
||||
this.logger.info(`Found latest request for user ${userId}: ${latest.id}.`);
|
||||
} else {
|
||||
this.logger.warn(`No latest request found for user: ${userId}.`);
|
||||
}
|
||||
const latest = userRequests[0];
|
||||
if (!latest) {
|
||||
this.logger.info(`No latest avatar generation request found for user ID: ${userId}.`);
|
||||
return null;
|
||||
}
|
||||
this.logger.info(`Latest avatar generation request found for user ID: ${userId}, ID: ${latest.id}.`);
|
||||
return latest;
|
||||
return Promise.resolve(latest);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting avatar generation request with ID: ${id}`);
|
||||
const deleted = this.requests.delete(id);
|
||||
if (deleted) {
|
||||
this.logger.info(`Avatar generation request with ID: ${id} deleted successfully.`);
|
||||
this.logger.debug(`[InMemoryAvatarGenerationRepository] Deleting request with ID: ${id}.`);
|
||||
const requestToDelete = this.requests.get(id);
|
||||
if (requestToDelete) {
|
||||
this.requests.delete(id);
|
||||
const userRequests = this.userRequests.get(requestToDelete.userId);
|
||||
if (userRequests) {
|
||||
this.userRequests.set(requestToDelete.userId, userRequests.filter(req => req.id !== id));
|
||||
}
|
||||
this.logger.info(`Request ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Attempted to delete non-existent avatar generation request with ID: ${id}.`);
|
||||
this.logger.warn(`Request with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
adapters/media/ports/InMemoryFaceValidationAdapter.ts
Normal file
19
adapters/media/ports/InMemoryFaceValidationAdapter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { FaceValidationPort, FaceValidationResult } from '@gridpilot/core/media/application/ports/FaceValidationPort';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryFaceValidationAdapter implements FaceValidationPort {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryFaceValidationAdapter initialized.');
|
||||
}
|
||||
|
||||
async validateFacePhoto(imageData: string | Buffer): Promise<FaceValidationResult> {
|
||||
this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).');
|
||||
// Simulate a successful validation for any input for demo purposes
|
||||
return Promise.resolve({
|
||||
isValid: true,
|
||||
hasFace: true,
|
||||
faceCount: 1,
|
||||
confidence: 0.95,
|
||||
});
|
||||
}
|
||||
}
|
||||
28
adapters/media/ports/InMemoryImageServiceAdapter.ts
Normal file
28
adapters/media/ports/InMemoryImageServiceAdapter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { IImageServicePort } from '@gridpilot/racing/application/ports/IImageServicePort';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryImageServiceAdapter implements IImageServicePort {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryImageServiceAdapter initialized.');
|
||||
}
|
||||
|
||||
getDriverAvatar(driverId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting avatar for driver: ${driverId}`);
|
||||
return `https://cdn.example.com/avatars/${driverId}.png`; // Mock URL
|
||||
}
|
||||
|
||||
getTeamLogo(teamId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for team: ${teamId}`);
|
||||
return `https://cdn.example.com/logos/team-${teamId}.png`; // Mock URL
|
||||
}
|
||||
|
||||
getLeagueCover(leagueId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting cover for league: ${leagueId}`);
|
||||
return `https://cdn.example.com/covers/league-${leagueId}.png`; // Mock URL
|
||||
}
|
||||
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for league: ${leagueId}`);
|
||||
return `https://cdn.example.com/logos/league-${leagueId}.png`; // Mock URL
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,54 @@
|
||||
/**
|
||||
* In-Memory Implementation: InMemoryNotificationPreferenceRepository
|
||||
*
|
||||
* Provides an in-memory storage implementation for notification preferences.
|
||||
*/
|
||||
|
||||
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { INotificationPreferenceRepository } from '@gridpilot/core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { NotificationPreference } from '@gridpilot/core/notifications/domain/entities/NotificationPreference';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryNotificationPreferenceRepository implements INotificationPreferenceRepository {
|
||||
private preferences: Map<string, NotificationPreference> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, initialPreferences: NotificationPreference[] = []) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialPreferences: NotificationPreference[] = []) {
|
||||
this.logger.info('InMemoryNotificationPreferenceRepository initialized.');
|
||||
initialPreferences.forEach(pref => {
|
||||
this.preferences.set(pref.driverId, pref);
|
||||
this.logger.debug(`Seeded preference for driver: ${pref.driverId}`);
|
||||
});
|
||||
for (const pref of initialPreferences) {
|
||||
this.preferences.set(pref.id, pref);
|
||||
this.logger.debug(`Seeded preference: ${pref.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<NotificationPreference | null> {
|
||||
this.logger.debug(`Finding notification preference for driver: ${driverId}`);
|
||||
try {
|
||||
const preference = this.preferences.get(driverId) || null;
|
||||
if (preference) {
|
||||
this.logger.info(`Found preference for driver: ${driverId}`);
|
||||
} else {
|
||||
this.logger.warn(`Preference not found for driver: ${driverId}`);
|
||||
}
|
||||
return preference;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding preference for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Finding preferences for driver: ${driverId}`);
|
||||
const preference = this.preferences.get(driverId) ?? null;
|
||||
if (preference) {
|
||||
this.logger.info(`Found preferences for driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`No preferences found for driver: ${driverId}.`);
|
||||
}
|
||||
return Promise.resolve(preference);
|
||||
}
|
||||
|
||||
async save(preference: NotificationPreference): Promise<void> {
|
||||
this.logger.debug(`Saving notification preference for driver: ${preference.driverId}`);
|
||||
try {
|
||||
this.preferences.set(preference.driverId, preference);
|
||||
this.logger.info(`Preference for driver ${preference.driverId} saved successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving preference for driver ${preference.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Saving preferences for driver: ${preference.driverId}.`);
|
||||
this.preferences.set(preference.id, preference);
|
||||
this.logger.info(`Preferences for driver ${preference.driverId} saved successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async delete(driverId: string): Promise<void> {
|
||||
this.logger.debug(`Deleting notification preference for driver: ${driverId}`);
|
||||
try {
|
||||
if (this.preferences.delete(driverId)) {
|
||||
this.logger.info(`Preference for driver ${driverId} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Preference for driver ${driverId} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting preference for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Deleting preferences for driver: ${driverId}.`);
|
||||
if (this.preferences.delete(driverId)) {
|
||||
this.logger.info(`Preferences for driver ${driverId} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`No preferences found for driver ${driverId} to delete.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getOrCreateDefault(driverId: string): Promise<NotificationPreference> {
|
||||
this.logger.debug(`Getting or creating default notification preference for driver: ${driverId}`);
|
||||
try {
|
||||
const existing = this.preferences.get(driverId);
|
||||
if (existing) {
|
||||
this.logger.debug(`Existing preference found for driver: ${driverId}.`);
|
||||
return existing;
|
||||
}
|
||||
|
||||
this.logger.info(`Creating default preference for driver: ${driverId}.`);
|
||||
const defaultPreference = NotificationPreference.createDefault(driverId);
|
||||
this.preferences.set(driverId, defaultPreference);
|
||||
this.logger.info(`Default preference created and saved for driver: ${driverId}.`);
|
||||
return defaultPreference;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting or creating default preference for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryNotificationPreferenceRepository] Getting or creating default preferences for driver: ${driverId}.`);
|
||||
let preference = await this.findByDriverId(driverId);
|
||||
if (!preference) {
|
||||
this.logger.info(`Creating default preferences for new driver: ${driverId}.`);
|
||||
preference = NotificationPreference.createDefault(driverId);
|
||||
await this.save(preference);
|
||||
}
|
||||
return preference;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,163 +1,99 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryDriverRepository
|
||||
*
|
||||
* In-memory implementation of IDriverRepository.
|
||||
* Stores data in Map structure with UUID generation.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverRepository implements IDriverRepository {
|
||||
private drivers: Map<string, Driver>;
|
||||
private readonly logger: ILogger;
|
||||
private drivers: Map<string, Driver> = new Map();
|
||||
private iracingIdIndex: Map<string, string> = new Map(); // iracingId -> driverId
|
||||
|
||||
constructor(logger: ILogger, seedData?: Driver[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialDrivers: Driver[] = []) {
|
||||
this.logger.info('InMemoryDriverRepository initialized.');
|
||||
this.drivers = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(driver => {
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.debug(`Seeded driver: ${driver.id}.`);
|
||||
});
|
||||
for (const driver of initialDrivers) {
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.iracingIdIndex.set(driver.iracingId, driver.id);
|
||||
this.logger.debug(`Seeded driver: ${driver.id} (iRacing ID: ${driver.iracingId}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Driver | null> {
|
||||
this.logger.debug(`Finding driver by id: ${id}`);
|
||||
try {
|
||||
const driver = this.drivers.get(id) ?? null;
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with id ${id} not found.`);
|
||||
}
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Finding driver by ID: ${id}`);
|
||||
const driver = this.drivers.get(id) ?? null;
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(driver);
|
||||
}
|
||||
|
||||
async findByIRacingId(iracingId: string): Promise<Driver | null> {
|
||||
this.logger.debug(`Finding driver by iRacing id: ${iracingId}`);
|
||||
try {
|
||||
const driver = Array.from(this.drivers.values()).find(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver with iRacing id: ${iracingId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with iRacing id ${iracingId} not found.`);
|
||||
}
|
||||
return driver ?? null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver by iRacing id ${iracingId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Finding driver by iRacing ID: ${iracingId}`);
|
||||
const driverId = this.iracingIdIndex.get(iracingId);
|
||||
if (!driverId) {
|
||||
this.logger.warn(`Driver with iRacing ID ${iracingId} not found.`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findById(driverId);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Driver[]> {
|
||||
this.logger.debug('Finding all drivers.');
|
||||
try {
|
||||
const drivers = Array.from(this.drivers.values());
|
||||
this.logger.info(`Found ${drivers.length} drivers.`);
|
||||
return drivers;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all drivers:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryDriverRepository] Finding all drivers.');
|
||||
return Promise.resolve(Array.from(this.drivers.values()));
|
||||
}
|
||||
|
||||
async create(driver: Driver): Promise<Driver> {
|
||||
this.logger.debug(`Creating driver: ${driver.id}`);
|
||||
try {
|
||||
if (await this.exists(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} already exists.`);
|
||||
throw new Error(`Driver with ID ${driver.id} already exists`);
|
||||
}
|
||||
|
||||
if (await this.existsByIRacingId(driver.iracingId)) {
|
||||
this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`);
|
||||
throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`);
|
||||
}
|
||||
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.info(`Driver ${driver.id} created successfully.`);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating driver ${driver.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Creating driver: ${driver.id} (iRacing ID: ${driver.iracingId})`);
|
||||
if (this.drivers.has(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} already exists.`);
|
||||
throw new Error('Driver already exists');
|
||||
}
|
||||
if (this.iracingIdIndex.has(driver.iracingId)) {
|
||||
this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`);
|
||||
throw new Error('iRacing ID already registered');
|
||||
}
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.iracingIdIndex.set(driver.iracingId, driver.id);
|
||||
this.logger.info(`Driver ${driver.id} created successfully.`);
|
||||
return Promise.resolve(driver);
|
||||
}
|
||||
|
||||
async update(driver: Driver): Promise<Driver> {
|
||||
this.logger.debug(`Updating driver: ${driver.id}`);
|
||||
try {
|
||||
if (!await this.exists(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} not found for update.`);
|
||||
throw new Error(`Driver with ID ${driver.id} not found`);
|
||||
}
|
||||
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.info(`Driver ${driver.id} updated successfully.`);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating driver ${driver.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryDriverRepository] Updating driver: ${driver.id} (iRacing ID: ${driver.iracingId})`);
|
||||
if (!this.drivers.has(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} not found for update.`);
|
||||
throw new Error('Driver not found');
|
||||
}
|
||||
this.drivers.set(driver.id, driver);
|
||||
// Re-index iRacing ID if it changed
|
||||
const existingDriver = this.drivers.get(driver.id);
|
||||
if (existingDriver && existingDriver.iracingId !== driver.iracingId) {
|
||||
this.iracingIdIndex.delete(existingDriver.iracingId);
|
||||
this.iracingIdIndex.set(driver.iracingId, driver.id);
|
||||
}
|
||||
this.logger.info(`Driver ${driver.id} updated successfully.`);
|
||||
return Promise.resolve(driver);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting driver: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Driver with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Driver with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.logger.debug(`[InMemoryDriverRepository] Deleting driver with ID: ${id}`);
|
||||
const driver = this.drivers.get(id);
|
||||
if (driver) {
|
||||
this.drivers.delete(id);
|
||||
this.iracingIdIndex.delete(driver.iracingId);
|
||||
this.logger.info(`Driver ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting driver ${id}:`, error);
|
||||
throw error;
|
||||
} else {
|
||||
this.logger.warn(`Driver with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of driver with id: ${id}`);
|
||||
try {
|
||||
const exists = this.drivers.has(id);
|
||||
this.logger.debug(`Driver ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of driver with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryDriverRepository] Checking existence of driver with ID: ${id}`);
|
||||
return Promise.resolve(this.drivers.has(id));
|
||||
}
|
||||
|
||||
async existsByIRacingId(iracingId: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of driver with iRacing id: ${iracingId}`);
|
||||
try {
|
||||
const exists = Array.from(this.drivers.values()).some(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
this.logger.debug(`Driver with iRacing id ${iracingId} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of driver with iRacing id ${iracingId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryDriverRepository] Checking existence of driver with iRacing ID: ${iracingId}`);
|
||||
return Promise.resolve(this.iracingIdIndex.has(iracingId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
*/
|
||||
static generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,31 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryGameRepository
|
||||
*
|
||||
* In-memory implementation of IGameRepository.
|
||||
*/
|
||||
|
||||
import { Game } from '../../domain/entities/Game';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository';
|
||||
import { Game } from '@gridpilot/racing/domain/entities/Game';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryGameRepository implements IGameRepository {
|
||||
private games: Map<string, Game>;
|
||||
private readonly logger: ILogger;
|
||||
private games: Map<string, Game> = new Map();
|
||||
|
||||
constructor(logger: ILogger, seedData?: Game[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialGames: Game[] = []) {
|
||||
this.logger.info('InMemoryGameRepository initialized.');
|
||||
this.games = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(game => {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded game: ${game.id}.`);
|
||||
});
|
||||
} else {
|
||||
// Default seed data for common sim racing games
|
||||
const defaultGames = [
|
||||
Game.create({ id: 'iracing', name: 'iRacing' }),
|
||||
Game.create({ id: 'acc', name: 'Assetto Corsa Competizione' }),
|
||||
Game.create({ id: 'ac', name: 'Assetto Corsa' }),
|
||||
Game.create({ id: 'rf2', name: 'rFactor 2' }),
|
||||
Game.create({ id: 'ams2', name: 'Automobilista 2' }),
|
||||
Game.create({ id: 'lmu', name: 'Le Mans Ultimate' }),
|
||||
];
|
||||
defaultGames.forEach(game => {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded default game: ${game.id}.`);
|
||||
});
|
||||
for (const game of initialGames) {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded game: ${game.id} (${game.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Game | null> {
|
||||
this.logger.debug(`Finding game by id: ${id}`);
|
||||
try {
|
||||
const game = this.games.get(id) ?? null;
|
||||
if (game) {
|
||||
this.logger.info(`Found game: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Game with id ${id} not found.`);
|
||||
}
|
||||
return game;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding game by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryGameRepository] Finding game by ID: ${id}`);
|
||||
const game = this.games.get(id) ?? null;
|
||||
if (game) {
|
||||
this.logger.info(`Found game by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Game with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(game);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Game[]> {
|
||||
this.logger.debug('Finding all games.');
|
||||
try {
|
||||
const games = Array.from(this.games.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${games.length} games.`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all games:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryGameRepository] Finding all games.');
|
||||
return Promise.resolve(Array.from(this.games.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to add a game
|
||||
*/
|
||||
async create(game: Game): Promise<Game> {
|
||||
this.logger.debug(`Creating game: ${game.id}`);
|
||||
try {
|
||||
if (this.games.has(game.id)) {
|
||||
this.logger.warn(`Game with ID ${game.id} already exists.`);
|
||||
throw new Error(`Game with ID ${game.id} already exists`);
|
||||
}
|
||||
this.games.set(game.id, game);
|
||||
this.logger.info(`Game ${game.id} created successfully.`);
|
||||
return game;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating game ${game.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper to clear data
|
||||
*/
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all games.');
|
||||
this.games.clear();
|
||||
this.logger.info('All games cleared.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +1,102 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryLeagueMembershipRepository
|
||||
*
|
||||
* In-memory implementation of ILeagueMembershipRepository.
|
||||
* Stores memberships and join requests in maps keyed by league.
|
||||
*/
|
||||
|
||||
import type {
|
||||
LeagueMembership,
|
||||
JoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { LeagueMembership, JoinRequest } from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||
private membershipsByLeague: Map<string, LeagueMembership[]>;
|
||||
private joinRequestsByLeague: Map<string, JoinRequest[]>;
|
||||
private readonly logger: ILogger;
|
||||
private memberships: Map<string, LeagueMembership> = new Map(); // Key: `${leagueId}:${driverId}`
|
||||
private joinRequests: Map<string, JoinRequest> = new Map(); // Key: requestId
|
||||
|
||||
constructor(logger: ILogger, seedMemberships?: LeagueMembership[], seedJoinRequests?: JoinRequest[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialMemberships: LeagueMembership[] = [], initialJoinRequests: JoinRequest[] = []) {
|
||||
this.logger.info('InMemoryLeagueMembershipRepository initialized.');
|
||||
this.membershipsByLeague = new Map();
|
||||
this.joinRequestsByLeague = new Map();
|
||||
|
||||
if (seedMemberships) {
|
||||
seedMemberships.forEach((membership) => {
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
list.push(membership);
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
this.logger.debug(`Seeded membership for league ${membership.leagueId}, driver ${membership.driverId}.`);
|
||||
});
|
||||
for (const membership of initialMemberships) {
|
||||
this.memberships.set(`${membership.leagueId}:${membership.driverId}`, membership);
|
||||
this.logger.debug(`Seeded membership: ${membership.id}.`);
|
||||
}
|
||||
|
||||
if (seedJoinRequests) {
|
||||
seedJoinRequests.forEach((request) => {
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
list.push(request);
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
this.logger.debug(`Seeded join request for league ${request.leagueId}, driver ${request.driverId}.`);
|
||||
});
|
||||
for (const req of initialJoinRequests) {
|
||||
this.joinRequests.set(req.id, req);
|
||||
this.logger.debug(`Seeded join request: ${req.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
this.logger.debug(`Getting membership for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) {
|
||||
this.logger.warn(`No membership list found for league: ${leagueId}.`);
|
||||
return null;
|
||||
}
|
||||
const membership = list.find((m) => m.driverId === driverId) ?? null;
|
||||
if (membership) {
|
||||
this.logger.info(`Found membership for league: ${leagueId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership not found for league: ${leagueId}, driver: ${driverId}.`);
|
||||
}
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting membership for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting membership for league ${leagueId}, driver ${driverId}.`);
|
||||
const key = `${leagueId}:${driverId}`;
|
||||
const membership = this.memberships.get(key) ?? null;
|
||||
if (membership) {
|
||||
this.logger.info(`Found membership for league ${leagueId}, driver ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`No membership found for league ${leagueId}, driver ${driverId}.`);
|
||||
}
|
||||
return Promise.resolve(membership);
|
||||
}
|
||||
|
||||
async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding active membership for league ${leagueId}, driver ${driverId}.`);
|
||||
const membership = await this.getMembership(leagueId, driverId);
|
||||
return Promise.resolve(membership && membership.status === 'active' ? membership : null);
|
||||
}
|
||||
|
||||
async findAllByLeagueId(leagueId: string): Promise<LeagueMembership[]> {
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for league ${leagueId}.`);
|
||||
const filteredMemberships = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId);
|
||||
this.logger.info(`Found ${filteredMemberships.length} memberships for league ${leagueId}.`);
|
||||
return Promise.resolve(filteredMemberships);
|
||||
}
|
||||
|
||||
async findAllByDriverId(driverId: string): Promise<LeagueMembership[]> {
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for driver ${driverId}.`);
|
||||
const memberships = Array.from(this.memberships.values()).filter(mem => mem.driverId === driverId);
|
||||
this.logger.info(`Found ${memberships.length} memberships for driver ${driverId}.`);
|
||||
return Promise.resolve(memberships);
|
||||
}
|
||||
|
||||
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
|
||||
this.logger.debug(`Getting league members for league: ${leagueId}`);
|
||||
try {
|
||||
const members = [...(this.membershipsByLeague.get(leagueId) ?? [])];
|
||||
this.logger.info(`Found ${members.length} members for league: ${leagueId}.`);
|
||||
return members;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting league members for league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting active members for league ${leagueId}.`);
|
||||
const members = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId && mem.status === 'active');
|
||||
this.logger.info(`Found ${members.length} active members for league ${leagueId}.`);
|
||||
return Promise.resolve(members);
|
||||
}
|
||||
|
||||
async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
|
||||
this.logger.debug(`Getting join requests for league: ${leagueId}`);
|
||||
try {
|
||||
const requests = [...(this.joinRequestsByLeague.get(leagueId) ?? [])];
|
||||
this.logger.info(`Found ${requests.length} join requests for league: ${leagueId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting join requests for league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting join requests for league ${leagueId}.`);
|
||||
const requests = Array.from(this.joinRequests.values()).filter(req => req.leagueId === leagueId);
|
||||
this.logger.info(`Found ${requests.length} join requests for league ${leagueId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
|
||||
this.logger.debug(`Saving membership for league: ${membership.leagueId}, driver: ${membership.driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex(
|
||||
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = membership;
|
||||
this.logger.info(`Updated existing membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
|
||||
} else {
|
||||
list.push(membership);
|
||||
this.logger.info(`Created new membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
|
||||
}
|
||||
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving membership for league ${membership.leagueId}, driver ${membership.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Saving membership for ${membership.id}.`);
|
||||
const key = `${membership.leagueId}:${membership.driverId}`;
|
||||
this.memberships.set(key, membership);
|
||||
this.logger.info(`Membership ${membership.id} saved successfully.`);
|
||||
return Promise.resolve(membership);
|
||||
}
|
||||
|
||||
async removeMembership(leagueId: string, driverId: string): Promise<void> {
|
||||
this.logger.debug(`Removing membership for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) {
|
||||
this.logger.warn(`No membership list found for league: ${leagueId}. Cannot remove.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const next = list.filter((m) => m.driverId !== driverId);
|
||||
if (next.length < list.length) {
|
||||
this.membershipsByLeague.set(leagueId, next);
|
||||
this.logger.info(`Removed membership for league: ${leagueId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership not found for league: ${leagueId}, driver: ${driverId}. Cannot remove.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing membership for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Removing membership for league ${leagueId}, driver ${driverId}.`);
|
||||
const key = `${leagueId}:${driverId}`;
|
||||
if (this.memberships.delete(key)) {
|
||||
this.logger.info(`Membership for league ${leagueId}, driver ${driverId} removed successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership for league ${leagueId}, driver ${driverId} not found for removal.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async saveJoinRequest(request: JoinRequest): Promise<JoinRequest> {
|
||||
this.logger.debug(`Saving join request for league: ${request.leagueId}, driver: ${request.driverId}, id: ${request.id}`);
|
||||
try {
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex((r) => r.id === request.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = request;
|
||||
this.logger.info(`Updated existing join request: ${request.id}.`);
|
||||
} else {
|
||||
list.push(request);
|
||||
this.logger.info(`Created new join request: ${request.id}.`);
|
||||
}
|
||||
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving join request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Saving join request for ${request.id}.`);
|
||||
this.joinRequests.set(request.id, request);
|
||||
this.logger.info(`Join request ${request.id} saved successfully.`);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async removeJoinRequest(requestId: string): Promise<void> {
|
||||
this.logger.debug(`Removing join request with ID: ${requestId}`);
|
||||
try {
|
||||
let removed = false;
|
||||
for (const [leagueId, requests] of this.joinRequestsByLeague.entries()) {
|
||||
const next = requests.filter((r) => r.id !== requestId);
|
||||
if (next.length !== requests.length) {
|
||||
this.joinRequestsByLeague.set(leagueId, next);
|
||||
removed = true;
|
||||
this.logger.info(`Removed join request ${requestId} from league ${leagueId}.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
this.logger.warn(`Join request with ID ${requestId} not found for removal.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing join request ${requestId}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Removing join request: ${requestId}.`);
|
||||
if (this.joinRequests.delete(requestId)) {
|
||||
this.logger.info(`Join request ${requestId} removed successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Join request ${requestId} not found for removal.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,84 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryLeagueRepository
|
||||
*
|
||||
* In-memory implementation of ILeagueRepository.
|
||||
* Stores data in Map structure with UUID generation.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILeagueRepository } from '@gridpilot/core/racing/domain/repositories/ILeagueRepository';
|
||||
import { League } from '@gridpilot/core/racing/domain/entities/League';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
private leagues: Map<string, League>;
|
||||
private readonly logger: ILogger;
|
||||
private leagues: Map<string, League> = new Map();
|
||||
|
||||
constructor(logger: ILogger, seedData?: League[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialLeagues: League[] = []) {
|
||||
this.logger.info('InMemoryLeagueRepository initialized.');
|
||||
this.leagues = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(league => {
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.debug(`Seeded league: ${league.id}.`);
|
||||
});
|
||||
for (const league of initialLeagues) {
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.debug(`Seeded league: ${league.id} (${league.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<League | null> {
|
||||
this.logger.debug(`Finding league by id: ${id}`);
|
||||
try {
|
||||
const league = this.leagues.get(id) ?? null;
|
||||
if (league) {
|
||||
this.logger.info(`Found league: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`League with id ${id} not found.`);
|
||||
}
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding league by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(): Promise<League[]> {
|
||||
this.logger.debug('Finding all leagues.');
|
||||
try {
|
||||
const leagues = Array.from(this.leagues.values());
|
||||
this.logger.info(`Found ${leagues.length} leagues.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all leagues:', error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Finding league by ID: ${id}`);
|
||||
const league = this.leagues.get(id) ?? null;
|
||||
if (league) {
|
||||
this.logger.info(`Found league by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`League with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(league);
|
||||
}
|
||||
|
||||
async findByOwnerId(ownerId: string): Promise<League[]> {
|
||||
this.logger.debug(`Finding leagues by owner id: ${ownerId}`);
|
||||
try {
|
||||
const leagues = Array.from(this.leagues.values()).filter(
|
||||
league => league.ownerId === ownerId
|
||||
);
|
||||
this.logger.info(`Found ${leagues.length} leagues for owner id: ${ownerId}.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding leagues by owner id ${ownerId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Finding leagues by owner ID: ${ownerId}`);
|
||||
const ownedLeagues = Array.from(this.leagues.values()).filter(league => league.ownerId === ownerId);
|
||||
this.logger.info(`Found ${ownedLeagues.length} leagues for owner ID: ${ownerId}.`);
|
||||
return Promise.resolve(ownedLeagues);
|
||||
}
|
||||
|
||||
async searchByName(name: string): Promise<League[]> {
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Searching leagues by name: ${name}`);
|
||||
const matchingLeagues = Array.from(this.leagues.values()).filter(league =>
|
||||
league.name.toLowerCase().includes(name.toLowerCase()),
|
||||
);
|
||||
this.logger.info(`Found ${matchingLeagues.length} matching leagues for name search: ${name}.`);
|
||||
return Promise.resolve(matchingLeagues);
|
||||
}
|
||||
|
||||
async findAll(): Promise<League[]> {
|
||||
this.logger.debug('[InMemoryLeagueRepository] Finding all leagues.');
|
||||
return Promise.resolve(Array.from(this.leagues.values()));
|
||||
}
|
||||
|
||||
async create(league: League): Promise<League> {
|
||||
this.logger.debug(`Creating league: ${league.id}`);
|
||||
try {
|
||||
if (await this.exists(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} already exists.`);
|
||||
throw new Error(`League with ID ${league.id} already exists`);
|
||||
}
|
||||
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} created successfully.`);
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating league ${league.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Creating league: ${league.id} (${league.name})`);
|
||||
if (this.leagues.has(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} already exists.`);
|
||||
throw new Error('League already exists');
|
||||
}
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} created successfully.`);
|
||||
return Promise.resolve(league);
|
||||
}
|
||||
|
||||
async update(league: League): Promise<League> {
|
||||
this.logger.debug(`Updating league: ${league.id}`);
|
||||
try {
|
||||
if (!await this.exists(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} not found for update.`);
|
||||
throw new Error(`League with ID ${league.id} not found`);
|
||||
}
|
||||
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} updated successfully.`);
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating league ${league.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Updating league: ${league.id} (${league.name})`);
|
||||
if (!this.leagues.has(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} not found for update.`);
|
||||
throw new Error('League not found');
|
||||
}
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} updated successfully.`);
|
||||
return Promise.resolve(league);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting league: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`League with ID ${id} not found for deletion.`);
|
||||
throw new Error(`League with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.leagues.delete(id);
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Deleting league with ID: ${id}`);
|
||||
if (this.leagues.delete(id)) {
|
||||
this.logger.info(`League ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting league ${id}:`, error);
|
||||
throw error;
|
||||
} else {
|
||||
this.logger.warn(`League with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of league with id: ${id}`);
|
||||
try {
|
||||
const exists = this.leagues.has(id);
|
||||
this.logger.debug(`League ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of league with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryLeagueRepository] Checking existence of league with ID: ${id}`);
|
||||
return Promise.resolve(this.leagues.has(id));
|
||||
}
|
||||
|
||||
async searchByName(query: string): Promise<League[]> {
|
||||
this.logger.debug(`Searching leagues by name query: ${query}`);
|
||||
try {
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const leagues = Array.from(this.leagues.values()).filter(league =>
|
||||
league.name.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
this.logger.info(`Found ${leagues.length} leagues matching search query: ${query}.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error searching leagues by name query ${query}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
*/
|
||||
static generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import { LeagueScoringConfig } from '@gridpilot/racing/domain/entities/LeagueScoringConfig';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepository {
|
||||
private configs: Map<string, LeagueScoringConfig> = new Map(); // Key: seasonId
|
||||
|
||||
constructor(private readonly logger: ILogger, initialConfigs: LeagueScoringConfig[] = []) {
|
||||
this.logger.info('InMemoryLeagueScoringConfigRepository initialized.');
|
||||
for (const config of initialConfigs) {
|
||||
this.configs.set(config.seasonId, config);
|
||||
this.logger.debug(`Seeded league scoring config for season: ${config.seasonId}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findBySeasonId(seasonId: string): Promise<LeagueScoringConfig | null> {
|
||||
this.logger.debug(`[InMemoryLeagueScoringConfigRepository] Finding config for season: ${seasonId}.`);
|
||||
const config = this.configs.get(seasonId) ?? null;
|
||||
if (config) {
|
||||
this.logger.info(`Found config for season: ${seasonId}.`);
|
||||
} else {
|
||||
this.logger.warn(`No config found for season: ${seasonId}.`);
|
||||
}
|
||||
return Promise.resolve(config);
|
||||
}
|
||||
|
||||
async save(config: LeagueScoringConfig): Promise<LeagueScoringConfig> {
|
||||
this.logger.debug(`[InMemoryLeagueScoringConfigRepository] Saving config for season: ${config.seasonId}.`);
|
||||
this.configs.set(config.seasonId, config);
|
||||
this.logger.info(`Config for season ${config.seasonId} saved successfully.`);
|
||||
return Promise.resolve(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ILeagueStandingsRepository, RawStanding } from '@gridpilot/core/league/application/ports/ILeagueStandingsRepository';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueStandingsRepository implements ILeagueStandingsRepository {
|
||||
private standings: Map<string, RawStanding[]> = new Map(); // Key: leagueId
|
||||
|
||||
constructor(private readonly logger: ILogger, initialStandings: Record<string, RawStanding[]> = {}) {
|
||||
this.logger.info('InMemoryLeagueStandingsRepository initialized.');
|
||||
for (const leagueId in initialStandings) {
|
||||
// Ensure initialStandings[leagueId] is not undefined before setting
|
||||
if (initialStandings[leagueId] !== undefined) {
|
||||
this.standings.set(leagueId, initialStandings[leagueId]);
|
||||
this.logger.debug(`Seeded standings for league: ${leagueId}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueStandings(leagueId: string): Promise<RawStanding[]> {
|
||||
this.logger.debug(`[InMemoryLeagueStandingsRepository] Getting standings for league: ${leagueId}.`);
|
||||
const leagueStandings = this.standings.get(leagueId) ?? [];
|
||||
this.logger.info(`Found ${leagueStandings.length} standings for league: ${leagueId}.`);
|
||||
return Promise.resolve(leagueStandings);
|
||||
}
|
||||
|
||||
// Helper method for seeding/updating if needed by other in-memory repos
|
||||
public setLeagueStandings(leagueId: string, standings: RawStanding[]): void {
|
||||
this.standings.set(leagueId, standings);
|
||||
this.logger.debug(`[InMemoryLeagueStandingsRepository] Set standings for league: ${leagueId}.`);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,88 @@
|
||||
/**
|
||||
* In-Memory Implementation: InMemoryProtestRepository
|
||||
*
|
||||
* Provides an in-memory storage implementation for protests.
|
||||
*/
|
||||
|
||||
import type { Protest } from '../../domain/entities/Protest';
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IProtestRepository } from '@gridpilot/racing/domain/repositories/IProtestRepository';
|
||||
import { Protest, ProtestStatus } from '@gridpilot/racing/domain/entities/Protest';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryProtestRepository implements IProtestRepository {
|
||||
private protests: Map<string, Protest> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, initialProtests: Protest[] = []) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialProtests: Protest[] = []) {
|
||||
this.logger.info('InMemoryProtestRepository initialized.');
|
||||
initialProtests.forEach(protest => {
|
||||
for (const protest of initialProtests) {
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.debug(`Seeded protest: ${protest.id}`);
|
||||
});
|
||||
this.logger.debug(`Seeded protest: ${protest.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Protest | null> {
|
||||
this.logger.debug(`Finding protest by id: ${id}`);
|
||||
try {
|
||||
const protest = this.protests.get(id) || null;
|
||||
if (protest) {
|
||||
this.logger.info(`Found protest with id: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Protest with id ${id} not found.`);
|
||||
}
|
||||
return protest;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protest by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protest by ID: ${id}`);
|
||||
const protest = this.protests.get(id) ?? null;
|
||||
if (protest) {
|
||||
this.logger.info(`Found protest by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Protest with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(protest);
|
||||
}
|
||||
|
||||
async findByRaceId(raceId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests by race id: ${raceId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.raceId === raceId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests for race id: ${raceId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests for race: ${raceId}.`);
|
||||
const protests = Array.from(this.protests.values()).filter(p => p.raceId === raceId);
|
||||
this.logger.info(`Found ${protests.length} protests for race ${raceId}.`);
|
||||
return Promise.resolve(protests);
|
||||
}
|
||||
|
||||
async findByProtestingDriverId(driverId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests by protesting driver id: ${driverId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.protestingDriverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests by protesting driver id: ${driverId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by protesting driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests by protesting driver ID: ${driverId}.`);
|
||||
const protests = Array.from(this.protests.values()).filter(p => p.protestingDriverId === driverId);
|
||||
this.logger.info(`Found ${protests.length} protests by protesting driver ${driverId}.`);
|
||||
return Promise.resolve(protests);
|
||||
}
|
||||
|
||||
async findByAccusedDriverId(driverId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests by accused driver id: ${driverId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.accusedDriverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests by accused driver id: ${driverId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by accused driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests by accused driver ID: ${driverId}.`);
|
||||
const protests = Array.from(this.protests.values()).filter(p => p.accusedDriverId === driverId);
|
||||
this.logger.info(`Found ${protests.length} protests by accused driver ${driverId}.`);
|
||||
return Promise.resolve(protests);
|
||||
}
|
||||
|
||||
async findPending(): Promise<Protest[]> {
|
||||
this.logger.debug('Finding pending protests.');
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.isPending()
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} pending protests.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding pending protests:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryProtestRepository] Finding all pending protests.');
|
||||
const pendingProtests = Array.from(this.protests.values()).filter(p => p.status === 'pending');
|
||||
this.logger.info(`Found ${pendingProtests.length} pending protests.`);
|
||||
return Promise.resolve(pendingProtests);
|
||||
}
|
||||
|
||||
async findUnderReviewBy(stewardId: string): Promise<Protest[]> {
|
||||
this.logger.debug(`Finding protests under review by steward: ${stewardId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.reviewedBy === stewardId && protest.isUnderReview()
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests under review by steward: ${stewardId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests under review by steward ${stewardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Finding protests under review by steward: ${stewardId}.`);
|
||||
const underReviewProtests = Array.from(this.protests.values()).filter(p => p.reviewedBy === stewardId && p.status === 'under_review');
|
||||
this.logger.info(`Found ${underReviewProtests.length} protests under review by steward ${stewardId}.`);
|
||||
return Promise.resolve(underReviewProtests);
|
||||
}
|
||||
|
||||
async create(protest: Protest): Promise<void> {
|
||||
this.logger.debug(`Creating protest: ${protest.id}`);
|
||||
try {
|
||||
if (this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} already exists.`);
|
||||
throw new Error(`Protest with ID ${protest.id} already exists`);
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} created successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating protest ${protest.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryProtestRepository] Creating protest: ${protest.id}.`);
|
||||
if (this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} already exists.`);
|
||||
throw new Error('Protest already exists');
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} created successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async update(protest: Protest): Promise<void> {
|
||||
this.logger.debug(`Updating protest: ${protest.id}`);
|
||||
try {
|
||||
if (!this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} not found for update.`);
|
||||
throw new Error(`Protest with ID ${protest.id} not found`);
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} updated successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating protest ${protest.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryProtestRepository] Updating protest: ${protest.id}.`);
|
||||
if (!this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} not found for update.`);
|
||||
throw new Error('Protest not found');
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} updated successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of protest with id: ${id}`);
|
||||
try {
|
||||
const exists = this.protests.has(id);
|
||||
this.logger.debug(`Protest ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of protest with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryProtestRepository] Checking existence of protest with ID: ${id}.`);
|
||||
return Promise.resolve(this.protests.has(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +1,90 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryRaceRegistrationRepository
|
||||
*
|
||||
* In-memory implementation of IRaceRegistrationRepository.
|
||||
* Stores race registrations in Maps keyed by raceId and driverId.
|
||||
*/
|
||||
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
|
||||
type RaceRegistrationSeed = Pick<RaceRegistration, 'raceId' | 'driverId' | 'registeredAt'>;
|
||||
import { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
|
||||
private registrationsByRace: Map<string, Set<string>>;
|
||||
private registrationsByDriver: Map<string, Set<string>>;
|
||||
private readonly logger: ILogger;
|
||||
private registrations: Map<string, RaceRegistration> = new Map(); // Key: `${raceId}:${driverId}`
|
||||
|
||||
constructor(logger: ILogger, seedRegistrations?: RaceRegistrationSeed[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialRegistrations: RaceRegistration[] = []) {
|
||||
this.logger.info('InMemoryRaceRegistrationRepository initialized.');
|
||||
this.registrationsByRace = new Map();
|
||||
this.registrationsByDriver = new Map();
|
||||
|
||||
if (seedRegistrations) {
|
||||
this.logger.debug('Seeding with initial registrations', { count: seedRegistrations.length });
|
||||
seedRegistrations.forEach((registration) => {
|
||||
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt);
|
||||
});
|
||||
for (const reg of initialRegistrations) {
|
||||
this.registrations.set(reg.id, reg);
|
||||
this.logger.debug(`Seeded registration: ${reg.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
private addToIndexes(raceId: string, driverId: string, _registeredAt: Date): void {
|
||||
this.logger.debug('Attempting to add race registration to indexes', { raceId, driverId });
|
||||
let raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
raceSet = new Set();
|
||||
this.registrationsByRace.set(raceId, raceSet);
|
||||
this.logger.debug('Created new race set as none existed', { raceId });
|
||||
}
|
||||
raceSet.add(driverId);
|
||||
this.logger.debug('Added driver to race set', { raceId, driverId });
|
||||
|
||||
let driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (!driverSet) {
|
||||
driverSet = new Set();
|
||||
this.registrationsByDriver.set(driverId, driverSet);
|
||||
this.logger.debug('Created new driver set as none existed', { driverId });
|
||||
}
|
||||
driverSet.add(raceId);
|
||||
this.logger.debug('Added race to driver set', { raceId, driverId });
|
||||
this.logger.info('Successfully added race registration to indexes', { raceId, driverId });
|
||||
}
|
||||
|
||||
private removeFromIndexes(raceId: string, driverId: string): void {
|
||||
this.logger.debug('Attempting to remove race registration from indexes', { raceId, driverId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (raceSet) {
|
||||
raceSet.delete(driverId);
|
||||
this.logger.debug('Removed driver from race set', { raceId, driverId });
|
||||
if (raceSet.size === 0) {
|
||||
this.registrationsByRace.delete(raceId);
|
||||
this.logger.debug('Deleted race set as it is now empty', { raceId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Race set not found during removal, potential inconsistency', { raceId });
|
||||
}
|
||||
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (driverSet) {
|
||||
driverSet.delete(raceId);
|
||||
this.logger.debug('Removed race from driver set', { raceId, driverId });
|
||||
if (driverSet.size === 0) {
|
||||
this.registrationsByDriver.delete(driverId);
|
||||
this.logger.debug('Deleted driver set as it is now empty', { driverId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Driver set not found during removal, potential inconsistency', { driverId });
|
||||
}
|
||||
this.logger.info('Successfully removed race registration from indexes', { raceId, driverId });
|
||||
}
|
||||
|
||||
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
|
||||
this.logger.info('Checking if driver is registered for race', { raceId, driverId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('Race set not found, driver not registered', { raceId, driverId });
|
||||
return false;
|
||||
}
|
||||
const isRegistered = raceSet.has(driverId);
|
||||
this.logger.debug('Registration status result', { raceId, driverId, isRegistered });
|
||||
return isRegistered;
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Checking if driver ${driverId} is registered for race ${raceId}.`);
|
||||
const key = `${raceId}:${driverId}`;
|
||||
return Promise.resolve(this.registrations.has(key));
|
||||
}
|
||||
|
||||
async getRegisteredDrivers(raceId: string): Promise<string[]> {
|
||||
this.logger.info('Attempting to fetch registered drivers for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('No registered drivers found for race', { raceId });
|
||||
return [];
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registered drivers for race ${raceId}.`);
|
||||
const driverIds: string[] = [];
|
||||
for (const registration of this.registrations.values()) {
|
||||
if (registration.raceId === raceId) {
|
||||
driverIds.push(registration.driverId);
|
||||
}
|
||||
}
|
||||
const drivers = Array.from(raceSet.values());
|
||||
this.logger.debug('Found registered drivers for race', { raceId, count: drivers.length });
|
||||
this.logger.info('Successfully fetched registered drivers for race', { raceId, count: drivers.length });
|
||||
return drivers;
|
||||
this.logger.info(`Found ${driverIds.length} registered drivers for race ${raceId}.`);
|
||||
return Promise.resolve(driverIds);
|
||||
}
|
||||
|
||||
async getRegistrationCount(raceId: string): Promise<number> {
|
||||
this.logger.info('Attempting to get registration count for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
const count = raceSet ? raceSet.size : 0;
|
||||
this.logger.debug('Registration count for race', { raceId, count });
|
||||
this.logger.info('Returning registration count for race', { raceId, count });
|
||||
return count;
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`);
|
||||
const count = Array.from(this.registrations.values()).filter(reg => reg.raceId === raceId).length;
|
||||
this.logger.info(`Registration count for race ${raceId}: ${count}.`);
|
||||
return Promise.resolve(count);
|
||||
}
|
||||
|
||||
async register(registration: RaceRegistration): Promise<void> {
|
||||
this.logger.info('Attempting to register driver for race', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
const alreadyRegistered = await this.isRegistered(registration.raceId, registration.driverId);
|
||||
if (alreadyRegistered) {
|
||||
this.logger.warn('Driver already registered for race, registration aborted', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
throw new Error('Already registered for this race');
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Registering driver ${registration.driverId} for race ${registration.raceId}.`);
|
||||
if (await this.isRegistered(registration.raceId, registration.driverId)) {
|
||||
this.logger.warn(`Driver ${registration.driverId} already registered for race ${registration.raceId}.`);
|
||||
throw new Error('Driver already registered for this race');
|
||||
}
|
||||
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt);
|
||||
this.logger.info('Driver successfully registered for race', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
this.registrations.set(registration.id, registration);
|
||||
this.logger.info(`Driver ${registration.driverId} registered for race ${registration.raceId}.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async withdraw(raceId: string, driverId: string): Promise<void> {
|
||||
this.logger.info('Attempting to withdraw driver from race', { raceId, driverId });
|
||||
const alreadyRegistered = await this.isRegistered(raceId, driverId);
|
||||
if (!alreadyRegistered) {
|
||||
this.logger.warn('Driver not registered for race, withdrawal aborted', { raceId, driverId });
|
||||
throw new Error('Not registered for this race');
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Withdrawing driver ${driverId} from race ${raceId}.`);
|
||||
const key = `${raceId}:${driverId}`;
|
||||
if (!this.registrations.has(key)) {
|
||||
this.logger.warn(`Driver ${driverId} not registered for race ${raceId}. No withdrawal needed.`);
|
||||
throw new Error('Driver not registered for this race');
|
||||
}
|
||||
this.removeFromIndexes(raceId, driverId);
|
||||
this.logger.info('Driver successfully withdrew from race', { raceId, driverId });
|
||||
this.registrations.delete(key);
|
||||
this.logger.info(`Driver ${driverId} withdrawn from race ${raceId}.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getDriverRegistrations(driverId: string): Promise<string[]> {
|
||||
this.logger.info('Attempting to fetch registrations for driver', { driverId });
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (!driverSet) {
|
||||
this.logger.debug('No registrations found for driver', { driverId });
|
||||
return [];
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registrations for driver: ${driverId}.`);
|
||||
const raceIds: string[] = [];
|
||||
for (const registration of this.registrations.values()) {
|
||||
if (registration.driverId === driverId) {
|
||||
raceIds.push(registration.raceId);
|
||||
}
|
||||
}
|
||||
const registrations = Array.from(driverSet.values());
|
||||
this.logger.debug('Found registrations for driver', { driverId, count: registrations.length });
|
||||
this.logger.info('Successfully fetched registrations for driver', { driverId, count: registrations.length });
|
||||
return registrations;
|
||||
this.logger.info(`Found ${raceIds.length} registrations for driver ${driverId}.`);
|
||||
return Promise.resolve(raceIds);
|
||||
}
|
||||
|
||||
async clearRaceRegistrations(raceId: string): Promise<void> {
|
||||
this.logger.info('Attempting to clear all registrations for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('No registrations to clear for race (race set not found)', { raceId });
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug('Found registrations to clear', { raceId, count: raceSet.size });
|
||||
for (const driverId of raceSet.values()) {
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (driverSet) {
|
||||
driverSet.delete(raceId);
|
||||
if (driverSet.size === 0) {
|
||||
this.registrationsByDriver.delete(driverId);
|
||||
this.logger.debug('Deleted driver set as it is now empty during race clear', { raceId, driverId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Driver set not found during race clear, potential inconsistency', { raceId, driverId });
|
||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Clearing all registrations for race: ${raceId}.`);
|
||||
const registrationsToDelete: string[] = [];
|
||||
for (const registration of this.registrations.values()) {
|
||||
if (registration.raceId === raceId) {
|
||||
registrationsToDelete.push(registration.id);
|
||||
}
|
||||
this.logger.debug('Removed race from driver set during race clear', { raceId, driverId });
|
||||
}
|
||||
|
||||
this.registrationsByRace.delete(raceId);
|
||||
this.logger.info('Successfully cleared all registrations for race', { raceId });
|
||||
for (const id of registrationsToDelete) {
|
||||
this.registrations.delete(id);
|
||||
}
|
||||
this.logger.info(`Cleared ${registrationsToDelete.length} registrations for race ${raceId}.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,207 +1,110 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InMemoryRaceRepository
|
||||
*
|
||||
* In-memory implementation of IRaceRepository.
|
||||
* Stores data in Map structure with UUID generation.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRaceRepository implements IRaceRepository {
|
||||
private races: Map<string, Race>;
|
||||
private readonly logger: ILogger;
|
||||
private races: Map<string, Race> = new Map();
|
||||
|
||||
constructor(logger: ILogger, seedData?: Race[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialRaces: Race[] = []) {
|
||||
this.logger.info('InMemoryRaceRepository initialized.');
|
||||
this.races = new Map();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(race => {
|
||||
this.races.set(race.id, race);
|
||||
this.logger.debug(`Seeded race: ${race.id}.`);
|
||||
});
|
||||
for (const race of initialRaces) {
|
||||
this.races.set(race.id, race);
|
||||
this.logger.debug(`Seeded race: ${race.id} (${race.track}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Race | null> {
|
||||
this.logger.debug(`Finding race by id: ${id}`);
|
||||
try {
|
||||
const race = this.races.get(id) ?? null;
|
||||
if (race) {
|
||||
this.logger.info(`Found race: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Race with id ${id} not found.`);
|
||||
}
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding race by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding race by ID: ${id}`);
|
||||
const race = this.races.get(id) ?? null;
|
||||
if (race) {
|
||||
this.logger.info(`Found race by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Race with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(race);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Race[]> {
|
||||
this.logger.debug('Finding all races.');
|
||||
try {
|
||||
const races = Array.from(this.races.values());
|
||||
this.logger.info(`Found ${races.length} races.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all races:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemoryRaceRepository] Finding all races.');
|
||||
return Promise.resolve(Array.from(this.races.values()));
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
this.logger.debug(`Finding races by league id: ${leagueId}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race => race.leagueId === leagueId)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding races by league ID: ${leagueId}`);
|
||||
const races = Array.from(this.races.values()).filter(race => race.leagueId === leagueId);
|
||||
this.logger.info(`Found ${races.length} races for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(races);
|
||||
}
|
||||
|
||||
async findUpcomingByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
this.logger.debug(`Finding upcoming races by league id: ${leagueId}`);
|
||||
try {
|
||||
const now = new Date();
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'scheduled' &&
|
||||
race.scheduledAt > now
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} upcoming races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding upcoming races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding upcoming races by league ID: ${leagueId}`);
|
||||
const now = new Date();
|
||||
const upcomingRaces = Array.from(this.races.values()).filter(race =>
|
||||
race.leagueId === leagueId && race.status === 'scheduled' && race.scheduledAt > now
|
||||
);
|
||||
this.logger.info(`Found ${upcomingRaces.length} upcoming races for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(upcomingRaces);
|
||||
}
|
||||
|
||||
async findCompletedByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
this.logger.debug(`Finding completed races by league id: ${leagueId}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'completed'
|
||||
)
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} completed races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding completed races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding completed races by league ID: ${leagueId}`);
|
||||
const completedRaces = Array.from(this.races.values()).filter(race =>
|
||||
race.leagueId === leagueId && race.status === 'completed'
|
||||
);
|
||||
this.logger.info(`Found ${completedRaces.length} completed races for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(completedRaces);
|
||||
}
|
||||
|
||||
async findByStatus(status: RaceStatus): Promise<Race[]> {
|
||||
this.logger.debug(`Finding races by status: ${status}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race => race.status === status)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races with status: ${status}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding races by status: ${status}.`);
|
||||
const races = Array.from(this.races.values()).filter(race => race.status === status);
|
||||
this.logger.info(`Found ${races.length} races with status: ${status}.`);
|
||||
return Promise.resolve(races);
|
||||
}
|
||||
|
||||
async findByDateRange(startDate: Date, endDate: Date): Promise<Race[]> {
|
||||
this.logger.debug(`Finding races by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.scheduledAt >= startDate &&
|
||||
race.scheduledAt <= endDate
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races in date range.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by date range:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Finding races by date range: ${startDate.toISOString()} - ${endDate.toISOString()}.`);
|
||||
const races = Array.from(this.races.values()).filter(race =>
|
||||
race.scheduledAt >= startDate && race.scheduledAt <= endDate
|
||||
);
|
||||
this.logger.info(`Found ${races.length} races within date range.`);
|
||||
return Promise.resolve(races);
|
||||
}
|
||||
|
||||
async create(race: Race): Promise<Race> {
|
||||
this.logger.debug(`Creating race: ${race.id}`);
|
||||
try {
|
||||
if (await this.exists(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} already exists.`);
|
||||
throw new Error(`Race with ID ${race.id} already exists`);
|
||||
}
|
||||
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} created successfully.`);
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating race ${race.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryRaceRepository] Creating race: ${race.id} (${race.track}).`);
|
||||
if (this.races.has(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} already exists.`);
|
||||
throw new Error('Race already exists');
|
||||
}
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} created successfully.`);
|
||||
return Promise.resolve(race);
|
||||
}
|
||||
|
||||
async update(race: Race): Promise<Race> {
|
||||
this.logger.debug(`Updating race: ${race.id}`);
|
||||
try {
|
||||
if (!await this.exists(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} not found for update.`);
|
||||
throw new Error(`Race with ID ${race.id} not found`);
|
||||
}
|
||||
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} updated successfully.`);
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating race ${race.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemoryRaceRepository] Updating race: ${race.id} (${race.track}).`);
|
||||
if (!this.races.has(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} not found for update.`);
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} updated successfully.`);
|
||||
return Promise.resolve(race);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting race: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Race with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Race with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.races.delete(id);
|
||||
this.logger.debug(`[InMemoryRaceRepository] Deleting race with ID: ${id}.`);
|
||||
if (this.races.delete(id)) {
|
||||
this.logger.info(`Race ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting race ${id}:`, error);
|
||||
throw error;
|
||||
} else {
|
||||
this.logger.warn(`Race with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of race with id: ${id}`);
|
||||
try {
|
||||
const exists = this.races.has(id);
|
||||
this.logger.debug(`Race ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of race with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemoryRaceRepository] Checking existence of race with ID: ${id}.`);
|
||||
return Promise.resolve(this.races.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
*/
|
||||
static generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
|
||||
import { Season } from '@gridpilot/racing/domain/entities/Season';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySeasonRepository implements ISeasonRepository {
|
||||
private seasons: Map<string, Season> = new Map(); // Key: seasonId
|
||||
|
||||
constructor(private readonly logger: ILogger, initialSeasons: Season[] = []) {
|
||||
this.logger.info('InMemorySeasonRepository initialized.');
|
||||
for (const season of initialSeasons) {
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.debug(`Seeded season: ${season.id} (${season.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Season | null> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Finding season by ID: ${id}`);
|
||||
const season = this.seasons.get(id) ?? null;
|
||||
if (season) {
|
||||
this.logger.info(`Found season by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Season with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(season);
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Season[]> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Finding seasons by league ID: ${leagueId}`);
|
||||
const seasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId);
|
||||
this.logger.info(`Found ${seasons.length} seasons for league ID: ${leagueId}.`);
|
||||
return Promise.resolve(seasons);
|
||||
}
|
||||
|
||||
async create(season: Season): Promise<Season> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Creating season: ${season.id} (${season.name})`);
|
||||
if (this.seasons.has(season.id)) {
|
||||
this.logger.warn(`Season with ID ${season.id} already exists.`);
|
||||
throw new Error('Season already exists');
|
||||
}
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.info(`Season ${season.id} created successfully.`);
|
||||
return Promise.resolve(season);
|
||||
}
|
||||
|
||||
async add(season: Season): Promise<void> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Adding season: ${season.id} (${season.name})`);
|
||||
if (this.seasons.has(season.id)) {
|
||||
this.logger.warn(`Season with ID ${season.id} already exists (add method).`);
|
||||
throw new Error('Season already exists');
|
||||
}
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.info(`Season ${season.id} added successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async update(season: Season): Promise<void> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Updating season: ${season.id} (${season.name})`);
|
||||
if (!this.seasons.has(season.id)) {
|
||||
this.logger.warn(`Season with ID ${season.id} not found for update.`);
|
||||
throw new Error('Season not found');
|
||||
}
|
||||
this.seasons.set(season.id, season);
|
||||
this.logger.info(`Season ${season.id} updated successfully.`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Deleting season with ID: ${id}`);
|
||||
if (this.seasons.delete(id)) {
|
||||
this.logger.info(`Season ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Season with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async listByLeague(leagueId: string): Promise<Season[]> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Listing seasons by league ID: ${leagueId}`);
|
||||
const seasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId);
|
||||
return Promise.resolve(seasons);
|
||||
}
|
||||
|
||||
async listActiveByLeague(leagueId: string): Promise<Season[]> {
|
||||
this.logger.debug(`[InMemorySeasonRepository] Listing active seasons by league ID: ${leagueId}`);
|
||||
const activeSeasons = Array.from(this.seasons.values()).filter(season => season.leagueId === leagueId && season.status === 'active');
|
||||
return Promise.resolve(activeSeasons);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,98 @@
|
||||
/**
|
||||
* In-Memory Implementation: ISponsorRepository
|
||||
*
|
||||
* Mock repository for testing and development
|
||||
*/
|
||||
|
||||
import type { Sponsor } from '../../domain/entities/Sponsor';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ISponsorRepository } from '@gridpilot/core/racing/domain/repositories/ISponsorRepository';
|
||||
import { Sponsor } from '@gridpilot/core/racing/domain/entities/Sponsor';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorRepository implements ISponsorRepository {
|
||||
private sponsors: Map<string, Sponsor> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
private emailIndex: Map<string, string> = new Map(); // contactEmail -> sponsorId
|
||||
|
||||
constructor(logger: ILogger, seedData?: Sponsor[]) {
|
||||
this.logger = logger;
|
||||
constructor(private readonly logger: ILogger, initialSponsors: Sponsor[] = []) {
|
||||
this.logger.info('InMemorySponsorRepository initialized.');
|
||||
if (seedData) {
|
||||
this.seed(seedData);
|
||||
for (const sponsor of initialSponsors) {
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id);
|
||||
this.logger.debug(`Seeded sponsor: ${sponsor.id} (${sponsor.name}).`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Sponsor | null> {
|
||||
this.logger.debug(`Finding sponsor by id: ${id}`);
|
||||
try {
|
||||
const sponsor = this.sponsors.get(id) ?? null;
|
||||
if (sponsor) {
|
||||
this.logger.info(`Found sponsor: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with id ${id} not found.`);
|
||||
}
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor by id ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Finding sponsor by ID: ${id}`);
|
||||
const sponsor = this.sponsors.get(id) ?? null;
|
||||
if (sponsor) {
|
||||
this.logger.info(`Found sponsor by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(sponsor);
|
||||
}
|
||||
|
||||
async findAll(): Promise<Sponsor[]> {
|
||||
this.logger.debug('Finding all sponsors.');
|
||||
try {
|
||||
const sponsors = Array.from(this.sponsors.values());
|
||||
this.logger.info(`Found ${sponsors.length} sponsors.`);
|
||||
return sponsors;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all sponsors:', error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug('[InMemorySponsorRepository] Finding all sponsors.');
|
||||
return Promise.resolve(Array.from(this.sponsors.values()));
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<Sponsor | null> {
|
||||
this.logger.debug(`Finding sponsor by email: ${email}`);
|
||||
try {
|
||||
for (const sponsor of this.sponsors.values()) {
|
||||
if (sponsor.contactEmail === email) {
|
||||
this.logger.info(`Found sponsor with email: ${email}.`);
|
||||
return sponsor;
|
||||
}
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorRepository] Finding sponsor by email: ${email}`);
|
||||
const sponsorId = this.emailIndex.get(email.toLowerCase());
|
||||
if (!sponsorId) {
|
||||
this.logger.warn(`Sponsor with email ${email} not found.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor by email ${email}:`, error);
|
||||
throw error;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.findById(sponsorId);
|
||||
}
|
||||
|
||||
async create(sponsor: Sponsor): Promise<Sponsor> {
|
||||
this.logger.debug(`Creating sponsor: ${sponsor.id}`);
|
||||
try {
|
||||
if (this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} already exists.`);
|
||||
throw new Error('Sponsor with this ID already exists');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} created successfully.`);
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating sponsor ${sponsor.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Creating sponsor: ${sponsor.id} (${sponsor.name})`);
|
||||
if (this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} already exists.`);
|
||||
throw new Error('Sponsor already exists');
|
||||
}
|
||||
if (this.emailIndex.has(sponsor.contactEmail.toLowerCase())) {
|
||||
this.logger.warn(`Sponsor with email ${sponsor.contactEmail} already exists.`);
|
||||
throw new Error('Sponsor with this email already exists');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id);
|
||||
this.logger.info(`Sponsor ${sponsor.id} (${sponsor.name}) created successfully.`);
|
||||
return Promise.resolve(sponsor);
|
||||
}
|
||||
|
||||
async update(sponsor: Sponsor): Promise<Sponsor> {
|
||||
this.logger.debug(`Updating sponsor: ${sponsor.id}`);
|
||||
try {
|
||||
if (!this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} not found for update.`);
|
||||
throw new Error('Sponsor not found');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} updated successfully.`);
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating sponsor ${sponsor.id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Updating sponsor: ${sponsor.id} (${sponsor.name})`);
|
||||
if (!this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} not found for update.`);
|
||||
throw new Error('Sponsor not found');
|
||||
}
|
||||
const existingSponsor = this.sponsors.get(sponsor.id);
|
||||
// If email changed, update index
|
||||
if (existingSponsor && existingSponsor.contactEmail.toLowerCase() !== sponsor.contactEmail.toLowerCase()) {
|
||||
if (this.emailIndex.has(sponsor.contactEmail.toLowerCase()) && this.emailIndex.get(sponsor.contactEmail.toLowerCase()) !== sponsor.id) {
|
||||
this.logger.warn(`Cannot update sponsor ${sponsor.id} to email ${sponsor.contactEmail} as it's already taken.`);
|
||||
throw new Error('Sponsor with this email already exists');
|
||||
}
|
||||
this.emailIndex.delete(existingSponsor.contactEmail.toLowerCase());
|
||||
this.emailIndex.set(sponsor.contactEmail.toLowerCase(), sponsor.id);
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} (${sponsor.name}) updated successfully.`);
|
||||
return Promise.resolve(sponsor);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting sponsor: ${id}`);
|
||||
try {
|
||||
if (this.sponsors.delete(id)) {
|
||||
this.logger.info(`Sponsor ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with id ${id} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting sponsor ${id}:`, error);
|
||||
throw error;
|
||||
this.logger.debug(`[InMemorySponsorRepository] Deleting sponsor with ID: ${id}`);
|
||||
const sponsor = this.sponsors.get(id);
|
||||
if (sponsor) {
|
||||
this.sponsors.delete(id);
|
||||
this.emailIndex.delete(sponsor.contactEmail.toLowerCase());
|
||||
this.logger.info(`Sponsor ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsor with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of sponsor with id: ${id}`);
|
||||
try {
|
||||
const exists = this.sponsors.has(id);
|
||||
this.logger.debug(`Sponsor ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsor with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorRepository] Checking existence of sponsor with ID: ${id}`);
|
||||
return Promise.resolve(this.sponsors.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(sponsors: Sponsor[]): void {
|
||||
this.logger.debug(`Seeding ${sponsors.length} sponsors.`);
|
||||
try {
|
||||
for (const sponsor of sponsors) {
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.debug(`Seeded sponsor: ${sponsor.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${sponsors.length} sponsors.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsors:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsors.');
|
||||
this.sponsors.clear();
|
||||
this.logger.info('All sponsors cleared.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,232 +1,112 @@
|
||||
/**
|
||||
* InMemory implementation of ISponsorshipRequestRepository
|
||||
*/
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import {
|
||||
SponsorshipRequest,
|
||||
type SponsorableEntityType,
|
||||
type SponsorshipRequestStatus
|
||||
} from '../../domain/entities/SponsorshipRequest';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import { SponsorshipRequest, SponsorableEntityType, SponsorshipRequestStatus } from '@gridpilot/racing/domain/entities/SponsorshipRequest';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorshipRequestRepository implements ISponsorshipRequestRepository {
|
||||
private requests: Map<string, SponsorshipRequest> = new Map();
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, seedData?: SponsorshipRequest[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemorySponsorshipRequestRepository initialized.');
|
||||
if (seedData) {
|
||||
this.seed(seedData);
|
||||
constructor(private readonly logger: ILogger, initialRequests: SponsorshipRequest[] = []) {
|
||||
this.logger.info('InMemorySponsorshipRequestRepository initialized.');
|
||||
for (const req of initialRequests) {
|
||||
this.requests.set(req.id, req);
|
||||
this.logger.debug(`Seeded sponsorship request: ${req.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<SponsorshipRequest | null> {
|
||||
this.logger.debug(`Finding sponsorship request by id: ${id}`);
|
||||
try {
|
||||
const request = this.requests.get(id) ?? null;
|
||||
if (request) {
|
||||
this.logger.info(`Found sponsorship request: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsorship request with id ${id} not found.`);
|
||||
}
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship request by id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding request by ID: ${id}`);
|
||||
const request = this.requests.get(id) ?? null;
|
||||
if (request) {
|
||||
this.logger.info(`Found request by ID: ${id}.`);
|
||||
} else {
|
||||
this.logger.warn(`Request with ID ${id} not found.`);
|
||||
}
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.entityType === entityType && request.entityId === entityId
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for entity: ${entityType}, ${entityId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests for entity ${entityType}:${entityId}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.entityType === entityType && req.entityId === entityId);
|
||||
this.logger.info(`Found ${requests.length} requests for entity ${entityType}:${entityId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding pending sponsorship requests by entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} pending sponsorship requests for entity: ${entityType}, ${entityId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding pending sponsorship requests by entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding pending requests for entity ${entityType}:${entityId}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.entityType === entityType && req.entityId === entityId && req.status === 'pending');
|
||||
this.logger.info(`Found ${requests.length} pending requests for entity ${entityType}:${entityId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findBySponsorId(sponsorId: string): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for sponsor id: ${sponsorId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by sponsor id ${sponsorId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests by sponsor ID: ${sponsorId}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.sponsorId === sponsorId);
|
||||
this.logger.info(`Found ${requests.length} requests by sponsor ID: ${sponsorId}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findByStatus(status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by status: ${status}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.status === status
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests with status: ${status}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests by status: ${status}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.status === status);
|
||||
this.logger.info(`Found ${requests.length} requests with status: ${status}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async findBySponsorIdAndStatus(sponsorId: string, status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
|
||||
this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId} and status: ${status}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId && request.status === status
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for sponsor id: ${sponsorId}, status: ${status}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by sponsor id ${sponsorId}, status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Finding requests by sponsor ID ${sponsorId} and status ${status}.`);
|
||||
const requests = Array.from(this.requests.values()).filter(req => req.sponsorId === sponsorId && req.status === status);
|
||||
this.logger.info(`Found ${requests.length} requests by sponsor ID ${sponsorId} and status ${status}.`);
|
||||
return Promise.resolve(requests);
|
||||
}
|
||||
|
||||
async hasPendingRequest(sponsorId: string, entityType: SponsorableEntityType, entityId: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking for pending request from sponsor: ${sponsorId} for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const has = Array.from(this.requests.values()).some(
|
||||
request =>
|
||||
request.sponsorId === sponsorId &&
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.debug(`Pending request exists: ${has}.`);
|
||||
return has;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking for pending request from sponsor ${sponsorId} for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Checking for pending request for sponsor ${sponsorId}, entity ${entityType}:${entityId}.`);
|
||||
const exists = Array.from(this.requests.values()).some(req => req.sponsorId === sponsorId && req.entityType === entityType && req.entityId === entityId && req.status === 'pending');
|
||||
this.logger.info(`Pending request for sponsor ${sponsorId}, entity ${entityType}:${entityId} exists: ${exists}.`);
|
||||
return Promise.resolve(exists);
|
||||
}
|
||||
|
||||
async countPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<number> {
|
||||
this.logger.debug(`Counting pending requests for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const count = Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
).length;
|
||||
this.logger.info(`Counted ${count} pending requests for entity: ${entityType}, ${entityId}.`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting pending requests for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Counting pending requests for entity ${entityType}:${entityId}.`);
|
||||
const count = Array.from(this.requests.values()).filter(req => req.entityType === entityType && req.entityId === entityId && req.status === 'pending').length;
|
||||
this.logger.info(`Count of pending requests for entity ${entityType}:${entityId}: ${count}.`);
|
||||
return Promise.resolve(count);
|
||||
}
|
||||
|
||||
async create(request: SponsorshipRequest): Promise<SponsorshipRequest> {
|
||||
this.logger.debug(`Creating sponsorship request: ${request.id}`);
|
||||
try {
|
||||
if (this.requests.has(request.id)) {
|
||||
this.logger.warn(`SponsorshipRequest with ID ${request.id} already exists.`);
|
||||
throw new Error(`SponsorshipRequest with ID ${request.id} already exists`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`SponsorshipRequest ${request.id} created successfully.`);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating sponsorship request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Creating request: ${request.id}.`);
|
||||
if (this.requests.has(request.id)) {
|
||||
this.logger.warn(`Request with ID ${request.id} already exists.`);
|
||||
throw new Error('Sponsorship request already exists');
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`Sponsorship request ${request.id} created successfully.`);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async update(request: SponsorshipRequest): Promise<SponsorshipRequest> {
|
||||
this.logger.debug(`Updating sponsorship request: ${request.id}`);
|
||||
try {
|
||||
if (!this.requests.has(request.id)) {
|
||||
this.logger.warn(`SponsorshipRequest ${request.id} not found for update.`);
|
||||
throw new Error(`SponsorshipRequest ${request.id} not found`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`SponsorshipRequest ${request.id} updated successfully.`);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating sponsorship request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Updating request: ${request.id}.`);
|
||||
if (!this.requests.has(request.id)) {
|
||||
this.logger.warn(`Request with ID ${request.id} not found for update.`);
|
||||
throw new Error('Sponsorship request not found');
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`Sponsorship request ${request.id} updated successfully.`);
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.logger.debug(`Deleting sponsorship request: ${id}`);
|
||||
try {
|
||||
if (this.requests.delete(id)) {
|
||||
this.logger.info(`SponsorshipRequest ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`SponsorshipRequest with id ${id} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting sponsorship request ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Deleting request with ID: ${id}.`);
|
||||
if (this.requests.delete(id)) {
|
||||
this.logger.info(`Sponsorship request ${id} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Request with ID ${id} not found for deletion.`);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking existence of sponsorship request with id: ${id}`);
|
||||
try {
|
||||
const exists = this.requests.has(id);
|
||||
this.logger.debug(`Sponsorship request ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsorship request with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.logger.debug(`[InMemorySponsorshipRequestRepository] Checking existence of request with ID: ${id}.`);
|
||||
return Promise.resolve(this.requests.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(requests: SponsorshipRequest[]): void {
|
||||
this.logger.debug(`Seeding ${requests.length} sponsorship requests.`);
|
||||
try {
|
||||
for (const request of requests) {
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.debug(`Seeded sponsorship request: ${request.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${requests.length} sponsorship requests.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsorship requests:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data (for testing)
|
||||
*/
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsorship requests.');
|
||||
this.requests.clear();
|
||||
this.logger.info('All sponsorship requests cleared.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
adapters/racing/ports/InMemoryDriverRatingProvider.ts
Normal file
32
adapters/racing/ports/InMemoryDriverRatingProvider.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application/ports/DriverRatingProvider';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverRatingProvider implements DriverRatingProvider {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryDriverRatingProvider initialized.');
|
||||
}
|
||||
|
||||
getRating(driverId: string): number | null {
|
||||
this.logger.debug(`[InMemoryDriverRatingProvider] Getting rating for driver: ${driverId}`);
|
||||
// Mock data for demonstration purposes
|
||||
if (driverId === 'driver-1') {
|
||||
return 2500;
|
||||
}
|
||||
if (driverId === 'driver-2') {
|
||||
return 2400;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getRatings(driverIds: string[]): Map<string, number> {
|
||||
this.logger.debug(`[InMemoryDriverRatingProvider] Getting ratings for drivers: ${driverIds.join(', ')}`);
|
||||
const ratingsMap = new Map<string, number>();
|
||||
for (const driverId of driverIds) {
|
||||
const rating = this.getRating(driverId);
|
||||
if (rating !== null) {
|
||||
ratingsMap.set(driverId, rating);
|
||||
}
|
||||
}
|
||||
return ratingsMap;
|
||||
}
|
||||
}
|
||||
33
adapters/racing/services/InMemoryDriverStatsService.ts
Normal file
33
adapters/racing/services/InMemoryDriverStatsService.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { IDriverStatsService, DriverStats } from '@gridpilot/racing/domain/services/IDriverStatsService';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverStatsService implements IDriverStatsService {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryDriverStatsService initialized.');
|
||||
}
|
||||
|
||||
getDriverStats(driverId: string): DriverStats | null {
|
||||
this.logger.debug(`[InMemoryDriverStatsService] Getting stats for driver: ${driverId}`);
|
||||
|
||||
// Mock data for demonstration purposes
|
||||
if (driverId === 'driver-1') {
|
||||
return {
|
||||
rating: 2500,
|
||||
wins: 10,
|
||||
podiums: 15,
|
||||
totalRaces: 50,
|
||||
overallRank: 1,
|
||||
};
|
||||
}
|
||||
if (driverId === 'driver-2') {
|
||||
return {
|
||||
rating: 2400,
|
||||
wins: 8,
|
||||
podiums: 12,
|
||||
totalRaces: 45,
|
||||
overallRank: 2,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
20
adapters/racing/services/InMemoryRankingService.ts
Normal file
20
adapters/racing/services/InMemoryRankingService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { IRankingService, DriverRanking } from '@gridpilot/racing/domain/services/IRankingService';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRankingService implements IRankingService {
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('InMemoryRankingService initialized.');
|
||||
}
|
||||
|
||||
getAllDriverRankings(): DriverRanking[] {
|
||||
this.logger.debug('[InMemoryRankingService] Getting all driver rankings.');
|
||||
|
||||
// Mock data for demonstration purposes
|
||||
const mockRankings: DriverRanking[] = [
|
||||
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
|
||||
{ driverId: 'driver-2', rating: 2400, overallRank: 2 },
|
||||
{ driverId: 'driver-3', rating: 2300, overallRank: 3 },
|
||||
];
|
||||
return mockRankings;
|
||||
}
|
||||
}
|
||||
22
adapters/tsconfig.json
Normal file
22
adapters/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "../", // Base URL is the project root
|
||||
"paths": {
|
||||
"@gridpilot/core/*": ["core/*"],
|
||||
"@gridpilot/shared/*": ["core/shared/*"],
|
||||
"@gridpilot/identity/application/dto/*": ["core/identity/application/dto/*"],
|
||||
"@gridpilot/identity/application/ports/*": ["core/identity/application/ports/*"],
|
||||
"@gridpilot/identity/domain/repositories/*": ["core/identity/domain/repositories/*"],
|
||||
"@gridpilot/identity/domain/services/*": ["core/identity/domain/services/*"],
|
||||
"@gridpilot/racing/domain/repositories/*": ["core/racing/domain/repositories/*"],
|
||||
"@gridpilot/racing/domain/entities/*": ["core/racing/domain/entities/*"],
|
||||
"@gridpilot/racing/application/ports/*": ["core/racing/application/ports/*"]
|
||||
},
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "../" // Root directory is the project root
|
||||
},
|
||||
"include": ["**/*", "../core/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
|
||||
}
|
||||
6794
apps/api/package-lock.json
generated
Normal file
6794
apps/api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,15 +13,18 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@nestjs/testing": "^10.4.20",
|
||||
"@types/jest": "^30.0.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"@nestjs/testing": "^10.4.20"
|
||||
"ts-node-dev": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.20",
|
||||
"@nestjs/core": "^10.4.20",
|
||||
"@nestjs/platform-express": "^10.4.20",
|
||||
"@nestjs/swagger": "^7.4.2",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"pg": "^8.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -2,13 +2,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HelloController } from './presentation/hello.controller';
|
||||
import { HelloService } from './application/hello/hello.service';
|
||||
import { AnalyticsModule } from './application/analytics/analytics.module';
|
||||
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
|
||||
import { DatabaseModule } from './infrastructure/database/database.module';
|
||||
import { AuthModule } from './modules/auth/AuthModule';
|
||||
import { LeagueModule } from './modules/league/LeagueModule';
|
||||
import { RaceModule } from './modules/race/RaceModule';
|
||||
import { TeamModule } from './modules/team/TeamModule';
|
||||
import { SponsorModule } from './modules/sponsor/SponsorModule';
|
||||
import { DriverModule } from './modules/driver/DriverModule';
|
||||
import { MediaModule } from './modules/media/MediaModule';
|
||||
import { PaymentsModule } from './modules/payments/PaymentsModule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
AnalyticsModule
|
||||
AnalyticsModule,
|
||||
AuthModule,
|
||||
LeagueModule,
|
||||
RaceModule,
|
||||
TeamModule,
|
||||
SponsorModule,
|
||||
DriverModule,
|
||||
MediaModule,
|
||||
PaymentsModule,
|
||||
],
|
||||
controllers: [HelloController],
|
||||
providers: [HelloService],
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
|
||||
import { InMemoryPageViewRepository } from '../../../../adapters/persistence/inmemory/analytics/InMemoryPageViewRepository';
|
||||
import { TypeOrmEngagementRepository } from '../../../../adapters/persistence/typeorm/analytics/TypeOrmEngagementRepository';
|
||||
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
|
||||
import { AnalyticsController } from '../../presentation/analytics.controller';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
{
|
||||
provide: ILogger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useFactory: (dataSource: DataSource) => new TypeOrmEngagementRepository(dataSource.manager),
|
||||
inject: [getDataSourceToken()],
|
||||
},
|
||||
{
|
||||
provide: RecordPageViewUseCase,
|
||||
useFactory: (repo: IPageViewRepository, logger: ILogger) => new RecordPageViewUseCase(repo, logger),
|
||||
inject: [IPAGE_VIEW_REPO_TOKEN, ILogger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RecordEngagementUseCase,
|
||||
useFactory: (repo: IEngagementRepository, logger: ILogger) => new RecordEngagementUseCase(repo, logger),
|
||||
inject: [IENGAGEMENT_REPO_TOKEN, ILogger_TOKEN],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
29
apps/api/src/modules/analytics/AnalyticsController.ts
Normal file
29
apps/api/src/modules/analytics/AnalyticsController.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { RecordPageViewInput, RecordPageViewOutput, RecordEngagementInput, RecordEngagementOutput } from './dto/AnalyticsDto';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
@Post('page-view')
|
||||
async recordPageView(
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.analyticsService.recordPageView(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
async recordEngagement(
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.analyticsService.recordEngagement(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
}
|
||||
43
apps/api/src/modules/analytics/AnalyticsModule.ts
Normal file
43
apps/api/src/modules/analytics/AnalyticsModule.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
|
||||
import { InMemoryPageViewRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
||||
import { InMemoryEngagementRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
AnalyticsService,
|
||||
{
|
||||
provide: ILogger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IPAGE_VIEW_REPO_TOKEN,
|
||||
useClass: InMemoryPageViewRepository,
|
||||
},
|
||||
{
|
||||
provide: IENGAGEMENT_REPO_TOKEN,
|
||||
useExisting: InMemoryEngagementRepository, // Assuming TypeOrmEngagementRepository is not available
|
||||
},
|
||||
// No need for useExisting here if the original intent was to inject the concrete class when providing the TOKEN
|
||||
],
|
||||
exports: [
|
||||
AnalyticsService,
|
||||
ILogger_TOKEN,
|
||||
IPAGE_VIEW_REPO_TOKEN,
|
||||
IENGAGEMENT_REPO_TOKEN,
|
||||
],
|
||||
})
|
||||
export class AnalyticsModule {}
|
||||
83
apps/api/src/modules/analytics/AnalyticsService.ts
Normal file
83
apps/api/src/modules/analytics/AnalyticsService.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { RecordEngagementInput, RecordEngagementOutput, RecordPageViewInput, RecordPageViewOutput } from './dto/AnalyticsDto';
|
||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { PageView } from '@gridpilot/analytics/domain/entities/PageView';
|
||||
import { EngagementEvent } from '@gridpilot/analytics/domain/entities/EngagementEvent';
|
||||
|
||||
const ILogger_TOKEN = 'ILogger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
constructor(
|
||||
@Inject(IPAGE_VIEW_REPO_TOKEN) private readonly pageViewRepository: IPageViewRepository,
|
||||
@Inject(IENGAGEMENT_REPO_TOKEN) private readonly engagementRepository: IEngagementRepository,
|
||||
@Inject(ILogger_TOKEN) private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
this.logger.debug('Executing RecordPageViewUseCase', { input });
|
||||
try {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
visitorType: input.visitorType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
});
|
||||
|
||||
await this.pageViewRepository.save(pageView);
|
||||
this.logger.info('Page view recorded successfully', { pageViewId, input });
|
||||
return { pageViewId };
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording page view', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
this.logger.debug('Executing RecordEngagementUseCase', { input });
|
||||
try {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
action: input.action as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
});
|
||||
|
||||
await this.engagementRepository.save(event);
|
||||
this.logger.info('Engagement recorded successfully', { eventId, input });
|
||||
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording engagement', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
apps/api/src/modules/analytics/dto/AnalyticsDto.ts
Normal file
127
apps/api/src/modules/analytics/dto/AnalyticsDto.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsEnum, IsBoolean, IsNumber, IsObject } from 'class-validator';
|
||||
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum EntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum VisitorType {
|
||||
ANONYMOUS = 'anonymous',
|
||||
DRIVER = 'driver',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
export class RecordPageViewInput {
|
||||
@ApiProperty({ enum: EntityType })
|
||||
@IsEnum(EntityType)
|
||||
entityType: EntityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
visitorId?: string;
|
||||
|
||||
@ApiProperty({ enum: VisitorType })
|
||||
@IsEnum(VisitorType)
|
||||
visitorType: VisitorType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referrer?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userAgent?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export class RecordPageViewOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
pageViewId: string;
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementAction {
|
||||
CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
|
||||
CLICK_SPONSOR_URL = 'click_sponsor_url',
|
||||
DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
|
||||
JOIN_LEAGUE = 'join_league',
|
||||
REGISTER_RACE = 'register_race',
|
||||
VIEW_STANDINGS = 'view_standings',
|
||||
VIEW_SCHEDULE = 'view_schedule',
|
||||
SHARE_SOCIAL = 'share_social',
|
||||
CONTACT_SPONSOR = 'contact_sponsor',
|
||||
}
|
||||
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementEntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
}
|
||||
|
||||
export class RecordEngagementInput {
|
||||
@ApiProperty({ enum: EngagementAction })
|
||||
@IsEnum(EngagementAction)
|
||||
action: EngagementAction;
|
||||
|
||||
@ApiProperty({ enum: EngagementEntityType })
|
||||
@IsEnum(EngagementEntityType)
|
||||
entityType: EngagementEntityType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
entityId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
actorId?: string;
|
||||
|
||||
@ApiProperty({ enum: ['anonymous', 'driver', 'sponsor'] })
|
||||
@IsEnum(['anonymous', 'driver', 'sponsor'])
|
||||
actorType: 'anonymous' | 'driver' | 'sponsor';
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ required: false, type: 'object'/*, additionalProperties: { type: 'string' || 'number' || 'boolean' }*/ })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
export class RecordEngagementOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
eventId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
engagementWeight: number;
|
||||
}
|
||||
42
apps/api/src/modules/auth/AuthController.ts
Normal file
42
apps/api/src/modules/auth/AuthController.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParams, SignupParams, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('signup')
|
||||
async signup(@Body() params: SignupParams) {
|
||||
return this.authService.signupWithEmail(params);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() params: LoginParams) {
|
||||
return this.authService.loginWithEmail(params);
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
async getSession() {
|
||||
return this.authService.getCurrentSession();
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
async logout() {
|
||||
return this.authService.logout();
|
||||
}
|
||||
|
||||
@Get('iracing/start')
|
||||
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response) {
|
||||
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
|
||||
// In real application, you might want to store 'state' in a secure cookie or session.
|
||||
// For this example, we'll just redirect.
|
||||
res.redirect(HttpStatus.FOUND, redirectUrl);
|
||||
}
|
||||
|
||||
@Get('iracing/callback')
|
||||
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string) {
|
||||
return this.authService.loginWithIracingCallback({ code, state, returnTo });
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/auth/AuthModule.ts
Normal file
10
apps/api/src/modules/auth/AuthModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
import { AuthController } from './AuthController';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
64
apps/api/src/modules/auth/AuthProviders.ts
Normal file
64
apps/api/src/modules/auth/AuthProviders.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
|
||||
// Import interfaces and concrete implementations
|
||||
import { IAuthRepository } from '@gridpilot/core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@gridpilot/core/identity/domain/repositories/IUserRepository';
|
||||
import { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
import { ILogger } from '@gridpilot/core/shared/logging/ILogger';
|
||||
|
||||
import { InMemoryAuthRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
||||
import { InMemoryUserRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
import { InMemoryPasswordHashingService } from '../../../adapters/identity/services/InMemoryPasswordHashingService';
|
||||
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
|
||||
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort'; // Path from apps/api/src/modules/auth
|
||||
import { CookieIdentitySessionAdapter } from '../../../adapters/identity/session/CookieIdentitySessionAdapter';
|
||||
|
||||
// Define the tokens for dependency injection
|
||||
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
|
||||
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
|
||||
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
|
||||
export const LOGGER_TOKEN = 'ILogger';
|
||||
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
AuthService, // Provide the service itself
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: ILogger) => {
|
||||
// Seed initial users for InMemoryUserRepository
|
||||
const initialUsers: StoredUser[] = [
|
||||
// Example user (replace with actual test users as needed)
|
||||
{
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
passwordHash: 'demo_salt_moc.elpmaxe@tset', // "test@example.com" reversed
|
||||
displayName: 'Test User',
|
||||
salt: '', // Handled by hashing service
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
|
||||
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
|
||||
},
|
||||
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryUserRepository(logger), // Factory for InMemoryUserRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
useClass: InMemoryPasswordHashingService,
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IDENTITY_SESSION_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new CookieIdentitySessionAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
140
apps/api/src/modules/auth/AuthService.ts
Normal file
140
apps/api/src/modules/auth/AuthService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
// Core Use Cases
|
||||
import { LoginUseCase } from '../../../../core/identity/application/use-cases/LoginUseCase';
|
||||
import { SignupUseCase } from '../../../../core/identity/application/use-cases/SignupUseCase';
|
||||
import { GetCurrentSessionUseCase } from '../../../../core/identity/application/use-cases/GetCurrentSessionUseCase';
|
||||
import { LogoutUseCase } from '../../../../core/identity/application/use-cases/LogoutUseCase';
|
||||
import { StartIracingAuthRedirectUseCase } from '../../../../core/identity/application/use-cases/StartIracingAuthRedirectUseCase';
|
||||
import { LoginWithIracingCallbackUseCase } from '../../../../core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
|
||||
|
||||
// Core Interfaces and Tokens
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
import { IAuthRepository } from '../../../../core/identity/domain/repositories/IAuthRepository';
|
||||
import { IPasswordHashingService } from '../../../../core/identity/domain/services/PasswordHashingService';
|
||||
import { ILogger } from '../../../../core/shared/logging/ILogger';
|
||||
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort';
|
||||
import { UserId } from '../../../../core/identity/domain/value-objects/UserId';
|
||||
import { User } from '../../../../core/identity/domain/entities/User';
|
||||
import { IUserRepository } from '../../../../core/identity/domain/repositories/IUserRepository';
|
||||
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '../../../../core/identity/application/dto/AuthenticatedUserDTO';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly loginUseCase: LoginUseCase;
|
||||
private readonly signupUseCase: SignupUseCase;
|
||||
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
|
||||
private readonly logoutUseCase: LogoutUseCase;
|
||||
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
|
||||
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
|
||||
|
||||
constructor(
|
||||
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
|
||||
@Inject(PASSWORD_HASHING_SERVICE_TOKEN) private passwordHashingService: IPasswordHashingService,
|
||||
@Inject(LOGGER_TOKEN) private logger: ILogger,
|
||||
@Inject(IDENTITY_SESSION_PORT_TOKEN) private identitySessionPort: IdentitySessionPort,
|
||||
@Inject(USER_REPOSITORY_TOKEN) private userRepository: IUserRepository, // Inject IUserRepository here
|
||||
) {
|
||||
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
|
||||
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
|
||||
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
|
||||
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
|
||||
}
|
||||
|
||||
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
|
||||
return {
|
||||
userId: user.getId().value,
|
||||
email: user.getEmail(),
|
||||
displayName: user.getDisplayName(),
|
||||
// Map other fields as necessary
|
||||
iracingCustomerId: user.getIracingCustomerId() ?? undefined,
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
avatarUrl: user.getAvatarUrl() ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
this.logger.debug('[AuthService] Attempting to get current session.');
|
||||
const coreSession = await this.identitySessionPort.getCurrentSession();
|
||||
if (!coreSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(coreSession.user.id); // Use userRepository to fetch full user
|
||||
if (!user) {
|
||||
// If session exists but user doesn't in DB, perhaps clear session?
|
||||
this.logger.warn(`[AuthService] Session found for user ID ${coreSession.user.id}, but user not found in repository.`);
|
||||
await this.identitySessionPort.clearSession(); // Clear potentially stale session
|
||||
return null;
|
||||
}
|
||||
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(User.fromStored(user));
|
||||
|
||||
return {
|
||||
token: coreSession.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
|
||||
const user = await this.signupUseCase.execute(params.email, params.password, params.displayName);
|
||||
|
||||
// Create session after successful signup
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
|
||||
try {
|
||||
const user = await this.loginUseCase.execute(params.email, params.password);
|
||||
// Create session after successful login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`[AuthService] Login failed for email ${params.email}:`, error);
|
||||
throw new InternalServerErrorException('Login failed due to invalid credentials or server error.');
|
||||
}
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
|
||||
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
|
||||
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
|
||||
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
|
||||
return { redirectUrl: result.redirectUrl, state: result.state };
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
|
||||
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
|
||||
|
||||
// Create session after successful iRacing login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.logger.debug('[AuthService] Attempting logout.');
|
||||
await this.logoutUseCase.execute();
|
||||
}
|
||||
}
|
||||
55
apps/api/src/modules/auth/dto/AuthDto.ts
Normal file
55
apps/api/src/modules/auth/dto/AuthDto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthenticatedUserDTO {
|
||||
@ApiProperty()
|
||||
userId: string;
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export class AuthSessionDTO {
|
||||
@ApiProperty()
|
||||
token: string;
|
||||
@ApiProperty()
|
||||
user: AuthenticatedUserDTO;
|
||||
}
|
||||
|
||||
export class SignupParams {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
@ApiProperty()
|
||||
displayName: string;
|
||||
@ApiProperty({ required: false })
|
||||
iracingCustomerId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
primaryDriverId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class LoginParams {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class IracingAuthRedirectResult {
|
||||
@ApiProperty()
|
||||
redirectUrl: string;
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
}
|
||||
|
||||
export class LoginWithIracingCallbackParams {
|
||||
@ApiProperty()
|
||||
code: string;
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
@ApiProperty({ required: false })
|
||||
returnTo?: string;
|
||||
}
|
||||
49
apps/api/src/modules/driver/DriverController.ts
Normal file
49
apps/api/src/modules/driver/DriverController.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
|
||||
|
||||
@ApiTags('drivers')
|
||||
@Controller('drivers')
|
||||
export class DriverController {
|
||||
constructor(private readonly driverService: DriverService) {}
|
||||
|
||||
@Get('leaderboard')
|
||||
@ApiOperation({ summary: 'Get drivers leaderboard' })
|
||||
@ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardViewModel })
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
return this.driverService.getDriversLeaderboard();
|
||||
}
|
||||
|
||||
@Get('total-drivers')
|
||||
@ApiOperation({ summary: 'Get the total number of drivers' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDto })
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
return this.driverService.getTotalDrivers();
|
||||
}
|
||||
|
||||
@Post('complete-onboarding')
|
||||
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
|
||||
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
|
||||
async completeOnboarding(
|
||||
@Body() input: CompleteOnboardingInput,
|
||||
@Req() req: Request,
|
||||
): Promise<CompleteOnboardingOutput> {
|
||||
// Assuming userId is available from the request (e.g., via auth middleware)
|
||||
const userId = req['user'].userId; // Placeholder for actual user extraction
|
||||
return this.driverService.completeOnboarding(userId, input);
|
||||
}
|
||||
|
||||
@Get(':driverId/races/:raceId/registration-status')
|
||||
@ApiOperation({ summary: 'Get driver registration status for a specific race' })
|
||||
@ApiResponse({ status: 200, description: 'Driver registration status', type: DriverRegistrationStatusViewModel })
|
||||
async getDriverRegistrationStatus(
|
||||
@Param('driverId') driverId: string,
|
||||
@Param('raceId') raceId: string,
|
||||
): Promise<DriverRegistrationStatusViewModel> {
|
||||
return this.driverService.getDriverRegistrationStatus({ driverId, raceId });
|
||||
}
|
||||
|
||||
// Add other Driver endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/driver/DriverModule.ts
Normal file
10
apps/api/src/modules/driver/DriverModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriverController } from './DriverController';
|
||||
|
||||
@Module({
|
||||
controllers: [DriverController],
|
||||
providers: [DriverService],
|
||||
exports: [DriverService],
|
||||
})
|
||||
export class DriverModule {}
|
||||
75
apps/api/src/modules/driver/DriverProviders.ts
Normal file
75
apps/api/src/modules/driver/DriverProviders.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { DriverService } from './DriverService';
|
||||
|
||||
// Import core interfaces
|
||||
import { IDriverRepository } from '../../../../core/racing/domain/repositories/IDriverRepository';
|
||||
import { IRankingService } from '../../../../core/racing/domain/services/IRankingService';
|
||||
import { IDriverStatsService } from '../../../../core/racing/domain/services/IDriverStatsService';
|
||||
import { DriverRatingProvider } from '../../../../core/racing/application/ports/DriverRatingProvider';
|
||||
import { IImageServicePort } from '../../../../core/racing/application/ports/IImageServicePort';
|
||||
import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import { ILogger } from '../../../../core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService';
|
||||
import { InMemoryDriverStatsService } from '../../../adapters/racing/services/InMemoryDriverStatsService';
|
||||
import { InMemoryDriverRatingProvider } from '../../../adapters/racing/ports/InMemoryDriverRatingProvider';
|
||||
import { InMemoryImageServiceAdapter } from '../../../adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { InMemoryRaceRegistrationRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '../../../adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const RANKING_SERVICE_TOKEN = 'IRankingService';
|
||||
export const DRIVER_STATS_SERVICE_TOKEN = 'IDriverStatsService';
|
||||
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
|
||||
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
|
||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
DriverService, // Provide the service itself
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RANKING_SERVICE_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRankingService(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_STATS_SERVICE_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverStatsService(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_RATING_PROVIDER_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryDriverRatingProvider(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IMAGE_SERVICE_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryImageServiceAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRaceRegistrationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryNotificationPreferenceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
];
|
||||
46
apps/api/src/modules/driver/DriverService.ts
Normal file
46
apps/api/src/modules/driver/DriverService.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto';
|
||||
|
||||
@Injectable()
|
||||
export class DriverService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||
console.log('[DriverService] Returning mock driver leaderboard.');
|
||||
const drivers: DriverLeaderboardItemViewModel[] = [
|
||||
{ id: 'driver-1', name: 'Mock Driver 1', rating: 2500, skillLevel: 'Pro', nationality: 'DE', racesCompleted: 50, wins: 10, podiums: 20, isActive: true, rank: 1, avatarUrl: 'https://cdn.example.com/avatars/driver-1.png' },
|
||||
{ id: 'driver-2', name: 'Mock Driver 2', rating: 2400, skillLevel: 'Amateur', nationality: 'US', racesCompleted: 40, wins: 5, podiums: 15, isActive: true, rank: 2, avatarUrl: 'https://cdn.example.com/avatars/driver-2.png' },
|
||||
];
|
||||
return {
|
||||
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
|
||||
totalRaces: drivers.reduce((sum, item) => sum + (item.racesCompleted ?? 0), 0),
|
||||
totalWins: drivers.reduce((sum, item) => sum + (item.wins ?? 0), 0),
|
||||
activeCount: drivers.filter(d => d.isActive).length,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalDrivers(): Promise<DriverStatsDto> {
|
||||
console.log('[DriverService] Returning mock total drivers.');
|
||||
return {
|
||||
totalDrivers: 2,
|
||||
};
|
||||
}
|
||||
|
||||
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
|
||||
console.log('Completing onboarding for user:', userId, input);
|
||||
return {
|
||||
success: true,
|
||||
driverId: `driver-${userId}-onboarded`,
|
||||
};
|
||||
}
|
||||
|
||||
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
|
||||
console.log('Checking driver registration status:', query);
|
||||
return {
|
||||
isRegistered: false, // Mock response
|
||||
raceId: query.raceId,
|
||||
driverId: query.driverId,
|
||||
};
|
||||
}
|
||||
}
|
||||
138
apps/api/src/modules/driver/dto/DriverDto.ts
Normal file
138
apps/api/src/modules/driver/dto/DriverDto.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
|
||||
|
||||
export class DriverLeaderboardItemViewModel {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
rating: number;
|
||||
|
||||
@ApiProperty()
|
||||
skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
|
||||
|
||||
@ApiProperty()
|
||||
nationality: string;
|
||||
|
||||
@ApiProperty()
|
||||
racesCompleted: number;
|
||||
|
||||
@ApiProperty()
|
||||
wins: number;
|
||||
|
||||
@ApiProperty()
|
||||
podiums: number;
|
||||
|
||||
@ApiProperty()
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
rank: number;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class DriversLeaderboardViewModel {
|
||||
@ApiProperty({ type: [DriverLeaderboardItemViewModel] })
|
||||
drivers: DriverLeaderboardItemViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalRaces: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalWins: number;
|
||||
|
||||
@ApiProperty()
|
||||
activeCount: number;
|
||||
}
|
||||
|
||||
export class DriverStatsDto {
|
||||
@ApiProperty()
|
||||
totalDrivers: number;
|
||||
}
|
||||
|
||||
export class CompleteOnboardingInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
displayName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
country: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timezone?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export class CompleteOnboardingOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
driverId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export class GetDriverRegistrationStatusQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class DriverRegistrationStatusViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isRegistered: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class DriverDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string; // Display name or full name
|
||||
}
|
||||
|
||||
// Add other DTOs for driver-related logic as needed
|
||||
136
apps/api/src/modules/league/LeagueController.ts
Normal file
136
apps/api/src/modules/league/LeagueController.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel } from './dto/LeagueDto';
|
||||
import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries
|
||||
|
||||
@ApiTags('leagues')
|
||||
@Controller('leagues')
|
||||
export class LeagueController {
|
||||
constructor(private readonly leagueService: LeagueService) {}
|
||||
|
||||
@Get('all-with-capacity')
|
||||
@ApiOperation({ summary: 'Get all leagues with their capacity information' })
|
||||
@ApiResponse({ status: 200, description: 'List of leagues with capacity', type: AllLeaguesWithCapacityViewModel })
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Get('total-leagues')
|
||||
@ApiOperation({ summary: 'Get the total number of leagues' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of leagues', type: LeagueStatsDto })
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
return this.leagueService.getTotalLeagues();
|
||||
}
|
||||
|
||||
@Get(':leagueId/join-requests')
|
||||
@ApiOperation({ summary: 'Get all outstanding join requests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of join requests', type: [LeagueJoinRequestViewModel] })
|
||||
async getJoinRequests(@Param('leagueId') leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
// No specific query DTO needed for GET, leagueId from param
|
||||
return this.leagueService.getLeagueJoinRequests(leagueId);
|
||||
}
|
||||
|
||||
@Post(':leagueId/join-requests/approve')
|
||||
@ApiOperation({ summary: 'Approve a league join request' })
|
||||
@ApiBody({ type: ApproveJoinRequestInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Join request approved', type: ApproveJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async approveJoinRequest(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() input: ApproveJoinRequestInput,
|
||||
): Promise<ApproveJoinRequestOutput> {
|
||||
return this.leagueService.approveLeagueJoinRequest({ ...input, leagueId });
|
||||
}
|
||||
|
||||
@Post(':leagueId/join-requests/reject')
|
||||
@ApiOperation({ summary: 'Reject a league join request' })
|
||||
@ApiBody({ type: RejectJoinRequestInput })
|
||||
@ApiResponse({ status: 200, description: 'Join request rejected', type: RejectJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async rejectJoinRequest(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() input: RejectJoinRequestInput,
|
||||
): Promise<RejectJoinRequestOutput> {
|
||||
return this.leagueService.rejectLeagueJoinRequest({ ...input, leagueId });
|
||||
}
|
||||
|
||||
@Get(':leagueId/permissions/:performerDriverId')
|
||||
@ApiOperation({ summary: 'Get league admin permissions for a performer' })
|
||||
@ApiResponse({ status: 200, description: 'League admin permissions', type: LeagueAdminPermissionsViewModel })
|
||||
async getLeagueAdminPermissions(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
): Promise<LeagueAdminPermissionsViewModel> {
|
||||
// No specific input DTO needed for Get, parameters from path
|
||||
return this.leagueService.getLeagueAdminPermissions({ leagueId, performerDriverId });
|
||||
}
|
||||
|
||||
@Patch(':leagueId/members/:targetDriverId/remove')
|
||||
@ApiOperation({ summary: 'Remove a member from the league' })
|
||||
@ApiBody({ type: RemoveLeagueMemberInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Member removed successfully', type: RemoveLeagueMemberOutput })
|
||||
@ApiResponse({ status: 400, description: 'Cannot remove member' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async removeLeagueMember(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: RemoveLeagueMemberInput, // Body content for a patch often includes IDs
|
||||
): Promise<RemoveLeagueMemberOutput> {
|
||||
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
|
||||
}
|
||||
|
||||
@Patch(':leagueId/members/:targetDriverId/role')
|
||||
@ApiOperation({ summary: "Update a member's role in the league" })
|
||||
@ApiBody({ type: UpdateLeagueMemberRoleInput }) // Explicitly define body type for Swagger
|
||||
@ApiResponse({ status: 200, description: 'Member role updated successfully', type: UpdateLeagueMemberRoleOutput })
|
||||
@ApiResponse({ status: 400, description: 'Cannot update role' })
|
||||
@ApiResponse({ status: 404, description: 'Member not found' })
|
||||
async updateLeagueMemberRole(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: UpdateLeagueMemberRoleInput, // Body includes newRole, other for swagger
|
||||
): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
return this.leagueService.updateLeagueMemberRole({ leagueId, performerDriverId, targetDriverId, newRole: input.newRole });
|
||||
}
|
||||
|
||||
@Get(':leagueId/owner-summary/:ownerId')
|
||||
@ApiOperation({ summary: 'Get owner summary for a league' })
|
||||
@ApiResponse({ status: 200, description: 'League owner summary', type: LeagueOwnerSummaryViewModel })
|
||||
@ApiResponse({ status: 404, description: 'Owner or league not found' })
|
||||
async getLeagueOwnerSummary(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('ownerId') ownerId: string,
|
||||
): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId };
|
||||
return this.leagueService.getLeagueOwnerSummary(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/config')
|
||||
@ApiOperation({ summary: 'Get league full configuration' })
|
||||
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDto })
|
||||
async getLeagueFullConfig(
|
||||
@Param('leagueId') leagueId: string,
|
||||
): Promise<LeagueConfigFormModelDto | null> {
|
||||
const query: GetLeagueAdminConfigQuery = { leagueId };
|
||||
return this.leagueService.getLeagueFullConfig(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/protests')
|
||||
@ApiOperation({ summary: 'Get protests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsViewModel })
|
||||
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||
const query: GetLeagueProtestsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueProtests(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/seasons')
|
||||
@ApiOperation({ summary: 'Get seasons for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryViewModel] })
|
||||
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
const query: GetLeagueSeasonsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueSeasons(query);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/league/LeagueModule.ts
Normal file
10
apps/api/src/modules/league/LeagueModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { LeagueController } from './LeagueController';
|
||||
|
||||
@Module({
|
||||
controllers: [LeagueController],
|
||||
providers: [LeagueService],
|
||||
exports: [LeagueService],
|
||||
})
|
||||
export class LeagueModule {}
|
||||
83
apps/api/src/modules/league/LeagueProviders.ts
Normal file
83
apps/api/src/modules/league/LeagueProviders.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { LeagueService } from './LeagueService';
|
||||
|
||||
// Import core interfaces
|
||||
import { ILeagueRepository } from 'core/racing/domain/repositories/ILeagueRepository';
|
||||
import { ILeagueMembershipRepository } from 'core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { ILeagueStandingsRepository } from 'core/league/application/ports/ILeagueStandingsRepository';
|
||||
import { ISeasonRepository } from 'core/racing/domain/repositories/ISeasonRepository';
|
||||
import { ILeagueScoringConfigRepository } from 'core/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import { IGameRepository } from 'core/racing/domain/repositories/IGameRepository';
|
||||
import { IProtestRepository } from 'core/racing/domain/repositories/IProtestRepository';
|
||||
import { IRaceRepository } from 'core/racing/domain/repositories/IRaceRepository';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryLeagueStandingsRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||
import { InMemorySeasonRepository } from 'adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueScoringConfigRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||
import { InMemoryGameRepository } from 'adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||
import { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository';
|
||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
|
||||
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
|
||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
export const LeagueProviders: Provider[] = [
|
||||
LeagueService, // Provide the service itself
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueRepository(logger), // Factory for InMemoryLeagueRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueMembershipRepository(logger), // Factory for InMemoryLeagueMembershipRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SEASON_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GAME_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryGameRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PROTEST_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryProtestRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
];
|
||||
125
apps/api/src/modules/league/LeagueService.ts
Normal file
125
apps/api/src/modules/league/LeagueService.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto';
|
||||
import { DriverDto } from '../driver/dto/DriverDto'; // Using the local DTO for mock data
|
||||
import { RaceDto } from '../race/dto/RaceDto'; // Using the local DTO for mock data
|
||||
|
||||
const mockDriverData: Map<string, DriverDto> = new Map();
|
||||
mockDriverData.set('driver-owner-1', { id: 'driver-owner-1', name: 'Owner Driver' });
|
||||
mockDriverData.set('driver-1', { id: 'driver-1', name: 'Demo Driver 1' });
|
||||
mockDriverData.set('driver-2', { id: 'driver-2', name: 'Demo Driver 2' });
|
||||
|
||||
const mockRaceData: Map<string, RaceDto> = new Map();
|
||||
mockRaceData.set('race-1', { id: 'race-1', name: 'Test Race 1', date: new Date().toISOString() });
|
||||
mockRaceData.set('race-2', { id: 'race-2', name: 'Test Race 2', date: new Date().toISOString() });
|
||||
|
||||
@Injectable()
|
||||
export class LeagueService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
console.log('[LeagueService] Returning mock leagues with capacity.');
|
||||
return {
|
||||
leagues: [
|
||||
{ id: 'league-1', name: 'Global Racing', description: 'The premier league', ownerId: 'owner-1', settings: { maxDrivers: 100 }, createdAt: new Date().toISOString(), usedSlots: 50, socialLinks: { discordUrl: 'https://discord.gg/test' } },
|
||||
{ id: 'league-2', name: 'Amateur Series', description: 'Learn the ropes', ownerId: 'owner-2', settings: { maxDrivers: 50 }, createdAt: new Date().toISOString(), usedSlots: 20 },
|
||||
],
|
||||
totalCount: 2,
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
console.log('[LeagueService] Returning mock total leagues.');
|
||||
return { totalLeagues: 2 };
|
||||
}
|
||||
|
||||
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
console.log(`[LeagueService] Returning mock join requests for league: ${leagueId}.`);
|
||||
return [
|
||||
{
|
||||
id: 'join-req-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: new Date(),
|
||||
message: 'I want to join!',
|
||||
driver: mockDriverData.get('driver-1'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
|
||||
console.log('Approving join request:', input);
|
||||
return { success: true, message: 'Join request approved.' };
|
||||
}
|
||||
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
|
||||
console.log('Rejecting join request:', input);
|
||||
return { success: true, message: 'Join request rejected.' };
|
||||
}
|
||||
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
|
||||
console.log('Getting league admin permissions:', query);
|
||||
return { canRemoveMember: true, canUpdateRoles: true };
|
||||
}
|
||||
|
||||
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
|
||||
console.log('Removing league member:', input.leagueId, input.targetDriverId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
console.log('Updating league member role:', input.leagueId, input.targetDriverId, input.newRole);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
console.log('Getting league owner summary:', query);
|
||||
return {
|
||||
driver: mockDriverData.get(query.ownerId)!,
|
||||
rating: 2000,
|
||||
rank: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
|
||||
console.log('Getting league full config:', query);
|
||||
return {
|
||||
leagueId: 'league-1',
|
||||
basics: { name: 'Demo League', description: 'A demo league', visibility: 'public' },
|
||||
structure: { mode: 'solo' },
|
||||
championships: [],
|
||||
scoring: { type: 'standard', points: 10 },
|
||||
dropPolicy: { strategy: 'none' },
|
||||
timings: { raceDayOfWeek: 'Sunday', raceTimeHour: 20, raceTimeMinute: 0 },
|
||||
stewarding: {
|
||||
decisionMode: 'single_steward',
|
||||
requireDefense: false,
|
||||
defenseTimeLimit: 24,
|
||||
voteTimeLimit: 24,
|
||||
protestDeadlineHours: 2,
|
||||
stewardingClosesHours: 24,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
|
||||
console.log('Getting league protests:', query);
|
||||
return {
|
||||
protests: [
|
||||
{ id: 'protest-1', raceId: 'race-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', submittedAt: new Date(), description: 'Bad driving!', status: 'pending' },
|
||||
],
|
||||
racesById: { 'race-1': mockRaceData.get('race-1')! },
|
||||
driversById: { 'driver-1': mockDriverData.get('driver-1')!, 'driver-2': mockDriverData.get('driver-2')! },
|
||||
};
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
console.log('Getting league seasons:', query);
|
||||
return [
|
||||
{ seasonId: 'season-1', name: 'Season 1', status: 'active', startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), isPrimary: true, isParallelActive: false },
|
||||
{ seasonId: 'season-2', name: 'Season 2', status: 'upcoming', startDate: new Date('2026-01-01'), endDate: new Date('2026-12-31'), isPrimary: false, isParallelActive: false },
|
||||
];
|
||||
}
|
||||
}
|
||||
561
apps/api/src/modules/league/dto/LeagueDto.ts
Normal file
561
apps/api/src/modules/league/dto/LeagueDto.ts
Normal file
@@ -0,0 +1,561 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional, IsEnum, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { DriverDto } from '../../driver/dto/DriverDto';
|
||||
import { RaceDto } from '../../race/dto/RaceDto';
|
||||
|
||||
|
||||
export class LeagueSettingsDto {
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
maxDrivers?: number;
|
||||
// Add other league settings properties as needed
|
||||
}
|
||||
|
||||
export class LeagueWithCapacityViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
// ... other properties of LeagueWithCapacityViewModel
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: () => LeagueSettingsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueSettingsDto)
|
||||
settings: LeagueSettingsDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
usedSlots: number;
|
||||
|
||||
@ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
|
||||
@IsOptional()
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AllLeaguesWithCapacityViewModel {
|
||||
@ApiProperty({ type: [LeagueWithCapacityViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueWithCapacityViewModel)
|
||||
leagues: LeagueWithCapacityViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class LeagueStatsDto {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalLeagues: number;
|
||||
}
|
||||
|
||||
export class ProtestDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
protestingDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
accusedDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
submittedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
|
||||
@IsEnum(['pending', 'accepted', 'rejected'])
|
||||
status: 'pending' | 'accepted' | 'rejected';
|
||||
}
|
||||
|
||||
export class SeasonDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty({ enum: ['planned', 'active', 'completed'] })
|
||||
@IsEnum(['planned', 'active', 'completed'])
|
||||
status: 'planned' | 'active' | 'completed';
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isPrimary: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonGroupId?: string;
|
||||
}
|
||||
|
||||
export class LeagueJoinRequestViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
requestedAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
message?: string;
|
||||
|
||||
@ApiProperty({ type: () => DriverDto, required: false })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driver?: DriverDto;
|
||||
}
|
||||
|
||||
export class GetLeagueJoinRequestsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class ApproveJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class ApproveJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class RejectJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class RejectJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminPermissionsInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
}
|
||||
|
||||
export class LeagueAdminPermissionsViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canRemoveMember: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canUpdateRoles: boolean;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
targetDriverId: string;
|
||||
}
|
||||
|
||||
export class RemoveLeagueMemberOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class UpdateLeagueMemberRoleInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
performerDriverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
targetDriverId: string;
|
||||
|
||||
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
|
||||
@IsEnum(['owner', 'manager', 'member'])
|
||||
newRole: 'owner' | 'manager' | 'member';
|
||||
}
|
||||
|
||||
export class UpdateLeagueMemberRoleOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class GetLeagueOwnerSummaryQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueOwnerSummaryViewModel {
|
||||
@ApiProperty({ type: () => DriverDto })
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driver: DriverDto;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rating: number | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelBasicsDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ enum: ['public', 'private'] })
|
||||
@IsEnum(['public', 'private'])
|
||||
visibility: 'public' | 'private';
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelStructureDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsEnum(['solo', 'team'])
|
||||
mode: 'solo' | 'team';
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelScoringDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
points: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelDropPolicyDto {
|
||||
@ApiProperty({ enum: ['none', 'worst_n'] })
|
||||
@IsEnum(['none', 'worst_n'])
|
||||
strategy: 'none' | 'worst_n';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
n?: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelStewardingDto {
|
||||
@ApiProperty({ enum: ['single_steward', 'committee_vote'] })
|
||||
@IsEnum(['single_steward', 'committee_vote'])
|
||||
decisionMode: 'single_steward' | 'committee_vote';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
requiredVotes?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
requireDefense: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
defenseTimeLimit: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
voteTimeLimit: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
protestDeadlineHours: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
stewardingClosesHours: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
notifyAccusedOnProtest: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
notifyOnVoteRequired: boolean;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelTimingsDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceDayOfWeek: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
raceTimeHour: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
raceTimeMinute: number;
|
||||
}
|
||||
|
||||
export class LeagueConfigFormModelDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelBasicsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelBasicsDto)
|
||||
basics: LeagueConfigFormModelBasicsDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelStructureDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelStructureDto)
|
||||
structure: LeagueConfigFormModelStructureDto;
|
||||
|
||||
@ApiProperty({ type: [Object] })
|
||||
@IsArray()
|
||||
championships: any[];
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelScoringDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelScoringDto)
|
||||
scoring: LeagueConfigFormModelScoringDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelDropPolicyDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDropPolicyDto)
|
||||
dropPolicy: LeagueConfigFormModelDropPolicyDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelTimingsDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelTimingsDto)
|
||||
timings: LeagueConfigFormModelTimingsDto;
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelStewardingDto })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelStewardingDto)
|
||||
stewarding: LeagueConfigFormModelStewardingDto;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminConfigQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetLeagueAdminConfigOutput {
|
||||
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDto)
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export class LeagueAdminConfigViewModel {
|
||||
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueConfigFormModelDto)
|
||||
form: LeagueConfigFormModelDto | null;
|
||||
}
|
||||
|
||||
export class GetLeagueProtestsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueAdminProtestsViewModel {
|
||||
@ApiProperty({ type: [ProtestDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProtestDto)
|
||||
protests: ProtestDto[];
|
||||
|
||||
@ApiProperty({ type: () => RaceDto })
|
||||
@ValidateNested()
|
||||
@Type(() => RaceDto)
|
||||
racesById: { [raceId: string]: RaceDto };
|
||||
|
||||
@ApiProperty({ type: () => DriverDto })
|
||||
@ValidateNested()
|
||||
@Type(() => DriverDto)
|
||||
driversById: { [driverId: string]: DriverDto };
|
||||
}
|
||||
|
||||
export class GetLeagueSeasonsQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class LeagueSeasonSummaryViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
status: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
startDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isPrimary: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isParallelActive: boolean;
|
||||
}
|
||||
|
||||
export class LeagueAdminViewModel {
|
||||
@ApiProperty({ type: [LeagueJoinRequestViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueJoinRequestViewModel)
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
|
||||
@ApiProperty({ type: () => LeagueOwnerSummaryViewModel, nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueOwnerSummaryViewModel)
|
||||
ownerSummary: LeagueOwnerSummaryViewModel | null;
|
||||
|
||||
@ApiProperty({ type: () => LeagueAdminConfigViewModel })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueAdminConfigViewModel)
|
||||
config: LeagueAdminConfigViewModel;
|
||||
|
||||
@ApiProperty({ type: () => LeagueAdminProtestsViewModel })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueAdminProtestsViewModel)
|
||||
protests: LeagueAdminProtestsViewModel;
|
||||
|
||||
@ApiProperty({ type: [LeagueSeasonSummaryViewModel] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueSeasonSummaryViewModel)
|
||||
seasons: LeagueSeasonSummaryViewModel[];
|
||||
}
|
||||
26
apps/api/src/modules/media/MediaController.ts
Normal file
26
apps/api/src/modules/media/MediaController.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller, Post, Body, HttpStatus, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { MediaService } from './MediaService';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
|
||||
@ApiTags('media')
|
||||
@Controller('media')
|
||||
export class MediaController {
|
||||
constructor(private readonly mediaService: MediaService) {}
|
||||
|
||||
@Post('avatar/generate')
|
||||
@ApiOperation({ summary: 'Request avatar generation' })
|
||||
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput })
|
||||
async requestAvatarGeneration(
|
||||
@Body() input: RequestAvatarGenerationInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const result = await this.mediaService.requestAvatarGeneration(input);
|
||||
if (result.success) {
|
||||
res.status(HttpStatus.CREATED).json(result);
|
||||
} else {
|
||||
res.status(HttpStatus.BAD_REQUEST).json(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/media/MediaModule.ts
Normal file
10
apps/api/src/modules/media/MediaModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
import { MediaController } from './MediaController';
|
||||
|
||||
@Module({
|
||||
controllers: [MediaController],
|
||||
providers: [MediaService],
|
||||
exports: [MediaService],
|
||||
})
|
||||
export class MediaModule {}
|
||||
41
apps/api/src/modules/media/MediaProviders.ts
Normal file
41
apps/api/src/modules/media/MediaProviders.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
||||
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
||||
export const LOGGER_TOKEN = 'ILogger';
|
||||
|
||||
export const MediaProviders: Provider[] = [
|
||||
MediaService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryAvatarGenerationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: FACE_VALIDATION_PORT_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryFaceValidationAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
*/
|
||||
];
|
||||
20
apps/api/src/modules/media/MediaService.ts
Normal file
20
apps/api/src/modules/media/MediaService.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
|
||||
console.log('[MediaService] Returning mock avatar generation request. Input:', input);
|
||||
return {
|
||||
success: true,
|
||||
requestId: `req-${Date.now()}`,
|
||||
avatarUrls: [
|
||||
'https://cdn.example.com/avatars/mock-avatar-1.png',
|
||||
'https://cdn.example.com/avatars/mock-avatar-2.png',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
40
apps/api/src/modules/media/dto/MediaDto.ts
Normal file
40
apps/api/src/modules/media/dto/MediaDto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsBoolean } from 'class-validator';
|
||||
|
||||
export class RequestAvatarGenerationInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
facePhotoData: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
suitColor: string;
|
||||
}
|
||||
|
||||
export class RequestAvatarGenerationOutput {
|
||||
@ApiProperty({ type: Boolean })
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
requestId?: string;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
avatarUrls?: string[];
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// Assuming FacePhotoData and SuitColor are simple string types for DTO purposes
|
||||
export type FacePhotoData = string;
|
||||
export type SuitColor = string;
|
||||
96
apps/api/src/modules/payments/PaymentsController.ts
Normal file
96
apps/api/src/modules/payments/PaymentsController.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Query, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, GetPaymentsQuery, GetPaymentsOutput, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput } from './dto/PaymentsDto';
|
||||
|
||||
@ApiTags('payments')
|
||||
@Controller('payments')
|
||||
export class PaymentsController {
|
||||
constructor(private readonly paymentsService: PaymentsService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get payments based on filters' })
|
||||
@ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput })
|
||||
async getPayments(@Query() query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
return this.paymentsService.getPayments(query);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new payment' })
|
||||
@ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput })
|
||||
async createPayment(@Body() input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
return this.paymentsService.createPayment(input);
|
||||
}
|
||||
|
||||
@Patch('status')
|
||||
@ApiOperation({ summary: 'Update the status of a payment' })
|
||||
@ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput })
|
||||
async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
return this.paymentsService.updatePaymentStatus(input);
|
||||
}
|
||||
|
||||
@Get('membership-fees')
|
||||
@ApiOperation({ summary: 'Get membership fees and member payments' })
|
||||
@ApiResponse({ status: 200, description: 'Membership fee configuration and member payments', type: GetMembershipFeesOutput })
|
||||
async getMembershipFees(@Query() query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
return this.paymentsService.getMembershipFees(query);
|
||||
}
|
||||
|
||||
@Post('membership-fees')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create or update membership fee configuration' })
|
||||
@ApiResponse({ status: 201, description: 'Membership fee configuration created or updated', type: UpsertMembershipFeeOutput })
|
||||
async upsertMembershipFee(@Body() input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
return this.paymentsService.upsertMembershipFee(input);
|
||||
}
|
||||
|
||||
@Patch('membership-fees/member-payment')
|
||||
@ApiOperation({ summary: 'Record or update a member payment' })
|
||||
@ApiResponse({ status: 200, description: 'Member payment recorded or updated', type: UpdateMemberPaymentOutput })
|
||||
async updateMemberPayment(@Body() input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
return this.paymentsService.updateMemberPayment(input);
|
||||
}
|
||||
@Get('prizes')
|
||||
@ApiOperation({ summary: 'Get prizes for a league or season' })
|
||||
@ApiResponse({ status: 200, description: 'List of prizes', type: GetPrizesOutput })
|
||||
async getPrizes(@Query() query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
return this.paymentsService.getPrizes(query);
|
||||
}
|
||||
|
||||
@Post('prizes')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new prize' })
|
||||
@ApiResponse({ status: 201, description: 'Prize created', type: CreatePrizeOutput })
|
||||
async createPrize(@Body() input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
return this.paymentsService.createPrize(input);
|
||||
}
|
||||
|
||||
@Patch('prizes/award')
|
||||
@ApiOperation({ summary: 'Award a prize to a driver' })
|
||||
@ApiResponse({ status: 200, description: 'Prize awarded', type: AwardPrizeOutput })
|
||||
async awardPrize(@Body() input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
return this.paymentsService.awardPrize(input);
|
||||
}
|
||||
|
||||
@Delete('prizes')
|
||||
@ApiOperation({ summary: 'Delete a prize' })
|
||||
@ApiResponse({ status: 200, description: 'Prize deleted', type: DeletePrizeOutput })
|
||||
async deletePrize(@Query() query: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
return this.paymentsService.deletePrize(query);
|
||||
}
|
||||
@Get('wallets')
|
||||
@ApiOperation({ summary: 'Get wallet information and transactions' })
|
||||
@ApiResponse({ status: 200, description: 'Wallet and transaction data', type: GetWalletOutput })
|
||||
async getWallet(@Query() query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
return this.paymentsService.getWallet(query);
|
||||
}
|
||||
|
||||
@Post('wallets/transactions')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Process a wallet transaction (deposit or withdrawal)' })
|
||||
@ApiResponse({ status: 201, description: 'Wallet transaction processed', type: ProcessWalletTransactionOutput })
|
||||
async processWalletTransaction(@Body() input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
return this.paymentsService.processWalletTransaction(input);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/payments/PaymentsModule.ts
Normal file
10
apps/api/src/modules/payments/PaymentsModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { PaymentsController } from './PaymentsController';
|
||||
|
||||
@Module({
|
||||
controllers: [PaymentsController],
|
||||
providers: [PaymentsService],
|
||||
exports: [PaymentsService],
|
||||
})
|
||||
export class PaymentsModule {}
|
||||
67
apps/api/src/modules/payments/PaymentsProviders.ts
Normal file
67
apps/api/src/modules/payments/PaymentsProviders.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
// Import core interfaces
|
||||
import { IPaymentRepository } from 'core/payments/domain/repositories/IPaymentRepository';
|
||||
import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMembershipFeeRepository';
|
||||
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
|
||||
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
|
||||
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
|
||||
import { ILogger } from 'core/shared/logging/ILogger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { InMemoryMembershipFeeRepository } from 'adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryPrizeRepository } from 'adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||
import { InMemoryWalletRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { InMemoryPaymentGateway } from 'adapters/payments/ports/InMemoryPaymentGateway';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
|
||||
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
|
||||
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
|
||||
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
|
||||
export const PAYMENT_GATEWAY_TOKEN = 'IPaymentGateway';
|
||||
export const LOGGER_TOKEN = 'ILogger'; // Already defined in other Providers, but good to have here too
|
||||
|
||||
export const PaymentsProviders: Provider[] = [
|
||||
PaymentsService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPaymentRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: MEMBERSHIP_FEE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryMembershipFeeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PRIZE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPrizeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: WALLET_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryWalletRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_GATEWAY_TOKEN,
|
||||
useFactory: (logger: ILogger) => new InMemoryPaymentGateway(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
*/
|
||||
];
|
||||
346
apps/api/src/modules/payments/PaymentsService.ts
Normal file
346
apps/api/src/modules/payments/PaymentsService.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, PaymentDto, GetPaymentsQuery, GetPaymentsOutput, PaymentStatus, MembershipFeeDto, MemberPaymentDto, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, MembershipFeeType, MemberPaymentStatus, PrizeDto, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, PrizeType, WalletDto, TransactionDto, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput, TransactionType, ReferenceType } from './dto/PaymentsDto';
|
||||
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
|
||||
|
||||
const payments: Map<string, PaymentDto> = new Map();
|
||||
const membershipFees: Map<string, MembershipFeeDto> = new Map();
|
||||
const memberPayments: Map<string, MemberPaymentDto> = new Map();
|
||||
const prizes: Map<string, PrizeDto> = new Map();
|
||||
const wallets: Map<string, WalletDto> = new Map();
|
||||
const transactions: Map<string, TransactionDto> = new Map();
|
||||
|
||||
const PLATFORM_FEE_RATE = 0.10;
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
let results = Array.from(payments.values());
|
||||
|
||||
if (query.leagueId) {
|
||||
results = results.filter(p => p.leagueId === query.leagueId);
|
||||
}
|
||||
if (query.payerId) {
|
||||
results = results.filter(p => p.payerId === query.payerId);
|
||||
}
|
||||
if (query.type) {
|
||||
results = results.filter(p => p.type === query.type);
|
||||
}
|
||||
|
||||
return { payments: results };
|
||||
}
|
||||
|
||||
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
|
||||
|
||||
const platformFee = amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = amount - platformFee;
|
||||
|
||||
const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const payment: PaymentDto = {
|
||||
id,
|
||||
type,
|
||||
amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
payerId,
|
||||
payerType,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
payments.set(id, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
const { paymentId, status } = input;
|
||||
|
||||
const payment = payments.get(paymentId);
|
||||
if (!payment) {
|
||||
throw new Error('Payment not found');
|
||||
}
|
||||
|
||||
payment.status = status;
|
||||
if (status === PaymentStatus.COMPLETED) {
|
||||
payment.completedAt = new Date();
|
||||
}
|
||||
|
||||
payments.set(paymentId, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
const { leagueId, driverId } = query;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('leagueId is required');
|
||||
}
|
||||
|
||||
const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
|
||||
|
||||
let payments: MemberPaymentDto[] = [];
|
||||
if (driverId) {
|
||||
payments = Array.from(memberPayments.values()).filter(
|
||||
p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId
|
||||
);
|
||||
}
|
||||
|
||||
return { fee, payments };
|
||||
}
|
||||
|
||||
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
const { leagueId, seasonId, type, amount } = input;
|
||||
|
||||
// Check for existing fee config
|
||||
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
|
||||
|
||||
if (existingFee) {
|
||||
// Update existing fee
|
||||
existingFee.type = type;
|
||||
existingFee.amount = amount;
|
||||
existingFee.seasonId = seasonId || existingFee.seasonId;
|
||||
existingFee.enabled = amount > 0;
|
||||
existingFee.updatedAt = new Date();
|
||||
membershipFees.set(existingFee.id, existingFee);
|
||||
return { fee: existingFee };
|
||||
}
|
||||
|
||||
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const fee: MembershipFeeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
type,
|
||||
amount,
|
||||
enabled: amount > 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
membershipFees.set(id, fee);
|
||||
|
||||
return { fee };
|
||||
}
|
||||
|
||||
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
const { feeId, driverId, status, paidAt } = input;
|
||||
|
||||
const fee = membershipFees.get(feeId);
|
||||
if (!fee) {
|
||||
throw new Error('Membership fee configuration not found');
|
||||
}
|
||||
|
||||
// Find or create payment record
|
||||
let payment = Array.from(memberPayments.values()).find(
|
||||
p => p.feeId === feeId && p.driverId === driverId
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
const platformFee = fee.amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = fee.amount - platformFee;
|
||||
|
||||
const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
payment = {
|
||||
id: paymentId,
|
||||
feeId,
|
||||
driverId,
|
||||
amount: fee.amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
status: MemberPaymentStatus.PENDING,
|
||||
dueDate: new Date(),
|
||||
};
|
||||
memberPayments.set(paymentId, payment);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
payment.status = status;
|
||||
}
|
||||
if (paidAt || status === MemberPaymentStatus.PAID) {
|
||||
payment.paidAt = paidAt ? new Date(paidAt) : new Date();
|
||||
}
|
||||
|
||||
memberPayments.set(payment.id, payment);
|
||||
|
||||
return { payment };
|
||||
}
|
||||
|
||||
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
const { leagueId, seasonId } = query;
|
||||
|
||||
let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
|
||||
|
||||
if (seasonId) {
|
||||
results = results.filter(p => p.seasonId === seasonId);
|
||||
}
|
||||
|
||||
results.sort((a, b) => a.position - b.position);
|
||||
|
||||
return { prizes: results };
|
||||
}
|
||||
|
||||
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
const { leagueId, seasonId, position, name, amount, type, description } = input;
|
||||
|
||||
// Check for duplicate position
|
||||
const existingPrize = Array.from(prizes.values()).find(
|
||||
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
|
||||
);
|
||||
|
||||
if (existingPrize) {
|
||||
throw new Error(`Prize for position ${position} already exists`);
|
||||
}
|
||||
|
||||
const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const prize: PrizeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId,
|
||||
position,
|
||||
name,
|
||||
amount,
|
||||
type,
|
||||
description: description || undefined,
|
||||
awarded: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
prizes.set(id, prize);
|
||||
|
||||
return { prize };
|
||||
}
|
||||
|
||||
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
const { prizeId, driverId } = input;
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Prize has already been awarded');
|
||||
}
|
||||
|
||||
prize.awarded = true;
|
||||
prize.awardedTo = driverId;
|
||||
prize.awardedAt = new Date();
|
||||
|
||||
prizes.set(prizeId, prize);
|
||||
|
||||
return { prize };
|
||||
}
|
||||
|
||||
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
const { prizeId } = input;
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Cannot delete an awarded prize');
|
||||
}
|
||||
|
||||
prizes.delete(prizeId);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
const { leagueId } = query;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('LeagueId is required');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
const walletTransactions = Array.from(transactions.values())
|
||||
.filter(t => t.walletId === wallet!.id)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return { wallet, transactions: walletTransactions };
|
||||
}
|
||||
|
||||
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
const { leagueId, type, amount, description, referenceId, referenceType } = input;
|
||||
|
||||
if (!leagueId || !type || amount === undefined || !description) {
|
||||
throw new Error('Missing required fields: leagueId, type, amount, description');
|
||||
}
|
||||
|
||||
if (type !== TransactionType.DEPOSIT && type !== TransactionType.WITHDRAWAL) {
|
||||
throw new Error('Type must be "deposit" or "withdrawal"');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
if (type === TransactionType.WITHDRAWAL) {
|
||||
if (amount > wallet.balance) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
}
|
||||
|
||||
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const transaction: TransactionDto = {
|
||||
id: transactionId,
|
||||
walletId: wallet.id,
|
||||
type,
|
||||
amount,
|
||||
description,
|
||||
referenceId: referenceId || undefined,
|
||||
referenceType: referenceType || undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
transactions.set(transactionId, transaction);
|
||||
|
||||
if (type === TransactionType.DEPOSIT) {
|
||||
wallet.balance += amount;
|
||||
wallet.totalRevenue += amount;
|
||||
} else {
|
||||
wallet.balance -= amount;
|
||||
wallet.totalWithdrawn += amount;
|
||||
}
|
||||
|
||||
wallets.set(wallet.id, wallet);
|
||||
|
||||
return { wallet, transaction };
|
||||
}
|
||||
}
|
||||
566
apps/api/src/modules/payments/dto/PaymentsDto.ts
Normal file
566
apps/api/src/modules/payments/dto/PaymentsDto.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean } from 'class-validator';
|
||||
|
||||
export enum PaymentType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
}
|
||||
|
||||
export enum PayerType {
|
||||
SPONSOR = 'sponsor',
|
||||
DRIVER = 'driver',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'pending',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
|
||||
export class PaymentDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentType })
|
||||
@IsEnum(PaymentType)
|
||||
type: PaymentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
platformFee: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
netAmount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
payerId: string;
|
||||
|
||||
@ApiProperty({ enum: PayerType })
|
||||
@IsEnum(PayerType)
|
||||
payerType: PayerType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentStatus })
|
||||
@IsEnum(PaymentStatus)
|
||||
status: PaymentStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
export class CreatePaymentInput {
|
||||
@ApiProperty({ enum: PaymentType })
|
||||
@IsEnum(PaymentType)
|
||||
type: PaymentType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
payerId: string;
|
||||
|
||||
@ApiProperty({ enum: PayerType })
|
||||
@IsEnum(PayerType)
|
||||
payerType: PayerType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class CreatePaymentOutput {
|
||||
@ApiProperty({ type: PaymentDto })
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
paymentId: string;
|
||||
|
||||
@ApiProperty({ enum: PaymentStatus })
|
||||
@IsEnum(PaymentStatus)
|
||||
status: PaymentStatus;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusOutput {
|
||||
@ApiProperty({ type: PaymentDto })
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export enum MembershipFeeType {
|
||||
SEASON = 'season',
|
||||
MONTHLY = 'monthly',
|
||||
PER_RACE = 'per_race',
|
||||
}
|
||||
|
||||
export enum MemberPaymentStatus {
|
||||
PENDING = 'pending',
|
||||
PAID = 'paid',
|
||||
OVERDUE = 'overdue',
|
||||
}
|
||||
|
||||
export class MembershipFeeDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: MembershipFeeType })
|
||||
@IsEnum(MembershipFeeType)
|
||||
type: MembershipFeeType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
enabled: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export class MemberPaymentDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
feeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
platformFee: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
netAmount: number;
|
||||
|
||||
@ApiProperty({ enum: MemberPaymentStatus })
|
||||
@IsEnum(MemberPaymentStatus)
|
||||
status: MemberPaymentStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
dueDate: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
paidAt?: Date;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesOutput {
|
||||
@ApiProperty({ type: MembershipFeeDto, nullable: true })
|
||||
fee: MembershipFeeDto | null;
|
||||
|
||||
@ApiProperty({ type: [MemberPaymentDto] })
|
||||
payments: MemberPaymentDto[];
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
|
||||
@ApiProperty({ enum: MembershipFeeType })
|
||||
@IsEnum(MembershipFeeType)
|
||||
type: MembershipFeeType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeOutput {
|
||||
@ApiProperty({ type: MembershipFeeDto })
|
||||
fee: MembershipFeeDto;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
feeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: MemberPaymentStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(MemberPaymentStatus)
|
||||
status?: MemberPaymentStatus;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
paidAt?: Date | string;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentOutput {
|
||||
@ApiProperty({ type: MemberPaymentDto })
|
||||
payment: MemberPaymentDto;
|
||||
}
|
||||
|
||||
export class GetPaymentsQuery {
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
payerId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: PaymentType })
|
||||
@IsOptional()
|
||||
@IsEnum(PaymentType)
|
||||
type?: PaymentType;
|
||||
}
|
||||
|
||||
export class GetPaymentsOutput {
|
||||
@ApiProperty({ type: [PaymentDto] })
|
||||
payments: PaymentDto[];
|
||||
}
|
||||
|
||||
export enum PrizeType {
|
||||
CASH = 'cash',
|
||||
MERCHANDISE = 'merchandise',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export class PrizeDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
position: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ enum: PrizeType })
|
||||
@IsEnum(PrizeType)
|
||||
type: PrizeType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
awarded: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awardedTo?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
awardedAt?: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetPrizesQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class GetPrizesOutput {
|
||||
@ApiProperty({ type: [PrizeDto] })
|
||||
prizes: PrizeDto[];
|
||||
}
|
||||
|
||||
export class CreatePrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
position: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ enum: PrizeType })
|
||||
@IsEnum(PrizeType)
|
||||
type: PrizeType;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreatePrizeOutput {
|
||||
@ApiProperty({ type: PrizeDto })
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export class AwardPrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
prizeId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class AwardPrizeOutput {
|
||||
@ApiProperty({ type: PrizeDto })
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export class DeletePrizeInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
prizeId: string;
|
||||
}
|
||||
|
||||
export class DeletePrizeOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export enum TransactionType {
|
||||
DEPOSIT = 'deposit',
|
||||
WITHDRAWAL = 'withdrawal',
|
||||
PLATFORM_FEE = 'platform_fee',
|
||||
}
|
||||
|
||||
export enum ReferenceType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
PRIZE = 'prize',
|
||||
}
|
||||
|
||||
export class WalletDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
balance: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalRevenue: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalPlatformFees: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalWithdrawn: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export class TransactionDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
walletId: string;
|
||||
|
||||
@ApiProperty({ enum: TransactionType })
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ReferenceType })
|
||||
@IsOptional()
|
||||
@IsEnum(ReferenceType)
|
||||
referenceType?: ReferenceType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetWalletQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
}
|
||||
|
||||
export class GetWalletOutput {
|
||||
@ApiProperty({ type: WalletDto })
|
||||
wallet: WalletDto;
|
||||
|
||||
@ApiProperty({ type: [TransactionDto] })
|
||||
transactions: TransactionDto[];
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty({ enum: TransactionType })
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
referenceId?: string;
|
||||
|
||||
@ApiProperty({ required: false, enum: ReferenceType })
|
||||
@IsOptional()
|
||||
@IsEnum(ReferenceType)
|
||||
referenceType?: ReferenceType;
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionOutput {
|
||||
@ApiProperty({ type: WalletDto })
|
||||
wallet: WalletDto;
|
||||
|
||||
@ApiProperty({ type: TransactionDto })
|
||||
transaction: TransactionDto;
|
||||
}
|
||||
|
||||
26
apps/api/src/modules/race/RaceController.ts
Normal file
26
apps/api/src/modules/race/RaceController.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { RaceService } from './RaceService';
|
||||
import { AllRacesPageViewModel, RaceStatsDto } from './dto/RaceDto';
|
||||
|
||||
@ApiTags('races')
|
||||
@Controller('races')
|
||||
export class RaceController {
|
||||
constructor(private readonly raceService: RaceService) {}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all races' })
|
||||
@ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageViewModel })
|
||||
async getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
return this.raceService.getAllRaces();
|
||||
}
|
||||
|
||||
@Get('total-races')
|
||||
@ApiOperation({ summary: 'Get the total number of races' })
|
||||
@ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDto })
|
||||
async getTotalRaces(): Promise<RaceStatsDto> {
|
||||
return this.raceService.getTotalRaces();
|
||||
}
|
||||
|
||||
// Add other Race endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/race/RaceModule.ts
Normal file
10
apps/api/src/modules/race/RaceModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
import { RaceController } from './RaceController';
|
||||
|
||||
@Module({
|
||||
controllers: [RaceController],
|
||||
providers: [RaceService],
|
||||
exports: [RaceService],
|
||||
})
|
||||
export class RaceModule {}
|
||||
18
apps/api/src/modules/race/RaceProviders.ts
Normal file
18
apps/api/src/modules/race/RaceProviders.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
|
||||
export const RaceProviders: Provider[] = [
|
||||
RaceService,
|
||||
// In a functional setup, other providers would be here, e.g.:
|
||||
/*
|
||||
{
|
||||
provide: 'ILogger',
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: 'IRaceRepository',
|
||||
useClass: InMemoryRaceRepository,
|
||||
},
|
||||
// ... other providers
|
||||
*/
|
||||
];
|
||||
37
apps/api/src/modules/race/RaceService.ts
Normal file
37
apps/api/src/modules/race/RaceService.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllRacesPageViewModel, RaceStatsDto, ImportRaceResultsInput, ImportRaceResultsSummaryViewModel } from './dto/RaceDto';
|
||||
|
||||
@Injectable()
|
||||
export class RaceService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
console.log('[RaceService] Returning mock all races.');
|
||||
return Promise.resolve({
|
||||
races: [
|
||||
{ id: 'race-1', name: 'Global Race 1', date: new Date().toISOString(), leagueName: 'Global Racing' },
|
||||
{ id: 'race-2', name: 'Amateur Race 1', date: new Date().toISOString(), leagueName: 'Amateur Series' },
|
||||
],
|
||||
totalCount: 2,
|
||||
});
|
||||
}
|
||||
|
||||
getTotalRaces(): Promise<RaceStatsDto> {
|
||||
console.log('[RaceService] Returning mock total races.');
|
||||
return Promise.resolve({
|
||||
totalRaces: 2, // Placeholder
|
||||
});
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
console.log('Importing race results:', input);
|
||||
return {
|
||||
success: true,
|
||||
raceId: input.raceId,
|
||||
driversProcessed: 10, // Mock data
|
||||
resultsRecorded: 10, // Mock data
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
78
apps/api/src/modules/race/dto/RaceDto.ts
Normal file
78
apps/api/src/modules/race/dto/RaceDto.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsBoolean, IsNumber } from 'class-validator';
|
||||
|
||||
export class RaceViewModel {
|
||||
@ApiProperty()
|
||||
id: string; // Assuming a race has an ID
|
||||
|
||||
@ApiProperty()
|
||||
name: string; // Assuming a race has a name
|
||||
|
||||
@ApiProperty()
|
||||
date: string; // Assuming a race has a date
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
leagueName?: string; // Assuming a race might belong to a league
|
||||
|
||||
// Add more race-related properties as needed based on the DTO from the application layer
|
||||
}
|
||||
|
||||
export class AllRacesPageViewModel {
|
||||
@ApiProperty({ type: [RaceViewModel] })
|
||||
races: RaceViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class RaceStatsDto {
|
||||
@ApiProperty()
|
||||
totalRaces: number;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
resultsFileContent: string;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsSummaryViewModel {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
driversProcessed: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
resultsRecorded: number;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export class RaceDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
date: string;
|
||||
}
|
||||
48
apps/api/src/modules/sponsor/SponsorController.ts
Normal file
48
apps/api/src/modules/sponsor/SponsorController.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Controller, Get, Post, Body, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO } from './dto/SponsorDto';
|
||||
|
||||
@ApiTags('sponsors')
|
||||
@Controller('sponsors')
|
||||
export class SponsorController {
|
||||
constructor(private readonly sponsorService: SponsorService) {}
|
||||
|
||||
@Get('pricing')
|
||||
@ApiOperation({ summary: 'Get sponsorship pricing for an entity' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship pricing', type: GetEntitySponsorshipPricingResultDto })
|
||||
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
return this.sponsorService.getEntitySponsorshipPricing();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all sponsors' })
|
||||
@ApiResponse({ status: 200, description: 'List of sponsors', type: GetSponsorsOutput })
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return this.sponsorService.getSponsors();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new sponsor' })
|
||||
@ApiResponse({ status: 201, description: 'Sponsor created', type: CreateSponsorOutput })
|
||||
async createSponsor(@Body() input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
return this.sponsorService.createSponsor(input);
|
||||
}
|
||||
|
||||
// Add other Sponsor endpoints here based on other presenters
|
||||
@Get('dashboard/:sponsorId')
|
||||
@ApiOperation({ summary: 'Get sponsor dashboard metrics and sponsored leagues' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsor dashboard data', type: SponsorDashboardDTO })
|
||||
@ApiResponse({ status: 404, description: 'Sponsor not found' })
|
||||
async getSponsorDashboard(@Param('sponsorId') sponsorId: string): Promise<SponsorDashboardDTO | null> {
|
||||
return this.sponsorService.getSponsorDashboard({ sponsorId });
|
||||
}
|
||||
@Get(':sponsorId/sponsorships')
|
||||
@ApiOperation({ summary: 'Get all sponsorships for a given sponsor' })
|
||||
@ApiResponse({ status: 200, description: 'List of sponsorships', type: SponsorSponsorshipsDTO })
|
||||
@ApiResponse({ status: 404, description: 'Sponsor not found' })
|
||||
async getSponsorSponsorships(@Param('sponsorId') sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
|
||||
return this.sponsorService.getSponsorSponsorships({ sponsorId });
|
||||
}
|
||||
}
|
||||
10
apps/api/src/modules/sponsor/SponsorModule.ts
Normal file
10
apps/api/src/modules/sponsor/SponsorModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import { SponsorController } from './SponsorController';
|
||||
|
||||
@Module({
|
||||
controllers: [SponsorController],
|
||||
providers: [SponsorService],
|
||||
exports: [SponsorService],
|
||||
})
|
||||
export class SponsorModule {}
|
||||
5
apps/api/src/modules/sponsor/SponsorProviders.ts
Normal file
5
apps/api/src/modules/sponsor/SponsorProviders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SponsorService } from './SponsorService';
|
||||
|
||||
export const SponsorProviders = [
|
||||
SponsorService,
|
||||
];
|
||||
162
apps/api/src/modules/sponsor/SponsorService.ts
Normal file
162
apps/api/src/modules/sponsor/SponsorService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetEntitySponsorshipPricingResultDto, SponsorDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorshipDetailDTO, SponsorSponsorshipsDTO, SponsoredLeagueDTO, SponsorDashboardMetricsDTO, SponsorDashboardInvestmentDTO } from './dto/SponsorDto';
|
||||
|
||||
const sponsors: Map<string, SponsorDto> = new Map();
|
||||
|
||||
@Injectable()
|
||||
export class SponsorService {
|
||||
|
||||
constructor() {
|
||||
// Seed some demo sponsors for dashboard if empty
|
||||
if (sponsors.size === 0) {
|
||||
const demoSponsor1: SponsorDto = {
|
||||
id: 'sponsor-demo-1',
|
||||
name: 'Demo Sponsor Co.',
|
||||
contactEmail: 'contact@demosponsor.com',
|
||||
websiteUrl: 'https://demosponsor.com',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/aaaaaa/ffffff?text=DemoCo',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const demoSponsor2: SponsorDto = {
|
||||
id: 'sponsor-demo-2',
|
||||
name: 'Second Brand',
|
||||
contactEmail: 'info@secondbrand.net',
|
||||
websiteUrl: 'https://secondbrand.net',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/cccccc/ffffff?text=Brand2',
|
||||
createdAt: new Date(Date.now() - 86400000 * 5),
|
||||
};
|
||||
sponsors.set(demoSponsor1.id, demoSponsor1);
|
||||
sponsors.set(demoSponsor2.id, demoSponsor2);
|
||||
}
|
||||
}
|
||||
|
||||
getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
// This logic relies on external factors (e.g., pricing configuration, entity type)
|
||||
// For now, return mock data
|
||||
return Promise.resolve({
|
||||
pricing: [
|
||||
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
|
||||
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
|
||||
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return { sponsors: Array.from(sponsors.values()) };
|
||||
}
|
||||
|
||||
async createSponsor(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newSponsor: SponsorDto = {
|
||||
id,
|
||||
name: input.name,
|
||||
contactEmail: input.contactEmail,
|
||||
websiteUrl: input.websiteUrl,
|
||||
logoUrl: input.logoUrl,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
sponsors.set(id, newSponsor);
|
||||
return { sponsor: newSponsor };
|
||||
}
|
||||
|
||||
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simplified mock data for dashboard metrics and sponsored leagues
|
||||
const metrics: SponsorDashboardMetricsDTO = {
|
||||
impressions: 10000,
|
||||
impressionsChange: 12.5,
|
||||
uniqueViewers: 7000,
|
||||
viewersChange: 8.3,
|
||||
races: 50,
|
||||
drivers: 100,
|
||||
exposure: 75,
|
||||
exposureChange: 5.2,
|
||||
};
|
||||
|
||||
const sponsoredLeagues: SponsoredLeagueDTO[] = [
|
||||
{ id: 'league-1', name: 'League 1', tier: 'main', drivers: 50, races: 10, impressions: 5000, status: 'active' },
|
||||
{ id: 'league-2', name: 'League 2', tier: 'secondary', drivers: 30, races: 5, impressions: 1500, status: 'upcoming' },
|
||||
];
|
||||
|
||||
const investment: SponsorDashboardInvestmentDTO = {
|
||||
activeSponsorships: 2,
|
||||
totalInvestment: 5000,
|
||||
costPerThousandViews: 0.5,
|
||||
};
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics,
|
||||
sponsoredLeagues,
|
||||
investment,
|
||||
};
|
||||
}
|
||||
|
||||
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const sponsorshipDetails: SponsorshipDetailDTO[] = [
|
||||
{
|
||||
id: 'sponsorship-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
seasonId: 'season-1',
|
||||
seasonName: 'Season 1',
|
||||
seasonStartDate: new Date('2025-01-01'),
|
||||
seasonEndDate: new Date('2025-12-31'),
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
pricing: { amount: 1000, currency: 'USD' },
|
||||
platformFee: { amount: 100, currency: 'USD' },
|
||||
netAmount: { amount: 900, currency: 'USD' },
|
||||
metrics: { drivers: 50, races: 10, completedRaces: 8, impressions: 5000 },
|
||||
createdAt: new Date('2024-12-01'),
|
||||
activatedAt: new Date('2025-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'sponsorship-2',
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'League 2',
|
||||
seasonId: 'season-2',
|
||||
seasonName: 'Season 2',
|
||||
tier: 'secondary',
|
||||
status: 'pending',
|
||||
pricing: { amount: 500, currency: 'USD' },
|
||||
platformFee: { amount: 50, currency: 'USD' },
|
||||
netAmount: { amount: 450, currency: 'USD' },
|
||||
metrics: { drivers: 30, races: 5, completedRaces: 0, impressions: 0 },
|
||||
createdAt: new Date('2025-03-15'),
|
||||
},
|
||||
];
|
||||
|
||||
const totalInvestment = sponsorshipDetails.reduce((sum, s) => sum + s.pricing.amount, 0);
|
||||
const totalPlatformFees = sponsorshipDetails.reduce((sum, s) => sum + s.platformFee.amount, 0);
|
||||
const activeSponsorships = sponsorshipDetails.filter(s => s.status === 'active').length;
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
summary: {
|
||||
totalSponsorships: sponsorshipDetails.length,
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
299
apps/api/src/modules/sponsor/dto/SponsorDto.ts
Normal file
299
apps/api/src/modules/sponsor/dto/SponsorDto.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean, IsUrl, IsEmail } from 'class-validator';
|
||||
|
||||
export class SponsorshipPricingItemDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
level: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
price: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingResultDto {
|
||||
@ApiProperty({ type: [SponsorshipPricingItemDto] })
|
||||
pricing: SponsorshipPricingItemDto[];
|
||||
}
|
||||
|
||||
export class SponsorDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class GetSponsorsOutput {
|
||||
@ApiProperty({ type: [SponsorDto] })
|
||||
sponsors: SponsorDto[];
|
||||
}
|
||||
|
||||
export class CreateSponsorInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl()
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class CreateSponsorOutput {
|
||||
@ApiProperty({ type: SponsorDto })
|
||||
sponsor: SponsorDto;
|
||||
}
|
||||
|
||||
export class GetSponsorDashboardQueryParams {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export class SponsoredLeagueDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ enum: ['main', 'secondary'] })
|
||||
@IsEnum(['main', 'secondary'])
|
||||
tier: 'main' | 'secondary';
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
drivers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
races: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressions: number;
|
||||
|
||||
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
|
||||
@IsEnum(['active', 'upcoming', 'completed'])
|
||||
status: 'active' | 'upcoming' | 'completed';
|
||||
}
|
||||
|
||||
export class SponsorDashboardMetricsDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressions: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
impressionsChange: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
uniqueViewers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
viewersChange: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
races: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
drivers: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
exposure: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
exposureChange: number;
|
||||
}
|
||||
|
||||
export class SponsorDashboardInvestmentDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
activeSponsorships: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalInvestment: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
costPerThousandViews: number;
|
||||
}
|
||||
|
||||
export class SponsorDashboardDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorName: string;
|
||||
|
||||
@ApiProperty({ type: SponsorDashboardMetricsDTO })
|
||||
metrics: SponsorDashboardMetricsDTO;
|
||||
|
||||
@ApiProperty({ type: [SponsoredLeagueDTO] })
|
||||
sponsoredLeagues: SponsoredLeagueDTO[];
|
||||
|
||||
@ApiProperty({ type: SponsorDashboardInvestmentDTO })
|
||||
investment: SponsorDashboardInvestmentDTO;
|
||||
}
|
||||
|
||||
export class GetSponsorSponsorshipsQueryParams {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export class SponsorshipDetailDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
seasonName: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
seasonStartDate?: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
seasonEndDate?: Date;
|
||||
|
||||
@ApiProperty({ enum: ['main', 'secondary'] })
|
||||
@IsEnum(['main', 'secondary'])
|
||||
tier: 'main' | 'secondary';
|
||||
|
||||
@ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
|
||||
@IsEnum(['pending', 'active', 'expired', 'cancelled'])
|
||||
status: 'pending' | 'active' | 'expired' | 'cancelled';
|
||||
|
||||
@ApiProperty()
|
||||
pricing: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
platformFee: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
netAmount: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
metrics: {
|
||||
drivers: number;
|
||||
races: number;
|
||||
completedRaces: number;
|
||||
impressions: number;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
activatedAt?: Date;
|
||||
}
|
||||
|
||||
export class SponsorSponsorshipsDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
sponsorName: string;
|
||||
|
||||
@ApiProperty({ type: [SponsorshipDetailDTO] })
|
||||
sponsorships: SponsorshipDetailDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
summary: {
|
||||
totalSponsorships: number;
|
||||
activeSponsorships: number;
|
||||
totalInvestment: number;
|
||||
totalPlatformFees: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Add other DTOs for sponsor-related logic as needed
|
||||
19
apps/api/src/modules/team/TeamController.ts
Normal file
19
apps/api/src/modules/team/TeamController.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { TeamService } from './TeamService';
|
||||
import { AllTeamsViewModel } from './dto/TeamDto';
|
||||
|
||||
@ApiTags('teams')
|
||||
@Controller('teams')
|
||||
export class TeamController {
|
||||
constructor(private readonly teamService: TeamService) {}
|
||||
|
||||
@Get('all')
|
||||
@ApiOperation({ summary: 'Get all teams' })
|
||||
@ApiResponse({ status: 200, description: 'List of all teams', type: AllTeamsViewModel })
|
||||
async getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
return this.teamService.getAllTeams();
|
||||
}
|
||||
|
||||
// Add other Team endpoints here based on other presenters
|
||||
}
|
||||
10
apps/api/src/modules/team/TeamModule.ts
Normal file
10
apps/api/src/modules/team/TeamModule.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamService } from './TeamService';
|
||||
import { TeamController } from './TeamController';
|
||||
|
||||
@Module({
|
||||
controllers: [TeamController],
|
||||
providers: [TeamService],
|
||||
exports: [TeamService],
|
||||
})
|
||||
export class TeamModule {}
|
||||
5
apps/api/src/modules/team/TeamProviders.ts
Normal file
5
apps/api/src/modules/team/TeamProviders.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TeamService } from './TeamService';
|
||||
|
||||
export const TeamProviders = [
|
||||
TeamService,
|
||||
];
|
||||
43
apps/api/src/modules/team/TeamService.ts
Normal file
43
apps/api/src/modules/team/TeamService.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService {
|
||||
getAllTeams(): Promise<AllTeamsViewModel> {
|
||||
// TODO: Implement actual logic to fetch all teams
|
||||
return Promise.resolve({
|
||||
teams: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private teams: Map<string, TeamDto> = new Map(); // In-memory store for teams
|
||||
|
||||
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
|
||||
const { teamId, driverId } = query;
|
||||
|
||||
const team = this.teams.get(teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mock membership and roles
|
||||
const membership: MembershipDto = {
|
||||
role: driverId === team.ownerId ? MembershipRole.OWNER : MembershipRole.MEMBER,
|
||||
joinedAt: new Date(Date.now() - 86400000 * 30), // Joined 30 days ago
|
||||
isActive: true, // Always active for mock
|
||||
};
|
||||
|
||||
const isOwner = team.ownerId === driverId;
|
||||
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
|
||||
|
||||
return {
|
||||
team: team,
|
||||
membership,
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
// Add other methods related to Team logic here based on other presenters
|
||||
}
|
||||
121
apps/api/src/modules/team/dto/TeamDto.ts
Normal file
121
apps/api/src/modules/team/dto/TeamDto.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class TeamLeagueDto {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class TeamListItemViewModel {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
tag?: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
memberCount: number;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export class AllTeamsViewModel {
|
||||
@ApiProperty({ type: [TeamListItemViewModel] })
|
||||
teams: TeamListItemViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator';
|
||||
|
||||
export class TeamDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
tag: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export enum MembershipRole {
|
||||
OWNER = 'owner',
|
||||
MANAGER = 'manager',
|
||||
MEMBER = 'member',
|
||||
}
|
||||
|
||||
export enum MembershipStatus {
|
||||
ACTIVE = 'active',
|
||||
PENDING = 'pending',
|
||||
INVITED = 'invited',
|
||||
INACTIVE = 'inactive',
|
||||
}
|
||||
|
||||
export class MembershipDto {
|
||||
@ApiProperty({ enum: MembershipRole })
|
||||
@IsEnum(MembershipRole)
|
||||
role: MembershipRole;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
joinedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class DriverTeamViewModel {
|
||||
@ApiProperty({ type: TeamDto })
|
||||
team: TeamDto;
|
||||
|
||||
@ApiProperty({ type: MembershipDto })
|
||||
membership: MembershipDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isOwner: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
teamId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
|
||||
import type { RecordPageViewInput, RecordPageViewOutput } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import type { RecordEngagementInput, RecordEngagementOutput } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller('analytics')
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly recordPageViewUseCase: RecordPageViewUseCase,
|
||||
private readonly recordEngagementUseCase: RecordEngagementUseCase,
|
||||
) {}
|
||||
|
||||
@Post('page-view')
|
||||
async recordPageView(
|
||||
@Body() input: RecordPageViewInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordPageViewOutput = await this.recordPageViewUseCase.execute(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
|
||||
@Post('engagement')
|
||||
async recordEngagement(
|
||||
@Body() input: RecordEngagementInput,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const output: RecordEngagementOutput = await this.recordEngagementUseCase.execute(input);
|
||||
res.status(HttpStatus.CREATED).json(output);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,18 @@
|
||||
"@gridpilot/analytics/domain/entities/*": [
|
||||
"../../core/analytics/domain/entities/*"
|
||||
],
|
||||
"@gridpilot/core/identity/domain/repositories/*": [
|
||||
"../../core/identity/domain/repositories/*"
|
||||
],
|
||||
"@gridpilot/core/identity/domain/services/*": [
|
||||
"../../core/identity/domain/services/*"
|
||||
],
|
||||
"@gridpilot/core/identity/domain/entities/*": [
|
||||
"../../core/identity/domain/entities/*"
|
||||
],
|
||||
"@gridpilot/core/shared/logging/*": [
|
||||
"../../core/shared/logging/*"
|
||||
],
|
||||
"@nestjs/testing": [
|
||||
"./node_modules/@nestjs/testing"
|
||||
]
|
||||
|
||||
62
apps/website/lib/apiClient.ts
Normal file
62
apps/website/lib/apiClient.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, data?: object): Promise<T | void> {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const config: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (data) {
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${path}`, config);
|
||||
|
||||
if (!response.ok) {
|
||||
// Attempt to read error message from response body
|
||||
let errorData: any;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch (e) {
|
||||
errorData = { message: response.statusText };
|
||||
}
|
||||
throw new Error(errorData.message || `API request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : undefined;
|
||||
}
|
||||
|
||||
get<T>(path: string): Promise<T | void> {
|
||||
return this.request<T>('GET', path);
|
||||
}
|
||||
|
||||
post<T>(path: string, data: object): Promise<T | void> {
|
||||
return this.request<T>('POST', path, data);
|
||||
}
|
||||
|
||||
put<T>(path: string, data: object): Promise<T | void> {
|
||||
return this.request<T>('PUT', path, data);
|
||||
}
|
||||
|
||||
delete<T>(path: string): Promise<T | void> {
|
||||
return this.request<T>('DELETE', path);
|
||||
}
|
||||
|
||||
patch<T>(path: string, data: object): Promise<T | void> {
|
||||
return this.request<T>('PATCH', path, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the API client with your backend's base URL
|
||||
// You might want to get this from an environment variable
|
||||
export const api = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
|
||||
50
apps/website/lib/auth/AuthApiClient.ts
Normal file
50
apps/website/lib/auth/AuthApiClient.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { api } from '../apiClient';
|
||||
import type {
|
||||
AuthenticatedUserDTO,
|
||||
AuthSessionDTO,
|
||||
SignupParams,
|
||||
LoginParams,
|
||||
IracingAuthRedirectResult,
|
||||
LoginWithIracingCallbackParams,
|
||||
} from '../../../apps/api/src/modules/auth/dto/AuthDto'; // Using generated API DTOs
|
||||
|
||||
export class AuthApiClient {
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
try {
|
||||
return await api.get<AuthSessionDTO>('/auth/session');
|
||||
} catch (error) {
|
||||
// Handle error, e.g., if session is not found or API is down
|
||||
console.error('Error fetching current session:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
|
||||
return api.post<AuthSessionDTO>('/auth/signup', params);
|
||||
}
|
||||
|
||||
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
|
||||
return api.post<AuthSessionDTO>('/auth/login', params);
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
const query = returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : '';
|
||||
return api.get<IracingAuthRedirectResult>(`/auth/iracing/start${query}`);
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
const query = new URLSearchParams();
|
||||
query.append('code', params.code);
|
||||
query.append('state', params.state);
|
||||
if (params.returnTo) {
|
||||
query.append('returnTo', params.returnTo);
|
||||
}
|
||||
return await api.get<AuthSessionDTO>(`/auth/iracing/callback?${query.toString()}`);
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
return api.post<void>('/auth/logout', {});
|
||||
}
|
||||
}
|
||||
|
||||
export const authApiClient = new AuthApiClient();
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { AuthenticatedUserDTO } from '@gridpilot/identity/application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '@gridpilot/identity/application/dto/AuthSessionDTO';
|
||||
|
||||
export type AuthUser = AuthenticatedUserDTO;
|
||||
export type AuthSession = AuthSessionDTO;
|
||||
|
||||
export interface SignupParams {
|
||||
email: string;
|
||||
password: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface LoginParams {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthService {
|
||||
getCurrentSession(): Promise<AuthSession | null>;
|
||||
|
||||
// Email/password authentication
|
||||
signupWithEmail(params: SignupParams): Promise<AuthSession>;
|
||||
loginWithEmail(params: LoginParams): Promise<AuthSession>;
|
||||
|
||||
// iRacing OAuth (demo)
|
||||
startIracingAuthRedirect(
|
||||
returnTo?: string,
|
||||
): Promise<{ redirectUrl: string; state: string }>;
|
||||
loginWithIracingCallback(params: {
|
||||
code: string;
|
||||
state: string;
|
||||
returnTo?: string;
|
||||
}): Promise<AuthSession>;
|
||||
|
||||
logout(): Promise<void>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { AuthService } from './AuthService';
|
||||
import { InMemoryAuthService } from './InMemoryAuthService';
|
||||
import { getDIContainer } from '../di-container';
|
||||
import { DI_TOKENS } from '../di-tokens';
|
||||
|
||||
export function getAuthService(): AuthService {
|
||||
const container = getDIContainer();
|
||||
if (!container.isRegistered(DI_TOKENS.AuthService)) {
|
||||
throw new Error(
|
||||
`${DI_TOKENS.AuthService.description} not registered in DI container.`,
|
||||
);
|
||||
}
|
||||
return container.resolve<AuthService>(DI_TOKENS.AuthService);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,982 +0,0 @@
|
||||
/**
|
||||
* Dependency Injection Container - TSyringe Facade
|
||||
*
|
||||
* Provides backward-compatible API for accessing dependencies managed by TSyringe.
|
||||
*/
|
||||
|
||||
import { configureDIContainer, getDIContainer } from './di-config';
|
||||
import { DI_TOKENS } from './di-tokens';
|
||||
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
|
||||
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/IStandingRepository';
|
||||
import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository';
|
||||
import type { IProtestRepository } from '@gridpilot/racing/domain/repositories/IProtestRepository';
|
||||
import type { IGameRepository } from '@gridpilot/racing/domain/repositories/IGameRepository';
|
||||
import type { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '@gridpilot/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository';
|
||||
import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository';
|
||||
import type {
|
||||
ITeamRepository,
|
||||
ITeamMembershipRepository,
|
||||
IRaceRegistrationRepository,
|
||||
} from '@gridpilot/racing';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { LeagueMembership, JoinRequest } from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { ImageServicePort } from '@gridpilot/media';
|
||||
|
||||
// Notifications package imports
|
||||
import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application';
|
||||
import type {
|
||||
SendNotificationUseCase,
|
||||
MarkNotificationReadUseCase,
|
||||
GetUnreadNotificationsUseCase
|
||||
} from '@gridpilot/notifications/application';
|
||||
import type {
|
||||
JoinLeagueUseCase,
|
||||
RegisterForRaceUseCase,
|
||||
WithdrawFromRaceUseCase,
|
||||
CreateTeamUseCase,
|
||||
JoinTeamUseCase,
|
||||
LeaveTeamUseCase,
|
||||
ApproveTeamJoinRequestUseCase,
|
||||
RejectTeamJoinRequestUseCase,
|
||||
UpdateTeamUseCase,
|
||||
GetAllTeamsUseCase,
|
||||
GetTeamDetailsUseCase,
|
||||
GetTeamMembersUseCase,
|
||||
GetTeamJoinRequestsUseCase,
|
||||
GetDriverTeamUseCase,
|
||||
CreateLeagueWithSeasonAndScoringUseCase,
|
||||
FileProtestUseCase,
|
||||
ReviewProtestUseCase,
|
||||
ApplyPenaltyUseCase,
|
||||
QuickPenaltyUseCase,
|
||||
RequestProtestDefenseUseCase,
|
||||
SubmitProtestDefenseUseCase,
|
||||
GetSponsorDashboardUseCase,
|
||||
GetSponsorSponsorshipsUseCase,
|
||||
ApplyForSponsorshipUseCase,
|
||||
AcceptSponsorshipRequestUseCase,
|
||||
RejectSponsorshipRequestUseCase,
|
||||
GetPendingSponsorshipRequestsUseCase,
|
||||
GetEntitySponsorshipPricingUseCase,
|
||||
} from '@gridpilot/racing/application';
|
||||
import type { IsDriverRegisteredForRaceUseCase } from '@gridpilot/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import type { GetRaceRegistrationsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceRegistrationsUseCase';
|
||||
import type { GetRaceWithSOFUseCase } from '@gridpilot/racing/application/use-cases/GetRaceWithSOFUseCase';
|
||||
import type { GetRaceProtestsUseCase } from '@gridpilot/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
import type { GetRacePenaltiesUseCase } from '@gridpilot/racing/application/use-cases/GetRacePenaltiesUseCase';
|
||||
import type { GetRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetRacesPageDataUseCase';
|
||||
import type { GetRaceDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceDetailUseCase';
|
||||
import type { GetRaceResultsDetailUseCase } from '@gridpilot/racing/application/use-cases/GetRaceResultsDetailUseCase';
|
||||
import type { GetAllRacesPageDataUseCase } from '@gridpilot/racing/application/use-cases/GetAllRacesPageDataUseCase';
|
||||
import type { GetProfileOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
import type { UpdateDriverProfileUseCase } from '@gridpilot/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import type { GetDriversLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import type { GetTeamsLeaderboardUseCase } from '@gridpilot/racing/application/use-cases/GetTeamsLeaderboardUseCase';
|
||||
import type { GetLeagueStandingsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import type { GetLeagueDriverSeasonStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueDriverSeasonStatsUseCase';
|
||||
import type { GetAllLeaguesWithCapacityUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import type { GetAllLeaguesWithCapacityAndScoringUseCase } from '@gridpilot/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
import type { ListLeagueScoringPresetsUseCase } from '@gridpilot/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
|
||||
import type { GetLeagueScoringConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueScoringConfigUseCase';
|
||||
import type { GetLeagueFullConfigUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import type { GetLeagueStatsUseCase } from '@gridpilot/racing/application/use-cases/GetLeagueStatsUseCase';
|
||||
import type { CancelRaceUseCase } from '@gridpilot/racing/application/use-cases/CancelRaceUseCase';
|
||||
import type { ImportRaceResultsUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsUseCase';
|
||||
import type { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '@gridpilot/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISponsorshipPricingRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { TransferLeagueOwnershipUseCase } from '@gridpilot/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import type { DriverRatingProvider } from '@gridpilot/racing/application';
|
||||
import type { PreviewLeagueScheduleUseCase } from '@gridpilot/racing/application';
|
||||
import type { LeagueScoringPresetProvider } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type { LeagueScoringPresetDTO } from '@gridpilot/racing/application/ports/LeagueScoringPresetProvider';
|
||||
import type { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase';
|
||||
import type { ListSeasonsForLeagueUseCase } from '@gridpilot/racing/application/use-cases/SeasonUseCases';
|
||||
import { createDemoDriverStats, getDemoLeagueRankings, type DriverStats } from '@gridpilot/testing-support';
|
||||
|
||||
/**
|
||||
* DI Container - TSyringe Facade
|
||||
* Provides singleton access to TSyringe container with lazy initialization
|
||||
*/
|
||||
class DIContainer {
|
||||
private static instance: DIContainer;
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {
|
||||
// Private constructor for singleton pattern
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure TSyringe container is configured
|
||||
*/
|
||||
private ensureInitialized(): void {
|
||||
if (this.initialized) return;
|
||||
configureDIContainer();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(): DIContainer {
|
||||
if (!DIContainer.instance) {
|
||||
DIContainer.instance = new DIContainer();
|
||||
}
|
||||
return DIContainer.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the container (useful for testing)
|
||||
*/
|
||||
static reset(): void {
|
||||
DIContainer.instance = new DIContainer();
|
||||
DIContainer.instance.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository getters - resolve from TSyringe container
|
||||
*/
|
||||
get driverRepository(): IDriverRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IDriverRepository>(DI_TOKENS.DriverRepository);
|
||||
}
|
||||
|
||||
get leagueRepository(): ILeagueRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ILeagueRepository>(DI_TOKENS.LeagueRepository);
|
||||
}
|
||||
|
||||
get raceRepository(): IRaceRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IRaceRepository>(DI_TOKENS.RaceRepository);
|
||||
}
|
||||
|
||||
get resultRepository(): IResultRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IResultRepository>(DI_TOKENS.ResultRepository);
|
||||
}
|
||||
|
||||
get standingRepository(): IStandingRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IStandingRepository>(DI_TOKENS.StandingRepository);
|
||||
}
|
||||
|
||||
get penaltyRepository(): IPenaltyRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IPenaltyRepository>(DI_TOKENS.PenaltyRepository);
|
||||
}
|
||||
|
||||
get protestRepository(): IProtestRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IProtestRepository>(DI_TOKENS.ProtestRepository);
|
||||
}
|
||||
|
||||
get raceRegistrationRepository(): IRaceRegistrationRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IRaceRegistrationRepository>(DI_TOKENS.RaceRegistrationRepository);
|
||||
}
|
||||
|
||||
get leagueMembershipRepository(): ILeagueMembershipRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ILeagueMembershipRepository>(DI_TOKENS.LeagueMembershipRepository);
|
||||
}
|
||||
|
||||
get gameRepository(): IGameRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IGameRepository>(DI_TOKENS.GameRepository);
|
||||
}
|
||||
|
||||
get seasonRepository(): ISeasonRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISeasonRepository>(DI_TOKENS.SeasonRepository);
|
||||
}
|
||||
|
||||
get leagueScoringConfigRepository(): ILeagueScoringConfigRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ILeagueScoringConfigRepository>(DI_TOKENS.LeagueScoringConfigRepository);
|
||||
}
|
||||
|
||||
get leagueScoringPresetProvider(): LeagueScoringPresetProvider {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<LeagueScoringPresetProvider>(DI_TOKENS.LeagueScoringPresetProvider);
|
||||
}
|
||||
|
||||
get joinLeagueUseCase(): JoinLeagueUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<JoinLeagueUseCase>(DI_TOKENS.JoinLeagueUseCase);
|
||||
}
|
||||
|
||||
get registerForRaceUseCase(): RegisterForRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RegisterForRaceUseCase>(DI_TOKENS.RegisterForRaceUseCase);
|
||||
}
|
||||
|
||||
get withdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<WithdrawFromRaceUseCase>(DI_TOKENS.WithdrawFromRaceUseCase);
|
||||
}
|
||||
|
||||
get isDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IsDriverRegisteredForRaceUseCase>(DI_TOKENS.IsDriverRegisteredForRaceUseCase);
|
||||
}
|
||||
|
||||
get getRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceRegistrationsUseCase>(DI_TOKENS.GetRaceRegistrationsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueStandingsUseCase>(DI_TOKENS.GetLeagueStandingsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueDriverSeasonStatsUseCase>(DI_TOKENS.GetLeagueDriverSeasonStatsUseCase);
|
||||
}
|
||||
|
||||
get getAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityUseCase);
|
||||
}
|
||||
|
||||
get getAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllLeaguesWithCapacityAndScoringUseCase>(DI_TOKENS.GetAllLeaguesWithCapacityAndScoringUseCase);
|
||||
}
|
||||
|
||||
get listSeasonsForLeagueUseCase(): ListSeasonsForLeagueUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ListSeasonsForLeagueUseCase>(DI_TOKENS.ListSeasonsForLeagueUseCase);
|
||||
}
|
||||
|
||||
get listLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ListLeagueScoringPresetsUseCase>(DI_TOKENS.ListLeagueScoringPresetsUseCase);
|
||||
}
|
||||
|
||||
get getLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueScoringConfigUseCase>(DI_TOKENS.GetLeagueScoringConfigUseCase);
|
||||
}
|
||||
|
||||
get getLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueFullConfigUseCase>(DI_TOKENS.GetLeagueFullConfigUseCase);
|
||||
}
|
||||
|
||||
get previewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<PreviewLeagueScheduleUseCase>(DI_TOKENS.PreviewLeagueScheduleUseCase);
|
||||
}
|
||||
|
||||
get getRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceWithSOFUseCase>(DI_TOKENS.GetRaceWithSOFUseCase);
|
||||
}
|
||||
|
||||
get getLeagueStatsUseCase(): GetLeagueStatsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetLeagueStatsUseCase>(DI_TOKENS.GetLeagueStatsUseCase);
|
||||
}
|
||||
|
||||
get getRacesPageDataUseCase(): GetRacesPageDataUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRacesPageDataUseCase>(DI_TOKENS.GetRacesPageDataUseCase);
|
||||
}
|
||||
|
||||
get getAllRacesPageDataUseCase(): GetAllRacesPageDataUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllRacesPageDataUseCase>(DI_TOKENS.GetAllRacesPageDataUseCase);
|
||||
}
|
||||
|
||||
get getRaceDetailUseCase(): GetRaceDetailUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceDetailUseCase>(DI_TOKENS.GetRaceDetailUseCase);
|
||||
}
|
||||
|
||||
get getRaceResultsDetailUseCase(): GetRaceResultsDetailUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceResultsDetailUseCase>(DI_TOKENS.GetRaceResultsDetailUseCase);
|
||||
}
|
||||
|
||||
get getDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDriversLeaderboardUseCase>(DI_TOKENS.GetDriversLeaderboardUseCase);
|
||||
}
|
||||
|
||||
get getTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamsLeaderboardUseCase>(DI_TOKENS.GetTeamsLeaderboardUseCase);
|
||||
}
|
||||
|
||||
get getDashboardOverviewUseCase(): GetDashboardOverviewUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDashboardOverviewUseCase>(DI_TOKENS.GetDashboardOverviewUseCase);
|
||||
}
|
||||
|
||||
get getProfileOverviewUseCase(): GetProfileOverviewUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetProfileOverviewUseCase>(DI_TOKENS.GetProfileOverviewUseCase);
|
||||
}
|
||||
|
||||
get updateDriverProfileUseCase(): UpdateDriverProfileUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<UpdateDriverProfileUseCase>(DI_TOKENS.UpdateDriverProfileUseCase);
|
||||
}
|
||||
|
||||
get driverRatingProvider(): DriverRatingProvider {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<DriverRatingProvider>(DI_TOKENS.DriverRatingProvider);
|
||||
}
|
||||
|
||||
get cancelRaceUseCase(): CancelRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<CancelRaceUseCase>(DI_TOKENS.CancelRaceUseCase);
|
||||
}
|
||||
|
||||
get completeRaceUseCase(): import('@gridpilot/racing/application/use-cases/CompleteRaceUseCase').CompleteRaceUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<import('@gridpilot/racing/application/use-cases/CompleteRaceUseCase').CompleteRaceUseCase>(DI_TOKENS.CompleteRaceUseCase);
|
||||
}
|
||||
|
||||
get importRaceResultsUseCase(): ImportRaceResultsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ImportRaceResultsUseCase>(DI_TOKENS.ImportRaceResultsUseCase);
|
||||
}
|
||||
|
||||
get createLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<CreateLeagueWithSeasonAndScoringUseCase>(DI_TOKENS.CreateLeagueWithSeasonAndScoringUseCase);
|
||||
}
|
||||
|
||||
get createTeamUseCase(): CreateTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<CreateTeamUseCase>(DI_TOKENS.CreateTeamUseCase);
|
||||
}
|
||||
|
||||
get joinTeamUseCase(): JoinTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<JoinTeamUseCase>(DI_TOKENS.JoinTeamUseCase);
|
||||
}
|
||||
|
||||
get leaveTeamUseCase(): LeaveTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<LeaveTeamUseCase>(DI_TOKENS.LeaveTeamUseCase);
|
||||
}
|
||||
|
||||
get approveTeamJoinRequestUseCase(): ApproveTeamJoinRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ApproveTeamJoinRequestUseCase>(DI_TOKENS.ApproveTeamJoinRequestUseCase);
|
||||
}
|
||||
|
||||
get rejectTeamJoinRequestUseCase(): RejectTeamJoinRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RejectTeamJoinRequestUseCase>(DI_TOKENS.RejectTeamJoinRequestUseCase);
|
||||
}
|
||||
|
||||
get updateTeamUseCase(): UpdateTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<UpdateTeamUseCase>(DI_TOKENS.UpdateTeamUseCase);
|
||||
}
|
||||
|
||||
get getAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetAllTeamsUseCase>(DI_TOKENS.GetAllTeamsUseCase);
|
||||
}
|
||||
|
||||
get getTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamDetailsUseCase>(DI_TOKENS.GetTeamDetailsUseCase);
|
||||
}
|
||||
|
||||
get getTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamMembersUseCase>(DI_TOKENS.GetTeamMembersUseCase);
|
||||
}
|
||||
|
||||
get getTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetTeamJoinRequestsUseCase>(DI_TOKENS.GetTeamJoinRequestsUseCase);
|
||||
}
|
||||
|
||||
get getDriverTeamUseCase(): GetDriverTeamUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetDriverTeamUseCase>(DI_TOKENS.GetDriverTeamUseCase);
|
||||
}
|
||||
|
||||
get teamRepository(): ITeamRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ITeamRepository>(DI_TOKENS.TeamRepository);
|
||||
}
|
||||
|
||||
get teamMembershipRepository(): ITeamMembershipRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ITeamMembershipRepository>(DI_TOKENS.TeamMembershipRepository);
|
||||
}
|
||||
|
||||
get feedRepository(): IFeedRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<IFeedRepository>(DI_TOKENS.FeedRepository);
|
||||
}
|
||||
|
||||
get socialRepository(): ISocialGraphRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISocialGraphRepository>(DI_TOKENS.SocialRepository);
|
||||
}
|
||||
|
||||
get imageService(): ImageServicePort {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ImageServicePort>(DI_TOKENS.ImageService);
|
||||
}
|
||||
|
||||
get trackRepository(): ITrackRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ITrackRepository>(DI_TOKENS.TrackRepository);
|
||||
}
|
||||
|
||||
get carRepository(): ICarRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ICarRepository>(DI_TOKENS.CarRepository);
|
||||
}
|
||||
|
||||
get notificationRepository(): INotificationRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<INotificationRepository>(DI_TOKENS.NotificationRepository);
|
||||
}
|
||||
|
||||
get notificationPreferenceRepository(): INotificationPreferenceRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<INotificationPreferenceRepository>(DI_TOKENS.NotificationPreferenceRepository);
|
||||
}
|
||||
|
||||
get sendNotificationUseCase(): SendNotificationUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<SendNotificationUseCase>(DI_TOKENS.SendNotificationUseCase);
|
||||
}
|
||||
|
||||
get markNotificationReadUseCase(): MarkNotificationReadUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<MarkNotificationReadUseCase>(DI_TOKENS.MarkNotificationReadUseCase);
|
||||
}
|
||||
|
||||
get getUnreadNotificationsUseCase(): GetUnreadNotificationsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetUnreadNotificationsUseCase>(DI_TOKENS.GetUnreadNotificationsUseCase);
|
||||
}
|
||||
|
||||
get fileProtestUseCase(): FileProtestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<FileProtestUseCase>(DI_TOKENS.FileProtestUseCase);
|
||||
}
|
||||
|
||||
get reviewProtestUseCase(): ReviewProtestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ReviewProtestUseCase>(DI_TOKENS.ReviewProtestUseCase);
|
||||
}
|
||||
|
||||
get applyPenaltyUseCase(): ApplyPenaltyUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ApplyPenaltyUseCase>(DI_TOKENS.ApplyPenaltyUseCase);
|
||||
}
|
||||
|
||||
get quickPenaltyUseCase(): QuickPenaltyUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<QuickPenaltyUseCase>(DI_TOKENS.QuickPenaltyUseCase);
|
||||
}
|
||||
|
||||
get getRaceProtestsUseCase(): GetRaceProtestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRaceProtestsUseCase>(DI_TOKENS.GetRaceProtestsUseCase);
|
||||
}
|
||||
|
||||
get getRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetRacePenaltiesUseCase>(DI_TOKENS.GetRacePenaltiesUseCase);
|
||||
}
|
||||
|
||||
get requestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RequestProtestDefenseUseCase>(DI_TOKENS.RequestProtestDefenseUseCase);
|
||||
}
|
||||
|
||||
get submitProtestDefenseUseCase(): SubmitProtestDefenseUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<SubmitProtestDefenseUseCase>(DI_TOKENS.SubmitProtestDefenseUseCase);
|
||||
}
|
||||
|
||||
get transferLeagueOwnershipUseCase(): TransferLeagueOwnershipUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<TransferLeagueOwnershipUseCase>(DI_TOKENS.TransferLeagueOwnershipUseCase);
|
||||
}
|
||||
|
||||
get sponsorRepository(): ISponsorRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISponsorRepository>(DI_TOKENS.SponsorRepository);
|
||||
}
|
||||
|
||||
get seasonSponsorshipRepository(): ISeasonSponsorshipRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISeasonSponsorshipRepository>(DI_TOKENS.SeasonSponsorshipRepository);
|
||||
}
|
||||
|
||||
get getSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetSponsorDashboardUseCase>(DI_TOKENS.GetSponsorDashboardUseCase);
|
||||
}
|
||||
|
||||
get getSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetSponsorSponsorshipsUseCase>(DI_TOKENS.GetSponsorSponsorshipsUseCase);
|
||||
}
|
||||
|
||||
get sponsorshipRequestRepository(): ISponsorshipRequestRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISponsorshipRequestRepository>(DI_TOKENS.SponsorshipRequestRepository);
|
||||
}
|
||||
|
||||
get sponsorshipPricingRepository(): ISponsorshipPricingRepository {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ISponsorshipPricingRepository>(DI_TOKENS.SponsorshipPricingRepository);
|
||||
}
|
||||
|
||||
get applyForSponsorshipUseCase(): ApplyForSponsorshipUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<ApplyForSponsorshipUseCase>(DI_TOKENS.ApplyForSponsorshipUseCase);
|
||||
}
|
||||
|
||||
get acceptSponsorshipRequestUseCase(): AcceptSponsorshipRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<AcceptSponsorshipRequestUseCase>(DI_TOKENS.AcceptSponsorshipRequestUseCase);
|
||||
}
|
||||
|
||||
get rejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<RejectSponsorshipRequestUseCase>(DI_TOKENS.RejectSponsorshipRequestUseCase);
|
||||
}
|
||||
|
||||
get getPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetPendingSponsorshipRequestsUseCase>(DI_TOKENS.GetPendingSponsorshipRequestsUseCase);
|
||||
}
|
||||
|
||||
get getEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
|
||||
this.ensureInitialized();
|
||||
return getDIContainer().resolve<GetEntitySponsorshipPricingUseCase>(DI_TOKENS.GetEntitySponsorshipPricingUseCase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exported accessor functions
|
||||
*/
|
||||
export function getDriverRepository(): IDriverRepository {
|
||||
return DIContainer.getInstance().driverRepository;
|
||||
}
|
||||
|
||||
export function getLeagueRepository(): ILeagueRepository {
|
||||
return DIContainer.getInstance().leagueRepository;
|
||||
}
|
||||
|
||||
export function getRaceRepository(): IRaceRepository {
|
||||
return DIContainer.getInstance().raceRepository;
|
||||
}
|
||||
|
||||
export function getResultRepository(): IResultRepository {
|
||||
return DIContainer.getInstance().resultRepository;
|
||||
}
|
||||
|
||||
export function getStandingRepository(): IStandingRepository {
|
||||
return DIContainer.getInstance().standingRepository;
|
||||
}
|
||||
|
||||
export function getPenaltyRepository(): IPenaltyRepository {
|
||||
return DIContainer.getInstance().penaltyRepository;
|
||||
}
|
||||
|
||||
export function getProtestRepository(): IProtestRepository {
|
||||
return DIContainer.getInstance().protestRepository;
|
||||
}
|
||||
|
||||
export function getRaceRegistrationRepository(): IRaceRegistrationRepository {
|
||||
return DIContainer.getInstance().raceRegistrationRepository;
|
||||
}
|
||||
|
||||
export function getLeagueMembershipRepository(): ILeagueMembershipRepository {
|
||||
return DIContainer.getInstance().leagueMembershipRepository;
|
||||
}
|
||||
|
||||
export function getJoinLeagueUseCase(): JoinLeagueUseCase {
|
||||
return DIContainer.getInstance().joinLeagueUseCase;
|
||||
}
|
||||
|
||||
export function getRegisterForRaceUseCase(): RegisterForRaceUseCase {
|
||||
return DIContainer.getInstance().registerForRaceUseCase;
|
||||
}
|
||||
|
||||
export function getWithdrawFromRaceUseCase(): WithdrawFromRaceUseCase {
|
||||
return DIContainer.getInstance().withdrawFromRaceUseCase;
|
||||
}
|
||||
|
||||
export function getIsDriverRegisteredForRaceUseCase(): IsDriverRegisteredForRaceUseCase {
|
||||
return DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query facade for checking if a driver is registered for a race.
|
||||
*/
|
||||
export function getIsDriverRegisteredForRaceQuery(): {
|
||||
execute(input: { raceId: string; driverId: string }): Promise<boolean>;
|
||||
} {
|
||||
const useCase = DIContainer.getInstance().isDriverRegisteredForRaceUseCase;
|
||||
return {
|
||||
async execute(input: { raceId: string; driverId: string }): Promise<boolean> {
|
||||
const result = await useCase.execute(input);
|
||||
return Boolean(result);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getGetRaceRegistrationsUseCase(): GetRaceRegistrationsUseCase {
|
||||
return DIContainer.getInstance().getRaceRegistrationsUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueStandingsUseCase(): GetLeagueStandingsUseCase {
|
||||
return DIContainer.getInstance().getLeagueStandingsUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueDriverSeasonStatsUseCase(): GetLeagueDriverSeasonStatsUseCase {
|
||||
return DIContainer.getInstance().getLeagueDriverSeasonStatsUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllLeaguesWithCapacityUseCase(): GetAllLeaguesWithCapacityUseCase {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllLeaguesWithCapacityAndScoringUseCase(): GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
return DIContainer.getInstance().getAllLeaguesWithCapacityAndScoringUseCase;
|
||||
}
|
||||
|
||||
export function getListSeasonsForLeagueUseCase(): ListSeasonsForLeagueUseCase {
|
||||
return DIContainer.getInstance().listSeasonsForLeagueUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueScoringConfigUseCase(): GetLeagueScoringConfigUseCase {
|
||||
return DIContainer.getInstance().getLeagueScoringConfigUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueFullConfigUseCase(): GetLeagueFullConfigUseCase {
|
||||
return DIContainer.getInstance().getLeagueFullConfigUseCase;
|
||||
}
|
||||
|
||||
export function getPreviewLeagueScheduleUseCase(): PreviewLeagueScheduleUseCase {
|
||||
return DIContainer.getInstance().previewLeagueScheduleUseCase;
|
||||
}
|
||||
|
||||
export function getListLeagueScoringPresetsUseCase(): ListLeagueScoringPresetsUseCase {
|
||||
return DIContainer.getInstance().listLeagueScoringPresetsUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight query facade for listing league scoring presets.
|
||||
* Returns an object with an execute() method for use in UI code.
|
||||
*/
|
||||
export function getListLeagueScoringPresetsQuery(): {
|
||||
execute(): Promise<LeagueScoringPresetDTO[]>;
|
||||
} {
|
||||
const useCase = DIContainer.getInstance().listLeagueScoringPresetsUseCase;
|
||||
return {
|
||||
async execute(): Promise<LeagueScoringPresetDTO[]> {
|
||||
const presenter = new LeagueScoringPresetsPresenter();
|
||||
await useCase.execute(undefined as void, presenter);
|
||||
const viewModel = presenter.getViewModel();
|
||||
return viewModel.presets;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getCreateLeagueWithSeasonAndScoringUseCase(): CreateLeagueWithSeasonAndScoringUseCase {
|
||||
return DIContainer.getInstance().createLeagueWithSeasonAndScoringUseCase;
|
||||
}
|
||||
|
||||
export function getCancelRaceUseCase(): CancelRaceUseCase {
|
||||
return DIContainer.getInstance().cancelRaceUseCase;
|
||||
}
|
||||
|
||||
export function getCompleteRaceUseCase(): import('@gridpilot/racing/application/use-cases/CompleteRaceUseCase').CompleteRaceUseCase {
|
||||
return DIContainer.getInstance().completeRaceUseCase;
|
||||
}
|
||||
|
||||
export function getImportRaceResultsUseCase(): ImportRaceResultsUseCase {
|
||||
return DIContainer.getInstance().importRaceResultsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceWithSOFUseCase(): GetRaceWithSOFUseCase {
|
||||
return DIContainer.getInstance().getRaceWithSOFUseCase;
|
||||
}
|
||||
|
||||
export function getGetLeagueStatsUseCase(): GetLeagueStatsUseCase {
|
||||
return DIContainer.getInstance().getLeagueStatsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRacesPageDataUseCase(): GetRacesPageDataUseCase {
|
||||
return DIContainer.getInstance().getRacesPageDataUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllRacesPageDataUseCase(): GetAllRacesPageDataUseCase {
|
||||
return DIContainer.getInstance().getAllRacesPageDataUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceDetailUseCase(): GetRaceDetailUseCase {
|
||||
return DIContainer.getInstance().getRaceDetailUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceResultsDetailUseCase(): GetRaceResultsDetailUseCase {
|
||||
return DIContainer.getInstance().getRaceResultsDetailUseCase;
|
||||
}
|
||||
|
||||
export function getGetDriversLeaderboardUseCase(): GetDriversLeaderboardUseCase {
|
||||
return DIContainer.getInstance().getDriversLeaderboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamsLeaderboardUseCase(): GetTeamsLeaderboardUseCase {
|
||||
return DIContainer.getInstance().getTeamsLeaderboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetDashboardOverviewUseCase(): GetDashboardOverviewUseCase {
|
||||
return DIContainer.getInstance().getDashboardOverviewUseCase;
|
||||
}
|
||||
|
||||
export function getGetProfileOverviewUseCase(): GetProfileOverviewUseCase {
|
||||
return DIContainer.getInstance().getProfileOverviewUseCase;
|
||||
}
|
||||
|
||||
export function getUpdateDriverProfileUseCase(): UpdateDriverProfileUseCase {
|
||||
return DIContainer.getInstance().updateDriverProfileUseCase;
|
||||
}
|
||||
|
||||
export function getDriverRatingProvider(): DriverRatingProvider {
|
||||
return DIContainer.getInstance().driverRatingProvider;
|
||||
}
|
||||
|
||||
export function getTeamRepository(): ITeamRepository {
|
||||
return DIContainer.getInstance().teamRepository;
|
||||
}
|
||||
|
||||
export function getTeamMembershipRepository(): ITeamMembershipRepository {
|
||||
return DIContainer.getInstance().teamMembershipRepository;
|
||||
}
|
||||
|
||||
export function getCreateTeamUseCase(): CreateTeamUseCase {
|
||||
return DIContainer.getInstance().createTeamUseCase;
|
||||
}
|
||||
|
||||
export function getJoinTeamUseCase(): JoinTeamUseCase {
|
||||
return DIContainer.getInstance().joinTeamUseCase;
|
||||
}
|
||||
|
||||
export function getLeaveTeamUseCase(): LeaveTeamUseCase {
|
||||
return DIContainer.getInstance().leaveTeamUseCase;
|
||||
}
|
||||
|
||||
export function getApproveTeamJoinRequestUseCase(): ApproveTeamJoinRequestUseCase {
|
||||
return DIContainer.getInstance().approveTeamJoinRequestUseCase;
|
||||
}
|
||||
|
||||
export function getRejectTeamJoinRequestUseCase(): RejectTeamJoinRequestUseCase {
|
||||
return DIContainer.getInstance().rejectTeamJoinRequestUseCase;
|
||||
}
|
||||
|
||||
export function getUpdateTeamUseCase(): UpdateTeamUseCase {
|
||||
return DIContainer.getInstance().updateTeamUseCase;
|
||||
}
|
||||
|
||||
export function getGetAllTeamsUseCase(): GetAllTeamsUseCase {
|
||||
return DIContainer.getInstance().getAllTeamsUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamDetailsUseCase(): GetTeamDetailsUseCase {
|
||||
return DIContainer.getInstance().getTeamDetailsUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamMembersUseCase(): GetTeamMembersUseCase {
|
||||
return DIContainer.getInstance().getTeamMembersUseCase;
|
||||
}
|
||||
|
||||
export function getGetTeamJoinRequestsUseCase(): GetTeamJoinRequestsUseCase {
|
||||
return DIContainer.getInstance().getTeamJoinRequestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetDriverTeamUseCase(): GetDriverTeamUseCase {
|
||||
return DIContainer.getInstance().getDriverTeamUseCase;
|
||||
}
|
||||
|
||||
export function getFeedRepository(): IFeedRepository {
|
||||
return DIContainer.getInstance().feedRepository;
|
||||
}
|
||||
|
||||
export function getSocialRepository(): ISocialGraphRepository {
|
||||
return DIContainer.getInstance().socialRepository;
|
||||
}
|
||||
|
||||
export function getImageService(): ImageServicePort {
|
||||
return DIContainer.getInstance().imageService;
|
||||
}
|
||||
|
||||
export function getTrackRepository(): ITrackRepository {
|
||||
return DIContainer.getInstance().trackRepository;
|
||||
}
|
||||
|
||||
export function getCarRepository(): ICarRepository {
|
||||
return DIContainer.getInstance().carRepository;
|
||||
}
|
||||
|
||||
export function getSeasonRepository(): ISeasonRepository {
|
||||
return DIContainer.getInstance().seasonRepository;
|
||||
}
|
||||
|
||||
export function getNotificationRepository(): INotificationRepository {
|
||||
return DIContainer.getInstance().notificationRepository;
|
||||
}
|
||||
|
||||
export function getNotificationPreferenceRepository(): INotificationPreferenceRepository {
|
||||
return DIContainer.getInstance().notificationPreferenceRepository;
|
||||
}
|
||||
|
||||
export function getSendNotificationUseCase(): SendNotificationUseCase {
|
||||
return DIContainer.getInstance().sendNotificationUseCase;
|
||||
}
|
||||
|
||||
export function getMarkNotificationReadUseCase(): MarkNotificationReadUseCase {
|
||||
return DIContainer.getInstance().markNotificationReadUseCase;
|
||||
}
|
||||
|
||||
export function getGetUnreadNotificationsUseCase(): GetUnreadNotificationsUseCase {
|
||||
return DIContainer.getInstance().getUnreadNotificationsUseCase;
|
||||
}
|
||||
|
||||
export function getFileProtestUseCase(): FileProtestUseCase {
|
||||
return DIContainer.getInstance().fileProtestUseCase;
|
||||
}
|
||||
|
||||
export function getReviewProtestUseCase(): ReviewProtestUseCase {
|
||||
return DIContainer.getInstance().reviewProtestUseCase;
|
||||
}
|
||||
|
||||
export function getApplyPenaltyUseCase(): ApplyPenaltyUseCase {
|
||||
return DIContainer.getInstance().applyPenaltyUseCase;
|
||||
}
|
||||
|
||||
export function getQuickPenaltyUseCase(): QuickPenaltyUseCase {
|
||||
return DIContainer.getInstance().quickPenaltyUseCase;
|
||||
}
|
||||
|
||||
export function getGetRaceProtestsUseCase(): GetRaceProtestsUseCase {
|
||||
return DIContainer.getInstance().getRaceProtestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetRacePenaltiesUseCase(): GetRacePenaltiesUseCase {
|
||||
return DIContainer.getInstance().getRacePenaltiesUseCase;
|
||||
}
|
||||
|
||||
export function getRequestProtestDefenseUseCase(): RequestProtestDefenseUseCase {
|
||||
return DIContainer.getInstance().requestProtestDefenseUseCase;
|
||||
}
|
||||
|
||||
export function getSubmitProtestDefenseUseCase(): SubmitProtestDefenseUseCase {
|
||||
return DIContainer.getInstance().submitProtestDefenseUseCase;
|
||||
}
|
||||
|
||||
export function getTransferLeagueOwnershipUseCase(): TransferLeagueOwnershipUseCase {
|
||||
return DIContainer.getInstance().transferLeagueOwnershipUseCase;
|
||||
}
|
||||
|
||||
export function getSponsorRepository(): ISponsorRepository {
|
||||
return DIContainer.getInstance().sponsorRepository;
|
||||
}
|
||||
|
||||
export function getSeasonSponsorshipRepository(): ISeasonSponsorshipRepository {
|
||||
return DIContainer.getInstance().seasonSponsorshipRepository;
|
||||
}
|
||||
|
||||
export function getGetSponsorDashboardUseCase(): GetSponsorDashboardUseCase {
|
||||
return DIContainer.getInstance().getSponsorDashboardUseCase;
|
||||
}
|
||||
|
||||
export function getGetSponsorSponsorshipsUseCase(): GetSponsorSponsorshipsUseCase {
|
||||
return DIContainer.getInstance().getSponsorSponsorshipsUseCase;
|
||||
}
|
||||
|
||||
export function getSponsorshipRequestRepository(): ISponsorshipRequestRepository {
|
||||
return DIContainer.getInstance().sponsorshipRequestRepository;
|
||||
}
|
||||
|
||||
export function getSponsorshipPricingRepository(): ISponsorshipPricingRepository {
|
||||
return DIContainer.getInstance().sponsorshipPricingRepository;
|
||||
}
|
||||
|
||||
export function getApplyForSponsorshipUseCase(): ApplyForSponsorshipUseCase {
|
||||
return DIContainer.getInstance().applyForSponsorshipUseCase;
|
||||
}
|
||||
|
||||
export function getAcceptSponsorshipRequestUseCase(): AcceptSponsorshipRequestUseCase {
|
||||
return DIContainer.getInstance().acceptSponsorshipRequestUseCase;
|
||||
}
|
||||
|
||||
export function getRejectSponsorshipRequestUseCase(): RejectSponsorshipRequestUseCase {
|
||||
return DIContainer.getInstance().rejectSponsorshipRequestUseCase;
|
||||
}
|
||||
|
||||
export function getGetPendingSponsorshipRequestsUseCase(): GetPendingSponsorshipRequestsUseCase {
|
||||
return DIContainer.getInstance().getPendingSponsorshipRequestsUseCase;
|
||||
}
|
||||
|
||||
export function getGetEntitySponsorshipPricingUseCase(): GetEntitySponsorshipPricingUseCase {
|
||||
return DIContainer.getInstance().getEntitySponsorshipPricingUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset function for testing
|
||||
*/
|
||||
export function resetContainer(): void {
|
||||
DIContainer.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export stats from testing-support for backward compatibility
|
||||
*/
|
||||
export type { DriverStats };
|
||||
|
||||
/**
|
||||
* Get driver statistics and rankings
|
||||
* These functions access the demo driver stats registered in the DI container
|
||||
*/
|
||||
export function getDriverStats(driverId: string): DriverStats | null {
|
||||
const container = DIContainer.getInstance();
|
||||
// Ensure container is initialized
|
||||
container['ensureInitialized']();
|
||||
const stats = getDIContainer().resolve<Record<string, DriverStats>>(DI_TOKENS.DriverStats);
|
||||
return stats[driverId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all driver rankings sorted by rating
|
||||
*/
|
||||
export function getAllDriverRankings(): DriverStats[] {
|
||||
const container = DIContainer.getInstance();
|
||||
// Ensure container is initialized
|
||||
container['ensureInitialized']();
|
||||
const stats = getDIContainer().resolve<Record<string, DriverStats>>(DI_TOKENS.DriverStats);
|
||||
return Object.values(stats).sort((a, b) => b.rating - a.rating);
|
||||
}
|
||||
export { getDemoLeagueRankings as getLeagueRankings };
|
||||
@@ -1,177 +0,0 @@
|
||||
/**
|
||||
* Dependency Injection tokens for TSyringe container (Website)
|
||||
*/
|
||||
|
||||
export const DI_TOKENS = {
|
||||
// Repositories
|
||||
DriverRepository: Symbol.for('IDriverRepository'),
|
||||
LeagueRepository: Symbol.for('ILeagueRepository'),
|
||||
RaceRepository: Symbol.for('IRaceRepository'),
|
||||
ResultRepository: Symbol.for('IResultRepository'),
|
||||
StandingRepository: Symbol.for('IStandingRepository'),
|
||||
LeagueStandingsRepository: Symbol.for('ILeagueStandingsRepository'),
|
||||
PenaltyRepository: Symbol.for('IPenaltyRepository'),
|
||||
ProtestRepository: Symbol.for('IProtestRepository'),
|
||||
TeamRepository: Symbol.for('ITeamRepository'),
|
||||
TeamMembershipRepository: Symbol.for('ITeamMembershipRepository'),
|
||||
RaceRegistrationRepository: Symbol.for('IRaceRegistrationRepository'),
|
||||
LeagueMembershipRepository: Symbol.for('ILeagueMembershipRepository'),
|
||||
GameRepository: Symbol.for('IGameRepository'),
|
||||
SeasonRepository: Symbol.for('ISeasonRepository'),
|
||||
LeagueScoringConfigRepository: Symbol.for('ILeagueScoringConfigRepository'),
|
||||
TrackRepository: Symbol.for('ITrackRepository'),
|
||||
CarRepository: Symbol.for('ICarRepository'),
|
||||
FeedRepository: Symbol.for('IFeedRepository'),
|
||||
SocialRepository: Symbol.for('ISocialGraphRepository'),
|
||||
NotificationRepository: Symbol.for('INotificationRepository'),
|
||||
NotificationPreferenceRepository: Symbol.for('INotificationPreferenceRepository'),
|
||||
SponsorRepository: Symbol.for('ISponsorRepository'),
|
||||
SeasonSponsorshipRepository: Symbol.for('ISeasonSponsorshipRepository'),
|
||||
SponsorshipRequestRepository: Symbol.for('ISponsorshipRequestRepository'),
|
||||
SponsorshipPricingRepository: Symbol.for('ISponsorshipPricingRepository'),
|
||||
PageViewRepository: Symbol.for('IPageViewRepository'),
|
||||
EngagementRepository: Symbol.for('IEngagementRepository'),
|
||||
UserRepository: Symbol.for('IUserRepository'),
|
||||
SponsorAccountRepository: Symbol.for('ISponsorAccountRepository'),
|
||||
LiveryRepository: Symbol.for('ILiveryRepository'),
|
||||
ChampionshipStandingRepository: Symbol.for('IChampionshipStandingRepository'),
|
||||
LeagueWalletRepository: Symbol.for('ILeagueWalletRepository'),
|
||||
TransactionRepository: Symbol.for('ITransactionRepository'),
|
||||
SessionRepository: Symbol.for('ISessionRepository'),
|
||||
AchievementRepository: Symbol.for('IAchievementRepository'),
|
||||
UserRatingRepository: Symbol.for('IUserRatingRepository'),
|
||||
|
||||
// Providers
|
||||
LeagueScoringPresetProvider: Symbol.for('LeagueScoringPresetProvider'),
|
||||
DriverRatingProvider: Symbol.for('DriverRatingProvider'),
|
||||
|
||||
// Services
|
||||
ImageService: Symbol.for('ImageServicePort'),
|
||||
NotificationGatewayRegistry: Symbol.for('NotificationGatewayRegistry'),
|
||||
Logger: Symbol.for('ILogger'),
|
||||
AuthService: Symbol.for('AuthService'),
|
||||
|
||||
// Auth dependencies
|
||||
IdentityProvider: Symbol.for('IdentityProvider'),
|
||||
SessionPort: Symbol.for('SessionPort'),
|
||||
|
||||
// Use Cases - Auth
|
||||
StartAuthUseCase: Symbol.for('StartAuthUseCase'),
|
||||
GetCurrentUserSessionUseCase: Symbol.for('GetCurrentUserSessionUseCase'),
|
||||
HandleAuthCallbackUseCase: Symbol.for('HandleAuthCallbackUseCase'),
|
||||
LogoutUseCase: Symbol.for('LogoutUseCase'),
|
||||
SignupWithEmailUseCase: Symbol.for('SignupWithEmailUseCase'),
|
||||
LoginWithEmailUseCase: Symbol.for('LoginWithEmailUseCase'),
|
||||
|
||||
// Use Cases - Analytics
|
||||
RecordPageViewUseCase: Symbol.for('RecordPageViewUseCase'),
|
||||
RecordEngagementUseCase: Symbol.for('RecordEngagementUseCase'),
|
||||
|
||||
// Use Cases - Racing
|
||||
JoinLeagueUseCase: Symbol.for('JoinLeagueUseCase'),
|
||||
RegisterForRaceUseCase: Symbol.for('RegisterForRaceUseCase'),
|
||||
WithdrawFromRaceUseCase: Symbol.for('WithdrawFromRaceUseCase'),
|
||||
CreateLeagueWithSeasonAndScoringUseCase: Symbol.for('CreateLeagueWithSeasonAndScoringUseCase'),
|
||||
TransferLeagueOwnershipUseCase: Symbol.for('TransferLeagueOwnershipUseCase'),
|
||||
CancelRaceUseCase: Symbol.for('CancelRaceUseCase'),
|
||||
CompleteRaceUseCase: Symbol.for('CompleteRaceUseCase'),
|
||||
ImportRaceResultsUseCase: Symbol.for('ImportRaceResultsUseCase'),
|
||||
|
||||
// Queries - Dashboard
|
||||
GetDashboardOverviewUseCase: Symbol.for('GetDashboardOverviewUseCase'),
|
||||
GetProfileOverviewUseCase: Symbol.for('GetProfileOverviewUseCase'),
|
||||
|
||||
// Use Cases - Teams
|
||||
CreateTeamUseCase: Symbol.for('CreateTeamUseCase'),
|
||||
JoinTeamUseCase: Symbol.for('JoinTeamUseCase'),
|
||||
LeaveTeamUseCase: Symbol.for('LeaveTeamUseCase'),
|
||||
ApproveTeamJoinRequestUseCase: Symbol.for('ApproveTeamJoinRequestUseCase'),
|
||||
RejectTeamJoinRequestUseCase: Symbol.for('RejectTeamJoinRequestUseCase'),
|
||||
UpdateTeamUseCase: Symbol.for('UpdateTeamUseCase'),
|
||||
|
||||
// Use Cases - Stewarding
|
||||
FileProtestUseCase: Symbol.for('FileProtestUseCase'),
|
||||
ReviewProtestUseCase: Symbol.for('ReviewProtestUseCase'),
|
||||
ApplyPenaltyUseCase: Symbol.for('ApplyPenaltyUseCase'),
|
||||
QuickPenaltyUseCase: Symbol.for('QuickPenaltyUseCase'),
|
||||
RequestProtestDefenseUseCase: Symbol.for('RequestProtestDefenseUseCase'),
|
||||
SubmitProtestDefenseUseCase: Symbol.for('SubmitProtestDefenseUseCase'),
|
||||
|
||||
// Use Cases - Notifications
|
||||
SendNotificationUseCase: Symbol.for('SendNotificationUseCase'),
|
||||
MarkNotificationReadUseCase: Symbol.for('MarkNotificationReadUseCase'),
|
||||
|
||||
// Queries - Racing
|
||||
IsDriverRegisteredForRaceUseCase: Symbol.for('IsDriverRegisteredForRaceUseCase'),
|
||||
GetRaceRegistrationsUseCase: Symbol.for('GetRaceRegistrationsUseCase'),
|
||||
GetLeagueStandingsUseCase: Symbol.for('GetLeagueStandingsUseCase'),
|
||||
GetLeagueDriverSeasonStatsUseCase: Symbol.for('GetLeagueDriverSeasonStatsUseCase'),
|
||||
GetAllLeaguesWithCapacityUseCase: Symbol.for('GetAllLeaguesWithCapacityUseCase'),
|
||||
GetAllLeaguesWithCapacityAndScoringUseCase: Symbol.for('GetAllLeaguesWithCapacityAndScoringUseCase'),
|
||||
ListLeagueScoringPresetsUseCase: Symbol.for('ListLeagueScoringPresetsUseCase'),
|
||||
GetLeagueScoringConfigUseCase: Symbol.for('GetLeagueScoringConfigUseCase'),
|
||||
GetLeagueFullConfigUseCase: Symbol.for('GetLeagueFullConfigUseCase'),
|
||||
PreviewLeagueScheduleUseCase: Symbol.for('PreviewLeagueScheduleUseCase'),
|
||||
GetRaceWithSOFUseCase: Symbol.for('GetRaceWithSOFUseCase'),
|
||||
GetLeagueStatsUseCase: Symbol.for('GetLeagueStatsUseCase'),
|
||||
ListSeasonsForLeagueUseCase: Symbol.for('ListSeasonsForLeagueUseCase'),
|
||||
GetRacesPageDataUseCase: Symbol.for('GetRacesPageDataUseCase'),
|
||||
GetAllRacesPageDataUseCase: Symbol.for('GetAllRacesPageDataUseCase'),
|
||||
GetRaceDetailUseCase: Symbol.for('GetRaceDetailUseCase'),
|
||||
GetRaceResultsDetailUseCase: Symbol.for('GetRaceResultsDetailUseCase'),
|
||||
GetDriversLeaderboardUseCase: Symbol.for('GetDriversLeaderboardUseCase'),
|
||||
GetTeamsLeaderboardUseCase: Symbol.for('GetTeamsLeaderboardUseCase'),
|
||||
|
||||
// Use Cases - Teams (Query-like)
|
||||
GetAllTeamsUseCase: Symbol.for('GetAllTeamsUseCase'),
|
||||
GetTeamDetailsUseCase: Symbol.for('GetTeamDetailsUseCase'),
|
||||
GetTeamMembersUseCase: Symbol.for('GetTeamMembersUseCase'),
|
||||
GetTeamJoinRequestsUseCase: Symbol.for('GetTeamJoinRequestsUseCase'),
|
||||
GetDriverTeamUseCase: Symbol.for('GetDriverTeamUseCase'),
|
||||
|
||||
// Queries - Stewarding
|
||||
GetRaceProtestsUseCase: Symbol.for('GetRaceProtestsUseCase'),
|
||||
GetRacePenaltiesUseCase: Symbol.for('GetRacePenaltiesUseCase'),
|
||||
|
||||
// Queries - Notifications
|
||||
GetUnreadNotificationsUseCase: Symbol.for('GetUnreadNotificationsUseCase'),
|
||||
|
||||
// Use Cases - Sponsors
|
||||
GetSponsorDashboardUseCase: Symbol.for('GetSponsorDashboardUseCase'),
|
||||
GetSponsorSponsorshipsUseCase: Symbol.for('GetSponsorSponsorshipsUseCase'),
|
||||
GetPendingSponsorshipRequestsUseCase: Symbol.for('GetPendingSponsorshipRequestsUseCase'),
|
||||
GetEntitySponsorshipPricingUseCase: Symbol.for('GetEntitySponsorshipPricingUseCase'),
|
||||
|
||||
// Use Cases - Sponsorship
|
||||
ApplyForSponsorshipUseCase: Symbol.for('ApplyForSponsorshipUseCase'),
|
||||
AcceptSponsorshipRequestUseCase: Symbol.for('AcceptSponsorshipRequestUseCase'),
|
||||
RejectSponsorshipRequestUseCase: Symbol.for('RejectSponsorshipRequestUseCase'),
|
||||
|
||||
// Use Cases - Driver Profile
|
||||
UpdateDriverProfileUseCase: Symbol.for('UpdateDriverProfileUseCase'),
|
||||
|
||||
// Data
|
||||
DriverStats: Symbol.for('DriverStats'),
|
||||
|
||||
// Presenters - Racing
|
||||
LeagueStandingsPresenter: Symbol.for('ILeagueStandingsPresenter'),
|
||||
RaceWithSOFPresenter: Symbol.for('IRaceWithSOFPresenter'),
|
||||
RaceProtestsPresenter: Symbol.for('IRaceProtestsPresenter'),
|
||||
RacePenaltiesPresenter: Symbol.for('IRacePenaltiesPresenter'),
|
||||
RaceRegistrationsPresenter: Symbol.for('IRaceRegistrationsPresenter'),
|
||||
DriverRegistrationStatusPresenter: Symbol.for('IDriverRegistrationStatusPresenter'),
|
||||
RaceDetailPresenter: Symbol.for('IRaceDetailPresenter'),
|
||||
RaceResultsDetailPresenter: Symbol.for('IRaceResultsDetailPresenter'),
|
||||
ImportRaceResultsPresenter: Symbol.for('IImportRaceResultsPresenter'),
|
||||
DashboardOverviewPresenter: Symbol.for('IDashboardOverviewPresenter'),
|
||||
ProfileOverviewPresenter: Symbol.for('IProfileOverviewPresenter'),
|
||||
|
||||
// Presenters - Sponsors
|
||||
SponsorDashboardPresenter: Symbol.for('ISponsorDashboardPresenter'),
|
||||
SponsorSponsorshipsPresenter: Symbol.for('ISponsorSponsorshipsPresenter'),
|
||||
PendingSponsorshipRequestsPresenter: Symbol.for('IPendingSponsorshipRequestsPresenter'),
|
||||
EntitySponsorshipPricingPresenter: Symbol.for('IEntitySponsorshipPricingPresenter'),
|
||||
LeagueSchedulePreviewPresenter: Symbol.for('ILeagueSchedulePreviewPresenter'),
|
||||
} as const;
|
||||
|
||||
export type DITokens = typeof DI_TOKENS;
|
||||
@@ -30,7 +30,8 @@
|
||||
"@gridpilot/media/*": ["../../core/media/*"],
|
||||
"@gridpilot/shared/logging": ["../../core/shared/logging"],
|
||||
"@gridpilot/shared/*": ["../../core/shared/*"],
|
||||
"@gridpilot/core/*": ["../../core/*"]
|
||||
"@gridpilot/core/*": ["../../core/*"],
|
||||
"@gridpilot/api/*": ["../../apps/api/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* Repository Interface: IEngagementRepository
|
||||
*
|
||||
* Defines persistence operations for EngagementEvent entities.
|
||||
*/
|
||||
|
||||
import type { EngagementEvent, EngagementAction, EngagementEntityType } from '../entities/EngagementEvent';
|
||||
|
||||
export interface IEngagementRepository {
|
||||
@@ -14,4 +8,4 @@ export interface IEngagementRepository {
|
||||
findByDateRange(startDate: Date, endDate: Date): Promise<EngagementEvent[]>;
|
||||
countByAction(action: EngagementAction, entityId?: string, since?: Date): Promise<number>;
|
||||
getSponsorClicksForEntity(entityId: string, since?: Date): Promise<number>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { User } from '../../domain/entities/User';
|
||||
// No direct import of apps/api DTOs in core module
|
||||
|
||||
/**
|
||||
* Application Use Case: GetCurrentSessionUseCase
|
||||
*
|
||||
* Retrieves the current user session information.
|
||||
*/
|
||||
export class GetCurrentSessionUseCase {
|
||||
async execute(userId: string): Promise<User | null> {
|
||||
// TODO: Implement actual logic to retrieve user and session data
|
||||
console.warn('GetCurrentSessionUseCase: Method not implemented.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { User } from '../../domain/entities/User';
|
||||
|
||||
export interface LoginWithIracingCallbackParams {
|
||||
code: string;
|
||||
state?: string;
|
||||
returnTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application Use Case: LoginWithIracingCallbackUseCase
|
||||
*
|
||||
* Handles the callback after iRacing authentication.
|
||||
*/
|
||||
export class LoginWithIracingCallbackUseCase {
|
||||
async execute(params: LoginWithIracingCallbackParams): Promise<User> {
|
||||
// TODO: Implement actual logic for handling iRacing OAuth callback
|
||||
console.warn('LoginWithIracingCallbackUseCase: Method not implemented.');
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface IracingAuthRedirectResult {
|
||||
redirectUrl: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application Use Case: StartIracingAuthRedirectUseCase
|
||||
*
|
||||
* Initiates the iRacing authentication flow.
|
||||
*/
|
||||
export class StartIracingAuthRedirectUseCase {
|
||||
async execute(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
// TODO: Implement actual logic for initiating iRacing OAuth redirect
|
||||
console.warn('StartIracingAuthRedirectUseCase: Method not implemented.');
|
||||
return {
|
||||
redirectUrl: '/mock-iracing-redirect',
|
||||
state: 'mock-state',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export interface StoredUser {
|
||||
displayName: string;
|
||||
passwordHash: string;
|
||||
salt: string;
|
||||
primaryDriverId?: string;
|
||||
primaryDriverId?: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export interface ILogger {
|
||||
debug(message: string, ...args: any[]): void;
|
||||
info(message: string, ...args: any[]): void;
|
||||
|
||||
85
package-lock.json
generated
85
package-lock.json
generated
@@ -54,6 +54,7 @@
|
||||
"@nestjs/common": "^10.4.20",
|
||||
"@nestjs/core": "^10.4.20",
|
||||
"@nestjs/platform-express": "^10.4.20",
|
||||
"@nestjs/swagger": "^7.4.2",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"pg": "^8.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
@@ -3121,6 +3122,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/tsdoc": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz",
|
||||
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -3201,6 +3208,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
||||
"integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"class-transformer": "^0.4.0 || ^0.5.0",
|
||||
"class-validator": "^0.13.0 || ^0.14.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz",
|
||||
@@ -3222,6 +3249,51 @@
|
||||
"@nestjs/core": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/swagger": {
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz",
|
||||
"integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/tsdoc": "^0.15.0",
|
||||
"@nestjs/mapped-types": "2.0.5",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"swagger-ui-dist": "5.17.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/static": "^6.0.0 || ^7.0.0",
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/static": {
|
||||
"optional": true
|
||||
},
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/swagger/node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.20.tgz",
|
||||
@@ -5472,7 +5544,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
@@ -11619,6 +11690,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -15552,6 +15629,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.17.14",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz",
|
||||
"integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
|
||||
Reference in New Issue
Block a user