fix issues in adapters

This commit is contained in:
2025-12-22 22:46:15 +01:00
parent 41b27402dc
commit 1efd971032
25 changed files with 144 additions and 103 deletions

View File

@@ -163,12 +163,6 @@ export function loadAutomationConfig(): AutomationEnvironmentConfig {
}; };
} }
/**
* Type guard to validate automation mode string.
*/
function isValidAutomationMode(value: string | undefined): value is AutomationMode {
return value === 'production' || value === 'development' || value === 'test';
}
/** /**
* Type guard to validate legacy automation mode string. * Type guard to validate legacy automation mode string.

View File

@@ -1,7 +1,7 @@
import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase'; import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
import { CreateAchievementUseCase } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase'; import { CreateAchievementUseCase } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase';
import type { Logger } from '@core/shared/application/Logger'; import type { Logger } from '@core/shared/application';
import { import {
DRIVER_ACHIEVEMENTS, DRIVER_ACHIEVEMENTS,
STEWARD_ACHIEVEMENTS, STEWARD_ACHIEVEMENTS,

View File

@@ -218,12 +218,12 @@ export const leagueScoringPresets: LeagueScoringPreset[] = [
dropScorePolicy, dropScorePolicy,
}; };
return { return LeagueScoringConfig.create({
id: `lsc-${seasonId}-endurance-main-double`, id: `lsc-${seasonId}-endurance-main-double`,
seasonId, seasonId,
scoringPresetId: 'endurance-main-double', scoringPresetId: 'endurance-main-double',
championships: [championship], championships: [championship],
}; });
}, },
}, },
]; ];

View File

@@ -10,16 +10,16 @@ import { getLeagueScoringPresetById } from './LeagueScoringPresets';
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
class SilentLogger implements Logger { class SilentLogger implements Logger {
debug(...args: unknown[]): void { debug(..._args: unknown[]): void {
// console.debug(...args); // console.debug(...args);
} }
info(...args: unknown[]): void { info(..._args: unknown[]): void {
// console.info(...args); // console.info(...args);
} }
warn(...args: unknown[]): void { warn(..._args: unknown[]): void {
// console.warn(...args); // console.warn(...args);
} }
error(...args: unknown[]): void { error(..._args: unknown[]): void {
// console.error(...args); // console.error(...args);
} }
} }

View File

@@ -142,7 +142,7 @@ export class InMemoryAchievementRepository implements IAchievementRepository {
this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`); this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`);
return null; return null;
} catch (error) { } catch (error) {
this.logger.error(`Error finding user achievement for user ${userId}, achievement ${achievementId}:`, error); this.logger.error(`Error finding user achievement for user ${userId}, achievement ${achievementId}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }

View File

@@ -51,7 +51,7 @@ export class InMemoryUserRatingRepository implements IUserRatingRepository {
this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`); this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`);
return results; return results;
} catch (error) { } catch (error) {
this.logger.error(`Error finding user ratings for user ids ${userIds.join(', ')}:`, error); this.logger.error(`Error finding user ratings for user ids ${userIds.join(', ')}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }

View File

@@ -6,7 +6,7 @@ export class InMemoryFaceValidationAdapter implements FaceValidationPort {
this.logger.info('InMemoryFaceValidationAdapter initialized.'); this.logger.info('InMemoryFaceValidationAdapter initialized.');
} }
async validateFacePhoto(imageData: string | Buffer): Promise<FaceValidationResult> { async validateFacePhoto(_imageData: string | Buffer): Promise<FaceValidationResult> {
this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).'); this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).');
// Simulate a successful validation for any input for demo purposes // Simulate a successful validation for any input for demo purposes
return Promise.resolve({ return Promise.resolve({

View File

@@ -5,18 +5,18 @@
* Currently a stub - to be implemented when Discord integration is needed. * Currently a stub - to be implemented when Discord integration is needed.
*/ */
import type { Notification } from '../../domain/entities/Notification'; import type { Notification } from '@core/notifications/domain/entities/Notification';
import type { import type {
INotificationGateway, NotificationGateway,
NotificationDeliveryResult NotificationDeliveryResult
} from '../../application/ports/INotificationGateway'; } from '@core/notifications/application/ports/NotificationGateway';
import type { NotificationChannel } from '../../domain/types/NotificationTypes'; import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
export interface DiscordAdapterConfig { export interface DiscordAdapterConfig {
webhookUrl?: string; webhookUrl?: string;
} }
export class DiscordNotificationAdapter implements INotificationGateway { export class DiscordNotificationAdapter implements NotificationGateway {
private readonly channel: NotificationChannel = 'discord'; private readonly channel: NotificationChannel = 'discord';
private webhookUrl: string | undefined; private webhookUrl: string | undefined;

View File

@@ -5,12 +5,12 @@
* Currently a stub - to be implemented when email integration is needed. * Currently a stub - to be implemented when email integration is needed.
*/ */
import type { Notification } from '../../domain/entities/Notification'; import type { Notification } from '@core/notifications/domain/entities/Notification';
import type { import type {
INotificationGateway, NotificationGateway,
NotificationDeliveryResult NotificationDeliveryResult
} from '../../application/ports/INotificationGateway'; } from '@core/notifications/application/ports/NotificationGateway';
import type { NotificationChannel } from '../../domain/types/NotificationTypes'; import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
export interface EmailAdapterConfig { export interface EmailAdapterConfig {
smtpHost?: string; smtpHost?: string;
@@ -20,7 +20,7 @@ export interface EmailAdapterConfig {
fromAddress?: string; fromAddress?: string;
} }
export class EmailNotificationAdapter implements INotificationGateway { export class EmailNotificationAdapter implements NotificationGateway {
private readonly channel: NotificationChannel = 'email'; private readonly channel: NotificationChannel = 'email';
private config: EmailAdapterConfig; private config: EmailAdapterConfig;

View File

@@ -1,27 +1,34 @@
/** /**
* Infrastructure Adapter: InAppNotificationAdapter * Infrastructure Adapter: InAppNotificationAdapter (Stub)
* *
* Handles in-app notifications (stored in database, shown in UI). * Handles in-app notifications.
* This is the primary/default notification channel. * Currently a stub - to be implemented when in-app notification system is needed.
*/ */
import type { Notification } from '../../domain/entities/Notification'; import type { Notification } from '@core/notifications/domain/entities/Notification';
import type { import type {
INotificationGateway, NotificationGateway,
NotificationDeliveryResult NotificationDeliveryResult
} from '../../application/ports/INotificationGateway'; } from '@core/notifications/application/ports/NotificationGateway';
import type { NotificationChannel } from '../../domain/types/NotificationTypes'; import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
export class InAppNotificationAdapter implements INotificationGateway { export class InAppNotificationAdapter implements NotificationGateway {
private readonly channel: NotificationChannel = 'in_app'; private readonly channel: NotificationChannel = 'in_app';
/** constructor() {
* For in_app, sending is essentially a no-op since the notification // In-app notifications don't need external configuration
* is already persisted by the use case. This just confirms delivery. }
*/
async send(notification: Notification): Promise<NotificationDeliveryResult> { async send(notification: Notification): Promise<NotificationDeliveryResult> {
// In-app notifications are stored directly in the repository // In-app notifications are stored in the database, so this is a stub
// This adapter just confirms the "delivery" was successful // that simulates successful delivery
console.log(`[InApp Stub] Notification stored in database:`, {
id: notification.id,
recipientId: notification.recipientId,
title: notification.title,
body: notification.body,
});
return { return {
success: true, success: true,
channel: this.channel, channel: this.channel,
@@ -35,7 +42,8 @@ export class InAppNotificationAdapter implements INotificationGateway {
} }
isConfigured(): boolean { isConfigured(): boolean {
return true; // Always configured // In-app notifications are always configured
return true;
} }
getChannel(): NotificationChannel { getChannel(): NotificationChannel {

View File

@@ -4,31 +4,31 @@
* Manages notification gateways and routes notifications to appropriate channels. * Manages notification gateways and routes notifications to appropriate channels.
*/ */
import type { Notification } from '../../domain/entities/Notification'; import type { Notification } from '@core/notifications/domain/entities/Notification';
import type { NotificationChannel } from '../../domain/types/NotificationTypes'; import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
import type { import type {
INotificationGateway, NotificationGateway,
INotificationGatewayRegistry, NotificationGatewayRegistry as INotificationGatewayRegistry,
NotificationDeliveryResult NotificationDeliveryResult
} from '../../application/ports/INotificationGateway'; } from '@core/notifications/application/ports/NotificationGateway';
export class NotificationGatewayRegistry implements INotificationGatewayRegistry { export class NotificationGatewayRegistry implements INotificationGatewayRegistry {
private gateways: Map<NotificationChannel, INotificationGateway> = new Map(); private gateways: Map<NotificationChannel, NotificationGateway> = new Map();
constructor(initialGateways: INotificationGateway[] = []) { constructor(initialGateways: NotificationGateway[] = []) {
initialGateways.forEach(gateway => this.register(gateway)); initialGateways.forEach(gateway => this.register(gateway));
} }
register(gateway: INotificationGateway): void { register(gateway: NotificationGateway): void {
const channel = gateway.getChannel(); const channel = gateway.getChannel();
this.gateways.set(channel, gateway); this.gateways.set(channel, gateway);
} }
getGateway(channel: NotificationChannel): INotificationGateway | null { getGateway(channel: NotificationChannel): NotificationGateway | null {
return this.gateways.get(channel) || null; return this.gateways.get(channel) || null;
} }
getAllGateways(): INotificationGateway[] { getAllGateways(): NotificationGateway[] {
return Array.from(this.gateways.values()); return Array.from(this.gateways.values());
} }

View File

@@ -4,9 +4,9 @@
* Provides an in-memory storage implementation for notifications. * Provides an in-memory storage implementation for notifications.
*/ */
import { Notification } from '../../domain/entities/Notification'; import { Notification } from '@core/notifications/domain/entities/Notification';
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { INotificationRepository } from '@core/notifications/domain/repositories/INotificationRepository';
import type { NotificationType } from '../../domain/types/NotificationTypes'; import type { NotificationType } from '@core/notifications/domain/types/NotificationTypes';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';
export class InMemoryNotificationRepository implements INotificationRepository { export class InMemoryNotificationRepository implements INotificationRepository {
@@ -75,7 +75,7 @@ export class InMemoryNotificationRepository implements INotificationRepository {
this.logger.info(`Found ${notifications.length} notifications for recipient ID: ${recipientId}, type: ${type}.`); this.logger.info(`Found ${notifications.length} notifications for recipient ID: ${recipientId}, type: ${type}.`);
return notifications; return notifications;
} catch (error) { } catch (error) {
this.logger.error(`Error finding notifications for recipient ID ${recipientId}, type ${type}:`, error); this.logger.error(`Error finding notifications for recipient ID ${recipientId}, type ${type}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }
@@ -141,7 +141,6 @@ export class InMemoryNotificationRepository implements INotificationRepository {
async deleteAllByRecipientId(recipientId: string): Promise<void> { async deleteAllByRecipientId(recipientId: string): Promise<void> {
this.logger.debug(`Deleting all notifications for recipient ID: ${recipientId}`); this.logger.debug(`Deleting all notifications for recipient ID: ${recipientId}`);
try { try {
const initialCount = this.notifications.size;
const toDelete = Array.from(this.notifications.values()) const toDelete = Array.from(this.notifications.values())
.filter(n => n.recipientId === recipientId) .filter(n => n.recipientId === recipientId)
.map(n => n.id); .map(n => n.id);

View File

@@ -1,5 +1,6 @@
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository'; import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { LeagueMembership, JoinRequest } from '@core/racing/domain/entities/LeagueMembership'; import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
import { Logger } from '@core/shared/application'; import { Logger } from '@core/shared/application';
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository { export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
@@ -25,33 +26,33 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo
async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<LeagueMembership | null> { async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding active membership for league ${leagueId}, driver ${driverId}.`); this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding active membership for league ${leagueId}, driver ${driverId}.`);
const membership = await this.getMembership(leagueId, driverId); const membership = await this.getMembership(leagueId, driverId);
return Promise.resolve(membership && membership.status === 'active' ? membership : null); return Promise.resolve(membership && membership.status.toString() === 'active' ? membership : null);
} }
async findAllByLeagueId(leagueId: string): Promise<LeagueMembership[]> { async findAllByLeagueId(leagueId: string): Promise<LeagueMembership[]> {
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for league ${leagueId}.`); this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for league ${leagueId}.`);
const filteredMemberships = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId); const filteredMemberships = Array.from(this.memberships.values()).filter(mem => mem.leagueId.toString() === leagueId);
this.logger.info(`Found ${filteredMemberships.length} memberships for league ${leagueId}.`); this.logger.info(`Found ${filteredMemberships.length} memberships for league ${leagueId}.`);
return Promise.resolve(filteredMemberships); return Promise.resolve(filteredMemberships);
} }
async findAllByDriverId(driverId: string): Promise<LeagueMembership[]> { async findAllByDriverId(driverId: string): Promise<LeagueMembership[]> {
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for driver ${driverId}.`); this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for driver ${driverId}.`);
const memberships = Array.from(this.memberships.values()).filter(mem => mem.driverId === driverId); const memberships = Array.from(this.memberships.values()).filter(mem => mem.driverId.toString() === driverId);
this.logger.info(`Found ${memberships.length} memberships for driver ${driverId}.`); this.logger.info(`Found ${memberships.length} memberships for driver ${driverId}.`);
return Promise.resolve(memberships); return Promise.resolve(memberships);
} }
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> { async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting active members for league ${leagueId}.`); 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'); const members = Array.from(this.memberships.values()).filter(mem => mem.leagueId.toString() === leagueId && mem.status.toString() === 'active');
this.logger.info(`Found ${members.length} active members for league ${leagueId}.`); this.logger.info(`Found ${members.length} active members for league ${leagueId}.`);
return Promise.resolve(members); return Promise.resolve(members);
} }
async getJoinRequests(leagueId: string): Promise<JoinRequest[]> { async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting join requests for league ${leagueId}.`); this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting join requests for league ${leagueId}.`);
const requests = Array.from(this.joinRequests.values()).filter(req => req.leagueId === leagueId); const requests = Array.from(this.joinRequests.values()).filter(req => req.leagueId.toString() === leagueId);
this.logger.info(`Found ${requests.length} join requests for league ${leagueId}.`); this.logger.info(`Found ${requests.length} join requests for league ${leagueId}.`);
return Promise.resolve(requests); return Promise.resolve(requests);
} }

View File

@@ -17,14 +17,14 @@ describe('InMemoryLeagueStandingsRepository', () => {
repository = new InMemoryLeagueStandingsRepository(mockLogger); repository = new InMemoryLeagueStandingsRepository(mockLogger);
}); });
const createTestStanding = (id: string, leagueId: string, driverId: string, position: number, points: number): RawStanding => ({ const createTestStanding = (_id: string, _leagueId: string, driverId: string, position: number, points: number): RawStanding => ({
id,
leagueId,
driverId, driverId,
position, position,
points, points,
wins: 0, wins: 0,
racesCompleted: 0, races: 0,
poles: 0,
podiums: 0,
}); });
describe('constructor', () => { describe('constructor', () => {

View File

@@ -4,7 +4,7 @@
* Provides an in-memory storage implementation for penalties. * Provides an in-memory storage implementation for penalties.
*/ */
import type { Penalty } from '@core/racing/domain/entities/Penalty'; import type { Penalty } from '@core/racing/domain/entities/penalty/Penalty';
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository'; import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
import type { Logger } from '@core/shared/application'; import type { Logger } from '@core/shared/application';

View File

@@ -27,6 +27,15 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo
return Promise.resolve(driverIds); return Promise.resolve(driverIds);
} }
async findByRaceId(raceId: string): Promise<RaceRegistration[]> {
this.logger.debug(`[InMemoryRaceRegistrationRepository] Finding all registrations for race ${raceId}.`);
const registrations = Array.from(this.registrations.values()).filter(
reg => reg.raceId.toString() === raceId
);
this.logger.info(`Found ${registrations.length} registrations for race ${raceId}.`);
return Promise.resolve(registrations);
}
async getRegistrationCount(raceId: string): Promise<number> { async getRegistrationCount(raceId: string): Promise<number> {
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`); this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`);
const count = Array.from(this.registrations.values()).filter(reg => reg.raceId.toString() === raceId).length; const count = Array.from(this.registrations.values()).filter(reg => reg.raceId.toString() === raceId).length;

View File

@@ -110,8 +110,15 @@ describe('InMemorySeasonRepository', () => {
await repository.create(season); await repository.create(season);
const updatedSeason = Season.create({ const updatedSeason = Season.create({
...season, id: season.id,
leagueId: season.leagueId.toString(),
gameId: season.gameId.toString(),
name: 'Updated Season', name: 'Updated Season',
status: season.status.toString() as 'planned' | 'active' | 'completed',
year: 2025,
order: 1,
startDate: new Date('2025-01-01'),
endDate: new Date('2025-12-31'),
}); });
await repository.update(updatedSeason); await repository.update(updatedSeason);
const found = await repository.findById('1'); const found = await repository.findById('1');

View File

@@ -48,7 +48,7 @@ describe('InMemorySponsorshipRequestRepository', () => {
it('should seed initial requests', () => { it('should seed initial requests', () => {
const request = createTestRequest('req-1'); const request = createTestRequest('req-1');
const repoWithSeed = new InMemorySponsorshipRequestRepository(mockLogger, [request]); new InMemorySponsorshipRequestRepository(mockLogger, [request]);
expect(mockLogger.debug).toHaveBeenCalledWith('Seeded sponsorship request: req-1.'); expect(mockLogger.debug).toHaveBeenCalledWith('Seeded sponsorship request: req-1.');
}); });
}); });

View File

@@ -46,11 +46,9 @@ export class InMemoryStandingRepository implements IStandingRepository {
const standings = Array.from(this.standings.values()) const standings = Array.from(this.standings.values())
.filter(standing => standing.leagueId.toString() === leagueId) .filter(standing => standing.leagueId.toString() === leagueId)
.sort((a, b) => { .sort((a, b) => {
// Sort by position (lower is better)
if (a.position.toNumber() !== b.position.toNumber()) { if (a.position.toNumber() !== b.position.toNumber()) {
return a.position.toNumber() - b.position.toNumber(); return a.position.toNumber() - b.position.toNumber();
} }
// If positions are equal, sort by points (higher is better)
return b.points.toNumber() - a.points.toNumber(); return b.points.toNumber() - a.points.toNumber();
}); });
this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`); this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`);
@@ -73,7 +71,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
} }
return standing; return standing;
} catch (error) { } catch (error) {
this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error); this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }
@@ -93,9 +91,9 @@ export class InMemoryStandingRepository implements IStandingRepository {
async save(standing: Standing): Promise<Standing> { async save(standing: Standing): Promise<Standing> {
this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`); this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`);
try { try {
const key = this.getKey(standing.leagueId, standing.driverId); const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
if (this.standings.has(key)) { if (this.standings.has(key)) {
this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`); this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver ${standing.driverId}.`);
} else { } else {
this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`); this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
} }
@@ -103,7 +101,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`); this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`);
return standing; return standing;
} catch (error) { } catch (error) {
this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error); this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }
@@ -112,7 +110,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
this.logger.debug(`Saving ${standings.length} standings.`); this.logger.debug(`Saving ${standings.length} standings.`);
try { try {
standings.forEach(standing => { standings.forEach(standing => {
const key = this.getKey(standing.leagueId, standing.driverId); const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
this.standings.set(key, standing); this.standings.set(key, standing);
}); });
this.logger.info(`${standings.length} standings saved successfully.`); this.logger.info(`${standings.length} standings saved successfully.`);
@@ -133,7 +131,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`); this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`);
} }
} catch (error) { } catch (error) {
this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error); this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }
@@ -163,7 +161,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`); this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`);
return exists; return exists;
} catch (error) { } catch (error) {
this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error); this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
throw error; throw error;
} }
} }
@@ -176,7 +174,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
throw new Error('Cannot recalculate standings: missing required repositories'); throw new Error('Cannot recalculate standings: missing required repositories');
} }
// Get league to determine points system
const league = await this.leagueRepository.findById(leagueId); const league = await this.leagueRepository.findById(leagueId);
if (!league) { if (!league) {
this.logger.warn(`League with ID ${leagueId} not found during recalculation.`); this.logger.warn(`League with ID ${leagueId} not found during recalculation.`);
@@ -184,7 +181,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
} }
this.logger.debug(`League ${leagueId} found for recalculation.`); this.logger.debug(`League ${leagueId} found for recalculation.`);
// Get points system
const resolvedPointsSystem = const resolvedPointsSystem =
league.settings.customPoints ?? league.settings.customPoints ??
this.pointsSystems[league.settings.pointsSystem] ?? this.pointsSystems[league.settings.pointsSystem] ??
@@ -196,7 +192,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
} }
this.logger.debug(`Resolved points system for league ${leagueId}.`); this.logger.debug(`Resolved points system for league ${leagueId}.`);
// Get all completed races for the league
const races = await this.raceRepository.findCompletedByLeagueId(leagueId); const races = await this.raceRepository.findCompletedByLeagueId(leagueId);
this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`); this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`);
@@ -205,7 +200,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
return []; return [];
} }
// Get all results for these races
const allResults = await Promise.all( const allResults = await Promise.all(
races.map(async race => { races.map(async race => {
this.logger.debug(`Fetching results for race ${race.id}.`); this.logger.debug(`Fetching results for race ${race.id}.`);
@@ -217,50 +211,44 @@ export class InMemoryStandingRepository implements IStandingRepository {
const results = allResults.flat(); const results = allResults.flat();
this.logger.debug(`Collected ${results.length} results from all completed races.`); this.logger.debug(`Collected ${results.length} results from all completed races.`);
// Calculate standings per driver
const standingsMap = new Map<string, Standing>(); const standingsMap = new Map<string, Standing>();
results.forEach(result => { results.forEach(result => {
let standing = standingsMap.get(result.driverId); const driverIdStr = result.driverId.toString();
let standing = standingsMap.get(driverIdStr);
if (!standing) { if (!standing) {
standing = Standing.create({ standing = Standing.create({
leagueId, leagueId,
driverId: result.driverId, driverId: driverIdStr,
}); });
this.logger.debug(`Created new standing for driver ${result.driverId} in league ${leagueId}.`); this.logger.debug(`Created new standing for driver ${driverIdStr} in league ${leagueId}.`);
} }
// Add points from this result standing = standing.addRaceResult(result.position.toNumber(), resolvedPointsSystem);
standing = standing.addRaceResult(result.position, resolvedPointsSystem); standingsMap.set(driverIdStr, standing);
standingsMap.set(result.driverId, standing); this.logger.debug(`Driver ${driverIdStr} in league ${leagueId} accumulated ${standing.points} points.`);
this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`);
}); });
this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`); this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`);
// Sort by points and assign positions
const sortedStandings = Array.from(standingsMap.values()) const sortedStandings = Array.from(standingsMap.values())
.sort((a, b) => { .sort((a, b) => {
if (b.points !== a.points) { if (b.points.toNumber() !== a.points.toNumber()) {
return b.points - a.points; return b.points.toNumber() - a.points.toNumber();
} }
// Tie-breaker: most wins
if (b.wins !== a.wins) { if (b.wins !== a.wins) {
return b.wins - a.wins; return b.wins - a.wins;
} }
// Tie-breaker: most races completed
return b.racesCompleted - a.racesCompleted; return b.racesCompleted - a.racesCompleted;
}); });
this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`); this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`);
// Assign positions
const updatedStandings = sortedStandings.map((standing, index) => { const updatedStandings = sortedStandings.map((standing, index) => {
const newStanding = standing.updatePosition(index + 1); const newStanding = standing.updatePosition(index + 1);
this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`); this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`);
return newStanding; return newStanding;
}); });
// Save all standings
await this.saveMany(updatedStandings); await this.saveMany(updatedStandings);
this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`); this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`);

View File

@@ -0,0 +1 @@
export * from './ports/ILeagueStandingsRepository';

View File

@@ -0,0 +1,13 @@
export interface RawStanding {
driverId: string;
position: number;
points: number;
races: number;
wins: number;
poles: number;
podiums: number;
}
export interface ILeagueStandingsRepository {
getLeagueStandings(leagueId: string): Promise<RawStanding[]>;
}

View File

@@ -45,6 +45,7 @@ export * from './use-cases/AcceptSponsorshipRequestUseCase';
export * from './use-cases/RejectSponsorshipRequestUseCase'; export * from './use-cases/RejectSponsorshipRequestUseCase';
export * from './use-cases/GetPendingSponsorshipRequestsUseCase'; export * from './use-cases/GetPendingSponsorshipRequestsUseCase';
export * from './use-cases/GetEntitySponsorshipPricingUseCase'; export * from './use-cases/GetEntitySponsorshipPricingUseCase';
export * from './ports/LeagueScoringPresetProvider';
// Re-export domain types for legacy callers (type-only) // Re-export domain types for legacy callers (type-only)
export type { export type {

View File

@@ -0,0 +1,17 @@
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
export interface LeagueScoringPresetDTO {
id: string;
name: string;
description: string;
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
sessionSummary: string;
bonusSummary: string;
dropPolicySummary: string;
}
export interface LeagueScoringPresetProvider {
listPresets(): LeagueScoringPresetDTO[];
getPresetById(id: string): LeagueScoringPresetDTO | undefined;
createScoringConfigFromPreset(presetId: string, seasonId: string): LeagueScoringConfig;
}

View File

@@ -3,3 +3,5 @@ export * from './AsyncUseCase';
export * from './Service'; export * from './Service';
export * from './Logger'; export * from './Logger';
export * from './UseCaseOutputPort'; export * from './UseCaseOutputPort';
export * from './ErrorReporter';
export * from './Result';

View File

@@ -1,2 +1,3 @@
export * from './DomainError'; export * from './DomainError';
export * from './ApplicationError'; export * from './ApplicationError';
export * from './ApplicationErrorCode';