From 1efd9710327c260c24ab3b6c29d6e2eafd67e12a Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 22 Dec 2025 22:46:15 +0100 Subject: [PATCH] fix issues in adapters --- .../automation/config/AutomationConfig.ts | 6 --- adapters/bootstrap/EnsureInitialData.ts | 2 +- adapters/bootstrap/LeagueScoringPresets.ts | 4 +- adapters/bootstrap/ScoringDemoSetup.ts | 8 ++-- .../inmemory/InMemoryAchievementRepository.ts | 2 +- .../inmemory/InMemoryUserRatingRepository.ts | 2 +- .../ports/InMemoryFaceValidationAdapter.ts | 2 +- .../gateways/DiscordNotificationGateway.ts | 14 +++--- .../gateways/EmailNotificationGateway.ts | 10 ++--- .../gateways/InAppNotificationGateway.ts | 38 +++++++++------- .../gateways/NotificationGatewayRegistry.ts | 24 +++++----- .../InMemoryNotificationRepository.ts | 9 ++-- .../InMemoryLeagueMembershipRepository.ts | 13 +++--- .../InMemoryLeagueStandingsRepository.test.ts | 8 ++-- .../inmemory/InMemoryPenaltyRepository.ts | 2 +- .../InMemoryRaceRegistrationRepository.ts | 9 ++++ .../inmemory/InMemorySeasonRepository.test.ts | 9 +++- ...MemorySponsorshipRequestRepository.test.ts | 2 +- .../inmemory/InMemoryStandingRepository.ts | 44 +++++++------------ core/league/application/index.ts | 1 + .../ports/ILeagueStandingsRepository.ts | 13 ++++++ core/racing/application/index.ts | 1 + .../ports/LeagueScoringPresetProvider.ts | 17 +++++++ core/shared/application/index.ts | 4 +- core/shared/errors/index.ts | 3 +- 25 files changed, 144 insertions(+), 103 deletions(-) create mode 100644 core/league/application/index.ts create mode 100644 core/league/application/ports/ILeagueStandingsRepository.ts create mode 100644 core/racing/application/ports/LeagueScoringPresetProvider.ts diff --git a/adapters/automation/config/AutomationConfig.ts b/adapters/automation/config/AutomationConfig.ts index b7bb6daac..a5ccfb229 100644 --- a/adapters/automation/config/AutomationConfig.ts +++ b/adapters/automation/config/AutomationConfig.ts @@ -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. diff --git a/adapters/bootstrap/EnsureInitialData.ts b/adapters/bootstrap/EnsureInitialData.ts index 70cd6d70a..eab747a50 100644 --- a/adapters/bootstrap/EnsureInitialData.ts +++ b/adapters/bootstrap/EnsureInitialData.ts @@ -1,7 +1,7 @@ import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase'; 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 { DRIVER_ACHIEVEMENTS, STEWARD_ACHIEVEMENTS, diff --git a/adapters/bootstrap/LeagueScoringPresets.ts b/adapters/bootstrap/LeagueScoringPresets.ts index 0b5e81eeb..9cdf3323d 100644 --- a/adapters/bootstrap/LeagueScoringPresets.ts +++ b/adapters/bootstrap/LeagueScoringPresets.ts @@ -218,12 +218,12 @@ export const leagueScoringPresets: LeagueScoringPreset[] = [ dropScorePolicy, }; - return { + return LeagueScoringConfig.create({ id: `lsc-${seasonId}-endurance-main-double`, seasonId, scoringPresetId: 'endurance-main-double', championships: [championship], - }; + }); }, }, ]; diff --git a/adapters/bootstrap/ScoringDemoSetup.ts b/adapters/bootstrap/ScoringDemoSetup.ts index 7755d470e..4184f6a19 100644 --- a/adapters/bootstrap/ScoringDemoSetup.ts +++ b/adapters/bootstrap/ScoringDemoSetup.ts @@ -10,16 +10,16 @@ import { getLeagueScoringPresetById } from './LeagueScoringPresets'; /* eslint-disable @typescript-eslint/no-unused-vars */ class SilentLogger implements Logger { - debug(...args: unknown[]): void { + debug(..._args: unknown[]): void { // console.debug(...args); } - info(...args: unknown[]): void { + info(..._args: unknown[]): void { // console.info(...args); } - warn(...args: unknown[]): void { + warn(..._args: unknown[]): void { // console.warn(...args); } - error(...args: unknown[]): void { + error(..._args: unknown[]): void { // console.error(...args); } } diff --git a/adapters/identity/persistence/inmemory/InMemoryAchievementRepository.ts b/adapters/identity/persistence/inmemory/InMemoryAchievementRepository.ts index 86bb3a384..f388823d6 100644 --- a/adapters/identity/persistence/inmemory/InMemoryAchievementRepository.ts +++ b/adapters/identity/persistence/inmemory/InMemoryAchievementRepository.ts @@ -142,7 +142,7 @@ export class InMemoryAchievementRepository implements IAchievementRepository { this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`); return null; } 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; } } diff --git a/adapters/identity/persistence/inmemory/InMemoryUserRatingRepository.ts b/adapters/identity/persistence/inmemory/InMemoryUserRatingRepository.ts index 966782cd0..743be112c 100644 --- a/adapters/identity/persistence/inmemory/InMemoryUserRatingRepository.ts +++ b/adapters/identity/persistence/inmemory/InMemoryUserRatingRepository.ts @@ -51,7 +51,7 @@ export class InMemoryUserRatingRepository implements IUserRatingRepository { this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`); return results; } 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; } } diff --git a/adapters/media/ports/InMemoryFaceValidationAdapter.ts b/adapters/media/ports/InMemoryFaceValidationAdapter.ts index e74890d99..7d7205ab7 100644 --- a/adapters/media/ports/InMemoryFaceValidationAdapter.ts +++ b/adapters/media/ports/InMemoryFaceValidationAdapter.ts @@ -6,7 +6,7 @@ export class InMemoryFaceValidationAdapter implements FaceValidationPort { this.logger.info('InMemoryFaceValidationAdapter initialized.'); } - async validateFacePhoto(imageData: string | Buffer): Promise { + async validateFacePhoto(_imageData: string | Buffer): Promise { this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).'); // Simulate a successful validation for any input for demo purposes return Promise.resolve({ diff --git a/adapters/notifications/gateways/DiscordNotificationGateway.ts b/adapters/notifications/gateways/DiscordNotificationGateway.ts index c6a73aa47..77190291a 100644 --- a/adapters/notifications/gateways/DiscordNotificationGateway.ts +++ b/adapters/notifications/gateways/DiscordNotificationGateway.ts @@ -5,18 +5,18 @@ * Currently a stub - to be implemented when Discord integration is needed. */ -import type { Notification } from '../../domain/entities/Notification'; -import type { - INotificationGateway, - NotificationDeliveryResult -} from '../../application/ports/INotificationGateway'; -import type { NotificationChannel } from '../../domain/types/NotificationTypes'; +import type { Notification } from '@core/notifications/domain/entities/Notification'; +import type { + NotificationGateway, + NotificationDeliveryResult +} from '@core/notifications/application/ports/NotificationGateway'; +import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes'; export interface DiscordAdapterConfig { webhookUrl?: string; } -export class DiscordNotificationAdapter implements INotificationGateway { +export class DiscordNotificationAdapter implements NotificationGateway { private readonly channel: NotificationChannel = 'discord'; private webhookUrl: string | undefined; diff --git a/adapters/notifications/gateways/EmailNotificationGateway.ts b/adapters/notifications/gateways/EmailNotificationGateway.ts index 9bb01d05c..110b9dd33 100644 --- a/adapters/notifications/gateways/EmailNotificationGateway.ts +++ b/adapters/notifications/gateways/EmailNotificationGateway.ts @@ -5,12 +5,12 @@ * 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 { - INotificationGateway, + NotificationGateway, NotificationDeliveryResult -} from '../../application/ports/INotificationGateway'; -import type { NotificationChannel } from '../../domain/types/NotificationTypes'; +} from '@core/notifications/application/ports/NotificationGateway'; +import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes'; export interface EmailAdapterConfig { smtpHost?: string; @@ -20,7 +20,7 @@ export interface EmailAdapterConfig { fromAddress?: string; } -export class EmailNotificationAdapter implements INotificationGateway { +export class EmailNotificationAdapter implements NotificationGateway { private readonly channel: NotificationChannel = 'email'; private config: EmailAdapterConfig; diff --git a/adapters/notifications/gateways/InAppNotificationGateway.ts b/adapters/notifications/gateways/InAppNotificationGateway.ts index 2f13e473a..a79d4e838 100644 --- a/adapters/notifications/gateways/InAppNotificationGateway.ts +++ b/adapters/notifications/gateways/InAppNotificationGateway.ts @@ -1,27 +1,34 @@ /** - * Infrastructure Adapter: InAppNotificationAdapter + * Infrastructure Adapter: InAppNotificationAdapter (Stub) * - * Handles in-app notifications (stored in database, shown in UI). - * This is the primary/default notification channel. + * Handles in-app notifications. + * 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 { - INotificationGateway, + NotificationGateway, NotificationDeliveryResult -} from '../../application/ports/INotificationGateway'; -import type { NotificationChannel } from '../../domain/types/NotificationTypes'; +} from '@core/notifications/application/ports/NotificationGateway'; +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'; - /** - * For in_app, sending is essentially a no-op since the notification - * is already persisted by the use case. This just confirms delivery. - */ + constructor() { + // In-app notifications don't need external configuration + } + async send(notification: Notification): Promise { - // In-app notifications are stored directly in the repository - // This adapter just confirms the "delivery" was successful + // In-app notifications are stored in the database, so this is a stub + // 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 { success: true, channel: this.channel, @@ -35,7 +42,8 @@ export class InAppNotificationAdapter implements INotificationGateway { } isConfigured(): boolean { - return true; // Always configured + // In-app notifications are always configured + return true; } getChannel(): NotificationChannel { diff --git a/adapters/notifications/gateways/NotificationGatewayRegistry.ts b/adapters/notifications/gateways/NotificationGatewayRegistry.ts index 2c1eba180..c7c68eb52 100644 --- a/adapters/notifications/gateways/NotificationGatewayRegistry.ts +++ b/adapters/notifications/gateways/NotificationGatewayRegistry.ts @@ -4,31 +4,31 @@ * Manages notification gateways and routes notifications to appropriate channels. */ -import type { Notification } from '../../domain/entities/Notification'; -import type { NotificationChannel } from '../../domain/types/NotificationTypes'; -import type { - INotificationGateway, - INotificationGatewayRegistry, - NotificationDeliveryResult -} from '../../application/ports/INotificationGateway'; +import type { Notification } from '@core/notifications/domain/entities/Notification'; +import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes'; +import type { + NotificationGateway, + NotificationGatewayRegistry as INotificationGatewayRegistry, + NotificationDeliveryResult +} from '@core/notifications/application/ports/NotificationGateway'; export class NotificationGatewayRegistry implements INotificationGatewayRegistry { - private gateways: Map = new Map(); + private gateways: Map = new Map(); - constructor(initialGateways: INotificationGateway[] = []) { + constructor(initialGateways: NotificationGateway[] = []) { initialGateways.forEach(gateway => this.register(gateway)); } - register(gateway: INotificationGateway): void { + register(gateway: NotificationGateway): void { const channel = gateway.getChannel(); this.gateways.set(channel, gateway); } - getGateway(channel: NotificationChannel): INotificationGateway | null { + getGateway(channel: NotificationChannel): NotificationGateway | null { return this.gateways.get(channel) || null; } - getAllGateways(): INotificationGateway[] { + getAllGateways(): NotificationGateway[] { return Array.from(this.gateways.values()); } diff --git a/adapters/notifications/persistence/inmemory/InMemoryNotificationRepository.ts b/adapters/notifications/persistence/inmemory/InMemoryNotificationRepository.ts index 72857e558..34ed859ae 100644 --- a/adapters/notifications/persistence/inmemory/InMemoryNotificationRepository.ts +++ b/adapters/notifications/persistence/inmemory/InMemoryNotificationRepository.ts @@ -4,9 +4,9 @@ * Provides an in-memory storage implementation for notifications. */ -import { Notification } from '../../domain/entities/Notification'; -import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; -import type { NotificationType } from '../../domain/types/NotificationTypes'; +import { Notification } from '@core/notifications/domain/entities/Notification'; +import type { INotificationRepository } from '@core/notifications/domain/repositories/INotificationRepository'; +import type { NotificationType } from '@core/notifications/domain/types/NotificationTypes'; import type { Logger } from '@core/shared/application'; 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}.`); return notifications; } 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; } } @@ -141,7 +141,6 @@ export class InMemoryNotificationRepository implements INotificationRepository { async deleteAllByRecipientId(recipientId: string): Promise { this.logger.debug(`Deleting all notifications for recipient ID: ${recipientId}`); try { - const initialCount = this.notifications.size; const toDelete = Array.from(this.notifications.values()) .filter(n => n.recipientId === recipientId) .map(n => n.id); diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts index 6bdbac7ee..6bad56705 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository.ts @@ -1,5 +1,6 @@ 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'; export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository { @@ -25,33 +26,33 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise { 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); + return Promise.resolve(membership && membership.status.toString() === 'active' ? membership : null); } async findAllByLeagueId(leagueId: string): Promise { 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}.`); return Promise.resolve(filteredMemberships); } async findAllByDriverId(driverId: string): Promise { 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}.`); return Promise.resolve(memberships); } async getLeagueMembers(leagueId: string): Promise { 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}.`); return Promise.resolve(members); } async getJoinRequests(leagueId: string): Promise { 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}.`); return Promise.resolve(requests); } diff --git a/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts b/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts index b2f986188..700df95c3 100644 --- a/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts +++ b/adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.test.ts @@ -17,14 +17,14 @@ describe('InMemoryLeagueStandingsRepository', () => { repository = new InMemoryLeagueStandingsRepository(mockLogger); }); - const createTestStanding = (id: string, leagueId: string, driverId: string, position: number, points: number): RawStanding => ({ - id, - leagueId, + const createTestStanding = (_id: string, _leagueId: string, driverId: string, position: number, points: number): RawStanding => ({ driverId, position, points, wins: 0, - racesCompleted: 0, + races: 0, + poles: 0, + podiums: 0, }); describe('constructor', () => { diff --git a/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts b/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts index 71542a09e..dc0793701 100644 --- a/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryPenaltyRepository.ts @@ -4,7 +4,7 @@ * 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 { Logger } from '@core/shared/application'; diff --git a/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts b/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts index ce1437bb3..4b6ce40eb 100644 --- a/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository.ts @@ -27,6 +27,15 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo return Promise.resolve(driverIds); } + async findByRaceId(raceId: string): Promise { + 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 { this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`); const count = Array.from(this.registrations.values()).filter(reg => reg.raceId.toString() === raceId).length; diff --git a/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts index da0cedef5..912725fba 100644 --- a/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts +++ b/adapters/racing/persistence/inmemory/InMemorySeasonRepository.test.ts @@ -110,8 +110,15 @@ describe('InMemorySeasonRepository', () => { await repository.create(season); const updatedSeason = Season.create({ - ...season, + id: season.id, + leagueId: season.leagueId.toString(), + gameId: season.gameId.toString(), 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); const found = await repository.findById('1'); diff --git a/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts b/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts index 57567a48e..3c2887213 100644 --- a/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts +++ b/adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository.test.ts @@ -48,7 +48,7 @@ describe('InMemorySponsorshipRequestRepository', () => { it('should seed initial requests', () => { const request = createTestRequest('req-1'); - const repoWithSeed = new InMemorySponsorshipRequestRepository(mockLogger, [request]); + new InMemorySponsorshipRequestRepository(mockLogger, [request]); expect(mockLogger.debug).toHaveBeenCalledWith('Seeded sponsorship request: req-1.'); }); }); diff --git a/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts b/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts index ca5f8513b..5eb05c207 100644 --- a/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts +++ b/adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts @@ -46,11 +46,9 @@ export class InMemoryStandingRepository implements IStandingRepository { const standings = Array.from(this.standings.values()) .filter(standing => standing.leagueId.toString() === leagueId) .sort((a, b) => { - // Sort by position (lower is better) if (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(); }); this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`); @@ -73,7 +71,7 @@ export class InMemoryStandingRepository implements IStandingRepository { } return standing; } 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; } } @@ -93,9 +91,9 @@ export class InMemoryStandingRepository implements IStandingRepository { async save(standing: Standing): Promise { this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`); try { - const key = this.getKey(standing.leagueId, standing.driverId); + const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString()); 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 { 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.`); return standing; } 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; } } @@ -112,7 +110,7 @@ export class InMemoryStandingRepository implements IStandingRepository { this.logger.debug(`Saving ${standings.length} standings.`); try { 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.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.`); } } 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; } } @@ -163,7 +161,7 @@ export class InMemoryStandingRepository implements IStandingRepository { this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`); return exists; } 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; } } @@ -176,7 +174,6 @@ export class InMemoryStandingRepository implements IStandingRepository { throw new Error('Cannot recalculate standings: missing required repositories'); } - // Get league to determine points system const league = await this.leagueRepository.findById(leagueId); if (!league) { 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.`); - // Get points system const resolvedPointsSystem = league.settings.customPoints ?? this.pointsSystems[league.settings.pointsSystem] ?? @@ -196,7 +192,6 @@ export class InMemoryStandingRepository implements IStandingRepository { } this.logger.debug(`Resolved points system for league ${leagueId}.`); - // Get all completed races for the league const races = await this.raceRepository.findCompletedByLeagueId(leagueId); this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`); @@ -205,7 +200,6 @@ export class InMemoryStandingRepository implements IStandingRepository { return []; } - // Get all results for these races const allResults = await Promise.all( races.map(async race => { this.logger.debug(`Fetching results for race ${race.id}.`); @@ -217,50 +211,44 @@ export class InMemoryStandingRepository implements IStandingRepository { const results = allResults.flat(); this.logger.debug(`Collected ${results.length} results from all completed races.`); - // Calculate standings per driver const standingsMap = new Map(); results.forEach(result => { - let standing = standingsMap.get(result.driverId); + const driverIdStr = result.driverId.toString(); + let standing = standingsMap.get(driverIdStr); if (!standing) { standing = Standing.create({ 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, resolvedPointsSystem); - standingsMap.set(result.driverId, standing); - this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`); + standing = standing.addRaceResult(result.position.toNumber(), resolvedPointsSystem); + standingsMap.set(driverIdStr, standing); + this.logger.debug(`Driver ${driverIdStr} in league ${leagueId} accumulated ${standing.points} points.`); }); this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`); - // Sort by points and assign positions const sortedStandings = Array.from(standingsMap.values()) .sort((a, b) => { - if (b.points !== a.points) { - return b.points - a.points; + if (b.points.toNumber() !== a.points.toNumber()) { + return b.points.toNumber() - a.points.toNumber(); } - // Tie-breaker: most wins if (b.wins !== a.wins) { return b.wins - a.wins; } - // Tie-breaker: most races completed return b.racesCompleted - a.racesCompleted; }); this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`); - // Assign positions const updatedStandings = sortedStandings.map((standing, index) => { const newStanding = standing.updatePosition(index + 1); this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`); return newStanding; }); - // Save all standings await this.saveMany(updatedStandings); this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`); diff --git a/core/league/application/index.ts b/core/league/application/index.ts new file mode 100644 index 000000000..05ba246a7 --- /dev/null +++ b/core/league/application/index.ts @@ -0,0 +1 @@ +export * from './ports/ILeagueStandingsRepository'; \ No newline at end of file diff --git a/core/league/application/ports/ILeagueStandingsRepository.ts b/core/league/application/ports/ILeagueStandingsRepository.ts new file mode 100644 index 000000000..d958bd9ca --- /dev/null +++ b/core/league/application/ports/ILeagueStandingsRepository.ts @@ -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; +} \ No newline at end of file diff --git a/core/racing/application/index.ts b/core/racing/application/index.ts index 38aed80a9..ca145b6e7 100644 --- a/core/racing/application/index.ts +++ b/core/racing/application/index.ts @@ -45,6 +45,7 @@ export * from './use-cases/AcceptSponsorshipRequestUseCase'; export * from './use-cases/RejectSponsorshipRequestUseCase'; export * from './use-cases/GetPendingSponsorshipRequestsUseCase'; export * from './use-cases/GetEntitySponsorshipPricingUseCase'; +export * from './ports/LeagueScoringPresetProvider'; // Re-export domain types for legacy callers (type-only) export type { diff --git a/core/racing/application/ports/LeagueScoringPresetProvider.ts b/core/racing/application/ports/LeagueScoringPresetProvider.ts new file mode 100644 index 000000000..e436e26c6 --- /dev/null +++ b/core/racing/application/ports/LeagueScoringPresetProvider.ts @@ -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; +} \ No newline at end of file diff --git a/core/shared/application/index.ts b/core/shared/application/index.ts index 0c2ef222f..411876e83 100644 --- a/core/shared/application/index.ts +++ b/core/shared/application/index.ts @@ -2,4 +2,6 @@ export * from './UseCase'; export * from './AsyncUseCase'; export * from './Service'; export * from './Logger'; -export * from './UseCaseOutputPort'; \ No newline at end of file +export * from './UseCaseOutputPort'; +export * from './ErrorReporter'; +export * from './Result'; \ No newline at end of file diff --git a/core/shared/errors/index.ts b/core/shared/errors/index.ts index 248af5e9e..66696df61 100644 --- a/core/shared/errors/index.ts +++ b/core/shared/errors/index.ts @@ -1,2 +1,3 @@ export * from './DomainError'; -export * from './ApplicationError'; \ No newline at end of file +export * from './ApplicationError'; +export * from './ApplicationErrorCode'; \ No newline at end of file