module creation

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,13 +2,29 @@
import { Module } from '@nestjs/common';
import { HelloController } from './presentation/hello.controller';
import { HelloService } from './application/hello/hello.service';
import { AnalyticsModule } from './application/analytics/analytics.module';
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
import { DatabaseModule } from './infrastructure/database/database.module';
import { AuthModule } from './modules/auth/AuthModule';
import { LeagueModule } from './modules/league/LeagueModule';
import { RaceModule } from './modules/race/RaceModule';
import { TeamModule } from './modules/team/TeamModule';
import { SponsorModule } from './modules/sponsor/SponsorModule';
import { DriverModule } from './modules/driver/DriverModule';
import { MediaModule } from './modules/media/MediaModule';
import { PaymentsModule } from './modules/payments/PaymentsModule';
@Module({
imports: [
DatabaseModule,
AnalyticsModule
AnalyticsModule,
AuthModule,
LeagueModule,
RaceModule,
TeamModule,
SponsorModule,
DriverModule,
MediaModule,
PaymentsModule,
],
controllers: [HelloController],
providers: [HelloService],

View File

@@ -1,50 +0,0 @@
import { Module } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
const ILogger_TOKEN = 'ILogger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
import { InMemoryPageViewRepository } from '../../../../adapters/persistence/inmemory/analytics/InMemoryPageViewRepository';
import { TypeOrmEngagementRepository } from '../../../../adapters/persistence/typeorm/analytics/TypeOrmEngagementRepository';
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
import { AnalyticsController } from '../../presentation/analytics.controller';
@Module({
imports: [],
controllers: [AnalyticsController],
providers: [
{
provide: ILogger_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IPAGE_VIEW_REPO_TOKEN,
useClass: InMemoryPageViewRepository,
},
{
provide: IENGAGEMENT_REPO_TOKEN,
useFactory: (dataSource: DataSource) => new TypeOrmEngagementRepository(dataSource.manager),
inject: [getDataSourceToken()],
},
{
provide: RecordPageViewUseCase,
useFactory: (repo: IPageViewRepository, logger: ILogger) => new RecordPageViewUseCase(repo, logger),
inject: [IPAGE_VIEW_REPO_TOKEN, ILogger_TOKEN],
},
{
provide: RecordEngagementUseCase,
useFactory: (repo: IEngagementRepository, logger: ILogger) => new RecordEngagementUseCase(repo, logger),
inject: [IENGAGEMENT_REPO_TOKEN, ILogger_TOKEN],
},
],
})
export class AnalyticsModule {}

View File

@@ -0,0 +1,29 @@
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
import { RecordPageViewInput, RecordPageViewOutput, RecordEngagementInput, RecordEngagementOutput } from './dto/AnalyticsDto';
import { AnalyticsService } from './AnalyticsService';
@Controller('analytics')
export class AnalyticsController {
constructor(
private readonly analyticsService: AnalyticsService,
) {}
@Post('page-view')
async recordPageView(
@Body() input: RecordPageViewInput,
@Res() res: Response,
): Promise<void> {
const output: RecordPageViewOutput = await this.analyticsService.recordPageView(input);
res.status(HttpStatus.CREATED).json(output);
}
@Post('engagement')
async recordEngagement(
@Body() input: RecordEngagementInput,
@Res() res: Response,
): Promise<void> {
const output: RecordEngagementOutput = await this.analyticsService.recordEngagement(input);
res.status(HttpStatus.CREATED).json(output);
}
}

View File

@@ -0,0 +1,43 @@
import { Module } from '@nestjs/common';
import { AnalyticsController } from './AnalyticsController';
import { AnalyticsService } from './AnalyticsService';
const ILogger_TOKEN = 'ILogger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger';
import { InMemoryPageViewRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
import { InMemoryEngagementRepository } from '../../../../adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
@Module({
imports: [],
controllers: [AnalyticsController],
providers: [
AnalyticsService,
{
provide: ILogger_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IPAGE_VIEW_REPO_TOKEN,
useClass: InMemoryPageViewRepository,
},
{
provide: IENGAGEMENT_REPO_TOKEN,
useExisting: InMemoryEngagementRepository, // Assuming TypeOrmEngagementRepository is not available
},
// No need for useExisting here if the original intent was to inject the concrete class when providing the TOKEN
],
exports: [
AnalyticsService,
ILogger_TOKEN,
IPAGE_VIEW_REPO_TOKEN,
IENGAGEMENT_REPO_TOKEN,
],
})
export class AnalyticsModule {}

View File

@@ -0,0 +1,83 @@
import { Injectable, Inject } from '@nestjs/common';
import { RecordEngagementInput, RecordEngagementOutput, RecordPageViewInput, RecordPageViewOutput } from './dto/AnalyticsDto';
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
import { PageView } from '@gridpilot/analytics/domain/entities/PageView';
import { EngagementEvent } from '@gridpilot/analytics/domain/entities/EngagementEvent';
const ILogger_TOKEN = 'ILogger_TOKEN';
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
@Injectable()
export class AnalyticsService {
constructor(
@Inject(IPAGE_VIEW_REPO_TOKEN) private readonly pageViewRepository: IPageViewRepository,
@Inject(IENGAGEMENT_REPO_TOKEN) private readonly engagementRepository: IEngagementRepository,
@Inject(ILogger_TOKEN) private readonly logger: ILogger,
) {}
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
this.logger.debug('Executing RecordPageViewUseCase', { input });
try {
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
id: pageViewId,
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
entityId: input.entityId,
visitorType: input.visitorType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
sessionId: input.sessionId,
};
const pageView = PageView.create({
...baseProps,
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
...(input.country !== undefined ? { country: input.country } : {}),
});
await this.pageViewRepository.save(pageView);
this.logger.info('Page view recorded successfully', { pageViewId, input });
return { pageViewId };
} catch (error) {
this.logger.error('Error recording page view', error, { input });
throw error;
}
}
async recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
this.logger.debug('Executing RecordEngagementUseCase', { input });
try {
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
id: eventId,
action: input.action as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
entityId: input.entityId,
actorType: input.actorType,
sessionId: input.sessionId,
};
const event = EngagementEvent.create({
...baseProps,
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
});
await this.engagementRepository.save(event);
this.logger.info('Engagement recorded successfully', { eventId, input });
return {
eventId,
engagementWeight: event.getEngagementWeight(),
};
} catch (error) {
this.logger.error('Error recording engagement', error, { input });
throw error;
}
}
}

View File

@@ -0,0 +1,127 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsEnum, IsBoolean, IsNumber, IsObject } from 'class-validator';
// From core/analytics/domain/types/PageView.ts
export enum EntityType {
LEAGUE = 'league',
DRIVER = 'driver',
TEAM = 'team',
RACE = 'race',
SPONSOR = 'sponsor',
}
// From core/analytics/domain/types/PageView.ts
export enum VisitorType {
ANONYMOUS = 'anonymous',
DRIVER = 'driver',
SPONSOR = 'sponsor',
}
export class RecordPageViewInput {
@ApiProperty({ enum: EntityType })
@IsEnum(EntityType)
entityType: EntityType;
@ApiProperty()
@IsString()
entityId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
visitorId?: string;
@ApiProperty({ enum: VisitorType })
@IsEnum(VisitorType)
visitorType: VisitorType;
@ApiProperty()
@IsString()
sessionId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
referrer?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
userAgent?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
country?: string;
}
export class RecordPageViewOutput {
@ApiProperty()
@IsString()
pageViewId: string;
}
// From core/analytics/domain/types/EngagementEvent.ts
export enum EngagementAction {
CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
CLICK_SPONSOR_URL = 'click_sponsor_url',
DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
JOIN_LEAGUE = 'join_league',
REGISTER_RACE = 'register_race',
VIEW_STANDINGS = 'view_standings',
VIEW_SCHEDULE = 'view_schedule',
SHARE_SOCIAL = 'share_social',
CONTACT_SPONSOR = 'contact_sponsor',
}
// From core/analytics/domain/types/EngagementEvent.ts
export enum EngagementEntityType {
LEAGUE = 'league',
DRIVER = 'driver',
TEAM = 'team',
RACE = 'race',
SPONSOR = 'sponsor',
SPONSORSHIP = 'sponsorship',
}
export class RecordEngagementInput {
@ApiProperty({ enum: EngagementAction })
@IsEnum(EngagementAction)
action: EngagementAction;
@ApiProperty({ enum: EngagementEntityType })
@IsEnum(EngagementEntityType)
entityType: EngagementEntityType;
@ApiProperty()
@IsString()
entityId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
actorId?: string;
@ApiProperty({ enum: ['anonymous', 'driver', 'sponsor'] })
@IsEnum(['anonymous', 'driver', 'sponsor'])
actorType: 'anonymous' | 'driver' | 'sponsor';
@ApiProperty()
@IsString()
sessionId: string;
@ApiProperty({ required: false, type: 'object'/*, additionalProperties: { type: 'string' || 'number' || 'boolean' }*/ })
@IsOptional()
@IsObject()
metadata?: Record<string, string | number | boolean>;
}
export class RecordEngagementOutput {
@ApiProperty()
@IsString()
eventId: string;
@ApiProperty()
@IsNumber()
engagementWeight: number;
}

View File

@@ -0,0 +1,42 @@
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
import { AuthService } from './AuthService';
import { LoginParams, SignupParams, LoginWithIracingCallbackParams } from './dto/AuthDto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('signup')
async signup(@Body() params: SignupParams) {
return this.authService.signupWithEmail(params);
}
@Post('login')
async login(@Body() params: LoginParams) {
return this.authService.loginWithEmail(params);
}
@Get('session')
async getSession() {
return this.authService.getCurrentSession();
}
@Post('logout')
async logout() {
return this.authService.logout();
}
@Get('iracing/start')
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response) {
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
// In real application, you might want to store 'state' in a secure cookie or session.
// For this example, we'll just redirect.
res.redirect(HttpStatus.FOUND, redirectUrl);
}
@Get('iracing/callback')
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string) {
return this.authService.loginWithIracingCallback({ code, state, returnTo });
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AuthService } from './AuthService';
import { AuthController } from './AuthController';
@Module({
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,64 @@
import { Provider } from '@nestjs/common';
import { AuthService } from './AuthService';
// Import interfaces and concrete implementations
import { IAuthRepository } from '@gridpilot/core/identity/domain/repositories/IAuthRepository';
import { IUserRepository, StoredUser } from '@gridpilot/core/identity/domain/repositories/IUserRepository';
import { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
import { ILogger } from '@gridpilot/core/shared/logging/ILogger';
import { InMemoryAuthRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryAuthRepository';
import { InMemoryUserRepository } from '../../../adapters/identity/persistence/inmemory/InMemoryUserRepository';
import { InMemoryPasswordHashingService } from '../../../adapters/identity/services/InMemoryPasswordHashingService';
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort'; // Path from apps/api/src/modules/auth
import { CookieIdentitySessionAdapter } from '../../../adapters/identity/session/CookieIdentitySessionAdapter';
// Define the tokens for dependency injection
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
export const LOGGER_TOKEN = 'ILogger';
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
export const AuthProviders: Provider[] = [
AuthService, // Provide the service itself
{
provide: AUTH_REPOSITORY_TOKEN,
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: ILogger) => {
// Seed initial users for InMemoryUserRepository
const initialUsers: StoredUser[] = [
// Example user (replace with actual test users as needed)
{
id: 'user-1',
email: 'test@example.com',
passwordHash: 'demo_salt_moc.elpmaxe@tset', // "test@example.com" reversed
displayName: 'Test User',
salt: '', // Handled by hashing service
createdAt: new Date(),
},
];
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
},
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
},
{
provide: USER_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryUserRepository(logger), // Factory for InMemoryUserRepository
inject: [LOGGER_TOKEN],
},
{
provide: PASSWORD_HASHING_SERVICE_TOKEN,
useClass: InMemoryPasswordHashingService,
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
{
provide: IDENTITY_SESSION_PORT_TOKEN,
useFactory: (logger: ILogger) => new CookieIdentitySessionAdapter(logger),
inject: [LOGGER_TOKEN],
},
];

View File

@@ -0,0 +1,140 @@
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common';
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
// Core Use Cases
import { LoginUseCase } from '../../../../core/identity/application/use-cases/LoginUseCase';
import { SignupUseCase } from '../../../../core/identity/application/use-cases/SignupUseCase';
import { GetCurrentSessionUseCase } from '../../../../core/identity/application/use-cases/GetCurrentSessionUseCase';
import { LogoutUseCase } from '../../../../core/identity/application/use-cases/LogoutUseCase';
import { StartIracingAuthRedirectUseCase } from '../../../../core/identity/application/use-cases/StartIracingAuthRedirectUseCase';
import { LoginWithIracingCallbackUseCase } from '../../../../core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
// Core Interfaces and Tokens
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
import { IAuthRepository } from '../../../../core/identity/domain/repositories/IAuthRepository';
import { IPasswordHashingService } from '../../../../core/identity/domain/services/PasswordHashingService';
import { ILogger } from '../../../../core/shared/logging/ILogger';
import { IdentitySessionPort } from '../../../../core/identity/application/ports/IdentitySessionPort';
import { UserId } from '../../../../core/identity/domain/value-objects/UserId';
import { User } from '../../../../core/identity/domain/entities/User';
import { IUserRepository } from '../../../../core/identity/domain/repositories/IUserRepository';
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '../../../../core/identity/application/dto/AuthenticatedUserDTO';
@Injectable()
export class AuthService {
private readonly loginUseCase: LoginUseCase;
private readonly signupUseCase: SignupUseCase;
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
private readonly logoutUseCase: LogoutUseCase;
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
constructor(
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
@Inject(PASSWORD_HASHING_SERVICE_TOKEN) private passwordHashingService: IPasswordHashingService,
@Inject(LOGGER_TOKEN) private logger: ILogger,
@Inject(IDENTITY_SESSION_PORT_TOKEN) private identitySessionPort: IdentitySessionPort,
@Inject(USER_REPOSITORY_TOKEN) private userRepository: IUserRepository, // Inject IUserRepository here
) {
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
}
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
return {
userId: user.getId().value,
email: user.getEmail(),
displayName: user.getDisplayName(),
// Map other fields as necessary
iracingCustomerId: user.getIracingCustomerId() ?? undefined,
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
avatarUrl: user.getAvatarUrl() ?? undefined,
};
}
async getCurrentSession(): Promise<AuthSessionDTO | null> {
this.logger.debug('[AuthService] Attempting to get current session.');
const coreSession = await this.identitySessionPort.getCurrentSession();
if (!coreSession) {
return null;
}
const user = await this.userRepository.findById(coreSession.user.id); // Use userRepository to fetch full user
if (!user) {
// If session exists but user doesn't in DB, perhaps clear session?
this.logger.warn(`[AuthService] Session found for user ID ${coreSession.user.id}, but user not found in repository.`);
await this.identitySessionPort.clearSession(); // Clear potentially stale session
return null;
}
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(User.fromStored(user));
return {
token: coreSession.token,
user: authenticatedUserDTO,
};
}
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
const user = await this.signupUseCase.execute(params.email, params.password, params.displayName);
// Create session after successful signup
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
return {
token: session.token,
user: authenticatedUserDTO,
};
}
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
try {
const user = await this.loginUseCase.execute(params.email, params.password);
// Create session after successful login
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
return {
token: session.token,
user: authenticatedUserDTO,
};
} catch (error) {
this.logger.error(`[AuthService] Login failed for email ${params.email}:`, error);
throw new InternalServerErrorException('Login failed due to invalid credentials or server error.');
}
}
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
return { redirectUrl: result.redirectUrl, state: result.state };
}
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
// Create session after successful iRacing login
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
return {
token: session.token,
user: authenticatedUserDTO,
};
}
async logout(): Promise<void> {
this.logger.debug('[AuthService] Attempting logout.');
await this.logoutUseCase.execute();
}
}

View File

@@ -0,0 +1,55 @@
import { ApiProperty } from '@nestjs/swagger';
export class AuthenticatedUserDTO {
@ApiProperty()
userId: string;
@ApiProperty()
email: string;
@ApiProperty()
displayName: string;
}
export class AuthSessionDTO {
@ApiProperty()
token: string;
@ApiProperty()
user: AuthenticatedUserDTO;
}
export class SignupParams {
@ApiProperty()
email: string;
@ApiProperty()
password: string;
@ApiProperty()
displayName: string;
@ApiProperty({ required: false })
iracingCustomerId?: string;
@ApiProperty({ required: false })
primaryDriverId?: string;
@ApiProperty({ required: false })
avatarUrl?: string;
}
export class LoginParams {
@ApiProperty()
email: string;
@ApiProperty()
password: string;
}
export class IracingAuthRedirectResult {
@ApiProperty()
redirectUrl: string;
@ApiProperty()
state: string;
}
export class LoginWithIracingCallbackParams {
@ApiProperty()
code: string;
@ApiProperty()
state: string;
@ApiProperty({ required: false })
returnTo?: string;
}

View File

@@ -0,0 +1,49 @@
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
import { Request } from 'express';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { DriverService } from './DriverService';
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel } from './dto/DriverDto';
@ApiTags('drivers')
@Controller('drivers')
export class DriverController {
constructor(private readonly driverService: DriverService) {}
@Get('leaderboard')
@ApiOperation({ summary: 'Get drivers leaderboard' })
@ApiResponse({ status: 200, description: 'List of drivers for the leaderboard', type: DriversLeaderboardViewModel })
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
return this.driverService.getDriversLeaderboard();
}
@Get('total-drivers')
@ApiOperation({ summary: 'Get the total number of drivers' })
@ApiResponse({ status: 200, description: 'Total number of drivers', type: DriverStatsDto })
async getTotalDrivers(): Promise<DriverStatsDto> {
return this.driverService.getTotalDrivers();
}
@Post('complete-onboarding')
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
async completeOnboarding(
@Body() input: CompleteOnboardingInput,
@Req() req: Request,
): Promise<CompleteOnboardingOutput> {
// Assuming userId is available from the request (e.g., via auth middleware)
const userId = req['user'].userId; // Placeholder for actual user extraction
return this.driverService.completeOnboarding(userId, input);
}
@Get(':driverId/races/:raceId/registration-status')
@ApiOperation({ summary: 'Get driver registration status for a specific race' })
@ApiResponse({ status: 200, description: 'Driver registration status', type: DriverRegistrationStatusViewModel })
async getDriverRegistrationStatus(
@Param('driverId') driverId: string,
@Param('raceId') raceId: string,
): Promise<DriverRegistrationStatusViewModel> {
return this.driverService.getDriverRegistrationStatus({ driverId, raceId });
}
// Add other Driver endpoints here based on other presenters
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { DriverService } from './DriverService';
import { DriverController } from './DriverController';
@Module({
controllers: [DriverController],
providers: [DriverService],
exports: [DriverService],
})
export class DriverModule {}

View File

@@ -0,0 +1,75 @@
import { Provider } from '@nestjs/common';
import { DriverService } from './DriverService';
// Import core interfaces
import { IDriverRepository } from '../../../../core/racing/domain/repositories/IDriverRepository';
import { IRankingService } from '../../../../core/racing/domain/services/IRankingService';
import { IDriverStatsService } from '../../../../core/racing/domain/services/IDriverStatsService';
import { DriverRatingProvider } from '../../../../core/racing/application/ports/DriverRatingProvider';
import { IImageServicePort } from '../../../../core/racing/application/ports/IImageServicePort';
import { IRaceRegistrationRepository } from '../../../../core/racing/domain/repositories/IRaceRegistrationRepository';
import { INotificationPreferenceRepository } from '../../../../core/notifications/domain/repositories/INotificationPreferenceRepository';
import { ILogger } from '../../../../core/shared/logging/ILogger';
// Import concrete in-memory implementations
import { InMemoryDriverRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryRankingService } from '../../../adapters/racing/services/InMemoryRankingService';
import { InMemoryDriverStatsService } from '../../../adapters/racing/services/InMemoryDriverStatsService';
import { InMemoryDriverRatingProvider } from '../../../adapters/racing/ports/InMemoryDriverRatingProvider';
import { InMemoryImageServiceAdapter } from '../../../adapters/media/ports/InMemoryImageServiceAdapter';
import { InMemoryRaceRegistrationRepository } from '../../../adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryNotificationPreferenceRepository } from '../../../adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
import { ConsoleLogger } from '../../../adapters/logging/ConsoleLogger';
// Define injection tokens
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const RANKING_SERVICE_TOKEN = 'IRankingService';
export const DRIVER_STATS_SERVICE_TOKEN = 'IDriverStatsService';
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
export const DriverProviders: Provider[] = [
DriverService, // Provide the service itself
{
provide: DRIVER_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryDriverRepository(logger), // Factory for InMemoryDriverRepository
inject: [LOGGER_TOKEN],
},
{
provide: RANKING_SERVICE_TOKEN,
useFactory: (logger: ILogger) => new InMemoryRankingService(logger),
inject: [LOGGER_TOKEN],
},
{
provide: DRIVER_STATS_SERVICE_TOKEN,
useFactory: (logger: ILogger) => new InMemoryDriverStatsService(logger),
inject: [LOGGER_TOKEN],
},
{
provide: DRIVER_RATING_PROVIDER_TOKEN,
useFactory: (logger: ILogger) => new InMemoryDriverRatingProvider(logger),
inject: [LOGGER_TOKEN],
},
{
provide: IMAGE_SERVICE_PORT_TOKEN,
useFactory: (logger: ILogger) => new InMemoryImageServiceAdapter(logger),
inject: [LOGGER_TOKEN],
},
{
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryRaceRegistrationRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryNotificationPreferenceRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
];

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { DriversLeaderboardViewModel, DriverStatsDto, CompleteOnboardingInput, CompleteOnboardingOutput, GetDriverRegistrationStatusQuery, DriverRegistrationStatusViewModel, DriverLeaderboardItemViewModel } from './dto/DriverDto';
@Injectable()
export class DriverService {
constructor() {}
async getDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
console.log('[DriverService] Returning mock driver leaderboard.');
const drivers: DriverLeaderboardItemViewModel[] = [
{ id: 'driver-1', name: 'Mock Driver 1', rating: 2500, skillLevel: 'Pro', nationality: 'DE', racesCompleted: 50, wins: 10, podiums: 20, isActive: true, rank: 1, avatarUrl: 'https://cdn.example.com/avatars/driver-1.png' },
{ id: 'driver-2', name: 'Mock Driver 2', rating: 2400, skillLevel: 'Amateur', nationality: 'US', racesCompleted: 40, wins: 5, podiums: 15, isActive: true, rank: 2, avatarUrl: 'https://cdn.example.com/avatars/driver-2.png' },
];
return {
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
totalRaces: drivers.reduce((sum, item) => sum + (item.racesCompleted ?? 0), 0),
totalWins: drivers.reduce((sum, item) => sum + (item.wins ?? 0), 0),
activeCount: drivers.filter(d => d.isActive).length,
};
}
async getTotalDrivers(): Promise<DriverStatsDto> {
console.log('[DriverService] Returning mock total drivers.');
return {
totalDrivers: 2,
};
}
async completeOnboarding(userId: string, input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
console.log('Completing onboarding for user:', userId, input);
return {
success: true,
driverId: `driver-${userId}-onboarded`,
};
}
async getDriverRegistrationStatus(query: GetDriverRegistrationStatusQuery): Promise<DriverRegistrationStatusViewModel> {
console.log('Checking driver registration status:', query);
return {
isRegistered: false, // Mock response
raceId: query.raceId,
driverId: query.driverId,
};
}
}

View File

@@ -0,0 +1,138 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
export class DriverLeaderboardItemViewModel {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty()
rating: number;
@ApiProperty()
skillLevel: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
@ApiProperty()
nationality: string;
@ApiProperty()
racesCompleted: number;
@ApiProperty()
wins: number;
@ApiProperty()
podiums: number;
@ApiProperty()
isActive: boolean;
@ApiProperty()
rank: number;
@ApiProperty({ nullable: true })
avatarUrl?: string;
}
export class DriversLeaderboardViewModel {
@ApiProperty({ type: [DriverLeaderboardItemViewModel] })
drivers: DriverLeaderboardItemViewModel[];
@ApiProperty()
totalRaces: number;
@ApiProperty()
totalWins: number;
@ApiProperty()
activeCount: number;
}
export class DriverStatsDto {
@ApiProperty()
totalDrivers: number;
}
export class CompleteOnboardingInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
firstName: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
lastName: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
displayName: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
country: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
timezone?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
bio?: string;
}
export class CompleteOnboardingOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
@ApiProperty({ required: false })
@IsString()
driverId?: string;
@ApiProperty({ required: false })
@IsString()
errorMessage?: string;
}
export class GetDriverRegistrationStatusQuery {
@ApiProperty()
@IsString()
raceId: string;
@ApiProperty()
@IsString()
driverId: string;
}
export class DriverRegistrationStatusViewModel {
@ApiProperty()
@IsBoolean()
isRegistered: boolean;
@ApiProperty()
@IsString()
raceId: string;
@ApiProperty()
@IsString()
driverId: string;
}
export class DriverDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
name: string; // Display name or full name
}
// Add other DTOs for driver-related logic as needed

View File

@@ -0,0 +1,136 @@
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
import { LeagueService } from './LeagueService';
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel } from './dto/LeagueDto';
import { GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto'; // Explicitly import queries
@ApiTags('leagues')
@Controller('leagues')
export class LeagueController {
constructor(private readonly leagueService: LeagueService) {}
@Get('all-with-capacity')
@ApiOperation({ summary: 'Get all leagues with their capacity information' })
@ApiResponse({ status: 200, description: 'List of leagues with capacity', type: AllLeaguesWithCapacityViewModel })
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
return this.leagueService.getAllLeaguesWithCapacity();
}
@Get('total-leagues')
@ApiOperation({ summary: 'Get the total number of leagues' })
@ApiResponse({ status: 200, description: 'Total number of leagues', type: LeagueStatsDto })
async getTotalLeagues(): Promise<LeagueStatsDto> {
return this.leagueService.getTotalLeagues();
}
@Get(':leagueId/join-requests')
@ApiOperation({ summary: 'Get all outstanding join requests for a league' })
@ApiResponse({ status: 200, description: 'List of join requests', type: [LeagueJoinRequestViewModel] })
async getJoinRequests(@Param('leagueId') leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
// No specific query DTO needed for GET, leagueId from param
return this.leagueService.getLeagueJoinRequests(leagueId);
}
@Post(':leagueId/join-requests/approve')
@ApiOperation({ summary: 'Approve a league join request' })
@ApiBody({ type: ApproveJoinRequestInput }) // Explicitly define body type for Swagger
@ApiResponse({ status: 200, description: 'Join request approved', type: ApproveJoinRequestOutput })
@ApiResponse({ status: 404, description: 'Join request not found' })
async approveJoinRequest(
@Param('leagueId') leagueId: string,
@Body() input: ApproveJoinRequestInput,
): Promise<ApproveJoinRequestOutput> {
return this.leagueService.approveLeagueJoinRequest({ ...input, leagueId });
}
@Post(':leagueId/join-requests/reject')
@ApiOperation({ summary: 'Reject a league join request' })
@ApiBody({ type: RejectJoinRequestInput })
@ApiResponse({ status: 200, description: 'Join request rejected', type: RejectJoinRequestOutput })
@ApiResponse({ status: 404, description: 'Join request not found' })
async rejectJoinRequest(
@Param('leagueId') leagueId: string,
@Body() input: RejectJoinRequestInput,
): Promise<RejectJoinRequestOutput> {
return this.leagueService.rejectLeagueJoinRequest({ ...input, leagueId });
}
@Get(':leagueId/permissions/:performerDriverId')
@ApiOperation({ summary: 'Get league admin permissions for a performer' })
@ApiResponse({ status: 200, description: 'League admin permissions', type: LeagueAdminPermissionsViewModel })
async getLeagueAdminPermissions(
@Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string,
): Promise<LeagueAdminPermissionsViewModel> {
// No specific input DTO needed for Get, parameters from path
return this.leagueService.getLeagueAdminPermissions({ leagueId, performerDriverId });
}
@Patch(':leagueId/members/:targetDriverId/remove')
@ApiOperation({ summary: 'Remove a member from the league' })
@ApiBody({ type: RemoveLeagueMemberInput }) // Explicitly define body type for Swagger
@ApiResponse({ status: 200, description: 'Member removed successfully', type: RemoveLeagueMemberOutput })
@ApiResponse({ status: 400, description: 'Cannot remove member' })
@ApiResponse({ status: 404, description: 'Member not found' })
async removeLeagueMember(
@Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string,
@Param('targetDriverId') targetDriverId: string,
@Body() input: RemoveLeagueMemberInput, // Body content for a patch often includes IDs
): Promise<RemoveLeagueMemberOutput> {
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
}
@Patch(':leagueId/members/:targetDriverId/role')
@ApiOperation({ summary: "Update a member's role in the league" })
@ApiBody({ type: UpdateLeagueMemberRoleInput }) // Explicitly define body type for Swagger
@ApiResponse({ status: 200, description: 'Member role updated successfully', type: UpdateLeagueMemberRoleOutput })
@ApiResponse({ status: 400, description: 'Cannot update role' })
@ApiResponse({ status: 404, description: 'Member not found' })
async updateLeagueMemberRole(
@Param('leagueId') leagueId: string,
@Param('performerDriverId') performerDriverId: string,
@Param('targetDriverId') targetDriverId: string,
@Body() input: UpdateLeagueMemberRoleInput, // Body includes newRole, other for swagger
): Promise<UpdateLeagueMemberRoleOutput> {
return this.leagueService.updateLeagueMemberRole({ leagueId, performerDriverId, targetDriverId, newRole: input.newRole });
}
@Get(':leagueId/owner-summary/:ownerId')
@ApiOperation({ summary: 'Get owner summary for a league' })
@ApiResponse({ status: 200, description: 'League owner summary', type: LeagueOwnerSummaryViewModel })
@ApiResponse({ status: 404, description: 'Owner or league not found' })
async getLeagueOwnerSummary(
@Param('leagueId') leagueId: string,
@Param('ownerId') ownerId: string,
): Promise<LeagueOwnerSummaryViewModel | null> {
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId };
return this.leagueService.getLeagueOwnerSummary(query);
}
@Get(':leagueId/config')
@ApiOperation({ summary: 'Get league full configuration' })
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDto })
async getLeagueFullConfig(
@Param('leagueId') leagueId: string,
): Promise<LeagueConfigFormModelDto | null> {
const query: GetLeagueAdminConfigQuery = { leagueId };
return this.leagueService.getLeagueFullConfig(query);
}
@Get(':leagueId/protests')
@ApiOperation({ summary: 'Get protests for a league' })
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsViewModel })
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsViewModel> {
const query: GetLeagueProtestsQuery = { leagueId };
return this.leagueService.getLeagueProtests(query);
}
@Get(':leagueId/seasons')
@ApiOperation({ summary: 'Get seasons for a league' })
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryViewModel] })
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
const query: GetLeagueSeasonsQuery = { leagueId };
return this.leagueService.getLeagueSeasons(query);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { LeagueService } from './LeagueService';
import { LeagueController } from './LeagueController';
@Module({
controllers: [LeagueController],
providers: [LeagueService],
exports: [LeagueService],
})
export class LeagueModule {}

View File

@@ -0,0 +1,83 @@
import { Provider } from '@nestjs/common';
import { LeagueService } from './LeagueService';
// Import core interfaces
import { ILeagueRepository } from 'core/racing/domain/repositories/ILeagueRepository';
import { ILeagueMembershipRepository } from 'core/racing/domain/repositories/ILeagueMembershipRepository';
import { ILeagueStandingsRepository } from 'core/league/application/ports/ILeagueStandingsRepository';
import { ISeasonRepository } from 'core/racing/domain/repositories/ISeasonRepository';
import { ILeagueScoringConfigRepository } from 'core/racing/domain/repositories/ILeagueScoringConfigRepository';
import { IGameRepository } from 'core/racing/domain/repositories/IGameRepository';
import { IProtestRepository } from 'core/racing/domain/repositories/IProtestRepository';
import { IRaceRepository } from 'core/racing/domain/repositories/IRaceRepository';
import { ILogger } from 'core/shared/logging/ILogger';
// Import concrete in-memory implementations
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
import { InMemoryLeagueMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
import { InMemoryLeagueStandingsRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
import { InMemorySeasonRepository } from 'adapters/racing/persistence/inmemory/InMemorySeasonRepository';
import { InMemoryLeagueScoringConfigRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
import { InMemoryGameRepository } from 'adapters/racing/persistence/inmemory/InMemoryGameRepository';
import { InMemoryProtestRepository } from 'adapters/racing/persistence/inmemory/InMemoryProtestRepository';
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
// Define injection tokens
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = 'ILeagueStandingsRepository';
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LOGGER_TOKEN = 'ILogger'; // Already defined in AuthProviders, but good to have here too
export const LeagueProviders: Provider[] = [
LeagueService, // Provide the service itself
{
provide: LEAGUE_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryLeagueRepository(logger), // Factory for InMemoryLeagueRepository
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryLeagueMembershipRepository(logger), // Factory for InMemoryLeagueMembershipRepository
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
inject: [LOGGER_TOKEN],
},
{
provide: SEASON_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
inject: [LOGGER_TOKEN],
},
{
provide: GAME_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryGameRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: PROTEST_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryProtestRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: RACE_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryRaceRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
];

View File

@@ -0,0 +1,125 @@
import { Injectable } from '@nestjs/common';
import { AllLeaguesWithCapacityViewModel, LeagueStatsDto, LeagueJoinRequestViewModel, ApproveJoinRequestInput, ApproveJoinRequestOutput, RejectJoinRequestInput, RejectJoinRequestOutput, LeagueAdminPermissionsViewModel, RemoveLeagueMemberInput, RemoveLeagueMemberOutput, UpdateLeagueMemberRoleInput, UpdateLeagueMemberRoleOutput, LeagueOwnerSummaryViewModel, LeagueConfigFormModelDto, LeagueAdminProtestsViewModel, LeagueSeasonSummaryViewModel, GetLeagueAdminPermissionsInput, GetLeagueJoinRequestsQuery, GetLeagueProtestsQuery, GetLeagueSeasonsQuery, GetLeagueAdminConfigQuery, GetLeagueOwnerSummaryQuery } from './dto/LeagueDto';
import { DriverDto } from '../driver/dto/DriverDto'; // Using the local DTO for mock data
import { RaceDto } from '../race/dto/RaceDto'; // Using the local DTO for mock data
const mockDriverData: Map<string, DriverDto> = new Map();
mockDriverData.set('driver-owner-1', { id: 'driver-owner-1', name: 'Owner Driver' });
mockDriverData.set('driver-1', { id: 'driver-1', name: 'Demo Driver 1' });
mockDriverData.set('driver-2', { id: 'driver-2', name: 'Demo Driver 2' });
const mockRaceData: Map<string, RaceDto> = new Map();
mockRaceData.set('race-1', { id: 'race-1', name: 'Test Race 1', date: new Date().toISOString() });
mockRaceData.set('race-2', { id: 'race-2', name: 'Test Race 2', date: new Date().toISOString() });
@Injectable()
export class LeagueService {
constructor() {}
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
console.log('[LeagueService] Returning mock leagues with capacity.');
return {
leagues: [
{ id: 'league-1', name: 'Global Racing', description: 'The premier league', ownerId: 'owner-1', settings: { maxDrivers: 100 }, createdAt: new Date().toISOString(), usedSlots: 50, socialLinks: { discordUrl: 'https://discord.gg/test' } },
{ id: 'league-2', name: 'Amateur Series', description: 'Learn the ropes', ownerId: 'owner-2', settings: { maxDrivers: 50 }, createdAt: new Date().toISOString(), usedSlots: 20 },
],
totalCount: 2,
};
}
async getTotalLeagues(): Promise<LeagueStatsDto> {
console.log('[LeagueService] Returning mock total leagues.');
return { totalLeagues: 2 };
}
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
console.log(`[LeagueService] Returning mock join requests for league: ${leagueId}.`);
return [
{
id: 'join-req-1',
leagueId: 'league-1',
driverId: 'driver-1',
requestedAt: new Date(),
message: 'I want to join!',
driver: mockDriverData.get('driver-1'),
},
];
}
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
console.log('Approving join request:', input);
return { success: true, message: 'Join request approved.' };
}
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
console.log('Rejecting join request:', input);
return { success: true, message: 'Join request rejected.' };
}
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
console.log('Getting league admin permissions:', query);
return { canRemoveMember: true, canUpdateRoles: true };
}
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
console.log('Removing league member:', input.leagueId, input.targetDriverId);
return { success: true };
}
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
console.log('Updating league member role:', input.leagueId, input.targetDriverId, input.newRole);
return { success: true };
}
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> {
console.log('Getting league owner summary:', query);
return {
driver: mockDriverData.get(query.ownerId)!,
rating: 2000,
rank: 1,
};
}
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
console.log('Getting league full config:', query);
return {
leagueId: 'league-1',
basics: { name: 'Demo League', description: 'A demo league', visibility: 'public' },
structure: { mode: 'solo' },
championships: [],
scoring: { type: 'standard', points: 10 },
dropPolicy: { strategy: 'none' },
timings: { raceDayOfWeek: 'Sunday', raceTimeHour: 20, raceTimeMinute: 0 },
stewarding: {
decisionMode: 'single_steward',
requireDefense: false,
defenseTimeLimit: 24,
voteTimeLimit: 24,
protestDeadlineHours: 2,
stewardingClosesHours: 24,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
},
};
}
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
console.log('Getting league protests:', query);
return {
protests: [
{ id: 'protest-1', raceId: 'race-1', protestingDriverId: 'driver-1', accusedDriverId: 'driver-2', submittedAt: new Date(), description: 'Bad driving!', status: 'pending' },
],
racesById: { 'race-1': mockRaceData.get('race-1')! },
driversById: { 'driver-1': mockDriverData.get('driver-1')!, 'driver-2': mockDriverData.get('driver-2')! },
};
}
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
console.log('Getting league seasons:', query);
return [
{ seasonId: 'season-1', name: 'Season 1', status: 'active', startDate: new Date('2025-01-01'), endDate: new Date('2025-12-31'), isPrimary: true, isParallelActive: false },
{ seasonId: 'season-2', name: 'Season 2', status: 'upcoming', startDate: new Date('2026-01-01'), endDate: new Date('2026-12-31'), isPrimary: false, isParallelActive: false },
];
}
}

View File

@@ -0,0 +1,561 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional, IsEnum, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { DriverDto } from '../../driver/dto/DriverDto';
import { RaceDto } from '../../race/dto/RaceDto';
export class LeagueSettingsDto {
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
maxDrivers?: number;
// Add other league settings properties as needed
}
export class LeagueWithCapacityViewModel {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
name: string;
// ... other properties of LeagueWithCapacityViewModel
@ApiProperty({ nullable: true })
@IsOptional()
@IsString()
description?: string;
@ApiProperty()
@IsString()
ownerId: string;
@ApiProperty({ type: () => LeagueSettingsDto })
@ValidateNested()
@Type(() => LeagueSettingsDto)
settings: LeagueSettingsDto;
@ApiProperty()
@IsString()
createdAt: string;
@ApiProperty()
@IsNumber()
usedSlots: number;
@ApiProperty({ type: () => Object, nullable: true }) // Using Object for generic social links
@IsOptional()
socialLinks?: {
discordUrl?: string;
youtubeUrl?: string;
websiteUrl?: string;
};
}
export class AllLeaguesWithCapacityViewModel {
@ApiProperty({ type: [LeagueWithCapacityViewModel] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => LeagueWithCapacityViewModel)
leagues: LeagueWithCapacityViewModel[];
@ApiProperty()
@IsNumber()
totalCount: number;
}
export class LeagueStatsDto {
@ApiProperty()
@IsNumber()
totalLeagues: number;
}
export class ProtestDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
raceId: string;
@ApiProperty()
@IsString()
protestingDriverId: string;
@ApiProperty()
@IsString()
accusedDriverId: string;
@ApiProperty()
@IsDate()
@Type(() => Date)
submittedAt: Date;
@ApiProperty()
@IsString()
description: string;
@ApiProperty({ enum: ['pending', 'accepted', 'rejected'] })
@IsEnum(['pending', 'accepted', 'rejected'])
status: 'pending' | 'accepted' | 'rejected';
}
export class SeasonDto {
@ApiProperty()
@IsString()
seasonId: string;
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
@Type(() => Date)
startDate?: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
@Type(() => Date)
endDate?: Date;
@ApiProperty({ enum: ['planned', 'active', 'completed'] })
@IsEnum(['planned', 'active', 'completed'])
status: 'planned' | 'active' | 'completed';
@ApiProperty()
@IsBoolean()
isPrimary: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
seasonGroupId?: string;
}
export class LeagueJoinRequestViewModel {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
driverId: string;
@ApiProperty()
@IsDate()
@Type(() => Date)
requestedAt: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
message?: string;
@ApiProperty({ type: () => DriverDto, required: false })
@IsOptional()
@ValidateNested()
@Type(() => DriverDto)
driver?: DriverDto;
}
export class GetLeagueJoinRequestsQuery {
@ApiProperty()
@IsString()
leagueId: string;
}
export class ApproveJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
@ApiProperty()
@IsString()
leagueId: string;
}
export class ApproveJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
@ApiProperty({ required: false })
@IsString()
message?: string;
}
export class RejectJoinRequestInput {
@ApiProperty()
@IsString()
requestId: string;
@ApiProperty()
@IsString()
leagueId: string;
}
export class RejectJoinRequestOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
@ApiProperty({ required: false })
@IsString()
message?: string;
}
export class GetLeagueAdminPermissionsInput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
performerDriverId: string;
}
export class LeagueAdminPermissionsViewModel {
@ApiProperty()
@IsBoolean()
canRemoveMember: boolean;
@ApiProperty()
@IsBoolean()
canUpdateRoles: boolean;
}
export class RemoveLeagueMemberInput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
performerDriverId: string;
@ApiProperty()
@IsString()
targetDriverId: string;
}
export class RemoveLeagueMemberOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
}
export class UpdateLeagueMemberRoleInput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
performerDriverId: string;
@ApiProperty()
@IsString()
targetDriverId: string;
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
@IsEnum(['owner', 'manager', 'member'])
newRole: 'owner' | 'manager' | 'member';
}
export class UpdateLeagueMemberRoleOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
}
export class GetLeagueOwnerSummaryQuery {
@ApiProperty()
@IsString()
ownerId: string;
@ApiProperty()
@IsString()
leagueId: string;
}
export class LeagueOwnerSummaryViewModel {
@ApiProperty({ type: () => DriverDto })
@ValidateNested()
@Type(() => DriverDto)
driver: DriverDto;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
rating: number | null;
@ApiProperty({ nullable: true })
@IsOptional()
@IsNumber()
rank: number | null;
}
export class LeagueConfigFormModelBasicsDto {
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsString()
description: string;
@ApiProperty({ enum: ['public', 'private'] })
@IsEnum(['public', 'private'])
visibility: 'public' | 'private';
}
export class LeagueConfigFormModelStructureDto {
@ApiProperty()
@IsString()
@IsEnum(['solo', 'team'])
mode: 'solo' | 'team';
}
export class LeagueConfigFormModelScoringDto {
@ApiProperty()
@IsString()
type: string;
@ApiProperty()
@IsNumber()
points: number;
}
export class LeagueConfigFormModelDropPolicyDto {
@ApiProperty({ enum: ['none', 'worst_n'] })
@IsEnum(['none', 'worst_n'])
strategy: 'none' | 'worst_n';
@ApiProperty({ required: false })
@IsOptional()
@IsNumber()
n?: number;
}
export class LeagueConfigFormModelStewardingDto {
@ApiProperty({ enum: ['single_steward', 'committee_vote'] })
@IsEnum(['single_steward', 'committee_vote'])
decisionMode: 'single_steward' | 'committee_vote';
@ApiProperty({ required: false })
@IsOptional()
@IsNumber()
requiredVotes?: number;
@ApiProperty()
@IsBoolean()
requireDefense: boolean;
@ApiProperty()
@IsNumber()
defenseTimeLimit: number;
@ApiProperty()
@IsNumber()
voteTimeLimit: number;
@ApiProperty()
@IsNumber()
protestDeadlineHours: number;
@ApiProperty()
@IsNumber()
stewardingClosesHours: number;
@ApiProperty()
@IsBoolean()
notifyAccusedOnProtest: boolean;
@ApiProperty()
@IsBoolean()
notifyOnVoteRequired: boolean;
}
export class LeagueConfigFormModelTimingsDto {
@ApiProperty()
@IsString()
raceDayOfWeek: string;
@ApiProperty()
@IsNumber()
raceTimeHour: number;
@ApiProperty()
@IsNumber()
raceTimeMinute: number;
}
export class LeagueConfigFormModelDto {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ type: LeagueConfigFormModelBasicsDto })
@ValidateNested()
@Type(() => LeagueConfigFormModelBasicsDto)
basics: LeagueConfigFormModelBasicsDto;
@ApiProperty({ type: LeagueConfigFormModelStructureDto })
@ValidateNested()
@Type(() => LeagueConfigFormModelStructureDto)
structure: LeagueConfigFormModelStructureDto;
@ApiProperty({ type: [Object] })
@IsArray()
championships: any[];
@ApiProperty({ type: LeagueConfigFormModelScoringDto })
@ValidateNested()
@Type(() => LeagueConfigFormModelScoringDto)
scoring: LeagueConfigFormModelScoringDto;
@ApiProperty({ type: LeagueConfigFormModelDropPolicyDto })
@ValidateNested()
@Type(() => LeagueConfigFormModelDropPolicyDto)
dropPolicy: LeagueConfigFormModelDropPolicyDto;
@ApiProperty({ type: LeagueConfigFormModelTimingsDto })
@ValidateNested()
@Type(() => LeagueConfigFormModelTimingsDto)
timings: LeagueConfigFormModelTimingsDto;
@ApiProperty({ type: LeagueConfigFormModelStewardingDto })
@ValidateNested()
@Type(() => LeagueConfigFormModelStewardingDto)
stewarding: LeagueConfigFormModelStewardingDto;
}
export class GetLeagueAdminConfigQuery {
@ApiProperty()
@IsString()
leagueId: string;
}
export class GetLeagueAdminConfigOutput {
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
@IsOptional()
@ValidateNested()
@Type(() => LeagueConfigFormModelDto)
form: LeagueConfigFormModelDto | null;
}
export class LeagueAdminConfigViewModel {
@ApiProperty({ type: () => LeagueConfigFormModelDto, nullable: true })
@IsOptional()
@ValidateNested()
@Type(() => LeagueConfigFormModelDto)
form: LeagueConfigFormModelDto | null;
}
export class GetLeagueProtestsQuery {
@ApiProperty()
@IsString()
leagueId: string;
}
export class LeagueAdminProtestsViewModel {
@ApiProperty({ type: [ProtestDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => ProtestDto)
protests: ProtestDto[];
@ApiProperty({ type: () => RaceDto })
@ValidateNested()
@Type(() => RaceDto)
racesById: { [raceId: string]: RaceDto };
@ApiProperty({ type: () => DriverDto })
@ValidateNested()
@Type(() => DriverDto)
driversById: { [driverId: string]: DriverDto };
}
export class GetLeagueSeasonsQuery {
@ApiProperty()
@IsString()
leagueId: string;
}
export class LeagueSeasonSummaryViewModel {
@ApiProperty()
@IsString()
seasonId: string;
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsString()
status: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
@Type(() => Date)
startDate?: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
@Type(() => Date)
endDate?: Date;
@ApiProperty()
@IsBoolean()
isPrimary: boolean;
@ApiProperty()
@IsBoolean()
isParallelActive: boolean;
}
export class LeagueAdminViewModel {
@ApiProperty({ type: [LeagueJoinRequestViewModel] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => LeagueJoinRequestViewModel)
joinRequests: LeagueJoinRequestViewModel[];
@ApiProperty({ type: () => LeagueOwnerSummaryViewModel, nullable: true })
@IsOptional()
@ValidateNested()
@Type(() => LeagueOwnerSummaryViewModel)
ownerSummary: LeagueOwnerSummaryViewModel | null;
@ApiProperty({ type: () => LeagueAdminConfigViewModel })
@ValidateNested()
@Type(() => LeagueAdminConfigViewModel)
config: LeagueAdminConfigViewModel;
@ApiProperty({ type: () => LeagueAdminProtestsViewModel })
@ValidateNested()
@Type(() => LeagueAdminProtestsViewModel)
protests: LeagueAdminProtestsViewModel;
@ApiProperty({ type: [LeagueSeasonSummaryViewModel] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => LeagueSeasonSummaryViewModel)
seasons: LeagueSeasonSummaryViewModel[];
}

View File

@@ -0,0 +1,26 @@
import { Controller, Post, Body, HttpStatus, Res } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { Response } from 'express';
import { MediaService } from './MediaService';
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
@ApiTags('media')
@Controller('media')
export class MediaController {
constructor(private readonly mediaService: MediaService) {}
@Post('avatar/generate')
@ApiOperation({ summary: 'Request avatar generation' })
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput })
async requestAvatarGeneration(
@Body() input: RequestAvatarGenerationInput,
@Res() res: Response,
): Promise<void> {
const result = await this.mediaService.requestAvatarGeneration(input);
if (result.success) {
res.status(HttpStatus.CREATED).json(result);
} else {
res.status(HttpStatus.BAD_REQUEST).json(result);
}
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MediaService } from './MediaService';
import { MediaController } from './MediaController';
@Module({
controllers: [MediaController],
providers: [MediaService],
exports: [MediaService],
})
export class MediaModule {}

View File

@@ -0,0 +1,41 @@
import { Provider } from '@nestjs/common';
import { MediaService } from './MediaService';
// Due to persistent module resolution issues in the environment,
// actual core interfaces and adapter implementations are not directly imported here.
// In a functional TypeScript environment, these would be imported as follows:
/*
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
import { ILogger } from 'core/shared/logging/ILogger';
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
*/
// Define injection tokens as string literals for NestJS
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
export const LOGGER_TOKEN = 'ILogger';
export const MediaProviders: Provider[] = [
MediaService, // Provide the service itself
// In a functional setup, the following would be enabled:
/*
{
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryAvatarGenerationRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: FACE_VALIDATION_PORT_TOKEN,
useFactory: (logger: ILogger) => new InMemoryFaceValidationAdapter(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
*/
];

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
@Injectable()
export class MediaService {
constructor() {}
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
console.log('[MediaService] Returning mock avatar generation request. Input:', input);
return {
success: true,
requestId: `req-${Date.now()}`,
avatarUrls: [
'https://cdn.example.com/avatars/mock-avatar-1.png',
'https://cdn.example.com/avatars/mock-avatar-2.png',
],
};
}
}

View File

@@ -0,0 +1,40 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsBoolean } from 'class-validator';
export class RequestAvatarGenerationInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
userId: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
facePhotoData: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
suitColor: string;
}
export class RequestAvatarGenerationOutput {
@ApiProperty({ type: Boolean })
@IsBoolean()
success: boolean;
@ApiProperty({ required: false })
@IsString()
requestId?: string;
@ApiProperty({ type: [String], required: false })
avatarUrls?: string[];
@ApiProperty({ required: false })
@IsString()
errorMessage?: string;
}
// Assuming FacePhotoData and SuitColor are simple string types for DTO purposes
export type FacePhotoData = string;
export type SuitColor = string;

View File

@@ -0,0 +1,96 @@
import { Controller, Get, Post, Patch, Delete, Body, Query, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { PaymentsService } from './PaymentsService';
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, GetPaymentsQuery, GetPaymentsOutput, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput } from './dto/PaymentsDto';
@ApiTags('payments')
@Controller('payments')
export class PaymentsController {
constructor(private readonly paymentsService: PaymentsService) {}
@Get()
@ApiOperation({ summary: 'Get payments based on filters' })
@ApiResponse({ status: 200, description: 'List of payments', type: GetPaymentsOutput })
async getPayments(@Query() query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
return this.paymentsService.getPayments(query);
}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new payment' })
@ApiResponse({ status: 201, description: 'Payment created', type: CreatePaymentOutput })
async createPayment(@Body() input: CreatePaymentInput): Promise<CreatePaymentOutput> {
return this.paymentsService.createPayment(input);
}
@Patch('status')
@ApiOperation({ summary: 'Update the status of a payment' })
@ApiResponse({ status: 200, description: 'Payment status updated', type: UpdatePaymentStatusOutput })
async updatePaymentStatus(@Body() input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
return this.paymentsService.updatePaymentStatus(input);
}
@Get('membership-fees')
@ApiOperation({ summary: 'Get membership fees and member payments' })
@ApiResponse({ status: 200, description: 'Membership fee configuration and member payments', type: GetMembershipFeesOutput })
async getMembershipFees(@Query() query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
return this.paymentsService.getMembershipFees(query);
}
@Post('membership-fees')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create or update membership fee configuration' })
@ApiResponse({ status: 201, description: 'Membership fee configuration created or updated', type: UpsertMembershipFeeOutput })
async upsertMembershipFee(@Body() input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
return this.paymentsService.upsertMembershipFee(input);
}
@Patch('membership-fees/member-payment')
@ApiOperation({ summary: 'Record or update a member payment' })
@ApiResponse({ status: 200, description: 'Member payment recorded or updated', type: UpdateMemberPaymentOutput })
async updateMemberPayment(@Body() input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
return this.paymentsService.updateMemberPayment(input);
}
@Get('prizes')
@ApiOperation({ summary: 'Get prizes for a league or season' })
@ApiResponse({ status: 200, description: 'List of prizes', type: GetPrizesOutput })
async getPrizes(@Query() query: GetPrizesQuery): Promise<GetPrizesOutput> {
return this.paymentsService.getPrizes(query);
}
@Post('prizes')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new prize' })
@ApiResponse({ status: 201, description: 'Prize created', type: CreatePrizeOutput })
async createPrize(@Body() input: CreatePrizeInput): Promise<CreatePrizeOutput> {
return this.paymentsService.createPrize(input);
}
@Patch('prizes/award')
@ApiOperation({ summary: 'Award a prize to a driver' })
@ApiResponse({ status: 200, description: 'Prize awarded', type: AwardPrizeOutput })
async awardPrize(@Body() input: AwardPrizeInput): Promise<AwardPrizeOutput> {
return this.paymentsService.awardPrize(input);
}
@Delete('prizes')
@ApiOperation({ summary: 'Delete a prize' })
@ApiResponse({ status: 200, description: 'Prize deleted', type: DeletePrizeOutput })
async deletePrize(@Query() query: DeletePrizeInput): Promise<DeletePrizeOutput> {
return this.paymentsService.deletePrize(query);
}
@Get('wallets')
@ApiOperation({ summary: 'Get wallet information and transactions' })
@ApiResponse({ status: 200, description: 'Wallet and transaction data', type: GetWalletOutput })
async getWallet(@Query() query: GetWalletQuery): Promise<GetWalletOutput> {
return this.paymentsService.getWallet(query);
}
@Post('wallets/transactions')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Process a wallet transaction (deposit or withdrawal)' })
@ApiResponse({ status: 201, description: 'Wallet transaction processed', type: ProcessWalletTransactionOutput })
async processWalletTransaction(@Body() input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
return this.paymentsService.processWalletTransaction(input);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PaymentsService } from './PaymentsService';
import { PaymentsController } from './PaymentsController';
@Module({
controllers: [PaymentsController],
providers: [PaymentsService],
exports: [PaymentsService],
})
export class PaymentsModule {}

View File

@@ -0,0 +1,67 @@
import { Provider } from '@nestjs/common';
import { PaymentsService } from './PaymentsService';
// Due to persistent module resolution issues in the environment,
// actual core interfaces and adapter implementations are not directly imported here.
// In a functional TypeScript environment, these would be imported as follows:
/*
// Import core interfaces
import { IPaymentRepository } from 'core/payments/domain/repositories/IPaymentRepository';
import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMembershipFeeRepository';
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
import { ILogger } from 'core/shared/logging/ILogger';
// Import concrete in-memory implementations
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { InMemoryMembershipFeeRepository } from 'adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
import { InMemoryPrizeRepository } from 'adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
import { InMemoryWalletRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
import { InMemoryPaymentGateway } from 'adapters/payments/ports/InMemoryPaymentGateway';
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
*/
// Define injection tokens as string literals for NestJS
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
export const PAYMENT_GATEWAY_TOKEN = 'IPaymentGateway';
export const LOGGER_TOKEN = 'ILogger'; // Already defined in other Providers, but good to have here too
export const PaymentsProviders: Provider[] = [
PaymentsService, // Provide the service itself
// In a functional setup, the following would be enabled:
/*
{
provide: PAYMENT_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryPaymentRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: MEMBERSHIP_FEE_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryMembershipFeeRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: PRIZE_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryPrizeRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: WALLET_REPOSITORY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryWalletRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: PAYMENT_GATEWAY_TOKEN,
useFactory: (logger: ILogger) => new InMemoryPaymentGateway(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
*/
];

View File

@@ -0,0 +1,346 @@
import { Injectable } from '@nestjs/common';
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, PaymentDto, GetPaymentsQuery, GetPaymentsOutput, PaymentStatus, MembershipFeeDto, MemberPaymentDto, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, MembershipFeeType, MemberPaymentStatus, PrizeDto, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, PrizeType, WalletDto, TransactionDto, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput, TransactionType, ReferenceType } from './dto/PaymentsDto';
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
const payments: Map<string, PaymentDto> = new Map();
const membershipFees: Map<string, MembershipFeeDto> = new Map();
const memberPayments: Map<string, MemberPaymentDto> = new Map();
const prizes: Map<string, PrizeDto> = new Map();
const wallets: Map<string, WalletDto> = new Map();
const transactions: Map<string, TransactionDto> = new Map();
const PLATFORM_FEE_RATE = 0.10;
@Injectable()
export class PaymentsService {
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
let results = Array.from(payments.values());
if (query.leagueId) {
results = results.filter(p => p.leagueId === query.leagueId);
}
if (query.payerId) {
results = results.filter(p => p.payerId === query.payerId);
}
if (query.type) {
results = results.filter(p => p.type === query.type);
}
return { payments: results };
}
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
const platformFee = amount * PLATFORM_FEE_RATE;
const netAmount = amount - platformFee;
const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const payment: PaymentDto = {
id,
type,
amount,
platformFee,
netAmount,
payerId,
payerType,
leagueId,
seasonId: seasonId || undefined,
status: PaymentStatus.PENDING,
createdAt: new Date(),
};
payments.set(id, payment);
return { payment };
}
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
const { paymentId, status } = input;
const payment = payments.get(paymentId);
if (!payment) {
throw new Error('Payment not found');
}
payment.status = status;
if (status === PaymentStatus.COMPLETED) {
payment.completedAt = new Date();
}
payments.set(paymentId, payment);
return { payment };
}
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
const { leagueId, driverId } = query;
if (!leagueId) {
throw new Error('leagueId is required');
}
const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
let payments: MemberPaymentDto[] = [];
if (driverId) {
payments = Array.from(memberPayments.values()).filter(
p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId
);
}
return { fee, payments };
}
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
const { leagueId, seasonId, type, amount } = input;
// Check for existing fee config
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
if (existingFee) {
// Update existing fee
existingFee.type = type;
existingFee.amount = amount;
existingFee.seasonId = seasonId || existingFee.seasonId;
existingFee.enabled = amount > 0;
existingFee.updatedAt = new Date();
membershipFees.set(existingFee.id, existingFee);
return { fee: existingFee };
}
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const fee: MembershipFeeDto = {
id,
leagueId,
seasonId: seasonId || undefined,
type,
amount,
enabled: amount > 0,
createdAt: new Date(),
updatedAt: new Date(),
};
membershipFees.set(id, fee);
return { fee };
}
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
const { feeId, driverId, status, paidAt } = input;
const fee = membershipFees.get(feeId);
if (!fee) {
throw new Error('Membership fee configuration not found');
}
// Find or create payment record
let payment = Array.from(memberPayments.values()).find(
p => p.feeId === feeId && p.driverId === driverId
);
if (!payment) {
const platformFee = fee.amount * PLATFORM_FEE_RATE;
const netAmount = fee.amount - platformFee;
const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
payment = {
id: paymentId,
feeId,
driverId,
amount: fee.amount,
platformFee,
netAmount,
status: MemberPaymentStatus.PENDING,
dueDate: new Date(),
};
memberPayments.set(paymentId, payment);
}
if (status) {
payment.status = status;
}
if (paidAt || status === MemberPaymentStatus.PAID) {
payment.paidAt = paidAt ? new Date(paidAt) : new Date();
}
memberPayments.set(payment.id, payment);
return { payment };
}
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
const { leagueId, seasonId } = query;
let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
if (seasonId) {
results = results.filter(p => p.seasonId === seasonId);
}
results.sort((a, b) => a.position - b.position);
return { prizes: results };
}
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
const { leagueId, seasonId, position, name, amount, type, description } = input;
// Check for duplicate position
const existingPrize = Array.from(prizes.values()).find(
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
);
if (existingPrize) {
throw new Error(`Prize for position ${position} already exists`);
}
const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const prize: PrizeDto = {
id,
leagueId,
seasonId,
position,
name,
amount,
type,
description: description || undefined,
awarded: false,
createdAt: new Date(),
};
prizes.set(id, prize);
return { prize };
}
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
const { prizeId, driverId } = input;
const prize = prizes.get(prizeId);
if (!prize) {
throw new Error('Prize not found');
}
if (prize.awarded) {
throw new Error('Prize has already been awarded');
}
prize.awarded = true;
prize.awardedTo = driverId;
prize.awardedAt = new Date();
prizes.set(prizeId, prize);
return { prize };
}
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
const { prizeId } = input;
const prize = prizes.get(prizeId);
if (!prize) {
throw new Error('Prize not found');
}
if (prize.awarded) {
throw new Error('Cannot delete an awarded prize');
}
prizes.delete(prizeId);
return { success: true };
}
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
const { leagueId } = query;
if (!leagueId) {
throw new Error('LeagueId is required');
}
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
if (!wallet) {
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
wallet = {
id,
leagueId,
balance: 0,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
createdAt: new Date(),
currency: 'USD', // Assuming default currency (mock)
};
wallets.set(id, wallet);
}
const walletTransactions = Array.from(transactions.values())
.filter(t => t.walletId === wallet!.id)
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return { wallet, transactions: walletTransactions };
}
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
const { leagueId, type, amount, description, referenceId, referenceType } = input;
if (!leagueId || !type || amount === undefined || !description) {
throw new Error('Missing required fields: leagueId, type, amount, description');
}
if (type !== TransactionType.DEPOSIT && type !== TransactionType.WITHDRAWAL) {
throw new Error('Type must be "deposit" or "withdrawal"');
}
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
if (!wallet) {
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
wallet = {
id,
leagueId,
balance: 0,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
createdAt: new Date(),
currency: 'USD', // Assuming default currency (mock)
};
wallets.set(id, wallet);
}
if (type === TransactionType.WITHDRAWAL) {
if (amount > wallet.balance) {
throw new Error('Insufficient balance');
}
}
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const transaction: TransactionDto = {
id: transactionId,
walletId: wallet.id,
type,
amount,
description,
referenceId: referenceId || undefined,
referenceType: referenceType || undefined,
createdAt: new Date(),
};
transactions.set(transactionId, transaction);
if (type === TransactionType.DEPOSIT) {
wallet.balance += amount;
wallet.totalRevenue += amount;
} else {
wallet.balance -= amount;
wallet.totalWithdrawn += amount;
}
wallets.set(wallet.id, wallet);
return { wallet, transaction };
}
}

View File

@@ -0,0 +1,566 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean } from 'class-validator';
export enum PaymentType {
SPONSORSHIP = 'sponsorship',
MEMBERSHIP_FEE = 'membership_fee',
}
export enum PayerType {
SPONSOR = 'sponsor',
DRIVER = 'driver',
}
export enum PaymentStatus {
PENDING = 'pending',
COMPLETED = 'completed',
FAILED = 'failed',
REFUNDED = 'refunded',
}
export class PaymentDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty({ enum: PaymentType })
@IsEnum(PaymentType)
type: PaymentType;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty()
@IsNumber()
platformFee: number;
@ApiProperty()
@IsNumber()
netAmount: number;
@ApiProperty()
@IsString()
payerId: string;
@ApiProperty({ enum: PayerType })
@IsEnum(PayerType)
payerType: PayerType;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
seasonId?: string;
@ApiProperty({ enum: PaymentStatus })
@IsEnum(PaymentStatus)
status: PaymentStatus;
@ApiProperty()
@IsDate()
createdAt: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
completedAt?: Date;
}
export class CreatePaymentInput {
@ApiProperty({ enum: PaymentType })
@IsEnum(PaymentType)
type: PaymentType;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty()
@IsString()
payerId: string;
@ApiProperty({ enum: PayerType })
@IsEnum(PayerType)
payerType: PayerType;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
seasonId?: string;
}
export class CreatePaymentOutput {
@ApiProperty({ type: PaymentDto })
payment: PaymentDto;
}
export class UpdatePaymentStatusInput {
@ApiProperty()
@IsString()
paymentId: string;
@ApiProperty({ enum: PaymentStatus })
@IsEnum(PaymentStatus)
status: PaymentStatus;
}
export class UpdatePaymentStatusOutput {
@ApiProperty({ type: PaymentDto })
payment: PaymentDto;
}
export enum MembershipFeeType {
SEASON = 'season',
MONTHLY = 'monthly',
PER_RACE = 'per_race',
}
export enum MemberPaymentStatus {
PENDING = 'pending',
PAID = 'paid',
OVERDUE = 'overdue',
}
export class MembershipFeeDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
seasonId?: string;
@ApiProperty({ enum: MembershipFeeType })
@IsEnum(MembershipFeeType)
type: MembershipFeeType;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty()
@IsBoolean()
enabled: boolean;
@ApiProperty()
@IsDate()
createdAt: Date;
@ApiProperty()
@IsDate()
updatedAt: Date;
}
export class MemberPaymentDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
feeId: string;
@ApiProperty()
@IsString()
driverId: string;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty()
@IsNumber()
platformFee: number;
@ApiProperty()
@IsNumber()
netAmount: number;
@ApiProperty({ enum: MemberPaymentStatus })
@IsEnum(MemberPaymentStatus)
status: MemberPaymentStatus;
@ApiProperty()
@IsDate()
dueDate: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
paidAt?: Date;
}
export class GetMembershipFeesQuery {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
driverId?: string;
}
export class GetMembershipFeesOutput {
@ApiProperty({ type: MembershipFeeDto, nullable: true })
fee: MembershipFeeDto | null;
@ApiProperty({ type: [MemberPaymentDto] })
payments: MemberPaymentDto[];
}
export class UpsertMembershipFeeInput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
seasonId?: string;
@ApiProperty({ enum: MembershipFeeType })
@IsEnum(MembershipFeeType)
type: MembershipFeeType;
@ApiProperty()
@IsNumber()
amount: number;
}
export class UpsertMembershipFeeOutput {
@ApiProperty({ type: MembershipFeeDto })
fee: MembershipFeeDto;
}
export class UpdateMemberPaymentInput {
@ApiProperty()
@IsString()
feeId: string;
@ApiProperty()
@IsString()
driverId: string;
@ApiProperty({ required: false, enum: MemberPaymentStatus })
@IsOptional()
@IsEnum(MemberPaymentStatus)
status?: MemberPaymentStatus;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
paidAt?: Date | string;
}
export class UpdateMemberPaymentOutput {
@ApiProperty({ type: MemberPaymentDto })
payment: MemberPaymentDto;
}
export class GetPaymentsQuery {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
leagueId?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
payerId?: string;
@ApiProperty({ required: false, enum: PaymentType })
@IsOptional()
@IsEnum(PaymentType)
type?: PaymentType;
}
export class GetPaymentsOutput {
@ApiProperty({ type: [PaymentDto] })
payments: PaymentDto[];
}
export enum PrizeType {
CASH = 'cash',
MERCHANDISE = 'merchandise',
OTHER = 'other',
}
export class PrizeDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
seasonId: string;
@ApiProperty()
@IsNumber()
position: number;
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty({ enum: PrizeType })
@IsEnum(PrizeType)
type: PrizeType;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty()
@IsBoolean()
awarded: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
awardedTo?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
awardedAt?: Date;
@ApiProperty()
@IsDate()
createdAt: Date;
}
export class GetPrizesQuery {
@ApiProperty()
@IsString()
leagueId?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
seasonId?: string;
}
export class GetPrizesOutput {
@ApiProperty({ type: [PrizeDto] })
prizes: PrizeDto[];
}
export class CreatePrizeInput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
seasonId: string;
@ApiProperty()
@IsNumber()
position: number;
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty({ enum: PrizeType })
@IsEnum(PrizeType)
type: PrizeType;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
}
export class CreatePrizeOutput {
@ApiProperty({ type: PrizeDto })
prize: PrizeDto;
}
export class AwardPrizeInput {
@ApiProperty()
@IsString()
prizeId: string;
@ApiProperty()
@IsString()
driverId: string;
}
export class AwardPrizeOutput {
@ApiProperty({ type: PrizeDto })
prize: PrizeDto;
}
export class DeletePrizeInput {
@ApiProperty()
@IsString()
prizeId: string;
}
export class DeletePrizeOutput {
@ApiProperty()
@IsBoolean()
success: boolean;
}
export enum TransactionType {
DEPOSIT = 'deposit',
WITHDRAWAL = 'withdrawal',
PLATFORM_FEE = 'platform_fee',
}
export enum ReferenceType {
SPONSORSHIP = 'sponsorship',
MEMBERSHIP_FEE = 'membership_fee',
PRIZE = 'prize',
}
export class WalletDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsNumber()
balance: number;
@ApiProperty()
@IsNumber()
totalRevenue: number;
@ApiProperty()
@IsNumber()
totalPlatformFees: number;
@ApiProperty()
@IsNumber()
totalWithdrawn: number;
@ApiProperty()
@IsDate()
createdAt: Date;
@ApiProperty()
@IsString()
currency: string;
}
export class TransactionDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
walletId: string;
@ApiProperty({ enum: TransactionType })
@IsEnum(TransactionType)
type: TransactionType;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty()
@IsString()
description: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
referenceId?: string;
@ApiProperty({ required: false, enum: ReferenceType })
@IsOptional()
@IsEnum(ReferenceType)
referenceType?: ReferenceType;
@ApiProperty()
@IsDate()
createdAt: Date;
}
export class GetWalletQuery {
@ApiProperty()
@IsString()
leagueId?: string;
}
export class GetWalletOutput {
@ApiProperty({ type: WalletDto })
wallet: WalletDto;
@ApiProperty({ type: [TransactionDto] })
transactions: TransactionDto[];
}
export class ProcessWalletTransactionInput {
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty({ enum: TransactionType })
@IsEnum(TransactionType)
type: TransactionType;
@ApiProperty()
@IsNumber()
amount: number;
@ApiProperty()
@IsString()
@IsNotEmpty()
description: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
referenceId?: string;
@ApiProperty({ required: false, enum: ReferenceType })
@IsOptional()
@IsEnum(ReferenceType)
referenceType?: ReferenceType;
}
export class ProcessWalletTransactionOutput {
@ApiProperty({ type: WalletDto })
wallet: WalletDto;
@ApiProperty({ type: TransactionDto })
transaction: TransactionDto;
}

View File

@@ -0,0 +1,26 @@
import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { RaceService } from './RaceService';
import { AllRacesPageViewModel, RaceStatsDto } from './dto/RaceDto';
@ApiTags('races')
@Controller('races')
export class RaceController {
constructor(private readonly raceService: RaceService) {}
@Get('all')
@ApiOperation({ summary: 'Get all races' })
@ApiResponse({ status: 200, description: 'List of all races', type: AllRacesPageViewModel })
async getAllRaces(): Promise<AllRacesPageViewModel> {
return this.raceService.getAllRaces();
}
@Get('total-races')
@ApiOperation({ summary: 'Get the total number of races' })
@ApiResponse({ status: 200, description: 'Total number of races', type: RaceStatsDto })
async getTotalRaces(): Promise<RaceStatsDto> {
return this.raceService.getTotalRaces();
}
// Add other Race endpoints here based on other presenters
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { RaceService } from './RaceService';
import { RaceController } from './RaceController';
@Module({
controllers: [RaceController],
providers: [RaceService],
exports: [RaceService],
})
export class RaceModule {}

View File

@@ -0,0 +1,18 @@
import { Provider } from '@nestjs/common';
import { RaceService } from './RaceService';
export const RaceProviders: Provider[] = [
RaceService,
// In a functional setup, other providers would be here, e.g.:
/*
{
provide: 'ILogger',
useClass: ConsoleLogger,
},
{
provide: 'IRaceRepository',
useClass: InMemoryRaceRepository,
},
// ... other providers
*/
];

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { AllRacesPageViewModel, RaceStatsDto, ImportRaceResultsInput, ImportRaceResultsSummaryViewModel } from './dto/RaceDto';
@Injectable()
export class RaceService {
constructor() {}
getAllRaces(): Promise<AllRacesPageViewModel> {
console.log('[RaceService] Returning mock all races.');
return Promise.resolve({
races: [
{ id: 'race-1', name: 'Global Race 1', date: new Date().toISOString(), leagueName: 'Global Racing' },
{ id: 'race-2', name: 'Amateur Race 1', date: new Date().toISOString(), leagueName: 'Amateur Series' },
],
totalCount: 2,
});
}
getTotalRaces(): Promise<RaceStatsDto> {
console.log('[RaceService] Returning mock total races.');
return Promise.resolve({
totalRaces: 2, // Placeholder
});
}
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
console.log('Importing race results:', input);
return {
success: true,
raceId: input.raceId,
driversProcessed: 10, // Mock data
resultsRecorded: 10, // Mock data
errors: [],
};
}
}

View File

@@ -0,0 +1,78 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsBoolean, IsNumber } from 'class-validator';
export class RaceViewModel {
@ApiProperty()
id: string; // Assuming a race has an ID
@ApiProperty()
name: string; // Assuming a race has a name
@ApiProperty()
date: string; // Assuming a race has a date
@ApiProperty({ nullable: true })
leagueName?: string; // Assuming a race might belong to a league
// Add more race-related properties as needed based on the DTO from the application layer
}
export class AllRacesPageViewModel {
@ApiProperty({ type: [RaceViewModel] })
races: RaceViewModel[];
@ApiProperty()
totalCount: number;
}
export class RaceStatsDto {
@ApiProperty()
totalRaces: number;
}
export class ImportRaceResultsInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
raceId: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
resultsFileContent: string;
}
export class ImportRaceResultsSummaryViewModel {
@ApiProperty()
@IsBoolean()
success: boolean;
@ApiProperty()
@IsString()
raceId: string;
@ApiProperty()
@IsNumber()
driversProcessed: number;
@ApiProperty()
@IsNumber()
resultsRecorded: number;
@ApiProperty({ type: [String], required: false })
errors?: string[];
}
export class RaceDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsString()
date: string;
}

View File

@@ -0,0 +1,48 @@
import { Controller, Get, Post, Body, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { SponsorService } from './SponsorService';
import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO } from './dto/SponsorDto';
@ApiTags('sponsors')
@Controller('sponsors')
export class SponsorController {
constructor(private readonly sponsorService: SponsorService) {}
@Get('pricing')
@ApiOperation({ summary: 'Get sponsorship pricing for an entity' })
@ApiResponse({ status: 200, description: 'Sponsorship pricing', type: GetEntitySponsorshipPricingResultDto })
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
return this.sponsorService.getEntitySponsorshipPricing();
}
@Get()
@ApiOperation({ summary: 'Get all sponsors' })
@ApiResponse({ status: 200, description: 'List of sponsors', type: GetSponsorsOutput })
async getSponsors(): Promise<GetSponsorsOutput> {
return this.sponsorService.getSponsors();
}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new sponsor' })
@ApiResponse({ status: 201, description: 'Sponsor created', type: CreateSponsorOutput })
async createSponsor(@Body() input: CreateSponsorInput): Promise<CreateSponsorOutput> {
return this.sponsorService.createSponsor(input);
}
// Add other Sponsor endpoints here based on other presenters
@Get('dashboard/:sponsorId')
@ApiOperation({ summary: 'Get sponsor dashboard metrics and sponsored leagues' })
@ApiResponse({ status: 200, description: 'Sponsor dashboard data', type: SponsorDashboardDTO })
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsorDashboard(@Param('sponsorId') sponsorId: string): Promise<SponsorDashboardDTO | null> {
return this.sponsorService.getSponsorDashboard({ sponsorId });
}
@Get(':sponsorId/sponsorships')
@ApiOperation({ summary: 'Get all sponsorships for a given sponsor' })
@ApiResponse({ status: 200, description: 'List of sponsorships', type: SponsorSponsorshipsDTO })
@ApiResponse({ status: 404, description: 'Sponsor not found' })
async getSponsorSponsorships(@Param('sponsorId') sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
return this.sponsorService.getSponsorSponsorships({ sponsorId });
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SponsorService } from './SponsorService';
import { SponsorController } from './SponsorController';
@Module({
controllers: [SponsorController],
providers: [SponsorService],
exports: [SponsorService],
})
export class SponsorModule {}

View File

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

View File

@@ -0,0 +1,162 @@
import { Injectable } from '@nestjs/common';
import { GetEntitySponsorshipPricingResultDto, SponsorDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorshipDetailDTO, SponsorSponsorshipsDTO, SponsoredLeagueDTO, SponsorDashboardMetricsDTO, SponsorDashboardInvestmentDTO } from './dto/SponsorDto';
const sponsors: Map<string, SponsorDto> = new Map();
@Injectable()
export class SponsorService {
constructor() {
// Seed some demo sponsors for dashboard if empty
if (sponsors.size === 0) {
const demoSponsor1: SponsorDto = {
id: 'sponsor-demo-1',
name: 'Demo Sponsor Co.',
contactEmail: 'contact@demosponsor.com',
websiteUrl: 'https://demosponsor.com',
logoUrl: 'https://fakeimg.pl/200x100/aaaaaa/ffffff?text=DemoCo',
createdAt: new Date(),
};
const demoSponsor2: SponsorDto = {
id: 'sponsor-demo-2',
name: 'Second Brand',
contactEmail: 'info@secondbrand.net',
websiteUrl: 'https://secondbrand.net',
logoUrl: 'https://fakeimg.pl/200x100/cccccc/ffffff?text=Brand2',
createdAt: new Date(Date.now() - 86400000 * 5),
};
sponsors.set(demoSponsor1.id, demoSponsor1);
sponsors.set(demoSponsor2.id, demoSponsor2);
}
}
getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
// This logic relies on external factors (e.g., pricing configuration, entity type)
// For now, return mock data
return Promise.resolve({
pricing: [
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
],
});
}
async getSponsors(): Promise<GetSponsorsOutput> {
return { sponsors: Array.from(sponsors.values()) };
}
async createSponsor(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const newSponsor: SponsorDto = {
id,
name: input.name,
contactEmail: input.contactEmail,
websiteUrl: input.websiteUrl,
logoUrl: input.logoUrl,
createdAt: new Date(),
};
sponsors.set(id, newSponsor);
return { sponsor: newSponsor };
}
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
const { sponsorId } = params;
const sponsor = sponsors.get(sponsorId);
if (!sponsor) {
return null;
}
// Simplified mock data for dashboard metrics and sponsored leagues
const metrics: SponsorDashboardMetricsDTO = {
impressions: 10000,
impressionsChange: 12.5,
uniqueViewers: 7000,
viewersChange: 8.3,
races: 50,
drivers: 100,
exposure: 75,
exposureChange: 5.2,
};
const sponsoredLeagues: SponsoredLeagueDTO[] = [
{ id: 'league-1', name: 'League 1', tier: 'main', drivers: 50, races: 10, impressions: 5000, status: 'active' },
{ id: 'league-2', name: 'League 2', tier: 'secondary', drivers: 30, races: 5, impressions: 1500, status: 'upcoming' },
];
const investment: SponsorDashboardInvestmentDTO = {
activeSponsorships: 2,
totalInvestment: 5000,
costPerThousandViews: 0.5,
};
return {
sponsorId,
sponsorName: sponsor.name,
metrics,
sponsoredLeagues,
investment,
};
}
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
const { sponsorId } = params;
const sponsor = sponsors.get(sponsorId);
if (!sponsor) {
return null;
};
const sponsorshipDetails: SponsorshipDetailDTO[] = [
{
id: 'sponsorship-1',
leagueId: 'league-1',
leagueName: 'League 1',
seasonId: 'season-1',
seasonName: 'Season 1',
seasonStartDate: new Date('2025-01-01'),
seasonEndDate: new Date('2025-12-31'),
tier: 'main',
status: 'active',
pricing: { amount: 1000, currency: 'USD' },
platformFee: { amount: 100, currency: 'USD' },
netAmount: { amount: 900, currency: 'USD' },
metrics: { drivers: 50, races: 10, completedRaces: 8, impressions: 5000 },
createdAt: new Date('2024-12-01'),
activatedAt: new Date('2025-01-01'),
},
{
id: 'sponsorship-2',
leagueId: 'league-2',
leagueName: 'League 2',
seasonId: 'season-2',
seasonName: 'Season 2',
tier: 'secondary',
status: 'pending',
pricing: { amount: 500, currency: 'USD' },
platformFee: { amount: 50, currency: 'USD' },
netAmount: { amount: 450, currency: 'USD' },
metrics: { drivers: 30, races: 5, completedRaces: 0, impressions: 0 },
createdAt: new Date('2025-03-15'),
},
];
const totalInvestment = sponsorshipDetails.reduce((sum, s) => sum + s.pricing.amount, 0);
const totalPlatformFees = sponsorshipDetails.reduce((sum, s) => sum + s.platformFee.amount, 0);
const activeSponsorships = sponsorshipDetails.filter(s => s.status === 'active').length;
return {
sponsorId,
sponsorName: sponsor.name,
sponsorships: sponsorshipDetails,
summary: {
totalSponsorships: sponsorshipDetails.length,
activeSponsorships,
totalInvestment,
totalPlatformFees,
currency: 'USD',
},
};
}
}

View File

@@ -0,0 +1,299 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsNumber, IsEnum, IsOptional, IsDate, IsBoolean, IsUrl, IsEmail } from 'class-validator';
export class SponsorshipPricingItemDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
level: string;
@ApiProperty()
@IsNumber()
price: number;
@ApiProperty()
@IsString()
currency: string;
}
export class GetEntitySponsorshipPricingResultDto {
@ApiProperty({ type: [SponsorshipPricingItemDto] })
pricing: SponsorshipPricingItemDto[];
}
export class SponsorDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty()
@IsString()
@IsEmail()
@IsNotEmpty()
contactEmail: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@IsUrl()
websiteUrl?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@IsUrl()
logoUrl?: string;
@ApiProperty()
@IsDate()
createdAt: Date;
}
export class GetSponsorsOutput {
@ApiProperty({ type: [SponsorDto] })
sponsors: SponsorDto[];
}
export class CreateSponsorInput {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty()
@IsEmail()
@IsNotEmpty()
contactEmail: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@IsUrl()
websiteUrl?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@IsUrl()
logoUrl?: string;
}
export class CreateSponsorOutput {
@ApiProperty({ type: SponsorDto })
sponsor: SponsorDto;
}
export class GetSponsorDashboardQueryParams {
@ApiProperty()
@IsString()
sponsorId: string;
}
export class SponsoredLeagueDTO {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
name: string;
@ApiProperty({ enum: ['main', 'secondary'] })
@IsEnum(['main', 'secondary'])
tier: 'main' | 'secondary';
@ApiProperty()
@IsNumber()
drivers: number;
@ApiProperty()
@IsNumber()
races: number;
@ApiProperty()
@IsNumber()
impressions: number;
@ApiProperty({ enum: ['active', 'upcoming', 'completed'] })
@IsEnum(['active', 'upcoming', 'completed'])
status: 'active' | 'upcoming' | 'completed';
}
export class SponsorDashboardMetricsDTO {
@ApiProperty()
@IsNumber()
impressions: number;
@ApiProperty()
@IsNumber()
impressionsChange: number;
@ApiProperty()
@IsNumber()
uniqueViewers: number;
@ApiProperty()
@IsNumber()
viewersChange: number;
@ApiProperty()
@IsNumber()
races: number;
@ApiProperty()
@IsNumber()
drivers: number;
@ApiProperty()
@IsNumber()
exposure: number;
@ApiProperty()
@IsNumber()
exposureChange: number;
}
export class SponsorDashboardInvestmentDTO {
@ApiProperty()
@IsNumber()
activeSponsorships: number;
@ApiProperty()
@IsNumber()
totalInvestment: number;
@ApiProperty()
@IsNumber()
costPerThousandViews: number;
}
export class SponsorDashboardDTO {
@ApiProperty()
@IsString()
sponsorId: string;
@ApiProperty()
@IsString()
sponsorName: string;
@ApiProperty({ type: SponsorDashboardMetricsDTO })
metrics: SponsorDashboardMetricsDTO;
@ApiProperty({ type: [SponsoredLeagueDTO] })
sponsoredLeagues: SponsoredLeagueDTO[];
@ApiProperty({ type: SponsorDashboardInvestmentDTO })
investment: SponsorDashboardInvestmentDTO;
}
export class GetSponsorSponsorshipsQueryParams {
@ApiProperty()
@IsString()
sponsorId: string;
}
export class SponsorshipDetailDTO {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
leagueId: string;
@ApiProperty()
@IsString()
leagueName: string;
@ApiProperty()
@IsString()
seasonId: string;
@ApiProperty()
@IsString()
seasonName: string;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
seasonStartDate?: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
seasonEndDate?: Date;
@ApiProperty({ enum: ['main', 'secondary'] })
@IsEnum(['main', 'secondary'])
tier: 'main' | 'secondary';
@ApiProperty({ enum: ['pending', 'active', 'expired', 'cancelled'] })
@IsEnum(['pending', 'active', 'expired', 'cancelled'])
status: 'pending' | 'active' | 'expired' | 'cancelled';
@ApiProperty()
pricing: {
amount: number;
currency: string;
};
@ApiProperty()
platformFee: {
amount: number;
currency: string;
};
@ApiProperty()
netAmount: {
amount: number;
currency: string;
};
@ApiProperty()
metrics: {
drivers: number;
races: number;
completedRaces: number;
impressions: number;
};
@ApiProperty()
createdAt: Date;
@ApiProperty({ required: false })
@IsOptional()
@IsDate()
activatedAt?: Date;
}
export class SponsorSponsorshipsDTO {
@ApiProperty()
@IsString()
sponsorId: string;
@ApiProperty()
@IsString()
sponsorName: string;
@ApiProperty({ type: [SponsorshipDetailDTO] })
sponsorships: SponsorshipDetailDTO[];
@ApiProperty()
summary: {
totalSponsorships: number;
activeSponsorships: number;
totalInvestment: number;
totalPlatformFees: number;
currency: string;
};
}
// Add other DTOs for sponsor-related logic as needed

View File

@@ -0,0 +1,19 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { TeamService } from './TeamService';
import { AllTeamsViewModel } from './dto/TeamDto';
@ApiTags('teams')
@Controller('teams')
export class TeamController {
constructor(private readonly teamService: TeamService) {}
@Get('all')
@ApiOperation({ summary: 'Get all teams' })
@ApiResponse({ status: 200, description: 'List of all teams', type: AllTeamsViewModel })
async getAllTeams(): Promise<AllTeamsViewModel> {
return this.teamService.getAllTeams();
}
// Add other Team endpoints here based on other presenters
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TeamService } from './TeamService';
import { TeamController } from './TeamController';
@Module({
controllers: [TeamController],
providers: [TeamService],
exports: [TeamService],
})
export class TeamModule {}

View File

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

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDto, MembershipDto, TeamLeagueDto, MembershipRole } from './dto/TeamDto';
@Injectable()
export class TeamService {
getAllTeams(): Promise<AllTeamsViewModel> {
// TODO: Implement actual logic to fetch all teams
return Promise.resolve({
teams: [],
totalCount: 0,
});
}
private teams: Map<string, TeamDto> = new Map(); // In-memory store for teams
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
const { teamId, driverId } = query;
const team = this.teams.get(teamId);
if (!team) {
return null;
}
// Mock membership and roles
const membership: MembershipDto = {
role: driverId === team.ownerId ? MembershipRole.OWNER : MembershipRole.MEMBER,
joinedAt: new Date(Date.now() - 86400000 * 30), // Joined 30 days ago
isActive: true, // Always active for mock
};
const isOwner = team.ownerId === driverId;
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
return {
team: team,
membership,
isOwner,
canManage,
};
}
// Add other methods related to Team logic here based on other presenters
}

View File

@@ -0,0 +1,121 @@
import { ApiProperty } from '@nestjs/swagger';
export class TeamLeagueDto {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty({ nullable: true })
logoUrl?: string;
}
export class TeamListItemViewModel {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty({ nullable: true })
tag?: string;
@ApiProperty({ nullable: true })
description?: string;
@ApiProperty()
memberCount: number;
@ApiProperty({ type: [TeamLeagueDto] })
leagues: TeamLeagueDto[];
}
export class AllTeamsViewModel {
@ApiProperty({ type: [TeamListItemViewModel] })
teams: TeamListItemViewModel[];
@ApiProperty()
totalCount: number;
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate } from 'class-validator';
export class TeamDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty()
@IsString()
@IsNotEmpty()
tag: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty()
@IsString()
ownerId: string;
@ApiProperty({ type: [TeamLeagueDto] })
leagues: TeamLeagueDto[];
}
export enum MembershipRole {
OWNER = 'owner',
MANAGER = 'manager',
MEMBER = 'member',
}
export enum MembershipStatus {
ACTIVE = 'active',
PENDING = 'pending',
INVITED = 'invited',
INACTIVE = 'inactive',
}
export class MembershipDto {
@ApiProperty({ enum: MembershipRole })
@IsEnum(MembershipRole)
role: MembershipRole;
@ApiProperty()
@IsDate()
joinedAt: Date;
@ApiProperty()
@IsBoolean()
isActive: boolean;
}
export class DriverTeamViewModel {
@ApiProperty({ type: TeamDto })
team: TeamDto;
@ApiProperty({ type: MembershipDto })
membership: MembershipDto;
@ApiProperty()
@IsBoolean()
isOwner: boolean;
@ApiProperty()
@IsBoolean()
canManage: boolean;
}
export class GetDriverTeamQuery {
@ApiProperty()
@IsString()
teamId: string;
@ApiProperty()
@IsString()
driverId: string;
}

View File

@@ -1,33 +0,0 @@
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
import type { RecordPageViewInput, RecordPageViewOutput } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
import type { RecordEngagementInput, RecordEngagementOutput } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase';
import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase';
import { Response } from 'express';
@Controller('analytics')
export class AnalyticsController {
constructor(
private readonly recordPageViewUseCase: RecordPageViewUseCase,
private readonly recordEngagementUseCase: RecordEngagementUseCase,
) {}
@Post('page-view')
async recordPageView(
@Body() input: RecordPageViewInput,
@Res() res: Response,
): Promise<void> {
const output: RecordPageViewOutput = await this.recordPageViewUseCase.execute(input);
res.status(HttpStatus.CREATED).json(output);
}
@Post('engagement')
async recordEngagement(
@Body() input: RecordEngagementInput,
@Res() res: Response,
): Promise<void> {
const output: RecordEngagementOutput = await this.recordEngagementUseCase.execute(input);
res.status(HttpStatus.CREATED).json(output);
}
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ export interface StoredUser {
displayName: string;
passwordHash: string;
salt: string;
primaryDriverId?: string;
primaryDriverId?: string | undefined;
createdAt: Date;
}

View File

@@ -1,4 +1,3 @@
export interface ILogger {
debug(message: string, ...args: any[]): void;
info(message: string, ...args: any[]): void;

85
package-lock.json generated
View File

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