diff --git a/.roo/rules-ask/rules.md b/.roo/rules-ask/rules.md deleted file mode 100644 index 05955bf7e..000000000 --- a/.roo/rules-ask/rules.md +++ /dev/null @@ -1,94 +0,0 @@ -# ❓ Ask - -## Purpose -Resolve **semantic ambiguity** and **intent uncertainty** so execution is unambiguous. - -Ask mode exists to: -- detect unclear wording -- collapse multiple interpretations into one -- make implicit intent explicit -- stabilize meaning before execution - -Ask mode works at the **meaning level**, not the technical level. - ---- - -## What Ask Does -Ask mode: -- examines the stated objective or scenario -- identifies ambiguity, vagueness, or conflicting interpretations -- resolves ambiguity into a single, explicit intent -- states the clarified intent directly - -Ask mode does NOT solve problems. -Ask mode does NOT design solutions. - ---- - -## What Ask Does NOT Do -Ask mode MUST NOT: -- ask questions to the user -- gather files, paths, or logs -- scan the repository -- interpret technical structure -- propose architecture or code -- suggest alternatives -- expand or narrow scope -- delegate work -- explain implementation -- discuss tests or UX - ---- - -## Output Rules (STRICT) -Ask mode output MUST: -- be **1–3 short lines** -- contain **statements**, not questions -- contain **no technical detail** -- contain **no options or alternatives** -- contain **no explanation or justification** -- contain **no commentary** - -The output states: -- what was ambiguous -- how the ambiguity is resolved -- what the clarified intent now is - ---- - -## Example Output Style -- “The instruction mixes outcome and mechanism; intent is outcome only.” -- “Scope applies to logic, not UI.” -- “Clarified intent: enforce naming consistency without refactoring.” - -(Examples illustrate style only.) - ---- - -## Context Handling -Ask mode operates ONLY on the context provided by the Orchestrator. - -If the intent cannot be resolved with the given context, output exactly: -“Ambiguity unresolved.” - -No guessing. No discovery. - ---- - -## Forbidden -Ask mode MUST NOT: -- ask follow-up questions -- hedge -- speculate -- teach concepts -- propose improvements -- perform analysis outside semantics -- produce long output - ---- - -## Completion -Ask mode is complete when: -- ambiguity is removed -- intent is explicit -- execution can proceed deterministically \ No newline at end of file diff --git a/apps/companion/main/di-config.ts b/apps/companion/main/di-config.ts index 5e32f2695..d42af2527 100644 --- a/apps/companion/main/di-config.ts +++ b/apps/companion/main/di-config.ts @@ -30,7 +30,7 @@ import { } from '@gridpilot/automation/infrastructure/adapters/automation'; import { MockAutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter'; import { AutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter'; -import { PinoLogAdapter } from '@gridpilot/automation/infrastructure/adapters/logging/PinoLogAdapter'; +import { ConsoleLogAdapter } from '@gridpilot/automation/infrastructure/adapters/logging/ConsoleLogAdapter'; import { NoOpLogAdapter } from '@gridpilot/automation/infrastructure/adapters/logging/NoOpLogAdapter'; import { loadAutomationConfig, @@ -107,7 +107,7 @@ export function configureDIContainer(): void { container.registerInstance(DI_TOKENS.AutomationMode, automationMode); // Logger (singleton) - const logger = process.env.NODE_ENV === 'test' ? new NoOpLogAdapter() : new PinoLogAdapter(loggingConfig); + const logger = process.env.NODE_ENV === 'test' ? new NoOpLogAdapter() : new ConsoleLogAdapter(); container.registerInstance(DI_TOKENS.Logger, logger); // Browser Mode Config Loader (singleton) @@ -120,7 +120,7 @@ export function configureDIContainer(): void { // Session Repository (singleton) container.register( DI_TOKENS.SessionRepository, - { useClass: InMemorySessionRepository }, + { useFactory: (c) => new InMemorySessionRepository(c.resolve(DI_TOKENS.Logger)) }, { lifecycle: Lifecycle.Singleton } ); @@ -225,7 +225,8 @@ export function configureDIContainer(): void { const startAutomationUseCase = new StartAutomationSessionUseCase( automationEngine, browserAutomation, - sessionRepository + sessionRepository, + logger ); container.registerInstance(DI_TOKENS.StartAutomationUseCase, startAutomationUseCase); @@ -235,17 +236,17 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.CheckAuthenticationUseCase, - new CheckAuthenticationUseCase(authService) + new CheckAuthenticationUseCase(authService, logger) ); container.registerInstance( DI_TOKENS.InitiateLoginUseCase, - new InitiateLoginUseCase(authService) + new InitiateLoginUseCase(authService, logger) ); container.registerInstance( DI_TOKENS.ClearSessionUseCase, - new ClearSessionUseCase(authService) + new ClearSessionUseCase(authService, logger) ); container.registerInstance( diff --git a/apps/website/lib/auth/InMemoryAuthService.ts b/apps/website/lib/auth/InMemoryAuthService.ts index 174a5635c..d41f11ebf 100644 --- a/apps/website/lib/auth/InMemoryAuthService.ts +++ b/apps/website/lib/auth/InMemoryAuthService.ts @@ -11,18 +11,25 @@ import { CookieIdentitySessionAdapter } from '@gridpilot/identity/infrastructure import { IracingDemoIdentityProviderAdapter } from '@gridpilot/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter'; import { InMemoryUserRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRepository'; import type { IUserRepository } from '@gridpilot/identity/domain/repositories/IUserRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; // Singleton user repository to persist across requests (in-memory demo) let userRepositoryInstance: IUserRepository | null = null; -function getUserRepository(): IUserRepository { +function getUserRepository(logger: ILogger): IUserRepository { if (!userRepositoryInstance) { - userRepositoryInstance = new InMemoryUserRepository(); + userRepositoryInstance = new InMemoryUserRepository(logger); } return userRepositoryInstance; } export class InMemoryAuthService implements AuthService { + private readonly logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + } + async getCurrentSession(): Promise { const sessionPort = new CookieIdentitySessionAdapter(); const useCase = new GetCurrentUserSessionUseCase(sessionPort); diff --git a/apps/website/lib/auth/index.ts b/apps/website/lib/auth/index.ts index 26e929e2b..ce39905c9 100644 --- a/apps/website/lib/auth/index.ts +++ b/apps/website/lib/auth/index.ts @@ -1,11 +1,14 @@ import type { AuthService } from './AuthService'; import { InMemoryAuthService } from './InMemoryAuthService'; - -let authService: AuthService | null = null; +import { getDIContainer } from '../di-container'; +import { DI_TOKENS } from '../di-tokens'; export function getAuthService(): AuthService { - if (!authService) { - authService = new InMemoryAuthService(); + const container = getDIContainer(); + if (!container.isRegistered(DI_TOKENS.AuthService)) { + throw new Error( + `${DI_TOKENS.AuthService.description} not registered in DI container.`, + ); } - return authService; + return container.resolve(DI_TOKENS.AuthService); } \ No newline at end of file diff --git a/apps/website/lib/di-config.ts b/apps/website/lib/di-config.ts index 41b034677..3086e395a 100644 --- a/apps/website/lib/di-config.ts +++ b/apps/website/lib/di-config.ts @@ -37,6 +37,31 @@ import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repos import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { ImageServicePort } from '@gridpilot/media'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; +import { ConsoleLogger } from '@gridpilot/shared/logging/ConsoleLogger'; +import type { IPageViewRepository, IEngagementRepository } from '@gridpilot/analytics'; +import { InMemoryPageViewRepository, InMemoryEngagementRepository } from '@gridpilot/analytics/infrastructure/repositories'; +import { RecordPageViewUseCase, RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases'; +import type { AuthService } from './auth/AuthService'; +import { InMemoryAuthService } from './auth/InMemoryAuthService'; +import type { IUserRepository, StoredUser } from '@gridpilot/identity/domain/repositories/IUserRepository'; +import { InMemoryUserRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRepository'; +import type { ISponsorAccountRepository, SponsorAccount } from '@gridpilot/identity/domain/repositories/ISponsorAccountRepository'; +import { InMemorySponsorAccountRepository } from '@gridpilot/identity/infrastructure/repositories/InMemorySponsorAccountRepository'; +import type { ILiveryRepository } from '@gridpilot/racing/domain/repositories/ILiveryRepository'; +import { InMemoryLiveryRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLiveryRepository'; +import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository'; +import { InMemoryChampionshipStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryScoringRepositories'; +import type { ILeagueWalletRepository } from '@gridpilot/racing/domain/repositories/ILeagueWalletRepository'; +import { InMemoryLeagueWalletRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueWalletRepository'; +import type { ITransactionRepository } from '@gridpilot/racing/domain/repositories/ITransactionRepository'; +import { InMemoryTransactionRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTransactionRepository'; +import type { ISessionRepository } from '@gridpilot/racing/domain/repositories/ISessionRepository'; +import { InMemorySessionRepository } from '@gridpilot/racing/infrastructure/repositories/InMemorySessionRepository'; +import type { IAchievementRepository } from '@gridpilot/identity/domain/repositories/IAchievementRepository'; +import { InMemoryAchievementRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryAchievementRepository'; +import type { IUserRatingRepository } from '@gridpilot/identity/domain/repositories/IUserRatingRepository'; +import { InMemoryUserRatingRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRatingRepository'; // Notifications import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application'; @@ -189,6 +214,9 @@ import { * Configure the DI container with all bindings for the website application */ export function configureDIContainer(): void { + // Register the logger + container.registerSingleton(DI_TOKENS.Logger, ConsoleLogger); + const logger = container.resolve(DI_TOKENS.Logger); // Clear any existing registrations container.clearInstances(); @@ -219,19 +247,29 @@ export function configureDIContainer(): void { // Register repositories container.registerInstance( DI_TOKENS.DriverRepository, - new InMemoryDriverRepository(seedData.drivers) + new InMemoryDriverRepository(logger, seedData.drivers) + ); + + container.registerInstance( + DI_TOKENS.PageViewRepository, + new InMemoryPageViewRepository(logger) + ); + + container.registerInstance( + DI_TOKENS.EngagementRepository, + new InMemoryEngagementRepository(logger) ); container.registerInstance( DI_TOKENS.LeagueRepository, - new InMemoryLeagueRepository(seedData.leagues) + new InMemoryLeagueRepository(logger, seedData.leagues) ); - const raceRepository = new InMemoryRaceRepository(seedData.races); + const raceRepository = new InMemoryRaceRepository(logger, seedData.races); container.registerInstance(DI_TOKENS.RaceRepository, raceRepository); // Result repository needs race repository for league-based queries - const resultRepository = new InMemoryResultRepository(seedData.results, raceRepository); + const resultRepository = new InMemoryResultRepository(logger, seedData.results, raceRepository); container.registerInstance(DI_TOKENS.ResultRepository, resultRepository); // Standing repository needs all three for recalculation @@ -239,6 +277,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.StandingRepository, new InMemoryStandingRepository( + logger, seedData.standings, resultRepository, raceRepository, @@ -293,7 +332,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.RaceRegistrationRepository, - new InMemoryRaceRegistrationRepository(seedRaceRegistrations) + new InMemoryRaceRegistrationRepository(logger, seedRaceRegistrations) ); // Seed penalties and protests @@ -449,12 +488,12 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.PenaltyRepository, - new InMemoryPenaltyRepository(seededPenalties) + new InMemoryPenaltyRepository(logger, seededPenalties) ); container.registerInstance( DI_TOKENS.ProtestRepository, - new InMemoryProtestRepository(seededProtests) + new InMemoryProtestRepository(logger, seededProtests) ); // Scoring repositories @@ -495,17 +534,17 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.GameRepository, - new InMemoryGameRepository([game]) + new InMemoryGameRepository(logger, [game]) ); container.registerInstance( DI_TOKENS.SeasonRepository, - new InMemorySeasonRepository(seededSeasons) + new InMemorySeasonRepository(logger, seededSeasons) ); container.registerInstance( DI_TOKENS.LeagueScoringConfigRepository, - new InMemoryLeagueScoringConfigRepository(seededScoringConfigs) + new InMemoryLeagueScoringConfigRepository(logger, seededScoringConfigs) ); // League memberships @@ -702,6 +741,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.LeagueMembershipRepository, new InMemoryLeagueMembershipRepository( + logger, seededMemberships as InMemoryLeagueMembershipSeed, seededJoinRequests, ) @@ -713,6 +753,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.TeamRepository, new InMemoryTeamRepository( + logger, seedData.teams.map((t) => ({ id: t.id, name: t.name, @@ -728,6 +769,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.TeamMembershipRepository, new InMemoryTeamMembershipRepository( + logger, seedData.memberships .filter((m) => m.teamId) .map((m) => ({ @@ -743,12 +785,12 @@ export function configureDIContainer(): void { // Track and Car repositories container.registerInstance( DI_TOKENS.TrackRepository, - new InMemoryTrackRepository(DEMO_TRACKS) + new InMemoryTrackRepository(logger, DEMO_TRACKS) ); container.registerInstance( DI_TOKENS.CarRepository, - new InMemoryCarRepository(DEMO_CARS) + new InMemoryCarRepository(logger, DEMO_CARS) ); // Sponsor repositories - seed with demo sponsors @@ -762,7 +804,7 @@ export function configureDIContainer(): void { }) ); - const sponsorRepo = new InMemorySponsorRepository(); + const sponsorRepo = new InMemorySponsorRepository(logger); sponsorRepo.seed(seededSponsors); container.registerInstance( DI_TOKENS.SponsorRepository, @@ -781,7 +823,7 @@ export function configureDIContainer(): void { }), ); - const seasonSponsorshipRepo = new InMemorySeasonSponsorshipRepository(); + const seasonSponsorshipRepo = new InMemorySeasonSponsorshipRepository(logger); seasonSponsorshipRepo.seed(seededSponsorships); container.registerInstance( DI_TOKENS.SeasonSponsorshipRepository, @@ -789,13 +831,13 @@ export function configureDIContainer(): void { ); // Sponsorship Request and Pricing repositories - const sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(); + const sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(logger); container.registerInstance( DI_TOKENS.SponsorshipRequestRepository, sponsorshipRequestRepo ); - const sponsorshipPricingRepo = new InMemorySponsorshipPricingRepository(); + const sponsorshipPricingRepo = new InMemorySponsorshipPricingRepository(logger); // Seed sponsorship pricings from demo data using domain SponsorshipPricing sponsorshipPricingRepo.seed(seedData.sponsorshipPricings ?? []); container.registerInstance( @@ -811,12 +853,12 @@ export function configureDIContainer(): void { // Social repositories container.registerInstance( DI_TOKENS.FeedRepository, - new InMemoryFeedRepository(seedData) + new InMemoryFeedRepository(logger, seedData) ); container.registerInstance( DI_TOKENS.SocialRepository, - new InMemorySocialGraphRepository(seedData) + new InMemorySocialGraphRepository(logger, seedData) ); // Image service @@ -828,12 +870,12 @@ export function configureDIContainer(): void { // Notification repositories container.registerInstance( DI_TOKENS.NotificationRepository, - new InMemoryNotificationRepository() + new InMemoryNotificationRepository(logger) ); container.registerInstance( DI_TOKENS.NotificationPreferenceRepository, - new InMemoryNotificationPreferenceRepository() + new InMemoryNotificationPreferenceRepository(logger) ); const notificationGatewayRegistry = new NotificationGatewayRegistry([ @@ -889,26 +931,39 @@ export function configureDIContainer(): void { const notificationPreferenceRepository = container.resolve(DI_TOKENS.NotificationPreferenceRepository); const feedRepository = container.resolve(DI_TOKENS.FeedRepository); const socialRepository = container.resolve(DI_TOKENS.SocialRepository); + const pageViewRepository = container.resolve(DI_TOKENS.PageViewRepository); + const engagementRepository = container.resolve(DI_TOKENS.EngagementRepository); + const logger = container.resolve(DI_TOKENS.Logger); // Register use cases - Racing container.registerInstance( DI_TOKENS.JoinLeagueUseCase, - new JoinLeagueUseCase(leagueMembershipRepository) + new JoinLeagueUseCase(leagueMembershipRepository, logger) + ); + + container.registerInstance( + DI_TOKENS.RecordPageViewUseCase, + new RecordPageViewUseCase(pageViewRepository, logger) + ); + + container.registerInstance( + DI_TOKENS.RecordEngagementUseCase, + new RecordEngagementUseCase(engagementRepository, logger) ); container.registerInstance( DI_TOKENS.RegisterForRaceUseCase, - new RegisterForRaceUseCase(raceRegistrationRepository, leagueMembershipRepository) + new RegisterForRaceUseCase(raceRegistrationRepository, leagueMembershipRepository, logger) ); container.registerInstance( DI_TOKENS.WithdrawFromRaceUseCase, - new WithdrawFromRaceUseCase(raceRegistrationRepository) + new WithdrawFromRaceUseCase(raceRegistrationRepository, logger) ); container.registerInstance( DI_TOKENS.CancelRaceUseCase, - new CancelRaceUseCase(raceRepository) + new CancelRaceUseCase(raceRepository, logger) ); container.registerInstance( @@ -918,7 +973,8 @@ export function configureDIContainer(): void { raceRegistrationRepository, resultRepository, standingRepository, - driverRatingProvider + driverRatingProvider, + logger ) ); @@ -928,7 +984,8 @@ export function configureDIContainer(): void { leagueRepository, seasonRepository, leagueScoringConfigRepository, - leagueScoringPresetProvider + leagueScoringPresetProvider, + logger ) ); @@ -945,7 +1002,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.JoinTeamUseCase, - new JoinTeamUseCase(teamRepository, teamMembershipRepository) + new JoinTeamUseCase(teamRepository, teamMembershipRepository, logger) ); container.registerInstance( @@ -955,7 +1012,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.ApproveTeamJoinRequestUseCase, - new ApproveTeamJoinRequestUseCase(teamMembershipRepository) + new ApproveTeamJoinRequestUseCase(teamMembershipRepository, logger) ); container.registerInstance( @@ -985,7 +1042,8 @@ export function configureDIContainer(): void { penaltyRepository, protestRepository, raceRepository, - leagueMembershipRepository + leagueMembershipRepository, + logger ) ); @@ -994,7 +1052,8 @@ export function configureDIContainer(): void { new QuickPenaltyUseCase( penaltyRepository, raceRepository, - leagueMembershipRepository + leagueMembershipRepository, + logger ) ); @@ -1014,13 +1073,14 @@ export function configureDIContainer(): void { new SendNotificationUseCase( notificationRepository, notificationPreferenceRepository, - notificationGatewayRegistry + notificationGatewayRegistry, + logger ) ); container.registerInstance( DI_TOKENS.MarkNotificationReadUseCase, - new MarkNotificationReadUseCase(notificationRepository) + new MarkNotificationReadUseCase(notificationRepository, logger) ); // Register queries - Racing @@ -1144,7 +1204,8 @@ export function configureDIContainer(): void { raceRepository, resultRepository, driverRatingProvider, - leagueStatsPresenter + leagueStatsPresenter, + logger ) ); @@ -1155,7 +1216,7 @@ export function configureDIContainer(): void { container.registerInstance( DI_TOKENS.GetAllRacesPageDataUseCase, - new GetAllRacesPageDataUseCase(raceRepository, leagueRepository) + new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, logger) ); const imageService = container.resolve(DI_TOKENS.ImageService); @@ -1194,7 +1255,8 @@ export function configureDIContainer(): void { resultRepository, driverRepository, standingRepository, - importRaceResultsPresenter + importRaceResultsPresenter, + logger ) ); @@ -1324,7 +1386,7 @@ export function configureDIContainer(): void { const allTeamsPresenter = new AllTeamsPresenter(); container.registerInstance( DI_TOKENS.GetAllTeamsUseCase, - new GetAllTeamsUseCase(teamRepository, teamMembershipRepository), + new GetAllTeamsUseCase(teamRepository, teamMembershipRepository, logger), ); container.registerInstance( @@ -1335,7 +1397,7 @@ export function configureDIContainer(): void { const teamMembersPresenter = new TeamMembersPresenter(); container.registerInstance( DI_TOKENS.GetTeamMembersUseCase, - new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter), + new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, logger, teamMembersPresenter), ); const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter(); @@ -1345,14 +1407,15 @@ export function configureDIContainer(): void { teamMembershipRepository, driverRepository, imageService, - teamJoinRequestsPresenter, + logger, + teamJoinRequestsPresenter ), ); const driverTeamPresenter = new DriverTeamPresenter(); container.registerInstance( DI_TOKENS.GetDriverTeamUseCase, - new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, driverTeamPresenter) + new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, logger, driverTeamPresenter) ); // Register queries - Stewarding @@ -1371,7 +1434,7 @@ export function configureDIContainer(): void { // Register queries - Notifications container.registerInstance( DI_TOKENS.GetUnreadNotificationsUseCase, - new GetUnreadNotificationsUseCase(notificationRepository) + new GetUnreadNotificationsUseCase(notificationRepository, logger) ); // Register use cases - Sponsors @@ -1421,7 +1484,8 @@ export function configureDIContainer(): void { sponsorshipPricingRepository, sponsorshipRequestRepository, seasonSponsorshipRepository, - entitySponsorshipPricingPresenter + entitySponsorshipPricingPresenter, + logger ) ); @@ -1430,7 +1494,8 @@ export function configureDIContainer(): void { new ApplyForSponsorshipUseCase( sponsorshipRequestRepository, sponsorshipPricingRepository, - sponsorRepository + sponsorRepository, + logger ) ); @@ -1440,6 +1505,7 @@ export function configureDIContainer(): void { sponsorshipRequestRepository, seasonSponsorshipRepository, seasonRepository, + logger ) ); diff --git a/apps/website/lib/di-tokens.ts b/apps/website/lib/di-tokens.ts index ddcb136af..4f124f179 100644 --- a/apps/website/lib/di-tokens.ts +++ b/apps/website/lib/di-tokens.ts @@ -28,6 +28,17 @@ export const DI_TOKENS = { 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'), @@ -36,6 +47,12 @@ export const DI_TOKENS = { // Services ImageService: Symbol.for('ImageServicePort'), NotificationGatewayRegistry: Symbol.for('NotificationGatewayRegistry'), + Logger: Symbol.for('ILogger'), + AuthService: Symbol.for('AuthService'), + + // Use Cases - Analytics + RecordPageViewUseCase: Symbol.for('RecordPageViewUseCase'), + RecordEngagementUseCase: Symbol.for('RecordEngagementUseCase'), // Use Cases - Racing JoinLeagueUseCase: Symbol.for('JoinLeagueUseCase'), diff --git a/packages/analytics/application/use-cases/GetEntityAnalyticsQuery.ts b/packages/analytics/application/use-cases/GetEntityAnalyticsQuery.ts index cda25cccd..a23f22775 100644 --- a/packages/analytics/application/use-cases/GetEntityAnalyticsQuery.ts +++ b/packages/analytics/application/use-cases/GetEntityAnalyticsQuery.ts @@ -6,6 +6,7 @@ */ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '@gridpilot/shared/application'; import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository'; @@ -47,56 +48,108 @@ export class GetEntityAnalyticsQuery constructor( private readonly pageViewRepository: IPageViewRepository, private readonly engagementRepository: IEngagementRepository, - private readonly snapshotRepository: IAnalyticsSnapshotRepository + private readonly snapshotRepository: IAnalyticsSnapshotRepository, + private readonly logger: ILogger ) {} async execute(input: GetEntityAnalyticsInput): Promise { + this.logger.debug(`Executing GetEntityAnalyticsQuery with input: ${JSON.stringify(input)}`); const period = input.period ?? 'weekly'; const now = new Date(); const since = input.since ?? this.getPeriodStartDate(now, period); + this.logger.debug(`Calculated period: ${period}, now: ${now.toISOString()}, since: ${since.toISOString()}`); // Get current metrics - const totalPageViews = await this.pageViewRepository.countByEntityId( - input.entityType, - input.entityId, - since - ); + let totalPageViews = 0; + try { + totalPageViews = await this.pageViewRepository.countByEntityId( + input.entityType, + input.entityId, + since + ); + this.logger.debug(`Total page views for entity ${input.entityId}: ${totalPageViews}`); + } catch (error) { + this.logger.error(`Error counting total page views for entity ${input.entityId}: ${error.message}`); + throw error; + } - const uniqueVisitors = await this.pageViewRepository.countUniqueVisitors( - input.entityType, - input.entityId, - since - ); + let uniqueVisitors = 0; + try { + uniqueVisitors = await this.pageViewRepository.countUniqueVisitors( + input.entityType, + input.entityId, + since + ); + this.logger.debug(`Unique visitors for entity ${input.entityId}: ${uniqueVisitors}`); + } catch (error) { + this.logger.error(`Error counting unique visitors for entity ${input.entityId}: ${error.message}`); + throw error; + } - const sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity( - input.entityId, - since - ); + let sponsorClicks = 0; + try { + sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity( + input.entityId, + since + ); + this.logger.debug(`Sponsor clicks for entity ${input.entityId}: ${sponsorClicks}`); + } catch (error) { + this.logger.error(`Error getting sponsor clicks for entity ${input.entityId}: ${error.message}`); + throw error; + } // Calculate engagement score (weighted sum of actions) - const engagementScore = await this.calculateEngagementScore(input.entityId, since); - + let engagementScore = 0; + try { + engagementScore = await this.calculateEngagementScore(input.entityId, since); + this.logger.debug(`Engagement score for entity ${input.entityId}: ${engagementScore}`); + } catch (error) { + this.logger.error(`Error calculating engagement score for entity ${input.entityId}: ${error.message}`); + throw error; + } + // Determine trust indicator const trustIndicator = this.determineTrustIndicator(totalPageViews, uniqueVisitors, engagementScore); + this.logger.debug(`Trust indicator for entity ${input.entityId}: ${trustIndicator}`); // Calculate exposure value (for sponsor ROI) const exposureValue = this.calculateExposureValue(totalPageViews, uniqueVisitors, sponsorClicks); + this.logger.debug(`Exposure value for entity ${input.entityId}: ${exposureValue}`); // Get previous period for trends const previousPeriodStart = this.getPreviousPeriodStart(since, period); - const previousPageViews = await this.pageViewRepository.countByEntityId( - input.entityType, - input.entityId, - previousPeriodStart - ) - totalPageViews; + this.logger.debug(`Previous period start: ${previousPeriodStart.toISOString()}`); - const previousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors( - input.entityType, - input.entityId, - previousPeriodStart - ) - uniqueVisitors; + let previousPageViews = 0; + try { + const fullPreviousPageViews = await this.pageViewRepository.countByEntityId( + input.entityType, + input.entityId, + previousPeriodStart + ); + previousPageViews = fullPreviousPageViews - totalPageViews; // This calculates change, not just previous period's total + this.logger.debug(`Previous period full page views: ${fullPreviousPageViews}, change: ${previousPageViews}`); + } catch (error) { + this.logger.error(`Error counting previous period page views for entity ${input.entityId}: ${error.message}`); + throw error; + } - return { + let previousUniqueVisitors = 0; + try { + const fullPreviousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors( + input.entityType, + input.entityId, + previousPeriodStart + ); + previousUniqueVisitors = fullPreviousUniqueVisitors - uniqueVisitors; // This calculates change, not just previous period's total + this.logger.debug(`Previous period full unique visitors: ${fullPreviousUniqueVisitors}, change: ${previousUniqueVisitors}`); + + } catch (error) { + this.logger.error(`Error counting previous period unique visitors for entity ${input.entityId}: ${error.message}`); + throw error; + } + + const result: EntityAnalyticsOutput = { entityType: input.entityType, entityId: input.entityId, summary: { @@ -118,9 +171,12 @@ export class GetEntityAnalyticsQuery label: this.formatPeriodLabel(since, now), }, }; + this.logger.info(`Successfully retrieved analytics for entity ${input.entityId}.`); + return result; } private getPeriodStartDate(now: Date, period: SnapshotPeriod): Date { + this.logger.debug(`Calculating period start date for "${period}" from ${now.toISOString()}`); const start = new Date(now); switch (period) { case 'daily': @@ -133,10 +189,12 @@ export class GetEntityAnalyticsQuery start.setMonth(start.getMonth() - 1); break; } + this.logger.debug(`Period start date calculated: ${start.toISOString()}`); return start; } private getPreviousPeriodStart(currentStart: Date, period: SnapshotPeriod): Date { + this.logger.debug(`Calculating previous period start date for "${period}" from ${currentStart.toISOString()}`); const start = new Date(currentStart); switch (period) { case 'daily': @@ -149,13 +207,23 @@ export class GetEntityAnalyticsQuery start.setMonth(start.getMonth() - 1); break; } + this.logger.debug(`Previous period start date calculated: ${start.toISOString()}`); return start; } private async calculateEngagementScore(entityId: string, since: Date): Promise { - // Base engagement from sponsor interactions - const sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(entityId, since); - return sponsorClicks * 10; // Weighted score + this.logger.debug(`Calculating engagement score for entity ${entityId} since ${since.toISOString()}`); + let sponsorClicks = 0; + try { + sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(entityId, since); + this.logger.debug(`Sponsor clicks for engagement score for entity ${entityId}: ${sponsorClicks}`); + } catch (error) { + this.logger.error(`Error getting sponsor clicks for engagement score for entity ${entityId}: ${error.message}`); + throw error; + } + const score = sponsorClicks * 10; // Weighted score + this.logger.debug(`Calculated engagement score for entity ${entityId}: ${score}`); + return score; } private determineTrustIndicator( @@ -163,8 +231,10 @@ export class GetEntityAnalyticsQuery uniqueVisitors: number, engagementScore: number ): 'high' | 'medium' | 'low' { + this.logger.debug(`Determining trust indicator with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, engagementScore: ${engagementScore}`); const engagementRate = pageViews > 0 ? engagementScore / pageViews : 0; const returningVisitorRate = pageViews > 0 ? (pageViews - uniqueVisitors) / pageViews : 0; + this.logger.debug(`Engagement rate: ${engagementRate}, Returning visitor rate: ${returningVisitorRate}`); if (engagementRate > 0.1 && returningVisitorRate > 0.3) { return 'high'; @@ -180,20 +250,33 @@ export class GetEntityAnalyticsQuery uniqueVisitors: number, sponsorClicks: number ): number { + this.logger.debug(`Calculating exposure value with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, sponsorClicks: ${sponsorClicks}`); // Simple exposure value calculation (could be monetized) - return (pageViews * 0.01) + (uniqueVisitors * 0.05) + (sponsorClicks * 0.50); + const exposure = (pageViews * 0.01) + (uniqueVisitors * 0.05) + (sponsorClicks * 0.50); + this.logger.debug(`Calculated exposure value: ${exposure}`); + return exposure; } private calculatePercentageChange(previous: number, current: number): number { - if (previous === 0) return current > 0 ? 100 : 0; - return Math.round(((current - previous) / previous) * 100); + this.logger.debug(`Calculating percentage change from previous: ${previous} to current: ${current}`); + if (previous === 0) { + const change = current > 0 ? 100 : 0; + this.logger.debug(`Percentage change (previous was 0): ${change}%`); + return change; + } + const change = Math.round(((current - previous) / previous) * 100); + this.logger.debug(`Percentage change: ${change}%`); + return change; } private formatPeriodLabel(start: Date, end: Date): string { + this.logger.debug(`Formatting period label from ${start.toISOString()} to ${end.toISOString()}`); const formatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', }); - return `${formatter.format(start)} - ${formatter.format(end)}`; + const label = `${formatter.format(start)} - ${formatter.format(end)}`; + this.logger.debug(`Formatted period label: "${label}"`); + return label; } } \ No newline at end of file diff --git a/packages/analytics/application/use-cases/RecordEngagementUseCase.ts b/packages/analytics/application/use-cases/RecordEngagementUseCase.ts index 5f0462a5f..127e1b6e9 100644 --- a/packages/analytics/application/use-cases/RecordEngagementUseCase.ts +++ b/packages/analytics/application/use-cases/RecordEngagementUseCase.ts @@ -5,6 +5,7 @@ */ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; @@ -25,31 +26,41 @@ export interface RecordEngagementOutput { export class RecordEngagementUseCase implements AsyncUseCase { - constructor(private readonly engagementRepository: IEngagementRepository) {} + constructor( + private readonly engagementRepository: IEngagementRepository, + private readonly logger: ILogger, + ) {} async execute(input: RecordEngagementInput): Promise { - const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + this.logger.debug('Executing RecordEngagementUseCase', { input }); + try { + const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const baseProps: Omit[0], 'timestamp'> = { - id: eventId, - action: input.action, - entityType: input.entityType, - entityId: input.entityId, - actorType: input.actorType, - sessionId: input.sessionId, - }; + const baseProps: Omit[0], 'timestamp'> = { + id: eventId, + action: input.action, + entityType: input.entityType, + 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 } : {}), - }); + const event = EngagementEvent.create({ + ...baseProps, + ...(input.actorId !== undefined ? { actorId: input.actorId } : {}), + ...(input.metadata !== undefined ? { metadata: input.metadata } : {}), + }); - await this.engagementRepository.save(event); + await this.engagementRepository.save(event); + this.logger.info('Engagement recorded successfully', { eventId, input }); - return { - eventId, - engagementWeight: event.getEngagementWeight(), - }; + return { + eventId, + engagementWeight: event.getEngagementWeight(), + }; + } catch (error) { + this.logger.error('Error recording engagement', error, { input }); + throw error; + } } -} \ No newline at end of file +} diff --git a/packages/analytics/application/use-cases/RecordPageViewUseCase.ts b/packages/analytics/application/use-cases/RecordPageViewUseCase.ts index 886452d03..26167af40 100644 --- a/packages/analytics/application/use-cases/RecordPageViewUseCase.ts +++ b/packages/analytics/application/use-cases/RecordPageViewUseCase.ts @@ -5,6 +5,7 @@ */ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; import { PageView } from '../../domain/entities/PageView'; import type { EntityType, VisitorType } from '../../domain/types/PageView'; import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; @@ -26,29 +27,38 @@ export interface RecordPageViewOutput { export class RecordPageViewUseCase implements AsyncUseCase { - constructor(private readonly pageViewRepository: IPageViewRepository) {} + constructor( + private readonly pageViewRepository: IPageViewRepository, + private readonly logger: ILogger, + ) {} async execute(input: RecordPageViewInput): Promise { - const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + this.logger.debug('Executing RecordPageViewUseCase', { input }); + try { + const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const baseProps: Omit[0], 'timestamp'> = { - id: pageViewId, - entityType: input.entityType, - entityId: input.entityId, - visitorType: input.visitorType, - sessionId: input.sessionId, - }; + const baseProps: Omit[0], 'timestamp'> = { + id: pageViewId, + entityType: input.entityType, + entityId: input.entityId, + visitorType: input.visitorType, + 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 } : {}), - }); + 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); - - return { pageViewId }; + 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; + } } -} \ No newline at end of file +} diff --git a/packages/analytics/domain/ports/ILogger.ts b/packages/analytics/domain/ports/ILogger.ts new file mode 100644 index 000000000..419726ade --- /dev/null +++ b/packages/analytics/domain/ports/ILogger.ts @@ -0,0 +1,6 @@ +export interface ILogger { + debug(message: string, ...args: any[]): void; + info(message: string, ...args: any[]): void; + warn(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/packages/analytics/infrastructure/repositories/InMemoryAnalyticsSnapshotRepository.ts b/packages/analytics/infrastructure/repositories/InMemoryAnalyticsSnapshotRepository.ts index 1fd292670..7ea670cc8 100644 --- a/packages/analytics/infrastructure/repositories/InMemoryAnalyticsSnapshotRepository.ts +++ b/packages/analytics/infrastructure/repositories/InMemoryAnalyticsSnapshotRepository.ts @@ -6,22 +6,56 @@ import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository'; import { AnalyticsSnapshot, type SnapshotPeriod, type SnapshotEntityType } from '../../domain/entities/AnalyticsSnapshot'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRepository { private snapshots: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + this.logger.info('InMemoryAnalyticsSnapshotRepository initialized.'); + } async save(snapshot: AnalyticsSnapshot): Promise { - this.snapshots.set(snapshot.id, snapshot); + this.logger.debug(`Saving AnalyticsSnapshot: ${snapshot.id}`); + try { + this.snapshots.set(snapshot.id, snapshot); + this.logger.info(`AnalyticsSnapshot ${snapshot.id} saved successfully.`); + } catch (error) { + this.logger.error(`Error saving AnalyticsSnapshot ${snapshot.id}:`, error); + throw error; + } } async findById(id: string): Promise { - return this.snapshots.get(id) ?? null; + this.logger.debug(`Finding AnalyticsSnapshot by ID: ${id}`); + try { + const snapshot = this.snapshots.get(id) ?? null; + if (snapshot) { + this.logger.info(`Found AnalyticsSnapshot with ID: ${id}`); + } else { + this.logger.warn(`AnalyticsSnapshot with ID ${id} not found.`); + } + return snapshot; + } catch (error) { + this.logger.error(`Error finding AnalyticsSnapshot by ID ${id}:`, error); + throw error; + } } async findByEntity(entityType: SnapshotEntityType, entityId: string): Promise { - return Array.from(this.snapshots.values()).filter( - s => s.entityType === entityType && s.entityId === entityId - ); + this.logger.debug(`Finding AnalyticsSnapshots by Entity: ${entityType}, ${entityId}`); + try { + const snapshots = Array.from(this.snapshots.values()).filter( + s => s.entityType === entityType && s.entityId === entityId + ); + this.logger.info(`Found ${snapshots.length} AnalyticsSnapshots for entity ${entityId}.`); + return snapshots; + } catch (error) { + this.logger.error(`Error finding AnalyticsSnapshots for entity ${entityId}:`, error); + throw error; + } } async findByPeriod( @@ -31,13 +65,25 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe startDate: Date, endDate: Date ): Promise { - return Array.from(this.snapshots.values()).find( - s => s.entityType === entityType && - s.entityId === entityId && - s.period === period && - s.startDate >= startDate && - s.endDate <= endDate - ) ?? null; + this.logger.debug(`Finding AnalyticsSnapshot by Period for entity ${entityId}, period ${period}, from ${startDate.toISOString()} to ${endDate.toISOString()}`); + try { + const snapshot = Array.from(this.snapshots.values()).find( + s => s.entityType === entityType && + s.entityId === entityId && + s.period === period && + s.startDate >= startDate && + s.endDate <= endDate + ) ?? null; + if (snapshot) { + this.logger.info(`Found AnalyticsSnapshot for entity ${entityId}, period ${period}.`); + } else { + this.logger.warn(`No AnalyticsSnapshot found for entity ${entityId}, period ${period}, from ${startDate.toISOString()} to ${endDate.toISOString()}.`); + } + return snapshot; + } catch (error) { + this.logger.error(`Error finding AnalyticsSnapshot by period for entity ${entityId}:`, error); + throw error; + } } async findLatest( @@ -45,11 +91,23 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe entityId: string, period: SnapshotPeriod ): Promise { - const matching = Array.from(this.snapshots.values()) - .filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period) - .sort((a, b) => b.endDate.getTime() - a.endDate.getTime()); - - return matching[0] ?? null; + this.logger.debug(`Finding latest AnalyticsSnapshot for entity ${entityId}, period ${period}`); + try { + const matching = Array.from(this.snapshots.values()) + .filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period) + .sort((a, b) => b.endDate.getTime() - a.endDate.getTime()); + + const snapshot = matching[0] ?? null; + if (snapshot) { + this.logger.info(`Found latest AnalyticsSnapshot for entity ${entityId}, period ${period}.`); + } else { + this.logger.warn(`No latest AnalyticsSnapshot found for entity ${entityId}, period ${period}.`); + } + return snapshot; + } catch (error) { + this.logger.error(`Error finding latest AnalyticsSnapshot for entity ${entityId}:`, error); + throw error; + } } async getHistoricalSnapshots( @@ -58,10 +116,18 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe period: SnapshotPeriod, limit: number ): Promise { - return Array.from(this.snapshots.values()) - .filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period) - .sort((a, b) => b.endDate.getTime() - a.endDate.getTime()) - .slice(0, limit); + this.logger.debug(`Getting historical AnalyticsSnapshots for entity ${entityId}, period ${period}, limit ${limit}`); + try { + const snapshots = Array.from(this.snapshots.values()) + .filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period) + .sort((a, b) => b.endDate.getTime() - a.endDate.getTime()) + .slice(0, limit); + this.logger.info(`Found ${snapshots.length} historical AnalyticsSnapshots for entity ${entityId}, period ${period}.`); + return snapshots; + } catch (error) { + this.logger.error(`Error getting historical AnalyticsSnapshots for entity ${entityId}:`, error); + throw error; + } } // Helper for testing diff --git a/packages/analytics/infrastructure/repositories/InMemoryEngagementRepository.ts b/packages/analytics/infrastructure/repositories/InMemoryEngagementRepository.ts index 50654a9fe..f9aec315f 100644 --- a/packages/analytics/infrastructure/repositories/InMemoryEngagementRepository.ts +++ b/packages/analytics/infrastructure/repositories/InMemoryEngagementRepository.ts @@ -6,59 +6,135 @@ import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent'; +import { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryEngagementRepository implements IEngagementRepository { private events: Map = new Map(); + private logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + this.logger.info('InMemoryEngagementRepository initialized.'); + } async save(event: EngagementEvent): Promise { - this.events.set(event.id, event); + this.logger.debug(`Attempting to save engagement event: ${event.id}`); + try { + this.events.set(event.id, event); + this.logger.info(`Successfully saved engagement event: ${event.id}`); + } catch (error) { + this.logger.error(`Error saving engagement event ${event.id}:`, error); + throw error; + } } async findById(id: string): Promise { - return this.events.get(id) ?? null; + this.logger.debug(`Attempting to find engagement event by ID: ${id}`); + try { + const event = this.events.get(id) ?? null; + if (event) { + this.logger.info(`Found engagement event by ID: ${id}`); + } else { + this.logger.warn(`Engagement event not found for ID: ${id}`); + // The original was info, but if a requested ID is not found that's more of a warning than an info. + } + return event; + } catch (error) { + this.logger.error(`Error finding engagement event by ID ${id}:`, error); + throw error; + } } async findByEntityId(entityType: EngagementEntityType, entityId: string): Promise { - return Array.from(this.events.values()).filter( - e => e.entityType === entityType && e.entityId === entityId - ); + this.logger.debug(`Attempting to find engagement events for entityType: ${entityType}, entityId: ${entityId}`); + try { + const events = Array.from(this.events.values()).filter( + e => e.entityType === entityType && e.entityId === entityId + ); + this.logger.info(`Found ${events.length} engagement events for entityType: ${entityType}, entityId: ${entityId}`); + return events; + } catch (error) { + this.logger.error(`Error finding engagement events by entity ID ${entityId}:`, error); + throw error; + } } async findByAction(action: EngagementAction): Promise { - return Array.from(this.events.values()).filter( - e => e.action === action - ); + this.logger.debug(`Attempting to find engagement events by action: ${action}`); + try { + const events = Array.from(this.events.values()).filter( + e => e.action === action + ); + this.logger.info(`Found ${events.length} engagement events for action: ${action}`); + return events; + } catch (error) { + this.logger.error(`Error finding engagement events by action ${action}:`, error); + throw error; + } } async findByDateRange(startDate: Date, endDate: Date): Promise { - return Array.from(this.events.values()).filter( - e => e.timestamp >= startDate && e.timestamp <= endDate - ); + this.logger.debug(`Attempting to find engagement events by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`); + try { + const events = Array.from(this.events.values()).filter( + e => e.timestamp >= startDate && e.timestamp <= endDate + ); + this.logger.info(`Found ${events.length} engagement events for date range.`); + return events; + } catch (error) { + this.logger.error(`Error finding engagement events by date range:`, error); + throw error; + } } async countByAction(action: EngagementAction, entityId?: string, since?: Date): Promise { - return Array.from(this.events.values()).filter( - e => e.action === action && - (!entityId || e.entityId === entityId) && - (!since || e.timestamp >= since) - ).length; + this.logger.debug(`Attempting to count engagement events for action: ${action}, entityId: ${entityId}, since: ${since?.toISOString()}`); + try { + const count = Array.from(this.events.values()).filter( + e => e.action === action && + (!entityId || e.entityId === entityId) && + (!since || e.timestamp >= since) + ).length; + this.logger.info(`Counted ${count} engagement events for action: ${action}, entityId: ${entityId}`); + return count; + } catch (error) { + this.logger.error(`Error counting engagement events by action ${action}:`, error); + throw error; + } } async getSponsorClicksForEntity(entityId: string, since?: Date): Promise { - return Array.from(this.events.values()).filter( - e => e.entityId === entityId && - (e.action === 'click_sponsor_logo' || e.action === 'click_sponsor_url') && - (!since || e.timestamp >= since) - ).length; + this.logger.debug(`Attempting to get sponsor clicks for entity ID: ${entityId}, since: ${since?.toISOString()}`); + try { + const count = Array.from(this.events.values()).filter( + e => e.entityId === entityId && + (e.action === 'click_sponsor_logo' || e.action === 'click_sponsor_url') && + (!since || e.timestamp >= since) + ).length; + this.logger.info(`Counted ${count} sponsor clicks for entity ID: ${entityId}`); + return count; + } catch (error) { + this.logger.error(`Error getting sponsor clicks for entity ID ${entityId}:`, error); + throw error; + } } // Helper for testing clear(): void { + this.logger.debug('Clearing all engagement events.'); this.events.clear(); + this.logger.info('All engagement events cleared.'); } // Helper for seeding demo data seed(events: EngagementEvent[]): void { - events.forEach(e => this.events.set(e.id, e)); + this.logger.debug(`Seeding ${events.length} engagement events.`); + try { + events.forEach(e => this.events.set(e.id, e)); + this.logger.info(`Successfully seeded ${events.length} engagement events.`); + } catch (error) { + this.logger.error(`Error seeding engagement events:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/analytics/infrastructure/repositories/InMemoryPageViewRepository.ts b/packages/analytics/infrastructure/repositories/InMemoryPageViewRepository.ts index 005354f98..a91daa777 100644 --- a/packages/analytics/infrastructure/repositories/InMemoryPageViewRepository.ts +++ b/packages/analytics/infrastructure/repositories/InMemoryPageViewRepository.ts @@ -6,65 +6,139 @@ import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; import { PageView, type EntityType } from '../../domain/entities/PageView'; +import { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryPageViewRepository implements IPageViewRepository { private pageViews: Map = new Map(); + private logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + this.logger.info('InMemoryPageViewRepository initialized.'); + } async save(pageView: PageView): Promise { - this.pageViews.set(pageView.id, pageView); + this.logger.debug(`Attempting to save page view: ${pageView.id}`); + try { + this.pageViews.set(pageView.id, pageView); + this.logger.info(`Successfully saved page view: ${pageView.id}`); + } catch (error) { + this.logger.error(`Error saving page view ${pageView.id}:`, error); + throw error; + } } async findById(id: string): Promise { - return this.pageViews.get(id) ?? null; + this.logger.debug(`Attempting to find page view by ID: ${id}`); + try { + const pageView = this.pageViews.get(id) ?? null; + if (pageView) { + this.logger.info(`Found page view by ID: ${id}`); + } else { + this.logger.warn(`Page view not found for ID: ${id}`); + } + return pageView; + } catch (error) { + this.logger.error(`Error finding page view by ID ${id}:`, error); + throw error; + } } async findByEntityId(entityType: EntityType, entityId: string): Promise { - return Array.from(this.pageViews.values()).filter( - pv => pv.entityType === entityType && pv.entityId === entityId - ); + this.logger.debug(`Attempting to find page views for entityType: ${entityType}, entityId: ${entityId}`); + try { + const pageViews = Array.from(this.pageViews.values()).filter( + pv => pv.entityType === entityType && pv.entityId === entityId + ); + this.logger.info(`Found ${pageViews.length} page views for entityType: ${entityType}, entityId: ${entityId}`); + return pageViews; + } catch (error) { + this.logger.error(`Error finding page views by entity ID ${entityId}:`, error); + throw error; + } } async findByDateRange(startDate: Date, endDate: Date): Promise { - return Array.from(this.pageViews.values()).filter( - pv => pv.timestamp >= startDate && pv.timestamp <= endDate - ); + this.logger.debug(`Attempting to find page views by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`); + try { + const pageViews = Array.from(this.pageViews.values()).filter( + pv => pv.timestamp >= startDate && pv.timestamp <= endDate + ); + this.logger.info(`Found ${pageViews.length} page views for date range.`); + return pageViews; + } catch (error) { + this.logger.error(`Error finding page views by date range:`, error); + throw error; + } } async findBySession(sessionId: string): Promise { - return Array.from(this.pageViews.values()).filter( - pv => pv.sessionId === sessionId - ); + this.logger.debug(`Attempting to find page views by session ID: ${sessionId}`); + try { + const pageViews = Array.from(this.pageViews.values()).filter( + pv => pv.sessionId === sessionId + ); + this.logger.info(`Found ${pageViews.length} page views for session ID: ${sessionId}`); + return pageViews; + } catch (error) { + this.logger.error(`Error finding page views by session ID ${sessionId}:`, error); + throw error; + } } async countByEntityId(entityType: EntityType, entityId: string, since?: Date): Promise { - return Array.from(this.pageViews.values()).filter( - pv => pv.entityType === entityType && - pv.entityId === entityId && - (!since || pv.timestamp >= since) - ).length; + this.logger.debug(`Attempting to count page views for entityType: ${entityType}, entityId: ${entityId}, since: ${since?.toISOString()}`); + try { + const count = Array.from(this.pageViews.values()).filter( + pv => pv.entityType === entityType && + pv.entityId === entityId && + (!since || pv.timestamp >= since) + ).length; + this.logger.info(`Counted ${count} page views for entityType: ${entityType}, entityId: ${entityId}`); + return count; + } catch (error) { + this.logger.error(`Error counting page views by entity ID ${entityId}:`, error); + throw error; + } } async countUniqueVisitors(entityType: EntityType, entityId: string, since?: Date): Promise { - const visitors = new Set(); - Array.from(this.pageViews.values()) - .filter( - pv => pv.entityType === entityType && - pv.entityId === entityId && - (!since || pv.timestamp >= since) - ) - .forEach(pv => { - visitors.add(pv.visitorId ?? pv.sessionId); - }); - return visitors.size; + this.logger.debug(`Attempting to count unique visitors for entityType: ${entityType}, entityId: ${entityId}, since: ${since?.toISOString()}`); + try { + const visitors = new Set(); + Array.from(this.pageViews.values()) + .filter( + pv => pv.entityType === entityType && + pv.entityId === entityId && + (!since || pv.timestamp >= since) + ) + .forEach(pv => { + visitors.add(pv.visitorId ?? pv.sessionId); + }); + this.logger.info(`Counted ${visitors.size} unique visitors for entityType: ${entityType}, entityId: ${entityId}`); + return visitors.size; + } catch (error) { + this.logger.error(`Error counting unique visitors for entity ID ${entityId}:`, error); + throw error; + } } // Helper for testing clear(): void { + this.logger.debug('Clearing all page views.'); this.pageViews.clear(); + this.logger.info('All page views cleared.'); } // Helper for seeding demo data seed(pageViews: PageView[]): void { - pageViews.forEach(pv => this.pageViews.set(pv.id, pv)); + this.logger.debug(`Seeding ${pageViews.length} page views.`); + try { + pageViews.forEach(pv => this.pageViews.set(pv.id, pv)); + this.logger.info(`Successfully seeded ${pageViews.length} page views.`); + } catch (error) { + this.logger.error(`Error seeding page views:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/automation/application/ports/ILogger.ts b/packages/automation/application/ports/ILogger.ts new file mode 100644 index 000000000..4d1243d09 --- /dev/null +++ b/packages/automation/application/ports/ILogger.ts @@ -0,0 +1,7 @@ +export interface ILogger { + debug(message: string, context?: Record): void; + info(message: string, context?: Record): void; + warn(message: string, context?: Record): void; + error(message: string, error?: Error, context?: Record): void; + verbose?(message: string, context?: Record): void; +} diff --git a/packages/automation/application/ports/LoggerPort.ts b/packages/automation/application/ports/LoggerPort.ts index cfaec3c56..bfe447fcf 100644 --- a/packages/automation/application/ports/LoggerPort.ts +++ b/packages/automation/application/ports/LoggerPort.ts @@ -1,10 +1,11 @@ import type { LogLevel } from './LoggerLogLevel'; import type { LogContext } from './LoggerContext'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; /** * LoggerPort - Port interface for application-layer logging. */ -export interface LoggerPort { +export interface LoggerPort extends ILogger { debug(message: string, context?: LogContext): void; info(message: string, context?: LogContext): void; warn(message: string, context?: LogContext): void; diff --git a/packages/automation/application/use-cases/CheckAuthenticationUseCase.ts b/packages/automation/application/use-cases/CheckAuthenticationUseCase.ts index 4bab26c5e..27af0d111 100644 --- a/packages/automation/application/use-cases/CheckAuthenticationUseCase.ts +++ b/packages/automation/application/use-cases/CheckAuthenticationUseCase.ts @@ -1,4 +1,5 @@ import { AuthenticationState } from '../../domain/value-objects/AuthenticationState'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import { Result } from '../../../shared/result/Result'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import { SessionLifetime } from '../../domain/value-objects/SessionLifetime'; @@ -16,6 +17,7 @@ import type { SessionValidatorPort } from '../ports/SessionValidatorPort'; */ export class CheckAuthenticationUseCase { constructor( + private readonly logger: ILogger, private readonly authService: AuthenticationServicePort, private readonly sessionValidator?: SessionValidatorPort ) {} @@ -30,63 +32,88 @@ export class CheckAuthenticationUseCase { requireServerValidation?: boolean; verifyPageContent?: boolean; }): Promise> { - // Step 1: File-based validation (fast) - const fileResult = await this.authService.checkSession(); - if (fileResult.isErr()) { - return fileResult; - } - - const fileState = fileResult.unwrap(); - - // Step 2: Check session expiry if authenticated - if (fileState === AuthenticationState.AUTHENTICATED) { - const expiryResult = await this.authService.getSessionExpiry(); - if (expiryResult.isErr()) { - // Don't fail completely if we can't get expiry, use file-based state - return Result.ok(fileState); + this.logger.debug('Executing CheckAuthenticationUseCase', { options }); + try { + // Step 1: File-based validation (fast) + this.logger.debug('Performing file-based authentication check.'); + const fileResult = await this.authService.checkSession(); + if (fileResult.isErr()) { + this.logger.error('File-based authentication check failed.', { error: fileResult.unwrapErr() }); + return fileResult; } + this.logger.info('File-based authentication check succeeded.'); - const expiry = expiryResult.unwrap(); - if (expiry !== null) { - try { - const sessionLifetime = new SessionLifetime(expiry); - if (sessionLifetime.isExpired()) { + const fileState = fileResult.unwrap(); + this.logger.debug(`File-based authentication state: ${fileState}`); + + // Step 2: Check session expiry if authenticated + if (fileState === AuthenticationState.AUTHENTICATED) { + this.logger.debug('Session is authenticated, checking expiry.'); + const expiryResult = await this.authService.getSessionExpiry(); + if (expiryResult.isErr()) { + this.logger.warn('Could not retrieve session expiry, proceeding with file-based state.', { error: expiryResult.unwrapErr() }); + // Don't fail completely if we can't get expiry, use file-based state + return Result.ok(fileState); + } + + const expiry = expiryResult.unwrap(); + if (expiry !== null) { + try { + const sessionLifetime = new SessionLifetime(expiry); + if (sessionLifetime.isExpired()) { + this.logger.info('Session has expired based on lifetime.'); + return Result.ok(AuthenticationState.EXPIRED); + } + this.logger.debug('Session is not expired.'); + } catch (error) { + this.logger.error('Invalid expiry date encountered, treating session as expired.', { expiry, error }); + // Invalid expiry date, treat as expired for safety return Result.ok(AuthenticationState.EXPIRED); } - } catch { - // Invalid expiry date, treat as expired for safety - return Result.ok(AuthenticationState.EXPIRED); } } - } - // Step 3: Optional page content verification - if (options?.verifyPageContent && fileState === AuthenticationState.AUTHENTICATED) { - const pageResult = await this.authService.verifyPageAuthentication(); - - if (pageResult.isOk()) { - const browserState = pageResult.unwrap(); - // If cookies valid but page shows login UI, session is expired - if (!browserState.isFullyAuthenticated()) { - return Result.ok(AuthenticationState.EXPIRED); + // Step 3: Optional page content verification + if (options?.verifyPageContent && fileState === AuthenticationState.AUTHENTICATED) { + this.logger.debug('Performing optional page content verification.'); + const pageResult = await this.authService.verifyPageAuthentication(); + + if (pageResult.isOk()) { + const browserState = pageResult.unwrap(); + // If cookies valid but page shows login UI, session is expired + if (!browserState.isFullyAuthenticated()) { + this.logger.info('Page content verification indicated session expired.'); + return Result.ok(AuthenticationState.EXPIRED); + } + this.logger.info('Page content verification succeeded.'); + } else { + this.logger.warn('Page content verification failed, proceeding with file-based state.', { error: pageResult.unwrapErr() }); + } + // Don't block on page verification errors, continue with file-based state + } + + // Step 4: Optional server-side validation + if (this.sessionValidator && fileState === AuthenticationState.AUTHENTICATED) { + this.logger.debug('Performing optional server-side validation.'); + const serverResult = await this.sessionValidator.validateSession(); + + // Don't block on server validation errors + if (serverResult.isOk()) { + const isValid = serverResult.unwrap(); + if (!isValid) { + this.logger.info('Server-side validation indicated session expired.'); + return Result.ok(AuthenticationState.EXPIRED); + } + this.logger.info('Server-side validation succeeded.'); + } else { + this.logger.warn('Server-side validation failed, proceeding with file-based state.', { error: serverResult.unwrapErr() }); } } - // Don't block on page verification errors, continue with file-based state + this.logger.info(`CheckAuthenticationUseCase completed successfully with state: ${fileState}`); + return Result.ok(fileState); + } catch (error) { + this.logger.error('An unexpected error occurred during authentication check.', { error }); + throw error; } - - // Step 4: Optional server-side validation - if (this.sessionValidator && fileState === AuthenticationState.AUTHENTICATED) { - const serverResult = await this.sessionValidator.validateSession(); - - // Don't block on server validation errors - if (serverResult.isOk()) { - const isValid = serverResult.unwrap(); - if (!isValid) { - return Result.ok(AuthenticationState.EXPIRED); - } - } - } - - return Result.ok(fileState); } } \ No newline at end of file diff --git a/packages/automation/application/use-cases/ClearSessionUseCase.ts b/packages/automation/application/use-cases/ClearSessionUseCase.ts index 874f787d8..c88d4c384 100644 --- a/packages/automation/application/use-cases/ClearSessionUseCase.ts +++ b/packages/automation/application/use-cases/ClearSessionUseCase.ts @@ -1,5 +1,6 @@ import { Result } from '../../../shared/result/Result'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use case for clearing the user's session (logout). @@ -8,7 +9,10 @@ import type { AuthenticationServicePort } from '../ports/AuthenticationServicePo * the user out. The next automation attempt will require re-authentication. */ export class ClearSessionUseCase { - constructor(private readonly authService: AuthenticationServicePort) {} + constructor( + private readonly authService: AuthenticationServicePort, + private readonly logger: ILogger, // Inject ILogger + ) {} /** * Execute the session clearing. @@ -16,6 +20,28 @@ export class ClearSessionUseCase { * @returns Result indicating success or failure */ async execute(): Promise> { - return this.authService.clearSession(); + this.logger.debug('Attempting to clear user session.', { + useCase: 'ClearSessionUseCase' + }); + try { + const result = await this.authService.clearSession(); + + if (result.isSuccess) { + this.logger.info('User session cleared successfully.', { + useCase: 'ClearSessionUseCase' + }); + } else { + this.logger.warn('Failed to clear user session.', { + useCase: 'ClearSessionUseCase', + error: result.error, + }); + } + return result; + } catch (error: any) { + this.logger.error('Error clearing user session.', error, { + useCase: 'ClearSessionUseCase' + }); + return Result.fail(error.message); + } } -} \ No newline at end of file +} diff --git a/packages/automation/application/use-cases/CompleteRaceCreationUseCase.ts b/packages/automation/application/use-cases/CompleteRaceCreationUseCase.ts index f6765005a..413c4f7d2 100644 --- a/packages/automation/application/use-cases/CompleteRaceCreationUseCase.ts +++ b/packages/automation/application/use-cases/CompleteRaceCreationUseCase.ts @@ -1,24 +1,30 @@ import { Result } from '../../../shared/result/Result'; import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult'; import type { CheckoutServicePort } from '../ports/CheckoutServicePort'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export class CompleteRaceCreationUseCase { - constructor(private readonly checkoutService: CheckoutServicePort) {} + constructor(private readonly checkoutService: CheckoutServicePort, private readonly logger: ILogger) {} async execute(sessionId: string): Promise> { + this.logger.debug(`Attempting to complete race creation for session ID: ${sessionId}`); if (!sessionId || sessionId.trim() === '') { + this.logger.error('Session ID is required for completing race creation.'); return Result.err(new Error('Session ID is required')); } const infoResult = await this.checkoutService.extractCheckoutInfo(); if (infoResult.isErr()) { + this.logger.error(`Failed to extract checkout info: ${infoResult.unwrapErr().message}`); return Result.err(infoResult.unwrapErr()); } const info = infoResult.unwrap(); + this.logger.debug(`Extracted checkout information: ${JSON.stringify(info)}`); if (!info.price) { + this.logger.error('Could not extract price from checkout page.'); return Result.err(new Error('Could not extract price from checkout page')); } @@ -29,9 +35,11 @@ export class CompleteRaceCreationUseCase { timestamp: new Date(), }); + this.logger.info(`Race creation completed successfully for session ID: ${sessionId}`); return Result.ok(raceCreationResult); } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); + this.logger.error(`Error completing race creation for session ID ${sessionId}: ${err.message}`); return Result.err(err); } } diff --git a/packages/automation/application/use-cases/ConfirmCheckoutUseCase.ts b/packages/automation/application/use-cases/ConfirmCheckoutUseCase.ts index 605779627..d414633a2 100644 --- a/packages/automation/application/use-cases/ConfirmCheckoutUseCase.ts +++ b/packages/automation/application/use-cases/ConfirmCheckoutUseCase.ts @@ -1,4 +1,5 @@ import { Result } from '../../../shared/result/Result'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { CheckoutServicePort } from '../ports/CheckoutServicePort'; import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort'; import { CheckoutStateEnum } from '../../domain/value-objects/CheckoutState'; @@ -14,26 +15,36 @@ export class ConfirmCheckoutUseCase { constructor( private readonly checkoutService: CheckoutServicePort, - private readonly confirmationPort: CheckoutConfirmationPort + private readonly confirmationPort: CheckoutConfirmationPort, + private readonly logger: ILogger, ) {} async execute(sessionMetadata?: SessionMetadata): Promise> { + this.logger.debug('Executing ConfirmCheckoutUseCase', { sessionMetadata }); + const infoResult = await this.checkoutService.extractCheckoutInfo(); if (infoResult.isErr()) { + this.logger.error('Failed to extract checkout info', { error: infoResult.unwrapErr() }); return Result.err(infoResult.unwrapErr()); } const info = infoResult.unwrap(); + this.logger.info('Extracted checkout info', { state: info.state.getValue(), price: info.price }); + if (info.state.getValue() === CheckoutStateEnum.INSUFFICIENT_FUNDS) { + this.logger.error('Insufficient funds to complete checkout'); return Result.err(new Error('Insufficient funds to complete checkout')); } if (!info.price) { + this.logger.error('Could not extract price from checkout page'); return Result.err(new Error('Could not extract price from checkout page')); } + this.logger.debug('Requesting checkout confirmation', { price: info.price, state: info.state.getValue(), sessionMetadata }); + // Request confirmation via port with full checkout context const confirmationResult = await this.confirmationPort.requestCheckoutConfirmation({ price: info.price, @@ -47,19 +58,31 @@ export class ConfirmCheckoutUseCase { }); if (confirmationResult.isErr()) { + this.logger.error('Checkout confirmation failed', { error: confirmationResult.unwrapErr() }); return Result.err(confirmationResult.unwrapErr()); } const confirmation = confirmationResult.unwrap(); + this.logger.info('Checkout confirmation received', { confirmation }); if (confirmation.isCancelled()) { + this.logger.error('Checkout cancelled by user'); return Result.err(new Error('Checkout cancelled by user')); } if (confirmation.isTimeout()) { + this.logger.error('Checkout confirmation timeout'); return Result.err(new Error('Checkout confirmation timeout')); } - return await this.checkoutService.proceedWithCheckout(); + this.logger.info('Proceeding with checkout'); + const checkoutResult = await this.checkoutService.proceedWithCheckout(); + + if (checkoutResult.isOk()) { + this.logger.info('Checkout process completed successfully.'); + } else { + this.logger.error('Checkout process failed', { error: checkoutResult.unwrapErr() }); + } + return checkoutResult; } } \ No newline at end of file diff --git a/packages/automation/application/use-cases/InitiateLoginUseCase.ts b/packages/automation/application/use-cases/InitiateLoginUseCase.ts index b3a05781b..e5ea3ed44 100644 --- a/packages/automation/application/use-cases/InitiateLoginUseCase.ts +++ b/packages/automation/application/use-cases/InitiateLoginUseCase.ts @@ -1,5 +1,6 @@ import { Result } from '../../../shared/result/Result'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; +import type { ILogger } from '../../../shared/logger/ILogger'; /** * Use case for initiating the manual login flow. @@ -9,7 +10,10 @@ import type { AuthenticationServicePort } from '../ports/AuthenticationServicePo * indicating successful login. */ export class InitiateLoginUseCase { - constructor(private readonly authService: AuthenticationServicePort) {} + constructor( + private readonly authService: AuthenticationServicePort, + private readonly logger: ILogger, + ) {} /** * Execute the login flow. @@ -18,6 +22,18 @@ export class InitiateLoginUseCase { * @returns Result indicating success (login complete) or failure (cancelled/timeout) */ async execute(): Promise> { - return this.authService.initiateLogin(); + this.logger.debug('Initiating login flow...'); + try { + const result = await this.authService.initiateLogin(); + if (result.isOk()) { + this.logger.info('Login flow initiated successfully.'); + } else { + this.logger.warn('Login flow initiation failed.', { error: result.error }); + } + return result; + } catch (error: any) { + this.logger.error('Error initiating login flow.', error); + return Result.fail(error.message || 'Unknown error during login initiation.'); + } } } \ No newline at end of file diff --git a/packages/automation/application/use-cases/StartAutomationSessionUseCase.ts b/packages/automation/application/use-cases/StartAutomationSessionUseCase.ts index 934e5cbd8..e0493253d 100644 --- a/packages/automation/application/use-cases/StartAutomationSessionUseCase.ts +++ b/packages/automation/application/use-cases/StartAutomationSessionUseCase.ts @@ -1,4 +1,5 @@ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import { AutomationSession } from '../../domain/entities/AutomationSession'; import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig'; import { AutomationEnginePort } from '../ports/AutomationEnginePort'; @@ -11,18 +12,26 @@ export class StartAutomationSessionUseCase constructor( private readonly automationEngine: AutomationEnginePort, private readonly browserAutomation: IBrowserAutomation, - private readonly sessionRepository: SessionRepositoryPort + private readonly sessionRepository: SessionRepositoryPort, + private readonly logger: ILogger ) {} async execute(config: HostedSessionConfig): Promise { + this.logger.debug('Starting automation session execution', { config }); + const session = AutomationSession.create(config); + this.logger.info(`Automation session created with ID: ${session.id}`); const validationResult = await this.automationEngine.validateConfiguration(config); if (!validationResult.isValid) { + this.logger.warn('Automation session configuration validation failed', { config, error: validationResult.error }); + this.logger.error('Automation session configuration validation failed', { config, error: validationResult.error }); throw new Error(validationResult.error); } + this.logger.debug('Automation session configuration validated successfully.'); await this.sessionRepository.save(session); + this.logger.info(`Automation session with ID: ${session.id} saved to repository.`); const dto: SessionDTO = { sessionId: session.id, @@ -34,6 +43,7 @@ export class StartAutomationSessionUseCase ...(session.errorMessage ? { errorMessage: session.errorMessage } : {}), }; + this.logger.debug('Automation session executed successfully, returning DTO.', { dto }); return dto; } } \ No newline at end of file diff --git a/packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase.ts b/packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase.ts index 75e6e34ed..e524018b8 100644 --- a/packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase.ts +++ b/packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase.ts @@ -1,6 +1,7 @@ import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import { Result } from '../../../shared/result/Result'; import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use case for verifying browser shows authenticated page state. @@ -8,22 +9,27 @@ import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAu */ export class VerifyAuthenticatedPageUseCase { constructor( - private readonly authService: AuthenticationServicePort + private readonly authService: AuthenticationServicePort, + private readonly logger: ILogger, ) {} async execute(): Promise> { + this.logger.debug('Executing VerifyAuthenticatedPageUseCase'); try { const result = await this.authService.verifyPageAuthentication(); if (result.isErr()) { const error = result.error ?? new Error('Page verification failed'); + this.logger.error(`Page verification failed: ${error.message}`, error); return Result.err(error); } const browserState = result.unwrap(); + this.logger.info('Successfully verified authenticated page state.'); return Result.ok(browserState); } catch (error) { const message = error instanceof Error ? error.message : String(error); + this.logger.error(`Page verification failed unexpectedly: ${message}`, error); return Result.err(new Error(`Page verification failed: ${message}`)); } } diff --git a/packages/automation/infrastructure/adapters/logging/ConsoleLogAdapter.ts b/packages/automation/infrastructure/adapters/logging/ConsoleLogAdapter.ts new file mode 100644 index 000000000..6e607eaf4 --- /dev/null +++ b/packages/automation/infrastructure/adapters/logging/ConsoleLogAdapter.ts @@ -0,0 +1,42 @@ +import { LoggerPort } from '../../../application/ports/LoggerPort'; +import { ConsoleLogger } from '../../../../shared/logging/ConsoleLogger'; +import { LogContext } from '../../../application/ports/LoggerContext'; + +export class ConsoleLogAdapter implements LoggerPort { + private consoleLogger: ConsoleLogger; + private readonly context: LogContext; + + constructor(context: LogContext = {}) { + this.consoleLogger = new ConsoleLogger(); + this.context = context; + } + + debug(message: string, context?: LogContext): void { + this.consoleLogger.debug(message, { ...this.context, ...context }); + } + + info(message: string, context?: LogContext): void { + this.consoleLogger.info(message, { ...this.context, ...context }); + } + + warn(message: string, context?: LogContext): void { + this.consoleLogger.warn(message, { ...this.context, ...context }); + } + + error(message: string, error?: Error, context?: LogContext): void { + this.consoleLogger.error(message, error, { ...this.context, ...context }); + } + + fatal(message: string, error?: Error, context?: LogContext): void { + this.consoleLogger.error(`FATAL: ${message}`, error, { ...this.context, ...context }); + } + + child(context: LogContext = {}): LoggerPort { + return new ConsoleLogAdapter({ ...this.context, ...context }); + } + + async flush(): Promise { + // No-op for console logger as it's synchronous + return Promise.resolve(); + } +} diff --git a/packages/automation/infrastructure/adapters/logging/NoOpLogAdapter.ts b/packages/automation/infrastructure/adapters/logging/NoOpLogAdapter.ts index 8fe590d66..8ca9e5349 100644 --- a/packages/automation/infrastructure/adapters/logging/NoOpLogAdapter.ts +++ b/packages/automation/infrastructure/adapters/logging/NoOpLogAdapter.ts @@ -1,7 +1,8 @@ import type { LoggerPort } from '../../../application/ports/LoggerPort'; import type { LogContext } from '../../../application/ports/LoggerContext'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; -export class NoOpLogAdapter implements LoggerPort { +export class NoOpLogAdapter implements LoggerPort, ILogger { debug(_message: string, _context?: LogContext): void {} info(_message: string, _context?: LogContext): void {} diff --git a/packages/automation/infrastructure/adapters/logging/PinoLogAdapter.ts b/packages/automation/infrastructure/adapters/logging/PinoLogAdapter.ts index 64d1b9813..195cc4205 100644 --- a/packages/automation/infrastructure/adapters/logging/PinoLogAdapter.ts +++ b/packages/automation/infrastructure/adapters/logging/PinoLogAdapter.ts @@ -2,6 +2,7 @@ import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerP import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext'; import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel'; import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; const LOG_LEVEL_PRIORITY: Record = { debug: 10, @@ -20,7 +21,7 @@ const LOG_LEVEL_PRIORITY: Record = { * * This provides structured JSON logging to stdout with the same interface. */ -export class PinoLogAdapter implements LoggerPort { +export class PinoLogAdapter implements LoggerPort, ILogger { private readonly config: LoggingEnvironmentConfig; private readonly baseContext: LogContext; private readonly levelPriority: number; diff --git a/packages/identity/infrastructure/repositories/InMemoryAchievementRepository.ts b/packages/identity/infrastructure/repositories/InMemoryAchievementRepository.ts index bcaedd6d4..9070903fc 100644 --- a/packages/identity/infrastructure/repositories/InMemoryAchievementRepository.ts +++ b/packages/identity/infrastructure/repositories/InMemoryAchievementRepository.ts @@ -14,17 +14,22 @@ import { } from '../../domain/entities/Achievement'; import { UserAchievement } from '../../domain/entities/UserAchievement'; import type { IAchievementRepository } from '../../domain/repositories/IAchievementRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryAchievementRepository implements IAchievementRepository { private achievements: Map = new Map(); private userAchievements: Map = new Map(); + private readonly logger: ILogger; - constructor() { + constructor(logger: ILogger) { + this.logger = logger; + this.logger.info('InMemoryAchievementRepository initialized.'); // Seed with predefined achievements this.seedAchievements(); } private seedAchievements(): void { + this.logger.debug('Seeding predefined achievements.'); const allAchievements = [ ...DRIVER_ACHIEVEMENTS, ...STEWARD_ACHIEVEMENTS, @@ -35,136 +40,252 @@ export class InMemoryAchievementRepository implements IAchievementRepository { for (const props of allAchievements) { const achievement = Achievement.create(props); this.achievements.set(achievement.id, achievement); + this.logger.debug(`Seeded achievement: ${achievement.id} (${achievement.name}).`); } + this.logger.info(`Seeded ${allAchievements.length} predefined achievements.`); } // Achievement operations async findAchievementById(id: string): Promise { - return this.achievements.get(id) ?? null; + this.logger.debug(`Finding achievement by id: ${id}`); + try { + const achievement = this.achievements.get(id) ?? null; + if (achievement) { + this.logger.info(`Found achievement: ${id}.`); + } else { + this.logger.warn(`Achievement with id ${id} not found.`); + } + return achievement; + } catch (error) { + this.logger.error(`Error finding achievement by id ${id}:`, error); + throw error; + } } async findAllAchievements(): Promise { - return Array.from(this.achievements.values()); + this.logger.debug('Finding all achievements.'); + try { + const achievements = Array.from(this.achievements.values()); + this.logger.info(`Found ${achievements.length} achievements.`); + return achievements; + } catch (error) { + this.logger.error('Error finding all achievements:', error); + throw error; + } } async findAchievementsByCategory(category: AchievementCategory): Promise { - return Array.from(this.achievements.values()) - .filter(a => a.category === category); + this.logger.debug(`Finding achievements by category: ${category}`); + try { + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === category); + this.logger.info(`Found ${achievements.length} achievements for category: ${category}.`); + return achievements; + } catch (error) { + this.logger.error(`Error finding achievements by category ${category}:`, error); + throw error; + } } async createAchievement(achievement: Achievement): Promise { - if (this.achievements.has(achievement.id)) { - throw new Error('Achievement with this ID already exists'); + this.logger.debug(`Creating achievement: ${achievement.id}`); + try { + if (this.achievements.has(achievement.id)) { + this.logger.warn(`Achievement with ID ${achievement.id} already exists.`); + throw new Error('Achievement with this ID already exists'); + } + this.achievements.set(achievement.id, achievement); + this.logger.info(`Achievement ${achievement.id} created successfully.`); + return achievement; + } catch (error) { + this.logger.error(`Error creating achievement ${achievement.id}:`, error); + throw error; } - this.achievements.set(achievement.id, achievement); - return achievement; } // UserAchievement operations async findUserAchievementById(id: string): Promise { - return this.userAchievements.get(id) ?? null; + this.logger.debug(`Finding user achievement by id: ${id}`); + try { + const userAchievement = this.userAchievements.get(id) ?? null; + if (userAchievement) { + this.logger.info(`Found user achievement: ${id}.`); + } else { + this.logger.warn(`User achievement with id ${id} not found.`); + } + return userAchievement; + } catch (error) { + this.logger.error(`Error finding user achievement by id ${id}:`, error); + throw error; + } } async findUserAchievementsByUserId(userId: string): Promise { - return Array.from(this.userAchievements.values()) - .filter(ua => ua.userId === userId); + this.logger.debug(`Finding user achievements by user id: ${userId}`); + try { + const userAchievements = Array.from(this.userAchievements.values()) + .filter(ua => ua.userId === userId); + this.logger.info(`Found ${userAchievements.length} user achievements for user id: ${userId}.`); + return userAchievements; + } catch (error) { + this.logger.error(`Error finding user achievements by user id ${userId}:`, error); + throw error; + } } async findUserAchievementByUserAndAchievement( - userId: string, + userId: string, achievementId: string ): Promise { - for (const ua of this.userAchievements.values()) { - if (ua.userId === userId && ua.achievementId === achievementId) { - return ua; + this.logger.debug(`Finding user achievement for user: ${userId}, achievement: ${achievementId}`); + try { + for (const ua of this.userAchievements.values()) { + if (ua.userId === userId && ua.achievementId === achievementId) { + this.logger.info(`Found user achievement for user: ${userId}, achievement: ${achievementId}.`); + return ua; + } } + 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); + throw error; } - return null; } async hasUserEarnedAchievement(userId: string, achievementId: string): Promise { - const ua = await this.findUserAchievementByUserAndAchievement(userId, achievementId); - return ua !== null && ua.isComplete(); + this.logger.debug(`Checking if user ${userId} earned achievement ${achievementId}`); + try { + const ua = await this.findUserAchievementByUserAndAchievement(userId, achievementId); + const hasEarned = ua !== null && ua.isComplete(); + this.logger.debug(`User ${userId} earned achievement ${achievementId}: ${hasEarned}.`); + return hasEarned; + } catch (error) { + this.logger.error(`Error checking if user ${userId} earned achievement ${achievementId}:`, error); + throw error; + } } async createUserAchievement(userAchievement: UserAchievement): Promise { - if (this.userAchievements.has(userAchievement.id)) { - throw new Error('UserAchievement with this ID already exists'); + this.logger.debug(`Creating user achievement: ${userAchievement.id}`); + try { + if (this.userAchievements.has(userAchievement.id)) { + this.logger.warn(`UserAchievement with ID ${userAchievement.id} already exists.`); + throw new Error('UserAchievement with this ID already exists'); + } + this.userAchievements.set(userAchievement.id, userAchievement); + this.logger.info(`UserAchievement ${userAchievement.id} created successfully.`); + return userAchievement; + } catch (error) { + this.logger.error(`Error creating user achievement ${userAchievement.id}:`, error); + throw error; } - this.userAchievements.set(userAchievement.id, userAchievement); - return userAchievement; } async updateUserAchievement(userAchievement: UserAchievement): Promise { - if (!this.userAchievements.has(userAchievement.id)) { - throw new Error('UserAchievement not found'); + this.logger.debug(`Updating user achievement: ${userAchievement.id}`); + try { + if (!this.userAchievements.has(userAchievement.id)) { + this.logger.warn(`UserAchievement with ID ${userAchievement.id} not found for update.`); + throw new Error('UserAchievement not found'); + } + this.userAchievements.set(userAchievement.id, userAchievement); + this.logger.info(`UserAchievement ${userAchievement.id} updated successfully.`); + return userAchievement; + } catch (error) { + this.logger.error(`Error updating user achievement ${userAchievement.id}:`, error); + throw error; } - this.userAchievements.set(userAchievement.id, userAchievement); - return userAchievement; } // Stats async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> { - const userStats = new Map(); + this.logger.debug(`Getting achievement leaderboard with limit: ${limit}`); + try { + const userStats = new Map(); - for (const ua of this.userAchievements.values()) { - if (!ua.isComplete()) continue; + for (const ua of this.userAchievements.values()) { + if (!ua.isComplete()) continue; - const achievement = this.achievements.get(ua.achievementId); - if (!achievement) continue; + const achievement = this.achievements.get(ua.achievementId); + if (!achievement) { + this.logger.warn(`Achievement ${ua.achievementId} not found while building leaderboard.`); + continue; + } - const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 }; - userStats.set(ua.userId, { - points: existing.points + achievement.points, - count: existing.count + 1, - }); + const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 }; + userStats.set(ua.userId, { + points: existing.points + achievement.points, + count: existing.count + 1, + }); + } + + const leaderboard = Array.from(userStats.entries()) + .map(([userId, stats]) => ({ userId, ...stats })) + .sort((a, b) => b.points - a.points) + .slice(0, limit); + this.logger.info(`Generated achievement leaderboard with ${leaderboard.length} entries.`); + return leaderboard; + } catch (error) { + this.logger.error(`Error getting achievement leaderboard:`, error); + throw error; } - - return Array.from(userStats.entries()) - .map(([userId, stats]) => ({ userId, ...stats })) - .sort((a, b) => b.points - a.points) - .slice(0, limit); } - async getUserAchievementStats(userId: string): Promise<{ - total: number; - points: number; - byCategory: Record + async getUserAchievementStats(userId: string): Promise<{ + total: number; + points: number; + byCategory: Record }> { - const userAchievements = await this.findUserAchievementsByUserId(userId); - const completedAchievements = userAchievements.filter(ua => ua.isComplete()); + this.logger.debug(`Getting achievement stats for user: ${userId}`); + try { + const userAchievements = await this.findUserAchievementsByUserId(userId); + const completedAchievements = userAchievements.filter(ua => ua.isComplete()); + this.logger.debug(`Found ${completedAchievements.length} completed achievements for user ${userId}.`); - const byCategory: Record = { - driver: 0, - steward: 0, - admin: 0, - community: 0, - }; + const byCategory: Record = { + driver: 0, + steward: 0, + admin: 0, + community: 0, + }; - let points = 0; + let points = 0; - for (const ua of completedAchievements) { - const achievement = this.achievements.get(ua.achievementId); - if (achievement) { - points += achievement.points; - byCategory[achievement.category]++; + for (const ua of completedAchievements) { + const achievement = this.achievements.get(ua.achievementId); + if (achievement) { + points += achievement.points; + byCategory[achievement.category]++; + } else { + this.logger.warn(`Achievement ${ua.achievementId} not found while calculating user stats for user ${userId}.`); + } } - } - return { - total: completedAchievements.length, - points, - byCategory, - }; + const stats = { + total: completedAchievements.length, + points, + byCategory, + }; + this.logger.info(`Generated achievement stats for user ${userId}:`, stats); + return stats; + } catch (error) { + this.logger.error(`Error getting user achievement stats for user ${userId}:`, error); + throw error; + } } // Test helpers clearUserAchievements(): void { + this.logger.debug('Clearing all user achievements.'); this.userAchievements.clear(); + this.logger.info('All user achievements cleared.'); } clear(): void { + this.logger.debug('Clearing all achievement data.'); this.achievements.clear(); this.userAchievements.clear(); + this.logger.info('All achievement data cleared.'); } } \ No newline at end of file diff --git a/packages/identity/infrastructure/repositories/InMemorySponsorAccountRepository.ts b/packages/identity/infrastructure/repositories/InMemorySponsorAccountRepository.ts index e11e7d66c..aafef7f82 100644 --- a/packages/identity/infrastructure/repositories/InMemorySponsorAccountRepository.ts +++ b/packages/identity/infrastructure/repositories/InMemorySponsorAccountRepository.ts @@ -7,42 +7,117 @@ import type { ISponsorAccountRepository } from '../../domain/repositories/ISponsorAccountRepository'; import type { SponsorAccount } from '../../domain/entities/SponsorAccount'; import type { UserId } from '../../domain/value-objects/UserId'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemorySponsorAccountRepository implements ISponsorAccountRepository { private accounts: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: SponsorAccount[]) { + this.logger = logger; + this.logger.info('InMemorySponsorAccountRepository initialized.'); + if (seedData) { + this.seed(seedData); + } + } async save(account: SponsorAccount): Promise { - this.accounts.set(account.getId().value, account); + this.logger.debug(`Saving sponsor account: ${account.getId().value}`); + try { + this.accounts.set(account.getId().value, account); + this.logger.info(`Sponsor account ${account.getId().value} saved successfully.`); + } catch (error) { + this.logger.error(`Error saving sponsor account ${account.getId().value}:`, error); + throw error; + } } async findById(id: UserId): Promise { - return this.accounts.get(id.value) ?? null; + this.logger.debug(`Finding sponsor account by id: ${id.value}`); + try { + const account = this.accounts.get(id.value) ?? null; + if (account) { + this.logger.info(`Found sponsor account: ${id.value}.`); + } else { + this.logger.warn(`Sponsor account with id ${id.value} not found.`); + } + return account; + } catch (error) { + this.logger.error(`Error finding sponsor account by id ${id.value}:`, error); + throw error; + } } async findBySponsorId(sponsorId: string): Promise { - return Array.from(this.accounts.values()).find( - a => a.getSponsorId() === sponsorId - ) ?? null; + this.logger.debug(`Finding sponsor account by sponsor id: ${sponsorId}`); + try { + const account = Array.from(this.accounts.values()).find( + a => a.getSponsorId() === sponsorId + ) ?? null; + if (account) { + this.logger.info(`Found sponsor account for sponsor id: ${sponsorId}.`); + } else { + this.logger.warn(`Sponsor account for sponsor id ${sponsorId} not found.`); + } + return account; + } catch (error) { + this.logger.error(`Error finding sponsor account by sponsor id ${sponsorId}:`, error); + throw error; + } } async findByEmail(email: string): Promise { - const normalizedEmail = email.toLowerCase().trim(); - return Array.from(this.accounts.values()).find( - a => a.getEmail().toLowerCase() === normalizedEmail - ) ?? null; + this.logger.debug(`Finding sponsor account by email: ${email}`); + try { + const normalizedEmail = email.toLowerCase().trim(); + const account = Array.from(this.accounts.values()).find( + a => a.getEmail().toLowerCase() === normalizedEmail + ) ?? null; + if (account) { + this.logger.info(`Found sponsor account by email: ${email}.`); + } else { + this.logger.warn(`Sponsor account with email ${email} not found.`); + } + return account; + } catch (error) { + this.logger.error(`Error finding sponsor account by email ${email}:`, error); + throw error; + } } async delete(id: UserId): Promise { - this.accounts.delete(id.value); + this.logger.debug(`Deleting sponsor account: ${id.value}`); + try { + if (this.accounts.delete(id.value)) { + this.logger.info(`Sponsor account ${id.value} deleted successfully.`); + } else { + this.logger.warn(`Sponsor account with id ${id.value} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting sponsor account ${id.value}:`, error); + throw error; + } } // Helper for testing clear(): void { + this.logger.debug('Clearing all sponsor accounts.'); this.accounts.clear(); + this.logger.info('All sponsor accounts cleared.'); } // Helper for seeding demo data seed(accounts: SponsorAccount[]): void { - accounts.forEach(a => this.accounts.set(a.getId().value, a)); + this.logger.debug(`Seeding ${accounts.length} sponsor accounts.`); + try { + accounts.forEach(a => { + this.accounts.set(a.getId().value, a); + this.logger.debug(`Seeded sponsor account: ${a.getId().value}.`); + }); + this.logger.info(`Successfully seeded ${accounts.length} sponsor accounts.`); + } catch (error) { + this.logger.error(`Error seeding sponsor accounts:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/identity/infrastructure/repositories/InMemoryUserRatingRepository.ts b/packages/identity/infrastructure/repositories/InMemoryUserRatingRepository.ts index 756f5980d..c9d20cee0 100644 --- a/packages/identity/infrastructure/repositories/InMemoryUserRatingRepository.ts +++ b/packages/identity/infrastructure/repositories/InMemoryUserRatingRepository.ts @@ -6,60 +6,148 @@ import { UserRating } from '../../domain/value-objects/UserRating'; import type { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryUserRatingRepository implements IUserRatingRepository { private ratings: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: UserRating[]) { + this.logger = logger; + this.logger.info('InMemoryUserRatingRepository initialized.'); + if (seedData) { + seedData.forEach(rating => this.ratings.set(rating.userId, rating)); + this.logger.debug(`Seeded ${seedData.length} user ratings.`); + } + } async findByUserId(userId: string): Promise { - return this.ratings.get(userId) ?? null; + this.logger.debug(`Finding user rating for user id: ${userId}`); + try { + const rating = this.ratings.get(userId) ?? null; + if (rating) { + this.logger.info(`Found user rating for user id: ${userId}.`); + } else { + this.logger.warn(`User rating for user id ${userId} not found.`); + } + return rating; + } catch (error) { + this.logger.error(`Error finding user rating for user id ${userId}:`, error); + throw error; + } } async findByUserIds(userIds: string[]): Promise { - const results: UserRating[] = []; - for (const userId of userIds) { - const rating = this.ratings.get(userId); - if (rating) { - results.push(rating); + this.logger.debug(`Finding user ratings for user ids: ${userIds.join(', ')}`); + try { + const results: UserRating[] = []; + for (const userId of userIds) { + const rating = this.ratings.get(userId); + if (rating) { + results.push(rating); + } else { + this.logger.warn(`User rating for user id ${userId} not found.`); + } } + 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); + throw error; } - return results; } async save(rating: UserRating): Promise { - this.ratings.set(rating.userId, rating); - return rating; + this.logger.debug(`Saving user rating for user id: ${rating.userId}`); + try { + if (this.ratings.has(rating.userId)) { + this.logger.debug(`Updating existing user rating for user id: ${rating.userId}.`); + } else { + this.logger.debug(`Creating new user rating for user id: ${rating.userId}.`); + } + this.ratings.set(rating.userId, rating); + this.logger.info(`User rating for user id ${rating.userId} saved successfully.`); + return rating; + } catch (error) { + this.logger.error(`Error saving user rating for user id ${rating.userId}:`, error); + throw error; + } } async getTopDrivers(limit: number): Promise { - return Array.from(this.ratings.values()) - .filter(r => r.driver.sampleSize > 0) - .sort((a, b) => b.driver.value - a.driver.value) - .slice(0, limit); + this.logger.debug(`Getting top ${limit} drivers.`); + try { + const topDrivers = Array.from(this.ratings.values()) + .filter(r => r.driver.sampleSize > 0) + .sort((a, b) => b.driver.value - a.driver.value) + .slice(0, limit); + this.logger.info(`Retrieved ${topDrivers.length} top drivers.`); + return topDrivers; + } catch (error) { + this.logger.error(`Error getting top drivers:`, error); + throw error; + } } async getTopTrusted(limit: number): Promise { - return Array.from(this.ratings.values()) - .filter(r => r.trust.sampleSize > 0) - .sort((a, b) => b.trust.value - a.trust.value) - .slice(0, limit); + this.logger.debug(`Getting top ${limit} trusted users.`); + try { + const topTrusted = Array.from(this.ratings.values()) + .filter(r => r.trust.sampleSize > 0) + .sort((a, b) => b.trust.value - a.trust.value) + .slice(0, limit); + this.logger.info(`Retrieved ${topTrusted.length} top trusted users.`); + return topTrusted; + } catch (error) { + this.logger.error(`Error getting top trusted users:`, error); + throw error; + } } async getEligibleStewards(): Promise { - return Array.from(this.ratings.values()) - .filter(r => r.canBeSteward()); + this.logger.debug('Getting eligible stewards.'); + try { + const eligibleStewards = Array.from(this.ratings.values()) + .filter(r => r.canBeSteward()); + this.logger.info(`Found ${eligibleStewards.length} eligible stewards.`); + return eligibleStewards; + } catch (error) { + this.logger.error(`Error getting eligible stewards:`, error); + throw error; + } } async findByDriverTier(tier: 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite'): Promise { - return Array.from(this.ratings.values()) - .filter(r => r.getDriverTier() === tier); + this.logger.debug(`Finding user ratings by driver tier: ${tier}`); + try { + const ratingsByTier = Array.from(this.ratings.values()) + .filter(r => r.getDriverTier() === tier); + this.logger.info(`Found ${ratingsByTier.length} user ratings for driver tier: ${tier}.`); + return ratingsByTier; + } catch (error) { + this.logger.error(`Error finding user ratings by driver tier ${tier}:`, error); + throw error; + } } async delete(userId: string): Promise { - this.ratings.delete(userId); + this.logger.debug(`Deleting user rating for user id: ${userId}`); + try { + if (this.ratings.delete(userId)) { + this.logger.info(`User rating for user id ${userId} deleted successfully.`); + } else { + this.logger.warn(`User rating for user id ${userId} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting user rating for user id ${userId}:`, error); + throw error; + } } // Test helper clear(): void { + this.logger.debug('Clearing all user ratings.'); this.ratings.clear(); + this.logger.info('All user ratings cleared.'); } } \ No newline at end of file diff --git a/packages/identity/infrastructure/repositories/InMemoryUserRepository.ts b/packages/identity/infrastructure/repositories/InMemoryUserRepository.ts index 81780d55c..891c9d472 100644 --- a/packages/identity/infrastructure/repositories/InMemoryUserRepository.ts +++ b/packages/identity/infrastructure/repositories/InMemoryUserRepository.ts @@ -5,52 +5,113 @@ */ import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryUserRepository implements IUserRepository { private users: Map = new Map(); private emailIndex: Map = new Map(); // email -> userId + private readonly logger: ILogger; - constructor(initialUsers: StoredUser[] = []) { + constructor(logger: ILogger, initialUsers: StoredUser[] = []) { + this.logger = logger; + this.logger.info('InMemoryUserRepository initialized.'); for (const user of initialUsers) { this.users.set(user.id, user); this.emailIndex.set(user.email.toLowerCase(), user.id); + this.logger.debug(`Seeded user: ${user.id} (${user.email}).`); } } async findByEmail(email: string): Promise { - const userId = this.emailIndex.get(email.toLowerCase()); - if (!userId) return null; - return this.users.get(userId) ?? null; + this.logger.debug(`Finding user by email: ${email}`); + try { + const userId = this.emailIndex.get(email.toLowerCase()); + if (!userId) { + this.logger.warn(`User with email ${email} not found.`); + return null; + } + const user = this.users.get(userId) ?? null; + if (user) { + this.logger.info(`Found user by email: ${email}.`); + } else { + this.logger.warn(`User with ID ${userId} (from email index) not found.`); + } + return user; + } catch (error) { + this.logger.error(`Error finding user by email ${email}:`, error); + throw error; + } } async findById(id: string): Promise { - return this.users.get(id) ?? null; + this.logger.debug(`Finding user by id: ${id}`); + try { + const user = this.users.get(id) ?? null; + if (user) { + this.logger.info(`Found user: ${id}.`); + } else { + this.logger.warn(`User with id ${id} not found.`); + } + return user; + } catch (error) { + this.logger.error(`Error finding user by id ${id}:`, error); + throw error; + } } async create(user: StoredUser): Promise { - if (this.emailIndex.has(user.email.toLowerCase())) { - throw new Error('Email already exists'); + this.logger.debug(`Creating user: ${user.id} with email: ${user.email}`); + try { + if (this.emailIndex.has(user.email.toLowerCase())) { + this.logger.warn(`Email ${user.email} already exists.`); + throw new Error('Email already exists'); + } + this.users.set(user.id, user); + this.emailIndex.set(user.email.toLowerCase(), user.id); + this.logger.info(`User ${user.id} (${user.email}) created successfully.`); + return user; + } catch (error) { + this.logger.error(`Error creating user ${user.id} (${user.email}):`, error); + throw error; } - this.users.set(user.id, user); - this.emailIndex.set(user.email.toLowerCase(), user.id); - return user; } async update(user: StoredUser): Promise { - const existing = this.users.get(user.id); - if (!existing) { - throw new Error('User not found'); + this.logger.debug(`Updating user: ${user.id} with email: ${user.email}`); + try { + const existing = this.users.get(user.id); + if (!existing) { + this.logger.warn(`User with ID ${user.id} not found for update.`); + throw new Error('User not found'); + } + // If email changed, update index + if (existing.email.toLowerCase() !== user.email.toLowerCase()) { + if (this.emailIndex.has(user.email.toLowerCase()) && this.emailIndex.get(user.email.toLowerCase()) !== user.id) { + this.logger.warn(`Cannot update user ${user.id} to email ${user.email} as it's already taken.`); + throw new Error('Email already exists for another user'); + } + this.logger.debug(`Updating email index from ${existing.email} to ${user.email}.`); + this.emailIndex.delete(existing.email.toLowerCase()); + this.emailIndex.set(user.email.toLowerCase(), user.id); + } + this.users.set(user.id, user); + this.logger.info(`User ${user.id} (${user.email}) updated successfully.`); + return user; + } catch (error) { + this.logger.error(`Error updating user ${user.id} (${user.email}):`, error); + throw error; } - // If email changed, update index - if (existing.email.toLowerCase() !== user.email.toLowerCase()) { - this.emailIndex.delete(existing.email.toLowerCase()); - this.emailIndex.set(user.email.toLowerCase(), user.id); - } - this.users.set(user.id, user); - return user; } async emailExists(email: string): Promise { - return this.emailIndex.has(email.toLowerCase()); + this.logger.debug(`Checking existence of email: ${email}`); + try { + const exists = this.emailIndex.has(email.toLowerCase()); + this.logger.debug(`Email ${email} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of email ${email}:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/media/application/use-cases/RequestAvatarGenerationUseCase.ts b/packages/media/application/use-cases/RequestAvatarGenerationUseCase.ts index e08f6b8bb..e5de23150 100644 --- a/packages/media/application/use-cases/RequestAvatarGenerationUseCase.ts +++ b/packages/media/application/use-cases/RequestAvatarGenerationUseCase.ts @@ -1,11 +1,4 @@ -/** - * Use Case: RequestAvatarGenerationUseCase - * - * Initiates the avatar generation process by validating the face photo - * and creating a generation request. - */ - -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { AsyncUseCase, ILogger } from '@gridpilot/shared/application'; import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository'; import type { FaceValidationPort } from '../ports/FaceValidationPort'; import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort'; @@ -32,89 +25,126 @@ export class RequestAvatarGenerationUseCase private readonly avatarRepository: IAvatarGenerationRepository, private readonly faceValidation: FaceValidationPort, private readonly avatarGeneration: AvatarGenerationPort, + private readonly logger: ILogger, ) {} async execute(command: RequestAvatarGenerationCommand): Promise { - // Create the generation request - const requestId = this.generateId(); - const request = AvatarGenerationRequest.create({ - id: requestId, - userId: command.userId, - facePhotoUrl: `data:image/jpeg;base64,${command.facePhotoData}`, - suitColor: command.suitColor, - ...(command.style ? { style: command.style } : {}), - }); + this.logger.debug( + `Executing RequestAvatarGenerationUseCase for userId: ${command.userId}`, + command, + ); - // Mark as validating - request.markAsValidating(); - await this.avatarRepository.save(request); + try { + // Create the generation request + const requestId = this.generateId(); + const request = AvatarGenerationRequest.create({ + id: requestId, + userId: command.userId, + facePhotoUrl: `data:image/jpeg;base64,${command.facePhotoData}`, + suitColor: command.suitColor, + ...(command.style ? { style: command.style } : {}), + }); - // Validate the face photo - const validationResult = await this.faceValidation.validateFacePhoto(command.facePhotoData); - - if (!validationResult.isValid) { - request.fail(validationResult.errorMessage || 'Face validation failed'); + this.logger.info(`Avatar generation request created with ID: ${requestId}`); + + // Mark as validating + request.markAsValidating(); await this.avatarRepository.save(request); + this.logger.debug(`Request ${requestId} marked as validating.`); + + // Validate the face photo + const validationResult = await this.faceValidation.validateFacePhoto(command.facePhotoData); + this.logger.debug( + `Face validation result for request ${requestId}:`, + validationResult, + ); + + if (!validationResult.isValid) { + const errorMessage = validationResult.errorMessage || 'Face validation failed'; + request.fail(errorMessage); + await this.avatarRepository.save(request); + this.logger.error(`Face validation failed for request ${requestId}: ${errorMessage}`); + return { + requestId, + status: 'failed', + errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face', + }; + } + + if (!validationResult.hasFace) { + const errorMessage = 'No face detected in the image'; + request.fail(errorMessage); + await this.avatarRepository.save(request); + this.logger.error(`No face detected for request ${requestId}: ${errorMessage}`); + return { + requestId, + status: 'failed', + errorMessage: 'No face detected. Please upload a photo that clearly shows your face.', + }; + } + + if (validationResult.faceCount > 1) { + const errorMessage = 'Multiple faces detected'; + request.fail(errorMessage); + await this.avatarRepository.save(request); + this.logger.error(`Multiple faces detected for request ${requestId}: ${errorMessage}`); + return { + requestId, + status: 'failed', + errorMessage: 'Multiple faces detected. Please upload a photo with only your face.', + }; + } + this.logger.info(`Face validation successful for request ${requestId}.`); + + // Mark as generating + request.markAsGenerating(); + await this.avatarRepository.save(request); + this.logger.debug(`Request ${requestId} marked as generating.`); + + // Generate avatars + const generationResult = await this.avatarGeneration.generateAvatars({ + facePhotoUrl: request.facePhotoUrl.value, + prompt: request.buildPrompt(), + suitColor: request.suitColor, + style: request.style, + count: 3, // Generate 3 options + }); + this.logger.debug( + `Avatar generation service result for request ${requestId}:`, + generationResult, + ); + + if (!generationResult.success) { + const errorMessage = generationResult.errorMessage || 'Avatar generation failed'; + request.fail(errorMessage); + await this.avatarRepository.save(request); + this.logger.error(`Avatar generation failed for request ${requestId}: ${errorMessage}`); + return { + requestId, + status: 'failed', + errorMessage: generationResult.errorMessage || 'Failed to generate avatars. Please try again.', + }; + } + + // Complete with generated avatars + const avatarUrls = generationResult.avatars.map(a => a.url); + request.completeWithAvatars(avatarUrls); + await this.avatarRepository.save(request); + this.logger.info(`Avatar generation completed successfully for request ${requestId}.`); + return { requestId, - status: 'failed', - errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face', + status: 'completed', + avatarUrls, }; + } catch (error) { + this.logger.error( + `An unexpected error occurred during avatar generation for userId: ${command.userId}`, + error, + ); + // Re-throw or return a generic error, depending on desired error handling strategy + throw error; } - - if (!validationResult.hasFace) { - request.fail('No face detected in the image'); - await this.avatarRepository.save(request); - return { - requestId, - status: 'failed', - errorMessage: 'No face detected. Please upload a photo that clearly shows your face.', - }; - } - - if (validationResult.faceCount > 1) { - request.fail('Multiple faces detected'); - await this.avatarRepository.save(request); - return { - requestId, - status: 'failed', - errorMessage: 'Multiple faces detected. Please upload a photo with only your face.', - }; - } - - // Mark as generating - request.markAsGenerating(); - await this.avatarRepository.save(request); - - // Generate avatars - const generationResult = await this.avatarGeneration.generateAvatars({ - facePhotoUrl: request.facePhotoUrl.value, - prompt: request.buildPrompt(), - suitColor: request.suitColor, - style: request.style, - count: 3, // Generate 3 options - }); - - if (!generationResult.success) { - request.fail(generationResult.errorMessage || 'Avatar generation failed'); - await this.avatarRepository.save(request); - return { - requestId, - status: 'failed', - errorMessage: generationResult.errorMessage || 'Failed to generate avatars. Please try again.', - }; - } - - // Complete with generated avatars - const avatarUrls = generationResult.avatars.map(a => a.url); - request.completeWithAvatars(avatarUrls); - await this.avatarRepository.save(request); - - return { - requestId, - status: 'completed', - avatarUrls, - }; } private generateId(): string { diff --git a/packages/media/application/use-cases/SelectAvatarUseCase.ts b/packages/media/application/use-cases/SelectAvatarUseCase.ts index a6025f16b..8587adc24 100644 --- a/packages/media/application/use-cases/SelectAvatarUseCase.ts +++ b/packages/media/application/use-cases/SelectAvatarUseCase.ts @@ -4,7 +4,7 @@ * Allows a user to select one of the generated avatars as their profile avatar. */ -import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { AsyncUseCase, ILogger } from '@gridpilot/shared/application'; import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository'; export interface SelectAvatarCommand { @@ -23,12 +23,16 @@ export class SelectAvatarUseCase implements AsyncUseCase { constructor( private readonly avatarRepository: IAvatarGenerationRepository, + private readonly logger: ILogger, ) {} async execute(command: SelectAvatarCommand): Promise { + this.logger.debug(`Executing SelectAvatarUseCase for userId: ${command.userId}, requestId: ${command.requestId}, avatarIndex: ${command.avatarIndex}`); + const request = await this.avatarRepository.findById(command.requestId); if (!request) { + this.logger.info(`Avatar generation request not found for requestId: ${command.requestId}`); return { success: false, errorMessage: 'Avatar generation request not found', @@ -36,6 +40,7 @@ export class SelectAvatarUseCase } if (request.userId !== command.userId) { + this.logger.info(`Permission denied for userId: ${command.userId} to select avatar for requestId: ${command.requestId}`); return { success: false, errorMessage: 'You do not have permission to select this avatar', @@ -43,6 +48,7 @@ export class SelectAvatarUseCase } if (request.status !== 'completed') { + this.logger.info(`Avatar generation not completed for requestId: ${command.requestId}, current status: ${request.status}`); return { success: false, errorMessage: 'Avatar generation is not yet complete', @@ -59,8 +65,10 @@ export class SelectAvatarUseCase ? { success: true, selectedAvatarUrl } : { success: true }; + this.logger.info(`Avatar selected successfully for userId: ${command.userId}, requestId: ${command.requestId}, selectedAvatarUrl: ${selectedAvatarUrl}`); return result; } catch (error) { + this.logger.error(`Failed to select avatar for userId: ${command.userId}, requestId: ${command.requestId}: ${error instanceof Error ? error.message : 'Unknown error'}`, error); return { success: false, errorMessage: error instanceof Error ? error.message : 'Failed to select avatar', diff --git a/packages/notifications/application/use-cases/GetUnreadNotificationsUseCase.ts b/packages/notifications/application/use-cases/GetUnreadNotificationsUseCase.ts index 6f7360501..fdf4e44e6 100644 --- a/packages/notifications/application/use-cases/GetUnreadNotificationsUseCase.ts +++ b/packages/notifications/application/use-cases/GetUnreadNotificationsUseCase.ts @@ -5,6 +5,7 @@ */ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { Notification } from '../../domain/entities/Notification'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; @@ -16,15 +17,27 @@ export interface UnreadNotificationsResult { export class GetUnreadNotificationsUseCase implements AsyncUseCase { constructor( private readonly notificationRepository: INotificationRepository, + private readonly logger: ILogger, ) {} async execute(recipientId: string): Promise { - const notifications = await this.notificationRepository.findUnreadByRecipientId(recipientId); - - return { - notifications, - totalCount: notifications.length, - }; + this.logger.debug(`Attempting to retrieve unread notifications for recipient ID: ${recipientId}`); + try { + const notifications = await this.notificationRepository.findUnreadByRecipientId(recipientId); + this.logger.info(`Successfully retrieved ${notifications.length} unread notifications for recipient ID: ${recipientId}`); + + if (notifications.length === 0) { + this.logger.warn(`No unread notifications found for recipient ID: ${recipientId}`); + } + + return { + notifications, + totalCount: notifications.length, + }; + } catch (error) { + this.logger.error(`Failed to retrieve unread notifications for recipient ID: ${recipientId}`, error); + throw error; + } } } diff --git a/packages/notifications/application/use-cases/MarkNotificationReadUseCase.ts b/packages/notifications/application/use-cases/MarkNotificationReadUseCase.ts index 4f9c52f24..0e958c9d4 100644 --- a/packages/notifications/application/use-cases/MarkNotificationReadUseCase.ts +++ b/packages/notifications/application/use-cases/MarkNotificationReadUseCase.ts @@ -7,6 +7,7 @@ import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import { NotificationDomainError } from '../../domain/errors/NotificationDomainError'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface MarkNotificationReadCommand { notificationId: string; @@ -16,25 +17,36 @@ export interface MarkNotificationReadCommand { export class MarkNotificationReadUseCase implements AsyncUseCase { constructor( private readonly notificationRepository: INotificationRepository, + private readonly logger: ILogger, ) {} async execute(command: MarkNotificationReadCommand): Promise { - const notification = await this.notificationRepository.findById(command.notificationId); - - if (!notification) { - throw new NotificationDomainError('Notification not found'); - } + this.logger.debug(`Attempting to mark notification ${command.notificationId} as read for recipient ${command.recipientId}`); + try { + const notification = await this.notificationRepository.findById(command.notificationId); + + if (!notification) { + this.logger.warn(`Notification not found for ID: ${command.notificationId}`); + throw new NotificationDomainError('Notification not found'); + } - if (notification.recipientId !== command.recipientId) { - throw new NotificationDomainError('Cannot mark another user\'s notification as read'); - } + if (notification.recipientId !== command.recipientId) { + this.logger.warn(`Unauthorized attempt to mark notification ${command.notificationId}. Recipient ID mismatch.`); + throw new NotificationDomainError('Cannot mark another user\'s notification as read'); + } - if (!notification.isUnread()) { - return; // Already read, nothing to do - } + if (!notification.isUnread()) { + this.logger.info(`Notification ${command.notificationId} is already read. Skipping update.`); + return; // Already read, nothing to do + } - const updatedNotification = notification.markAsRead(); - await this.notificationRepository.update(updatedNotification); + const updatedNotification = notification.markAsRead(); + await this.notificationRepository.update(updatedNotification); + this.logger.info(`Notification ${command.notificationId} successfully marked as read.`); + } catch (error) { + this.logger.error(`Failed to mark notification ${command.notificationId} as read: ${error.message}`); + throw error; + } } } diff --git a/packages/notifications/application/use-cases/NotificationPreferencesUseCases.ts b/packages/notifications/application/use-cases/NotificationPreferencesUseCases.ts index 9169ff514..76b39b8eb 100644 --- a/packages/notifications/application/use-cases/NotificationPreferencesUseCases.ts +++ b/packages/notifications/application/use-cases/NotificationPreferencesUseCases.ts @@ -5,6 +5,7 @@ */ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; import { NotificationPreference } from '../../domain/entities/NotificationPreference'; import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference'; import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository'; @@ -17,10 +18,19 @@ import { NotificationDomainError } from '../../domain/errors/NotificationDomainE export class GetNotificationPreferencesQuery implements AsyncUseCase { constructor( private readonly preferenceRepository: INotificationPreferenceRepository, + private readonly logger: ILogger, ) {} async execute(driverId: string): Promise { - return this.preferenceRepository.getOrCreateDefault(driverId); + this.logger.debug(`Fetching notification preferences for driver: ${driverId}`); + try { + const preferences = await this.preferenceRepository.getOrCreateDefault(driverId); + this.logger.info(`Successfully fetched preferences for driver: ${driverId}`); + return preferences; + } catch (error) { + this.logger.error(`Failed to fetch preferences for driver: ${driverId}`, error); + throw error; + } } } @@ -36,12 +46,20 @@ export interface UpdateChannelPreferenceCommand { export class UpdateChannelPreferenceUseCase implements AsyncUseCase { constructor( private readonly preferenceRepository: INotificationPreferenceRepository, + private readonly logger: ILogger, ) {} async execute(command: UpdateChannelPreferenceCommand): Promise { - const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); - const updated = preferences.updateChannel(command.channel, command.preference); - await this.preferenceRepository.save(updated); + this.logger.debug(`Updating channel preference for driver: ${command.driverId}, channel: ${command.channel}, preference: ${command.preference}`); + try { + const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); + const updated = preferences.updateChannel(command.channel, command.preference); + await this.preferenceRepository.save(updated); + this.logger.info(`Successfully updated channel preference for driver: ${command.driverId}`); + } catch (error) { + this.logger.error(`Failed to update channel preference for driver: ${command.driverId}, channel: ${command.channel}`, error); + throw error; + } } } @@ -57,12 +75,20 @@ export interface UpdateTypePreferenceCommand { export class UpdateTypePreferenceUseCase implements AsyncUseCase { constructor( private readonly preferenceRepository: INotificationPreferenceRepository, + private readonly logger: ILogger, ) {} async execute(command: UpdateTypePreferenceCommand): Promise { - const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); - const updated = preferences.updateTypePreference(command.type, command.preference); - await this.preferenceRepository.save(updated); + this.logger.debug(`Updating type preference for driver: ${command.driverId}, type: ${command.type}, preference: ${command.preference}`); + try { + const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); + const updated = preferences.updateTypePreference(command.type, command.preference); + await this.preferenceRepository.save(updated); + this.logger.info(`Successfully updated type preference for driver: ${command.driverId}`); + } catch (error) { + this.logger.error(`Failed to update type preference for driver: ${command.driverId}, type: ${command.type}`, error); + throw error; + } } } @@ -78,20 +104,30 @@ export interface UpdateQuietHoursCommand { export class UpdateQuietHoursUseCase implements AsyncUseCase { constructor( private readonly preferenceRepository: INotificationPreferenceRepository, + private readonly logger: ILogger, ) {} async execute(command: UpdateQuietHoursCommand): Promise { - // Validate hours if provided - if (command.startHour !== undefined && (command.startHour < 0 || command.startHour > 23)) { - throw new NotificationDomainError('Start hour must be between 0 and 23'); - } - if (command.endHour !== undefined && (command.endHour < 0 || command.endHour > 23)) { - throw new NotificationDomainError('End hour must be between 0 and 23'); - } + this.logger.debug(`Updating quiet hours for driver: ${command.driverId}, startHour: ${command.startHour}, endHour: ${command.endHour}`); + try { + // Validate hours if provided + if (command.startHour !== undefined && (command.startHour < 0 || command.startHour > 23)) { + this.logger.warn(`Invalid start hour provided for driver: ${command.driverId}. startHour: ${command.startHour}`); + throw new NotificationDomainError('Start hour must be between 0 and 23'); + } + if (command.endHour !== undefined && (command.endHour < 0 || command.endHour > 23)) { + this.logger.warn(`Invalid end hour provided for driver: ${command.driverId}. endHour: ${command.endHour}`); + throw new NotificationDomainError('End hour must be between 0 and 23'); + } - const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); - const updated = preferences.updateQuietHours(command.startHour, command.endHour); - await this.preferenceRepository.save(updated); + const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); + const updated = preferences.updateQuietHours(command.startHour, command.endHour); + await this.preferenceRepository.save(updated); + this.logger.info(`Successfully updated quiet hours for driver: ${command.driverId}`); + } catch (error) { + this.logger.error(`Failed to update quiet hours for driver: ${command.driverId}`, error); + throw error; + } } } diff --git a/packages/notifications/application/use-cases/SendNotificationUseCase.ts b/packages/notifications/application/use-cases/SendNotificationUseCase.ts index b34f91d60..282247146 100644 --- a/packages/notifications/application/use-cases/SendNotificationUseCase.ts +++ b/packages/notifications/application/use-cases/SendNotificationUseCase.ts @@ -7,6 +7,7 @@ import { v4 as uuid } from 'uuid'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import { Notification } from '../../domain/entities/Notification'; import type { NotificationData } from '../../domain/entities/Notification'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; @@ -48,11 +49,17 @@ export class SendNotificationUseCase implements AsyncUseCase { - // Get recipient's preferences - const preferences = await this.preferenceRepository.getOrCreateDefault(command.recipientId); + this.logger.debug('Executing SendNotificationUseCase', { command }); + try { + // Get recipient's preferences + this.logger.debug('Checking notification preferences.', { type: command.type, recipientId: command.recipientId }); + const preferences = await this.preferenceRepository.getOrCreateDefault(command.recipientId); // Check if this notification type is enabled if (!preferences.isTypeEnabled(command.type)) { diff --git a/packages/notifications/infrastructure/repositories/InMemoryNotificationPreferenceRepository.ts b/packages/notifications/infrastructure/repositories/InMemoryNotificationPreferenceRepository.ts index 040b86e98..a4b0a8cac 100644 --- a/packages/notifications/infrastructure/repositories/InMemoryNotificationPreferenceRepository.ts +++ b/packages/notifications/infrastructure/repositories/InMemoryNotificationPreferenceRepository.ts @@ -6,36 +6,79 @@ import { NotificationPreference } from '../../domain/entities/NotificationPreference'; import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryNotificationPreferenceRepository implements INotificationPreferenceRepository { private preferences: Map = new Map(); + private readonly logger: ILogger; - constructor(initialPreferences: NotificationPreference[] = []) { + constructor(logger: ILogger, initialPreferences: NotificationPreference[] = []) { + this.logger = logger; + this.logger.info('InMemoryNotificationPreferenceRepository initialized.'); initialPreferences.forEach(pref => { this.preferences.set(pref.driverId, pref); + this.logger.debug(`Seeded preference for driver: ${pref.driverId}`); }); } async findByDriverId(driverId: string): Promise { - return this.preferences.get(driverId) || 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; + } } async save(preference: NotificationPreference): Promise { - this.preferences.set(preference.driverId, preference); + 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; + } } async delete(driverId: string): Promise { - this.preferences.delete(driverId); + 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; + } } async getOrCreateDefault(driverId: string): Promise { - const existing = this.preferences.get(driverId); - if (existing) { - return existing; - } + 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; + } - const defaultPreference = NotificationPreference.createDefault(driverId); - this.preferences.set(driverId, defaultPreference); - return defaultPreference; + 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; + } } } \ No newline at end of file diff --git a/packages/notifications/infrastructure/repositories/InMemoryNotificationRepository.ts b/packages/notifications/infrastructure/repositories/InMemoryNotificationRepository.ts index 23eb3940e..4779ecdd8 100644 --- a/packages/notifications/infrastructure/repositories/InMemoryNotificationRepository.ts +++ b/packages/notifications/infrastructure/repositories/InMemoryNotificationRepository.ts @@ -7,77 +7,169 @@ import { Notification } from '../../domain/entities/Notification'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { NotificationType } from '../../domain/types/NotificationTypes'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryNotificationRepository implements INotificationRepository { private notifications: Map = new Map(); + private readonly logger: ILogger; - constructor(initialNotifications: Notification[] = []) { + constructor(logger: ILogger, initialNotifications: Notification[] = []) { + this.logger = logger; + this.logger.info('InMemoryNotificationRepository initialized.'); initialNotifications.forEach(notification => { this.notifications.set(notification.id, notification); + this.logger.debug(`Seeded notification: ${notification.id}`); }); } async findById(id: string): Promise { - return this.notifications.get(id) || null; + this.logger.debug(`Finding notification by ID: ${id}`); + try { + const notification = this.notifications.get(id) || null; + if (notification) { + this.logger.info(`Found notification with ID: ${id}`); + } else { + this.logger.warn(`Notification with ID ${id} not found.`); + } + return notification; + } catch (error) { + this.logger.error(`Error finding notification by ID ${id}:`, error); + throw error; + } } async findByRecipientId(recipientId: string): Promise { - return Array.from(this.notifications.values()) - .filter(n => n.recipientId === recipientId) - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + this.logger.debug(`Finding notifications for recipient ID: ${recipientId}`); + try { + const notifications = Array.from(this.notifications.values()) + .filter(n => n.recipientId === recipientId) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + this.logger.info(`Found ${notifications.length} notifications for recipient ID: ${recipientId}.`); + return notifications; + } catch (error) { + this.logger.error(`Error finding notifications for recipient ID ${recipientId}:`, error); + throw error; + } } async findUnreadByRecipientId(recipientId: string): Promise { - return Array.from(this.notifications.values()) - .filter(n => n.recipientId === recipientId && n.isUnread()) - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + this.logger.debug(`Finding unread notifications for recipient ID: ${recipientId}`); + try { + const notifications = Array.from(this.notifications.values()) + .filter(n => n.recipientId === recipientId && n.isUnread()) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + this.logger.info(`Found ${notifications.length} unread notifications for recipient ID: ${recipientId}.`); + return notifications; + } catch (error) { + this.logger.error(`Error finding unread notifications for recipient ID ${recipientId}:`, error); + throw error; + } } async findByRecipientIdAndType(recipientId: string, type: NotificationType): Promise { - return Array.from(this.notifications.values()) - .filter(n => n.recipientId === recipientId && n.type === type) - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + this.logger.debug(`Finding notifications for recipient ID: ${recipientId}, type: ${type}`); + try { + const notifications = Array.from(this.notifications.values()) + .filter(n => n.recipientId === recipientId && n.type === type) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + 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); + throw error; + } } async countUnreadByRecipientId(recipientId: string): Promise { - return Array.from(this.notifications.values()) - .filter(n => n.recipientId === recipientId && n.isUnread()) - .length; + this.logger.debug(`Counting unread notifications for recipient ID: ${recipientId}`); + try { + const count = Array.from(this.notifications.values()) + .filter(n => n.recipientId === recipientId && n.isUnread()) + .length; + this.logger.info(`Counted ${count} unread notifications for recipient ID: ${recipientId}.`); + return count; + } catch (error) { + this.logger.error(`Error counting unread notifications for recipient ID ${recipientId}:`, error); + throw error; + } } async create(notification: Notification): Promise { - if (this.notifications.has(notification.id)) { - throw new Error(`Notification with ID ${notification.id} already exists`); + this.logger.debug(`Creating notification: ${notification.id}`); + try { + if (this.notifications.has(notification.id)) { + this.logger.warn(`Notification with ID ${notification.id} already exists. Throwing error.`); + throw new Error(`Notification with ID ${notification.id} already exists`); + } + this.notifications.set(notification.id, notification); + this.logger.info(`Notification ${notification.id} created successfully.`); + } catch (error) { + this.logger.error(`Error creating notification ${notification.id}:`, error); + throw error; } - this.notifications.set(notification.id, notification); } async update(notification: Notification): Promise { - if (!this.notifications.has(notification.id)) { - throw new Error(`Notification with ID ${notification.id} not found`); + this.logger.debug(`Updating notification: ${notification.id}`); + try { + if (!this.notifications.has(notification.id)) { + this.logger.warn(`Notification with ID ${notification.id} not found for update. Throwing error.`); + throw new Error(`Notification with ID ${notification.id} not found`); + } + this.notifications.set(notification.id, notification); + this.logger.info(`Notification ${notification.id} updated successfully.`); + } catch (error) { + this.logger.error(`Error updating notification ${notification.id}:`, error); + throw error; } - this.notifications.set(notification.id, notification); } async delete(id: string): Promise { - this.notifications.delete(id); + this.logger.debug(`Deleting notification: ${id}`); + try { + if (this.notifications.delete(id)) { + this.logger.info(`Notification ${id} deleted successfully.`); + } else { + this.logger.warn(`Notification with ID ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting notification ${id}:`, error); + throw error; + } } async deleteAllByRecipientId(recipientId: string): Promise { - const toDelete = Array.from(this.notifications.values()) - .filter(n => n.recipientId === recipientId) - .map(n => n.id); - - toDelete.forEach(id => this.notifications.delete(id)); + 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); + + toDelete.forEach(id => this.notifications.delete(id)); + this.logger.info(`Deleted ${toDelete.length} notifications for recipient ID: ${recipientId}.`); + } catch (error) { + this.logger.error(`Error deleting all notifications for recipient ID ${recipientId}:`, error); + throw error; + } } async markAllAsReadByRecipientId(recipientId: string): Promise { - const toUpdate = Array.from(this.notifications.values()) - .filter(n => n.recipientId === recipientId && n.isUnread()); - - toUpdate.forEach(n => { - const updated = n.markAsRead(); - this.notifications.set(updated.id, updated); - }); + this.logger.debug(`Marking all notifications as read for recipient ID: ${recipientId}`); + try { + const toUpdate = Array.from(this.notifications.values()) + .filter(n => n.recipientId === recipientId && n.isUnread()); + + this.logger.info(`Found ${toUpdate.length} unread notifications to mark as read for recipient ID: ${recipientId}.`); + + toUpdate.forEach(n => { + const updated = n.markAsRead(); + this.notifications.set(updated.id, updated); + }); + this.logger.info(`Marked ${toUpdate.length} notifications as read for recipient ID: ${recipientId}.`); + } catch (error) { + this.logger.error(`Error marking all notifications as read for recipient ID ${recipientId}:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/AcceptSponsorshipRequestUseCase.ts b/packages/racing/application/use-cases/AcceptSponsorshipRequestUseCase.ts index 75dfcc82f..7e65224b6 100644 --- a/packages/racing/application/use-cases/AcceptSponsorshipRequestUseCase.ts +++ b/packages/racing/application/use-cases/AcceptSponsorshipRequestUseCase.ts @@ -5,6 +5,7 @@ * This creates an active sponsorship and notifies the sponsor. */ +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository'; import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; @@ -31,56 +32,73 @@ export class AcceptSponsorshipRequestUseCase private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository, private readonly seasonRepository: ISeasonRepository, + private readonly logger: ILogger, ) {} async execute(dto: AcceptSponsorshipRequestDTO): Promise { - // Find the request - const request = await this.sponsorshipRequestRepo.findById(dto.requestId); - if (!request) { - throw new Error('Sponsorship request not found'); - } - - if (!request.isPending()) { - throw new Error(`Cannot accept a ${request.status} sponsorship request`); - } - - // Accept the request - const acceptedRequest = request.accept(dto.respondedBy); - await this.sponsorshipRequestRepo.update(acceptedRequest); - - // If this is a season sponsorship, create the SeasonSponsorship record - let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - if (request.entityType === 'season') { - const season = await this.seasonRepository.findById(request.entityId); - if (!season) { - throw new Error('Season not found for sponsorship request'); + this.logger.debug(`Attempting to accept sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, respondedBy: dto.respondedBy }); + try { + // Find the request + const request = await this.sponsorshipRequestRepo.findById(dto.requestId); + if (!request) { + this.logger.warn(`Sponsorship request not found: ${dto.requestId}`, { requestId: dto.requestId }); + throw new Error('Sponsorship request not found'); } - const sponsorship = SeasonSponsorship.create({ - id: sponsorshipId, - seasonId: season.id, - leagueId: season.leagueId, - sponsorId: request.sponsorId, - tier: request.tier, - pricing: request.offeredAmount, - status: 'active', - }); - await this.seasonSponsorshipRepo.create(sponsorship); + if (!request.isPending()) { + this.logger.warn(`Cannot accept a ${request.status} sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, status: request.status }); + throw new Error(`Cannot accept a ${request.status} sponsorship request`); + } + + this.logger.info(`Sponsorship request ${dto.requestId} found and is pending. Proceeding with acceptance.`, { requestId: dto.requestId }); + + // Accept the request + const acceptedRequest = request.accept(dto.respondedBy); + await this.sponsorshipRequestRepo.update(acceptedRequest); + this.logger.debug(`Sponsorship request ${dto.requestId} accepted and updated in repository.`, { requestId: dto.requestId }); + + // If this is a season sponsorship, create the SeasonSponsorship record + let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + if (request.entityType === 'season') { + this.logger.debug(`Sponsorship request ${dto.requestId} is for a season. Creating SeasonSponsorship record.`, { requestId: dto.requestId, entityType: request.entityType }); + const season = await this.seasonRepository.findById(request.entityId); + if (!season) { + this.logger.warn(`Season not found for sponsorship request ${dto.requestId} and entityId ${request.entityId}`, { requestId: dto.requestId, entityId: request.entityId }); + throw new Error('Season not found for sponsorship request'); + } + + const sponsorship = SeasonSponsorship.create({ + id: sponsorshipId, + seasonId: season.id, + leagueId: season.leagueId, + sponsorId: request.sponsorId, + tier: request.tier, + pricing: request.offeredAmount, + status: 'active', + }); + await this.seasonSponsorshipRepo.create(sponsorship); + this.logger.info(`Season sponsorship ${sponsorshipId} created for request ${dto.requestId}.`, { sponsorshipId, requestId: dto.requestId }); + } + + // TODO: In a real implementation, we would: + // 1. Create notification for the sponsor + // 2. Process payment + // 3. Update wallet balances + + this.logger.info(`Sponsorship request ${acceptedRequest.id} successfully accepted.`, { requestId: acceptedRequest.id, sponsorshipId }); + + return { + requestId: acceptedRequest.id, + sponsorshipId, + status: 'accepted', + acceptedAt: acceptedRequest.respondedAt!, + platformFee: acceptedRequest.getPlatformFee().amount, + netAmount: acceptedRequest.getNetAmount().amount, + }; + } catch (error: any) { + this.logger.error(`Failed to accept sponsorship request ${dto.requestId}: ${error.message}`, { requestId: dto.requestId, error: error.message, stack: error.stack }); + throw error; } - - // TODO: In a real implementation, we would: - // 1. Create notification for the sponsor - // 2. Process payment - // 3. Update wallet balances - - return { - requestId: acceptedRequest.id, - sponsorshipId, - status: 'accepted', - acceptedAt: acceptedRequest.respondedAt!, - platformFee: acceptedRequest.getPlatformFee().amount, - netAmount: acceptedRequest.getNetAmount().amount, - }; } -} \ No newline at end of file +} diff --git a/packages/racing/application/use-cases/ApplyForSponsorshipUseCase.ts b/packages/racing/application/use-cases/ApplyForSponsorshipUseCase.ts index 708cee70f..7f6196c5b 100644 --- a/packages/racing/application/use-cases/ApplyForSponsorshipUseCase.ts +++ b/packages/racing/application/use-cases/ApplyForSponsorshipUseCase.ts @@ -16,6 +16,7 @@ import { EntityNotFoundError, BusinessRuleViolationError, } from '../errors/RacingApplicationError'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface ApplyForSponsorshipDTO { sponsorId: string; @@ -40,22 +41,28 @@ export class ApplyForSponsorshipUseCase private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository, private readonly sponsorRepo: ISponsorRepository, + private readonly logger: ILogger, ) {} async execute(dto: ApplyForSponsorshipDTO): Promise { + this.logger.debug('Attempting to apply for sponsorship', { dto }); + // Validate sponsor exists const sponsor = await this.sponsorRepo.findById(dto.sponsorId); if (!sponsor) { + this.logger.error('Sponsor not found', { sponsorId: dto.sponsorId }); throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId }); } // Check if entity accepts sponsorship applications const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId); if (!pricing) { + this.logger.warn('Sponsorship pricing not set up for this entity', { entityType: dto.entityType, entityId: dto.entityId }); throw new BusinessRuleViolationError('This entity has not set up sponsorship pricing'); } if (!pricing.acceptingApplications) { + this.logger.warn('Entity not accepting sponsorship applications', { entityType: dto.entityType, entityId: dto.entityId }); throw new BusinessRuleViolationError( 'This entity is not currently accepting sponsorship applications', ); @@ -64,6 +71,7 @@ export class ApplyForSponsorshipUseCase // Check if the requested tier slot is available const slotAvailable = pricing.isSlotAvailable(dto.tier); if (!slotAvailable) { + this.logger.warn(`No ${dto.tier} sponsorship slots are available for entity ${dto.entityId}`); throw new BusinessRuleViolationError( `No ${dto.tier} sponsorship slots are available`, ); @@ -76,6 +84,7 @@ export class ApplyForSponsorshipUseCase dto.entityId, ); if (hasPending) { + this.logger.warn('Sponsor already has a pending request for this entity', { sponsorId: dto.sponsorId, entityType: dto.entityType, entityId: dto.entityId }); throw new BusinessRuleViolationError( 'You already have a pending sponsorship request for this entity', ); @@ -84,6 +93,7 @@ export class ApplyForSponsorshipUseCase // Validate offered amount meets minimum price const minPrice = pricing.getPrice(dto.tier); if (minPrice && dto.offeredAmount < minPrice.amount) { + this.logger.warn(`Offered amount ${dto.offeredAmount} is less than minimum ${minPrice.amount} for entity ${dto.entityId}, tier ${dto.tier}`); throw new BusinessRuleViolationError( `Offered amount must be at least ${minPrice.format()}`, ); diff --git a/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts b/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts index 39088438d..2636ece93 100644 --- a/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts +++ b/packages/racing/application/use-cases/ApplyPenaltyUseCase.ts @@ -12,6 +12,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository' import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import { randomUUID } from 'crypto'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface ApplyPenaltyCommand { raceId: string; @@ -31,57 +32,73 @@ export class ApplyPenaltyUseCase private readonly protestRepository: IProtestRepository, private readonly raceRepository: IRaceRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, + private readonly logger: ILogger, ) {} async execute(command: ApplyPenaltyCommand): Promise<{ penaltyId: string }> { - // Validate race exists - const race = await this.raceRepository.findById(command.raceId); - if (!race) { - throw new Error('Race not found'); - } - - // Validate steward has authority (owner or admin of the league) - const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); - const stewardMembership = memberships.find( - m => m.driverId === command.stewardId && m.status === 'active' - ); - - if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) { - throw new Error('Only league owners and admins can apply penalties'); - } - - // If linked to a protest, validate the protest exists and is upheld - if (command.protestId) { - const protest = await this.protestRepository.findById(command.protestId); - if (!protest) { - throw new Error('Protest not found'); + this.logger.debug('ApplyPenaltyUseCase: Executing with command', command); + try { + // Validate race exists + const race = await this.raceRepository.findById(command.raceId); + if (!race) { + this.logger.warn(`ApplyPenaltyUseCase: Race with ID ${command.raceId} not found.`); + throw new Error('Race not found'); } - if (protest.status !== 'upheld') { - throw new Error('Can only create penalties for upheld protests'); + this.logger.debug(`ApplyPenaltyUseCase: Race ${race.id} found.`); + + // Validate steward has authority (owner or admin of the league) + const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); + const stewardMembership = memberships.find( + m => m.driverId === command.stewardId && m.status === 'active' + ); + + if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) { + this.logger.warn(`ApplyPenaltyUseCase: Steward ${command.stewardId} does not have authority for league ${race.leagueId}.`); + throw new Error('Only league owners and admins can apply penalties'); } - if (protest.raceId !== command.raceId) { - throw new Error('Protest is not for this race'); + this.logger.debug(`ApplyPenaltyUseCase: Steward ${command.stewardId} has authority.`); + + // If linked to a protest, validate the protest exists and is upheld + if (command.protestId) { + const protest = await this.protestRepository.findById(command.protestId); + if (!protest) { + this.logger.warn(`ApplyPenaltyUseCase: Protest with ID ${command.protestId} not found.`); + throw new Error('Protest not found'); + } + if (protest.status !== 'upheld') { + this.logger.warn(`ApplyPenaltyUseCase: Protest ${protest.id} is not upheld. Status: ${protest.status}`); + throw new Error('Can only create penalties for upheld protests'); + } + if (protest.raceId !== command.raceId) { + this.logger.warn(`ApplyPenaltyUseCase: Protest ${protest.id} is for race ${protest.raceId}, not ${command.raceId}.`); + throw new Error('Protest is not for this race'); + } + this.logger.debug(`ApplyPenaltyUseCase: Protest ${protest.id} is valid and upheld.`); } + + // Create the penalty + const penalty = Penalty.create({ + id: randomUUID(), + leagueId: race.leagueId, + raceId: command.raceId, + driverId: command.driverId, + type: command.type, + ...(command.value !== undefined ? { value: command.value } : {}), + reason: command.reason, + ...(command.protestId !== undefined ? { protestId: command.protestId } : {}), + issuedBy: command.stewardId, + status: 'pending', + issuedAt: new Date(), + ...(command.notes !== undefined ? { notes: command.notes } : {}), + }); + + await this.penaltyRepository.create(penalty); + this.logger.info(`ApplyPenaltyUseCase: Successfully applied penalty ${penalty.id} for driver ${command.driverId} in race ${command.raceId}.`); + + return { penaltyId: penalty.id }; + } catch (error) { + this.logger.error('ApplyPenaltyUseCase: Failed to apply penalty', { command, error: error.message }); + throw error; } - - // Create the penalty - const penalty = Penalty.create({ - id: randomUUID(), - leagueId: race.leagueId, - raceId: command.raceId, - driverId: command.driverId, - type: command.type, - ...(command.value !== undefined ? { value: command.value } : {}), - reason: command.reason, - ...(command.protestId !== undefined ? { protestId: command.protestId } : {}), - issuedBy: command.stewardId, - status: 'pending', - issuedAt: new Date(), - ...(command.notes !== undefined ? { notes: command.notes } : {}), - }); - - await this.penaltyRepository.create(penalty); - - return { penaltyId: penalty.id }; } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts b/packages/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts index 5d90a18ec..29b3441f4 100644 --- a/packages/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts +++ b/packages/racing/application/use-cases/ApproveTeamJoinRequestUseCase.ts @@ -1,3 +1,4 @@ +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; import type { TeamMembership, @@ -12,24 +13,31 @@ export class ApproveTeamJoinRequestUseCase implements AsyncUseCase { constructor( private readonly membershipRepository: ITeamMembershipRepository, + private readonly logger: ILogger, ) {} async execute(command: ApproveTeamJoinRequestCommandDTO): Promise { const { requestId } = command; + this.logger.debug( + `Attempting to approve team join request with ID: ${requestId}`, + ); // There is no repository method to look up a single request by ID, - // so we rely on the repository implementation to surface all relevant - // requests via getJoinRequests and search by ID here. - const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests( - // For the in-memory fake used in tests, the teamId argument is ignored - // and all requests are returned. - '' as string, - ); - const request = allRequests.find((r) => r.id === requestId); + try { + // There is no repository method to look up a single request by ID, + // so we rely on the repository implementation to surface all relevant + // requests via getJoinRequests and search by ID here. + const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests( + // For the in-memory fake used in tests, the teamId argument is ignored + // and all requests are returned.' + '' as string, + ); + const request = allRequests.find((r) => r.id === requestId); - if (!request) { - throw new Error('Join request not found'); - } + if (!request) { + this.logger.warn(`Team join request with ID ${requestId} not found`); + throw new Error('Join request not found'); + } const membership: TeamMembership = { teamId: request.teamId, @@ -40,6 +48,14 @@ export class ApproveTeamJoinRequestUseCase }; await this.membershipRepository.saveMembership(membership); + this.logger.info( + `Team membership created for driver ${request.driverId} in team ${request.teamId} from request ${requestId}`, + ); await this.membershipRepository.removeJoinRequest(requestId); + this.logger.info(`Team join request with ID ${requestId} removed`); + } catch (error) { + this.logger.error(`Failed to approve team join request ${requestId}:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/CancelRaceUseCase.ts b/packages/racing/application/use-cases/CancelRaceUseCase.ts index 84181eb21..cbdfaad6a 100644 --- a/packages/racing/application/use-cases/CancelRaceUseCase.ts +++ b/packages/racing/application/use-cases/CancelRaceUseCase.ts @@ -1,5 +1,6 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use Case: CancelRaceUseCase @@ -18,17 +19,26 @@ export class CancelRaceUseCase implements AsyncUseCase { constructor( private readonly raceRepository: IRaceRepository, + private readonly logger: ILogger, ) {} async execute(command: CancelRaceCommandDTO): Promise { const { raceId } = command; + this.logger.debug(`[CancelRaceUseCase] Executing for raceId: ${raceId}`); - const race = await this.raceRepository.findById(raceId); - if (!race) { - throw new Error('Race not found'); + try { + const race = await this.raceRepository.findById(raceId); + if (!race) { + this.logger.warn(`[CancelRaceUseCase] Race with ID ${raceId} not found.`); + throw new Error('Race not found'); + } + + const cancelledRace = race.cancel(); + await this.raceRepository.update(cancelledRace); + this.logger.info(`[CancelRaceUseCase] Race ${raceId} cancelled successfully.`); + } catch (error) { + this.logger.error(`[CancelRaceUseCase] Error cancelling race ${raceId}:`, error); + throw error; } - - const cancelledRace = race.cancel(); - await this.raceRepository.update(cancelledRace); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/CompleteRaceUseCase.ts b/packages/racing/application/use-cases/CompleteRaceUseCase.ts index a0545cd5a..d67004b74 100644 --- a/packages/racing/application/use-cases/CompleteRaceUseCase.ts +++ b/packages/racing/application/use-cases/CompleteRaceUseCase.ts @@ -6,6 +6,7 @@ import type { DriverRatingProvider } from '../ports/DriverRatingProvider'; import { Result } from '../../domain/entities/Result'; import { Standing } from '../../domain/entities/Standing'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use Case: CompleteRaceUseCase @@ -30,39 +31,55 @@ export class CompleteRaceUseCase private readonly resultRepository: IResultRepository, private readonly standingRepository: IStandingRepository, private readonly driverRatingProvider: DriverRatingProvider, + private readonly logger: ILogger, ) {} async execute(command: CompleteRaceCommandDTO): Promise { + this.logger.debug(`Executing CompleteRaceUseCase for raceId: ${command.raceId}`); const { raceId } = command; - const race = await this.raceRepository.findById(raceId); - if (!race) { - throw new Error('Race not found'); + try { + const race = await this.raceRepository.findById(raceId); + if (!race) { + this.logger.error(`Race with id ${raceId} not found.`); + throw new Error('Race not found'); + } + this.logger.debug(`Race ${raceId} found. Status: ${race.status}`); + + // Get registered drivers for this race + const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId); + if (registeredDriverIds.length === 0) { + this.logger.warn(`No registered drivers found for race ${raceId}.`); + throw new Error('Cannot complete race with no registered drivers'); + } + this.logger.info(`${registeredDriverIds.length} drivers registered for race ${raceId}. Generating results.`); + + // Get driver ratings + const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds); + this.logger.debug(`Driver ratings fetched for ${registeredDriverIds.length} drivers.`); + + // Generate realistic race results + const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings); + this.logger.debug(`Generated ${results.length} race results for race ${raceId}.`); + + // Save results + for (const result of results) { + await this.resultRepository.create(result); + } + this.logger.info(`Persisted ${results.length} race results for race ${raceId}.`); + + // Update standings + await this.updateStandings(race.leagueId, results); + this.logger.info(`Standings updated for league ${race.leagueId}.`); + + // Complete the race + const completedRace = race.complete(); + await this.raceRepository.update(completedRace); + this.logger.info(`Race ${raceId} successfully completed and updated.`); + } catch (error) { + this.logger.error(`Failed to complete race ${raceId}: ${error.message}`, error as Error); + throw error; } - - // Get registered drivers for this race - const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId); - if (registeredDriverIds.length === 0) { - throw new Error('Cannot complete race with no registered drivers'); - } - - // Get driver ratings - const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds); - - // Generate realistic race results - const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings); - - // Save results - for (const result of results) { - await this.resultRepository.create(result); - } - - // Update standings - await this.updateStandings(race.leagueId, results); - - // Complete the race - const completedRace = race.complete(); - await this.raceRepository.update(completedRace); } private generateRaceResults( @@ -70,6 +87,7 @@ export class CompleteRaceUseCase driverIds: string[], driverRatings: Map ): Result[] { + this.logger.debug(`Generating race results for race ${raceId} with ${driverIds.length} drivers.`); // Create driver performance data const driverPerformances = driverIds.map(driverId => ({ driverId, @@ -83,6 +101,7 @@ export class CompleteRaceUseCase const perfB = b.rating + (b.randomFactor * 200); return perfB - perfA; // Higher performance first }); + this.logger.debug(`Driver performances sorted for race ${raceId}.`); // Generate qualifying results for start positions (similar but different from race results) const qualiPerformances = driverPerformances.map(p => ({ @@ -94,6 +113,7 @@ export class CompleteRaceUseCase const perfB = b.rating + (b.randomFactor * 150); return perfB - perfA; }); + this.logger.debug(`Qualifying performances generated for race ${raceId}.`); // Generate results const results: Result[] = []; @@ -123,11 +143,13 @@ export class CompleteRaceUseCase }) ); } + this.logger.debug(`Individual results created for race ${raceId}.`); return results; } private async updateStandings(leagueId: string, results: Result[]): Promise { + this.logger.debug(`Updating standings for league ${leagueId} with ${results.length} results.`); // Group results by driver const resultsByDriver = new Map(); for (const result of results) { @@ -135,6 +157,7 @@ export class CompleteRaceUseCase existing.push(result); resultsByDriver.set(result.driverId, existing); } + this.logger.debug(`Results grouped by driver for league ${leagueId}.`); // Update or create standings for each driver for (const [driverId, driverResults] of resultsByDriver) { @@ -145,6 +168,9 @@ export class CompleteRaceUseCase leagueId, driverId, }); + this.logger.debug(`Created new standing for driver ${driverId} in league ${leagueId}.`); + } else { + this.logger.debug(`Found existing standing for driver ${driverId} in league ${leagueId}.`); } // Add all results for this driver (should be just one for this race) @@ -155,6 +181,8 @@ export class CompleteRaceUseCase } await this.standingRepository.save(standing); + this.logger.debug(`Standing saved for driver ${driverId} in league ${leagueId}.`); } + this.logger.info(`Standings update complete for league ${leagueId}.`); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/CompleteRaceUseCaseWithRatings.ts b/packages/racing/application/use-cases/CompleteRaceUseCaseWithRatings.ts index beec7ab12..aec546ae1 100644 --- a/packages/racing/application/use-cases/CompleteRaceUseCaseWithRatings.ts +++ b/packages/racing/application/use-cases/CompleteRaceUseCaseWithRatings.ts @@ -8,6 +8,7 @@ import { Standing } from '../../domain/entities/Standing'; import { RaceResultGenerator } from '../utils/RaceResultGenerator'; import { RatingUpdateService } from '@gridpilot/identity/domain/services/RatingUpdateService'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Enhanced CompleteRaceUseCase that includes rating updates @@ -25,42 +26,65 @@ export class CompleteRaceUseCaseWithRatings private readonly standingRepository: IStandingRepository, private readonly driverRatingProvider: DriverRatingProvider, private readonly ratingUpdateService: RatingUpdateService, + private readonly logger: ILogger, ) {} async execute(command: CompleteRaceCommandDTO): Promise { const { raceId } = command; + this.logger.debug(`Attempting to complete race with ID: ${raceId}`); - const race = await this.raceRepository.findById(raceId); - if (!race) { - throw new Error('Race not found'); + try { + const race = await this.raceRepository.findById(raceId); + if (!race) { + this.logger.error(`Race not found for ID: ${raceId}`); + throw new Error('Race not found'); + } + this.logger.debug(`Found race: ${race.id}`); + + // Get registered drivers for this race + const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId); + if (registeredDriverIds.length === 0) { + this.logger.warn(`No registered drivers for race ID: ${raceId}. Cannot complete race.`); + throw new Error('Cannot complete race with no registered drivers'); + } + this.logger.debug(`Found ${registeredDriverIds.length} registered drivers for race ID: ${raceId}`); + + // Get driver ratings + this.logger.debug('Fetching driver ratings...'); + const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds); + this.logger.debug('Driver ratings fetched.'); + + // Generate realistic race results + this.logger.debug('Generating race results...'); + const results = RaceResultGenerator.generateRaceResults(raceId, registeredDriverIds, driverRatings); + this.logger.info(`Generated ${results.length} race results for race ID: ${raceId}`); + + // Save results + this.logger.debug('Saving race results...'); + for (const result of results) { + await this.resultRepository.create(result); + } + this.logger.info('Race results saved successfully.'); + + // Update standings + this.logger.debug(`Updating standings for league ID: ${race.leagueId}`); + await this.updateStandings(race.leagueId, results); + this.logger.info('Standings updated successfully.'); + + // Update driver ratings based on performance + this.logger.debug('Updating driver ratings...'); + await this.updateDriverRatings(results, registeredDriverIds.length); + this.logger.info('Driver ratings updated successfully.'); + + // Complete the race + this.logger.debug(`Marking race ID: ${raceId} as complete...`); + const completedRace = race.complete(); + await this.raceRepository.update(completedRace); + this.logger.info(`Race ID: ${raceId} completed successfully.`); + } catch (error: any) { + this.logger.error(`Error completing race ${raceId}: ${error.message}`); + throw error; } - - // Get registered drivers for this race - const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId); - if (registeredDriverIds.length === 0) { - throw new Error('Cannot complete race with no registered drivers'); - } - - // Get driver ratings - const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds); - - // Generate realistic race results - const results = RaceResultGenerator.generateRaceResults(raceId, registeredDriverIds, driverRatings); - - // Save results - for (const result of results) { - await this.resultRepository.create(result); - } - - // Update standings - await this.updateStandings(race.leagueId, results); - - // Update driver ratings based on performance - await this.updateDriverRatings(results, registeredDriverIds.length); - - // Complete the race - const completedRace = race.complete(); - await this.raceRepository.update(completedRace); } private async updateStandings(leagueId: string, results: Result[]): Promise { @@ -105,4 +129,4 @@ export class CompleteRaceUseCaseWithRatings await this.ratingUpdateService.updateDriverRatingsAfterRace(driverResults); } -} \ No newline at end of file +} diff --git a/packages/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts b/packages/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts index 650ea398c..875372684 100644 --- a/packages/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts +++ b/packages/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts @@ -6,6 +6,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { LeagueScoringPresetProvider, LeagueScoringPresetDTO, @@ -61,107 +62,136 @@ export class CreateLeagueWithSeasonAndScoringUseCase async execute( command: CreateLeagueWithSeasonAndScoringCommand, ): Promise { - this.validate(command); + this.logger.debug('Executing CreateLeagueWithSeasonAndScoringUseCase', { command }); + try { + this.validate(command); + this.logger.info('Command validated successfully.'); - const leagueId = uuidv4(); + const leagueId = uuidv4(); + this.logger.debug(`Generated leagueId: ${leagueId}`); - const league = League.create({ - id: leagueId, - name: command.name, - description: command.description ?? '', - ownerId: command.ownerId, - settings: { - // Presets are attached at scoring-config level; league settings use a stable points system id. - pointsSystem: 'custom', - ...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}), - }, - }); + const league = League.create({ + id: leagueId, + name: command.name, + description: command.description ?? '', + ownerId: command.ownerId, + settings: { + pointsSystem: 'custom', + ...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}), + }, + }); - await this.leagueRepository.create(league); + await this.leagueRepository.create(league); + this.logger.info(`League ${league.name} (${league.id}) created successfully.`); - const seasonId = uuidv4(); - const season = Season.create({ - id: seasonId, - leagueId: league.id, - gameId: command.gameId, - name: `${command.name} Season 1`, - year: new Date().getFullYear(), - order: 1, - status: 'active', - startDate: new Date(), - endDate: new Date(), - }); + const seasonId = uuidv4(); + this.logger.debug(`Generated seasonId: ${seasonId}`); + const season = Season.create({ + id: seasonId, + leagueId: league.id, + gameId: command.gameId, + name: `${command.name} Season 1`, + year: new Date().getFullYear(), + order: 1, + status: 'active', + startDate: new Date(), + endDate: new Date(), + }); - await this.seasonRepository.create(season); + await this.seasonRepository.create(season); + this.logger.info(`Season ${season.name} (${season.id}) created for league ${league.id}.`); - const presetId = command.scoringPresetId ?? 'club-default'; - const preset: LeagueScoringPresetDTO | undefined = - this.presetProvider.getPresetById(presetId); + const presetId = command.scoringPresetId ?? 'club-default'; + this.logger.debug(`Attempting to retrieve scoring preset: ${presetId}`); + const preset: LeagueScoringPresetDTO | undefined = + this.presetProvider.getPresetById(presetId); - if (!preset) { - throw new Error(`Unknown scoring preset: ${presetId}`); + if (!preset) { + this.logger.error(`Unknown scoring preset: ${presetId}`); + throw new Error(`Unknown scoring preset: ${presetId}`); + } + this.logger.info(`Scoring preset ${preset.name} (${preset.id}) retrieved.`); + + + const scoringConfig: LeagueScoringConfig = { + id: uuidv4(), + seasonId, + scoringPresetId: preset.id, + championships: [], + }; + + const fullConfigFactory = (await import( + '../../infrastructure/repositories/InMemoryScoringRepositories' + )) as typeof import('../../infrastructure/repositories/InMemoryScoringRepositories'); + + const presetFromInfra = fullConfigFactory.getLeagueScoringPresetById( + preset.id, + ); + if (!presetFromInfra) { + this.logger.error(`Preset registry missing preset: ${preset.id}`); + throw new Error(`Preset registry missing preset: ${preset.id}`); + } + this.logger.debug(`Preset from infrastructure retrieved for ${preset.id}.`); + + const infraConfig = presetFromInfra.createConfig({ seasonId }); + const finalConfig: LeagueScoringConfig = { + ...infraConfig, + scoringPresetId: preset.id, + }; + + await this.leagueScoringConfigRepository.save(finalConfig); + this.logger.info(`Scoring configuration saved for season ${seasonId}.`); + + const result = { + leagueId: league.id, + seasonId, + scoringPresetId: preset.id, + scoringPresetName: preset.name, + }; + this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result }); + return result; + } catch (error: any) { + this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', { + command, + error: error.message, + stack: error.stack, + }); + throw error; } - - const scoringConfig: LeagueScoringConfig = { - id: uuidv4(), - seasonId, - scoringPresetId: preset.id, - championships: [], - }; - - // For the initial alpha slice, we keep using the preset's config shape from the in-memory registry. - // The preset registry is responsible for building the full LeagueScoringConfig; we only attach the preset id here. - const fullConfigFactory = (await import( - '../../infrastructure/repositories/InMemoryScoringRepositories' - )) as typeof import('../../infrastructure/repositories/InMemoryScoringRepositories'); - - const presetFromInfra = fullConfigFactory.getLeagueScoringPresetById( - preset.id, - ); - if (!presetFromInfra) { - throw new Error(`Preset registry missing preset: ${preset.id}`); - } - - const infraConfig = presetFromInfra.createConfig({ seasonId }); - const finalConfig: LeagueScoringConfig = { - ...infraConfig, - scoringPresetId: preset.id, - }; - - await this.leagueScoringConfigRepository.save(finalConfig); - - return { - leagueId: league.id, - seasonId, - scoringPresetId: preset.id, - scoringPresetName: preset.name, - }; } private validate(command: CreateLeagueWithSeasonAndScoringCommand): void { + this.logger.debug('Validating CreateLeagueWithSeasonAndScoringCommand', { command }); if (!command.name || command.name.trim().length === 0) { + this.logger.warn('Validation failed: League name is required', { command }); throw new Error('League name is required'); } if (!command.ownerId || command.ownerId.trim().length === 0) { + this.logger.warn('Validation failed: League ownerId is required', { command }); throw new Error('League ownerId is required'); } if (!command.gameId || command.gameId.trim().length === 0) { + this.logger.warn('Validation failed: gameId is required', { command }); throw new Error('gameId is required'); } if (!command.visibility) { + this.logger.warn('Validation failed: visibility is required', { command }); throw new Error('visibility is required'); } if (command.maxDrivers !== undefined && command.maxDrivers <= 0) { + this.logger.warn('Validation failed: maxDrivers must be greater than 0 when provided', { command }); throw new Error('maxDrivers must be greater than 0 when provided'); } - // Validate visibility-specific constraints const visibility = LeagueVisibility.fromString(command.visibility); if (visibility.isRanked()) { - // Ranked (public) leagues require minimum 10 drivers for competitive integrity const driverCount = command.maxDrivers ?? 0; if (driverCount < MIN_RANKED_LEAGUE_DRIVERS) { + this.logger.warn( + `Validation failed: Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. Current setting: ${driverCount}.`, + { command } + ); throw new Error( `Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. ` + `Current setting: ${driverCount}. ` + @@ -169,5 +199,6 @@ export class CreateLeagueWithSeasonAndScoringUseCase ); } } + this.logger.debug('Validation successful.'); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts b/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts index 91e5de191..86b7bf638 100644 --- a/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts +++ b/packages/racing/application/use-cases/GetAllRacesPageDataUseCase.ts @@ -1,5 +1,6 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { IAllRacesPagePresenter, AllRacesPageResultDTO, @@ -7,59 +8,68 @@ import type { AllRacesListItemViewModel, AllRacesFilterOptionsViewModel, } from '../presenters/IAllRacesPagePresenter'; -import type { UseCase } from '@gridpilot/shared/application'; +import type { UseCase } => '@gridpilot/shared/application'; export class GetAllRacesPageDataUseCase implements UseCase { constructor( private readonly raceRepository: IRaceRepository, private readonly leagueRepository: ILeagueRepository, + private readonly logger: ILogger, ) {} async execute(_input: void, presenter: IAllRacesPagePresenter): Promise { - const [allRaces, allLeagues] = await Promise.all([ - this.raceRepository.findAll(), - this.leagueRepository.findAll(), - ]); + this.logger.debug('Executing GetAllRacesPageDataUseCase'); + try { + const [allRaces, allLeagues] = await Promise.all([ + this.raceRepository.findAll(), + this.leagueRepository.findAll(), + ]); + this.logger.info(`Found ${allRaces.length} races and ${allLeagues.length} leagues.`); - const leagueMap = new Map(allLeagues.map((league) => [league.id, league.name])); + const leagueMap = new Map(allLeagues.map((league) => [league.id, league.name])); - const races: AllRacesListItemViewModel[] = allRaces - .slice() - .sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()) - .map((race) => ({ - id: race.id, - track: race.track, - car: race.car, - scheduledAt: race.scheduledAt.toISOString(), - status: race.status, - leagueId: race.leagueId, - leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League', - strengthOfField: race.strengthOfField ?? null, - })); + const races: AllRacesListItemViewModel[] = allRaces + .slice() + .sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()) + .map((race) => ({ + id: race.id, + track: race.track, + car: race.car, + scheduledAt: race.scheduledAt.toISOString(), + status: race.status, + leagueId: race.leagueId, + leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League', + strengthOfField: race.strengthOfField ?? null, + })); - const uniqueLeagues = new Map(); - for (const league of allLeagues) { - uniqueLeagues.set(league.id, { id: league.id, name: league.name }); + const uniqueLeagues = new Map(); + for (const league of allLeagues) { + uniqueLeagues.set(league.id, { id: league.id, name: league.name }); + } + + const filters: AllRacesFilterOptionsViewModel = { + statuses: [ + { value: 'all', label: 'All Statuses' }, + { value: 'scheduled', label: 'Scheduled' }, + { value: 'running', label: 'Live' }, + { value: 'completed', label: 'Completed' }, + { value: 'cancelled', label: 'Cancelled' }, + ], + leagues: Array.from(uniqueLeagues.values()), + }; + + const viewModel: AllRacesPageViewModel = { + races, + filters, + }; + + presenter.reset(); + presenter.present(viewModel); + this.logger.debug('Successfully presented all races page data.'); + } catch (error) { + this.logger.error('Error executing GetAllRacesPageDataUseCase', { error }); + throw error; } - - const filters: AllRacesFilterOptionsViewModel = { - statuses: [ - { value: 'all', label: 'All Statuses' }, - { value: 'scheduled', label: 'Scheduled' }, - { value: 'running', label: 'Live' }, - { value: 'completed', label: 'Completed' }, - { value: 'cancelled', label: 'Cancelled' }, - ], - leagues: Array.from(uniqueLeagues.values()), - }; - - const viewModel: AllRacesPageViewModel = { - races, - filters, - }; - - presenter.reset(); - presenter.present(viewModel); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetAllTeamsUseCase.ts b/packages/racing/application/use-cases/GetAllTeamsUseCase.ts index e849e0f25..ce43a5cfe 100644 --- a/packages/racing/application/use-cases/GetAllTeamsUseCase.ts +++ b/packages/racing/application/use-cases/GetAllTeamsUseCase.ts @@ -6,6 +6,7 @@ import type { } from '../presenters/IAllTeamsPresenter'; import type { UseCase } from '@gridpilot/shared/application'; import type { Team } from '../../domain/entities/Team'; +import { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use Case for retrieving all teams. @@ -17,33 +18,44 @@ export class GetAllTeamsUseCase constructor( private readonly teamRepository: ITeamRepository, private readonly teamMembershipRepository: ITeamMembershipRepository, + private readonly logger: ILogger, ) {} async execute(_input: void, presenter: IAllTeamsPresenter): Promise { + this.logger.debug('Executing GetAllTeamsUseCase'); presenter.reset(); - const teams = await this.teamRepository.findAll(); + try { + const teams = await this.teamRepository.findAll(); + if (teams.length === 0) { + this.logger.warn('No teams found.'); + } - const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all( - teams.map(async (team) => { - const memberCount = await this.teamMembershipRepository.countByTeamId(team.id); - return { - id: team.id, - name: team.name, - tag: team.tag, - description: team.description, - ownerId: team.ownerId, - leagues: [...team.leagues], - createdAt: team.createdAt, - memberCount, - }; - }), - ); + const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all( + teams.map(async (team) => { + const memberCount = await this.teamMembershipRepository.countByTeamId(team.id); + return { + id: team.id, + name: team.name, + tag: team.tag, + description: team.description, + ownerId: team.ownerId, + leagues: [...team.leagues], + createdAt: team.createdAt, + memberCount, + }; + }), + ); - const dto: AllTeamsResultDTO = { - teams: enrichedTeams, - }; + const dto: AllTeamsResultDTO = { + teams: enrichedTeams, + }; - presenter.present(dto); + presenter.present(dto); + this.logger.info('Successfully retrieved all teams.'); + } catch (error) { + this.logger.error('Error retrieving all teams:', error); + throw error; // Re-throw the error after logging + } } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetDriverTeamUseCase.ts b/packages/racing/application/use-cases/GetDriverTeamUseCase.ts index 3f370d3bb..025d240d6 100644 --- a/packages/racing/application/use-cases/GetDriverTeamUseCase.ts +++ b/packages/racing/application/use-cases/GetDriverTeamUseCase.ts @@ -6,6 +6,7 @@ import type { DriverTeamViewModel, } from '../presenters/IDriverTeamPresenter'; import type { UseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use Case for retrieving a driver's team. @@ -17,23 +18,29 @@ export class GetDriverTeamUseCase constructor( private readonly teamRepository: ITeamRepository, private readonly membershipRepository: ITeamMembershipRepository, + private readonly logger: ILogger, // Kept for backward compatibility; callers must pass their own presenter. // eslint-disable-next-line @typescript-eslint/no-unused-vars public readonly presenter: IDriverTeamPresenter, ) {} async execute(input: { driverId: string }, presenter: IDriverTeamPresenter): Promise { + this.logger.debug(`Executing GetDriverTeamUseCase for driverId: ${input.driverId}`); presenter.reset(); const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId); if (!membership) { + this.logger.warn(`No active membership found for driverId: ${input.driverId}`); return; } + this.logger.debug(`Found membership for driverId: ${input.driverId}, teamId: ${membership.teamId}`); const team = await this.teamRepository.findById(membership.teamId); if (!team) { + this.logger.error(`Team not found for teamId: ${membership.teamId}`); return; } + this.logger.debug(`Found team for teamId: ${team.id}, name: ${team.name}`); const dto: DriverTeamResultDTO = { team, @@ -42,5 +49,6 @@ export class GetDriverTeamUseCase }; presenter.present(dto); + this.logger.info(`Successfully presented driver team for driverId: ${input.driverId}`); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetEntitySponsorshipPricingUseCase.ts b/packages/racing/application/use-cases/GetEntitySponsorshipPricingUseCase.ts index 24ba30c75..ccca04373 100644 --- a/packages/racing/application/use-cases/GetEntitySponsorshipPricingUseCase.ts +++ b/packages/racing/application/use-cases/GetEntitySponsorshipPricingUseCase.ts @@ -12,6 +12,7 @@ import type { SponsorableEntityType } from '../../domain/entities/SponsorshipReq import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface GetEntitySponsorshipPricingDTO { entityType: SponsorableEntityType; @@ -46,74 +47,115 @@ export class GetEntitySponsorshipPricingUseCase private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository, private readonly presenter: IEntitySponsorshipPricingPresenter, + private readonly logger: ILogger, ) {} async execute(dto: GetEntitySponsorshipPricingDTO): Promise { - const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId); - - if (!pricing) { - this.presenter.present(null); - return; - } - - // Count pending requests by tier - const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity( - dto.entityType, - dto.entityId + this.logger.debug( + `Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, + { dto }, ); - const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length; - const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length; - // Count filled slots (for seasons, check SeasonSponsorship table) - let filledMainSlots = 0; - let filledSecondarySlots = 0; + try { + const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId); - if (dto.entityType === 'season') { - const sponsorships = await this.seasonSponsorshipRepo.findBySeasonId(dto.entityId); - const activeSponsorships = sponsorships.filter(s => s.isActive()); - filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length; - filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length; - } + if (!pricing) { + this.logger.warn( + `No pricing found for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Presenting null.`, + { dto }, + ); + this.presenter.present(null); + return; + } - const result: GetEntitySponsorshipPricingResultDTO = { - entityType: dto.entityType, - entityId: dto.entityId, - acceptingApplications: pricing.acceptingApplications, - ...(pricing.customRequirements !== undefined - ? { customRequirements: pricing.customRequirements } - : {}), - }; + this.logger.debug(`Found pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, { pricing }); - if (pricing.mainSlot) { - const mainMaxSlots = pricing.mainSlot.maxSlots; - result.mainSlot = { - tier: 'main', - price: pricing.mainSlot.price.amount, - currency: pricing.mainSlot.price.currency, - formattedPrice: pricing.mainSlot.price.format(), - benefits: pricing.mainSlot.benefits, - available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots, - maxSlots: mainMaxSlots, - filledSlots: filledMainSlots, - pendingRequests: pendingMainCount, + // Count pending requests by tier + const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity( + dto.entityType, + dto.entityId, + ); + const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length; + const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length; + + this.logger.debug( + `Pending requests counts: main=${pendingMainCount}, secondary=${pendingSecondaryCount}`, + ); + + // Count filled slots (for seasons, check SeasonSponsorship table) + let filledMainSlots = 0; + let filledSecondarySlots = 0; + + if (dto.entityType === 'season') { + const sponsorships = await this.seasonSponsorshipRepo.findBySeasonId(dto.entityId); + const activeSponsorships = sponsorships.filter(s => s.isActive()); + filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length; + filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length; + this.logger.debug( + `Filled slots for season: main=${filledMainSlots}, secondary=${filledSecondarySlots}`, + ); + } + + const result: GetEntitySponsorshipPricingResultDTO = { + entityType: dto.entityType, + entityId: dto.entityId, + acceptingApplications: pricing.acceptingApplications, + ...(pricing.customRequirements !== undefined + ? { customRequirements: pricing.customRequirements } + : {}), }; - } - if (pricing.secondarySlots) { - const secondaryMaxSlots = pricing.secondarySlots.maxSlots; - result.secondarySlot = { - tier: 'secondary', - price: pricing.secondarySlots.price.amount, - currency: pricing.secondarySlots.price.currency, - formattedPrice: pricing.secondarySlots.price.format(), - benefits: pricing.secondarySlots.benefits, - available: pricing.secondarySlots.available && filledSecondarySlots < secondaryMaxSlots, - maxSlots: secondaryMaxSlots, - filledSlots: filledSecondarySlots, - pendingRequests: pendingSecondaryCount, - }; - } + if (pricing.mainSlot) { + const mainMaxSlots = pricing.mainSlot.maxSlots; + result.mainSlot = { + tier: 'main', + price: pricing.mainSlot.price.amount, + currency: pricing.mainSlot.price.currency, + formattedPrice: pricing.mainSlot.price.format(), + benefits: pricing.mainSlot.benefits, + available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots, + maxSlots: mainMaxSlots, + filledSlots: filledMainSlots, + pendingRequests: pendingMainCount, + }; + this.logger.debug(`Main slot pricing information processed`, { mainSlot: result.mainSlot }); + } - this.presenter.present(result); + if (pricing.secondarySlots) { + const secondaryMaxSlots = pricing.secondarySlots.maxSlots; + result.secondarySlot = { + tier: 'secondary', + price: pricing.secondarySlots.price.amount, + currency: pricing.secondarySlots.price.currency, + formattedPrice: pricing.secondarySlots.price.format(), + benefits: pricing.secondarySlots.benefits, + available: + pricing.secondarySlots.available && filledSecondarySlots < secondaryMaxSlots, + maxSlots: secondaryMaxSlots, + filledSlots: filledSecondarySlots, + pendingRequests: pendingSecondaryCount, + }; + this.logger.debug(`Secondary slot pricing information processed`, { + secondarySlot: result.secondarySlot, + }); + } + + this.logger.info( + `Successfully retrieved and processed entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, + { result }, + ); + this.presenter.present(result); + } catch (error: unknown) { + let errorMessage = 'An unknown error occurred'; + if (error instanceof Error) { + errorMessage = error.message; + } + this.logger.error( + `Failed to get entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Error: ${errorMessage}`, + { error, dto }, + ); + // Re-throw the error or present an error state if the presenter supports it + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetLeagueStatsUseCase.ts b/packages/racing/application/use-cases/GetLeagueStatsUseCase.ts index 1df9ef2de..409b5f44d 100644 --- a/packages/racing/application/use-cases/GetLeagueStatsUseCase.ts +++ b/packages/racing/application/use-cases/GetLeagueStatsUseCase.ts @@ -5,10 +5,11 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; -import type { IResultRepository } from '../../domain/repositories/IResultRepository'; +import type { IResultRepository } from '../../domain/repositories/IRaceRepository'; import type { DriverRatingProvider } from '../ports/DriverRatingProvider'; import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import { ILogger } from '../../../shared/src/logging/ILogger'; import { AverageStrengthOfFieldCalculator, type StrengthOfFieldCalculator, @@ -31,55 +32,78 @@ export class GetLeagueStatsUseCase private readonly resultRepository: IResultRepository, private readonly driverRatingProvider: DriverRatingProvider, public readonly presenter: ILeagueStatsPresenter, + private readonly logger: ILogger, sofCalculator?: StrengthOfFieldCalculator, ) { this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator(); } async execute(params: GetLeagueStatsUseCaseParams): Promise { + this.logger.debug( + `Executing GetLeagueStatsUseCase with params: ${JSON.stringify(params)}`, + ); const { leagueId } = params; - const league = await this.leagueRepository.findById(leagueId); - if (!league) { - throw new Error(`League ${leagueId} not found`); - } - - const races = await this.raceRepository.findByLeagueId(leagueId); - const completedRaces = races.filter(r => r.status === 'completed'); - const scheduledRaces = races.filter(r => r.status === 'scheduled'); - - // Calculate SOF for each completed race - const sofValues: number[] = []; - - for (const race of completedRaces) { - // Use stored SOF if available - if (race.strengthOfField) { - sofValues.push(race.strengthOfField); - continue; + try { + const league = await this.leagueRepository.findById(leagueId); + if (!league) { + this.logger.error(`League ${leagueId} not found`); + throw new Error(`League ${leagueId} not found`); } - // Otherwise calculate from results - const results = await this.resultRepository.findByRaceId(race.id); - if (results.length === 0) continue; + const races = await this.raceRepository.findByLeagueId(leagueId); + const completedRaces = races.filter(r => r.status === 'completed'); + const scheduledRaces = races.filter(r => r.status === 'scheduled'); + this.logger.info( + `Found ${races.length} races for league ${leagueId}: ${completedRaces.length} completed, ${scheduledRaces.length} scheduled. `, + ); - const driverIds = results.map(r => r.driverId); - const ratings = this.driverRatingProvider.getRatings(driverIds); - const driverRatings = driverIds - .filter(id => ratings.has(id)) - .map(id => ({ driverId: id, rating: ratings.get(id)! })); + // Calculate SOF for each completed race + const sofValues: number[] = []; - const sof = this.sofCalculator.calculate(driverRatings); - if (sof !== null) { - sofValues.push(sof); + for (const race of completedRaces) { + // Use stored SOF if available + if (race.strengthOfField) { + this.logger.debug( + `Using stored Strength of Field for race ${race.id}: ${race.strengthOfField}`, + ); + sofValues.push(race.strengthOfField); + continue; + } + + // Otherwise calculate from results + const results = await this.resultRepository.findByRaceId(race.id); + if (results.length === 0) { + this.logger.debug(`No results found for race ${race.id}. Skipping SOF calculation.`); + continue; + } + + const driverIds = results.map(r => r.driverId); + const ratings = this.driverRatingProvider.getRatings(driverIds); + const driverRatings = driverIds + .filter(id => ratings.has(id)) + .map(id => ({ driverId: id, rating: ratings.get(id)! })); + + const sof = this.sofCalculator.calculate(driverRatings); + if (sof !== null) { + this.logger.debug(`Calculated Strength of Field for race ${race.id}: ${sof}`); + sofValues.push(sof); + } else { + this.logger.warn(`Could not calculate Strength of Field for race ${race.id}`); + } } - } - this.presenter.present( - leagueId, - races.length, - completedRaces.length, - scheduledRaces.length, - sofValues - ); + this.presenter.present( + leagueId, + races.length, + completedRaces.length, + scheduledRaces.length, + sofValues, + ); + this.logger.info(`Successfully presented league statistics for league ${leagueId}.`); + } catch (error) { + this.logger.error(`Error in GetLeagueStatsUseCase: ${error.message}`); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts b/packages/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts index ae550442f..5ca14ddb0 100644 --- a/packages/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts +++ b/packages/racing/application/use-cases/GetTeamJoinRequestsUseCase.ts @@ -7,6 +7,7 @@ import type { TeamJoinRequestsViewModel, } from '../presenters/ITeamJoinRequestsPresenter'; import type { UseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use Case for retrieving team join requests. @@ -19,33 +20,44 @@ export class GetTeamJoinRequestsUseCase private readonly membershipRepository: ITeamMembershipRepository, private readonly driverRepository: IDriverRepository, private readonly imageService: IImageServicePort, + private readonly logger: ILogger, // Kept for backward compatibility; callers must pass their own presenter. // eslint-disable-next-line @typescript-eslint/no-unused-vars public readonly presenter: ITeamJoinRequestsPresenter, ) {} async execute(input: { teamId: string }, presenter: ITeamJoinRequestsPresenter): Promise { + this.logger.debug('Executing GetTeamJoinRequestsUseCase', { teamId: input.teamId }); presenter.reset(); - const requests = await this.membershipRepository.getJoinRequests(input.teamId); + try { + const requests = await this.membershipRepository.getJoinRequests(input.teamId); + this.logger.info('Successfully retrieved team join requests', { teamId: input.teamId, count: requests.length }); - const driverNames: Record = {}; - const avatarUrls: Record = {}; + const driverNames: Record = {}; + const avatarUrls: Record = {}; - for (const request of requests) { - const driver = await this.driverRepository.findById(request.driverId); - if (driver) { - driverNames[request.driverId] = driver.name; + for (const request of requests) { + const driver = await this.driverRepository.findById(request.driverId); + if (driver) { + driverNames[request.driverId] = driver.name; + } else { + this.logger.warn(`Driver not found for ID: ${request.driverId} during join request processing.`); + } + avatarUrls[request.driverId] = this.imageService.getDriverAvatar(request.driverId); + this.logger.debug('Processed driver details for join request', { driverId: request.driverId }); } - avatarUrls[request.driverId] = this.imageService.getDriverAvatar(request.driverId); + + const dto: TeamJoinRequestsResultDTO = { + requests, + driverNames, + avatarUrls, + }; + + presenter.present(dto); + } catch (error) { + this.logger.error('Error retrieving team join requests', { teamId: input.teamId, error }); + throw error; } - - const dto: TeamJoinRequestsResultDTO = { - requests, - driverNames, - avatarUrls, - }; - - presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/GetTeamMembersUseCase.ts b/packages/racing/application/use-cases/GetTeamMembersUseCase.ts index b401f75f4..b9ae88254 100644 --- a/packages/racing/application/use-cases/GetTeamMembersUseCase.ts +++ b/packages/racing/application/use-cases/GetTeamMembersUseCase.ts @@ -7,6 +7,7 @@ import type { TeamMembersViewModel, } from '../presenters/ITeamMembersPresenter'; import type { UseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; /** * Use Case for retrieving team members. @@ -19,33 +20,45 @@ export class GetTeamMembersUseCase private readonly membershipRepository: ITeamMembershipRepository, private readonly driverRepository: IDriverRepository, private readonly imageService: IImageServicePort, + private readonly logger: ILogger, // Kept for backward compatibility; callers must pass their own presenter. // eslint-disable-next-line @typescript-eslint/no-unused-vars public readonly presenter: ITeamMembersPresenter, ) {} async execute(input: { teamId: string }, presenter: ITeamMembersPresenter): Promise { + this.logger.debug(`Executing GetTeamMembersUseCase for teamId: ${input.teamId}`); presenter.reset(); - const memberships = await this.membershipRepository.getTeamMembers(input.teamId); + try { + const memberships = await this.membershipRepository.getTeamMembers(input.teamId); + this.logger.info(`Found ${memberships.length} memberships for teamId: ${input.teamId}`); - const driverNames: Record = {}; - const avatarUrls: Record = {}; + const driverNames: Record = {}; + const avatarUrls: Record = {}; - for (const membership of memberships) { - const driver = await this.driverRepository.findById(membership.driverId); - if (driver) { - driverNames[membership.driverId] = driver.name; + for (const membership of memberships) { + this.logger.debug(`Processing membership for driverId: ${membership.driverId}`); + const driver = await this.driverRepository.findById(membership.driverId); + if (driver) { + driverNames[membership.driverId] = driver.name; + } else { + this.logger.warn(`Driver with ID ${membership.driverId} not found while fetching team members for team ${input.teamId}.`); + } + avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId); } - avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId); + + const dto: TeamMembersResultDTO = { + memberships, + driverNames, + avatarUrls, + }; + + presenter.present(dto); + this.logger.info(`Successfully presented team members for teamId: ${input.teamId}`); + } catch (error) { + this.logger.error(`Error in GetTeamMembersUseCase for teamId: ${input.teamId}, error: ${error instanceof Error ? error.message : String(error)}`); + throw error; } - - const dto: TeamMembersResultDTO = { - memberships, - driverNames, - avatarUrls, - }; - - presenter.present(dto); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/ImportRaceResultsUseCase.ts b/packages/racing/application/use-cases/ImportRaceResultsUseCase.ts index e20324166..83c4229a8 100644 --- a/packages/racing/application/use-cases/ImportRaceResultsUseCase.ts +++ b/packages/racing/application/use-cases/ImportRaceResultsUseCase.ts @@ -13,6 +13,7 @@ import type { IImportRaceResultsPresenter, ImportRaceResultsSummaryViewModel, } from '../presenters/IImportRaceResultsPresenter'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface ImportRaceResultDTO { id: string; @@ -39,53 +40,72 @@ export class ImportRaceResultsUseCase private readonly driverRepository: IDriverRepository, private readonly standingRepository: IStandingRepository, public readonly presenter: IImportRaceResultsPresenter, + private readonly logger: ILogger, ) {} async execute(params: ImportRaceResultsParams): Promise { + this.logger.debug('ImportRaceResultsUseCase:execute', { params }); const { raceId, results } = params; - const race = await this.raceRepository.findById(raceId); - if (!race) { - throw new EntityNotFoundError({ entity: 'race', id: raceId }); + try { + const race = await this.raceRepository.findById(raceId); + if (!race) { + this.logger.warn(`ImportRaceResultsUseCase: Race with ID ${raceId} not found.`); + throw new EntityNotFoundError({ entity: 'race', id: raceId }); + } + this.logger.debug(`ImportRaceResultsUseCase: Race ${raceId} found.`); + + const league = await this.leagueRepository.findById(race.leagueId); + if (!league) { + this.logger.warn(`ImportRaceResultsUseCase: League with ID ${race.leagueId} not found for race ${raceId}.`); + throw new EntityNotFoundError({ entity: 'league', id: race.leagueId }); + } + this.logger.debug(`ImportRaceResultsUseCase: League ${league.id} found.`); + + const existing = await this.resultRepository.existsByRaceId(raceId); + if (existing) { + this.logger.warn(`ImportRaceResultsUseCase: Results already exist for race ID: ${raceId}.`); + throw new BusinessRuleViolationError('Results already exist for this race'); + } + this.logger.debug(`ImportRaceResultsUseCase: No existing results for race ${raceId}.`); + + // Lookup drivers by iracingId and create results with driver.id + const entities = await Promise.all( + results.map(async (dto) => { + const driver = await this.driverRepository.findByIRacingId(dto.driverId); + if (!driver) { + this.logger.warn(`ImportRaceResultsUseCase: Driver with iRacing ID ${dto.driverId} not found for race ${raceId}.`); + throw new BusinessRuleViolationError(`Driver with iRacing ID ${dto.driverId} not found`); + } + return Result.create({ + id: dto.id, + raceId: dto.raceId, + driverId: driver.id, + position: dto.position, + fastestLap: dto.fastestLap, + incidents: dto.incidents, + startPosition: dto.startPosition, + }); + }), + ); + this.logger.debug('ImportRaceResultsUseCase:entities created', { count: entities.length }); + + await this.resultRepository.createMany(entities); + this.logger.info('ImportRaceResultsUseCase:race results created', { raceId }); + + await this.standingRepository.recalculate(league.id); + this.logger.info('ImportRaceResultsUseCase:standings recalculated', { leagueId: league.id }); + + const viewModel: ImportRaceResultsSummaryViewModel = { + importedCount: results.length, + standingsRecalculated: true, + }; + this.logger.debug('ImportRaceResultsUseCase:presenting view model', { viewModel }); + + this.presenter.present(viewModel); + } catch (error) { + this.logger.error('ImportRaceResultsUseCase:execution error', { error }); + throw error; } - - const league = await this.leagueRepository.findById(race.leagueId); - if (!league) { - throw new EntityNotFoundError({ entity: 'league', id: race.leagueId }); - } - - const existing = await this.resultRepository.existsByRaceId(raceId); - if (existing) { - throw new BusinessRuleViolationError('Results already exist for this race'); - } - - // Lookup drivers by iracingId and create results with driver.id - const entities = await Promise.all( - results.map(async (dto) => { - const driver = await this.driverRepository.findByIRacingId(dto.driverId); - if (!driver) { - throw new BusinessRuleViolationError(`Driver with iRacing ID ${dto.driverId} not found`); - } - return Result.create({ - id: dto.id, - raceId: dto.raceId, - driverId: driver.id, - position: dto.position, - fastestLap: dto.fastestLap, - incidents: dto.incidents, - startPosition: dto.startPosition, - }); - }), - ); - - await this.resultRepository.createMany(entities); - await this.standingRepository.recalculate(league.id); - - const viewModel: ImportRaceResultsSummaryViewModel = { - importedCount: results.length, - standingsRecalculated: true, - }; - - this.presenter.present(viewModel); } -} \ No newline at end of file +} diff --git a/packages/racing/application/use-cases/JoinLeagueUseCase.ts b/packages/racing/application/use-cases/JoinLeagueUseCase.ts index d0cf32ec3..f41b701da 100644 --- a/packages/racing/application/use-cases/JoinLeagueUseCase.ts +++ b/packages/racing/application/use-cases/JoinLeagueUseCase.ts @@ -1,3 +1,4 @@ +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { ILeagueMembershipRepository, } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; @@ -11,7 +12,10 @@ import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO'; import { BusinessRuleViolationError } from '../errors/RacingApplicationError'; export class JoinLeagueUseCase implements AsyncUseCase { - constructor(private readonly membershipRepository: ILeagueMembershipRepository) {} + constructor( + private readonly membershipRepository: ILeagueMembershipRepository, + private readonly logger: ILogger, + ) {} /** * Joins a driver to a league as an active member. @@ -21,20 +25,31 @@ export class JoinLeagueUseCase implements AsyncUseCase { + this.logger.debug('Attempting to join league', { command }); const { leagueId, driverId } = command; - const existing = await this.membershipRepository.getMembership(leagueId, driverId); - if (existing) { - throw new BusinessRuleViolationError('Already a member or have a pending request'); + try { + const existing = await this.membershipRepository.getMembership(leagueId, driverId); + if (existing) { + this.logger.warn('Driver already a member or has pending request', { leagueId, driverId }); + throw new BusinessRuleViolationError('Already a member or have a pending request'); + } + + const membership = LeagueMembership.create({ + leagueId, + driverId, + role: 'member' as MembershipRole, + status: 'active' as MembershipStatus, + }); + + const savedMembership = await this.membershipRepository.saveMembership(membership); + this.logger.info('Successfully joined league', { membershipId: savedMembership.id }); + return savedMembership; + } catch (error) { + if (!(error instanceof BusinessRuleViolationError)) { + this.logger.error('Failed to join league due to an unexpected error', { command, error: error.message }); + } + throw error; } - - const membership = LeagueMembership.create({ - leagueId, - driverId, - role: 'member' as MembershipRole, - status: 'active' as MembershipStatus, - }); - - return this.membershipRepository.saveMembership(membership); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/JoinTeamUseCase.ts b/packages/racing/application/use-cases/JoinTeamUseCase.ts index b5531310e..8bcf46b67 100644 --- a/packages/racing/application/use-cases/JoinTeamUseCase.ts +++ b/packages/racing/application/use-cases/JoinTeamUseCase.ts @@ -7,6 +7,7 @@ import type { } from '../../domain/types/TeamMembership'; import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import { BusinessRuleViolationError, EntityNotFoundError, @@ -16,36 +17,50 @@ export class JoinTeamUseCase implements AsyncUseCase { constructor( private readonly teamRepository: ITeamRepository, private readonly membershipRepository: ITeamMembershipRepository, + private readonly logger: ILogger, ) {} async execute(command: JoinTeamCommandDTO): Promise { + this.logger.debug('Attempting to join team', { command }); const { teamId, driverId } = command; - const existingActive = await this.membershipRepository.getActiveMembershipForDriver( - driverId, - ); - if (existingActive) { - throw new BusinessRuleViolationError('Driver already belongs to a team'); + try { + const existingActive = await this.membershipRepository.getActiveMembershipForDriver( + driverId, + ); + if (existingActive) { + this.logger.warn('Driver already belongs to a team', { driverId, teamId }); + throw new BusinessRuleViolationError('Driver already belongs to a team'); + } + + const existingMembership = await this.membershipRepository.getMembership(teamId, driverId); + if (existingMembership) { + this.logger.warn('Driver already has a pending or active membership request', { driverId, teamId }); + throw new BusinessRuleViolationError('Already a member or have a pending request'); + } + + const team = await this.teamRepository.findById(teamId); + if (!team) { + this.logger.error('Team not found', { entity: 'team', id: teamId }); + throw new EntityNotFoundError({ entity: 'team', id: teamId }); + } + + const membership: TeamMembership = { + teamId, + driverId, + role: 'driver' as TeamRole, + status: 'active' as TeamMembershipStatus, + joinedAt: new Date(), + }; + + await this.membershipRepository.saveMembership(membership); + this.logger.info('Driver successfully joined team', { driverId, teamId }); + } catch (error) { + if (error instanceof BusinessRuleViolationError || error instanceof EntityNotFoundError) { + throw error; + } + this.logger.error('Failed to join team due to an unexpected error', { error, command }); + throw error; } - - const existingMembership = await this.membershipRepository.getMembership(teamId, driverId); - if (existingMembership) { - throw new BusinessRuleViolationError('Already a member or have a pending request'); - } - - const team = await this.teamRepository.findById(teamId); - if (!team) { - throw new EntityNotFoundError({ entity: 'team', id: teamId }); - } - - const membership: TeamMembership = { - teamId, - driverId, - role: 'driver' as TeamRole, - status: 'active' as TeamMembershipStatus, - joinedAt: new Date(), - }; - - await this.membershipRepository.saveMembership(membership); } } \ No newline at end of file diff --git a/packages/racing/application/use-cases/QuickPenaltyUseCase.ts b/packages/racing/application/use-cases/QuickPenaltyUseCase.ts index 35c2a4945..e265c557c 100644 --- a/packages/racing/application/use-cases/QuickPenaltyUseCase.ts +++ b/packages/racing/application/use-cases/QuickPenaltyUseCase.ts @@ -11,6 +11,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository' import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import { randomUUID } from 'crypto'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface QuickPenaltyCommand { raceId: string; @@ -27,50 +28,60 @@ export class QuickPenaltyUseCase private readonly penaltyRepository: IPenaltyRepository, private readonly raceRepository: IRaceRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository, + private readonly logger: ILogger, ) {} async execute(command: QuickPenaltyCommand): Promise<{ penaltyId: string }> { - // Validate race exists - const race = await this.raceRepository.findById(command.raceId); - if (!race) { - throw new Error('Race not found'); + this.logger.debug('Executing QuickPenaltyUseCase', { command }); + try { + // Validate race exists + const race = await this.raceRepository.findById(command.raceId); + if (!race) { + this.logger.warn('Race not found', { raceId: command.raceId }); + throw new Error('Race not found'); + } + + // Validate admin has authority + const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); + const adminMembership = memberships.find( + m => m.driverId === command.adminId && m.status === 'active' + ); + + if (!adminMembership || (adminMembership.role !== 'owner' && adminMembership.role !== 'admin')) { + this.logger.warn('Unauthorized admin attempting to issue penalty', { adminId: command.adminId, leagueId: race.leagueId }); + throw new Error('Only league owners and admins can issue penalties'); + } + + // Map infraction + severity to penalty type and value + const { type, value, reason } = this.mapInfractionToPenalty( + command.infractionType, + command.severity + ); + + // Create the penalty + const penalty = Penalty.create({ + id: randomUUID(), + leagueId: race.leagueId, + raceId: command.raceId, + driverId: command.driverId, + type, + ...(value !== undefined ? { value } : {}), + reason, + issuedBy: command.adminId, + status: 'applied', // Quick penalties are applied immediately + issuedAt: new Date(), + appliedAt: new Date(), + ...(command.notes !== undefined ? { notes: command.notes } : {}), + }); + + await this.penaltyRepository.create(penalty); + + this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: command.raceId, driverId: command.driverId }); + return { penaltyId: penalty.id }; + } catch (error) { + this.logger.error('Failed to apply quick penalty', { command, error: error.message }); + throw error; } - - // Validate admin has authority - const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId); - const adminMembership = memberships.find( - m => m.driverId === command.adminId && m.status === 'active' - ); - - if (!adminMembership || (adminMembership.role !== 'owner' && adminMembership.role !== 'admin')) { - throw new Error('Only league owners and admins can issue penalties'); - } - - // Map infraction + severity to penalty type and value - const { type, value, reason } = this.mapInfractionToPenalty( - command.infractionType, - command.severity - ); - - // Create the penalty - const penalty = Penalty.create({ - id: randomUUID(), - leagueId: race.leagueId, - raceId: command.raceId, - driverId: command.driverId, - type, - ...(value !== undefined ? { value } : {}), - reason, - issuedBy: command.adminId, - status: 'applied', // Quick penalties are applied immediately - issuedAt: new Date(), - appliedAt: new Date(), - ...(command.notes !== undefined ? { notes: command.notes } : {}), - }); - - await this.penaltyRepository.create(penalty); - - return { penaltyId: penalty.id }; } private mapInfractionToPenalty( @@ -132,6 +143,7 @@ export class QuickPenaltyUseCase }; default: + this.logger.error(`Unknown infraction type: ${infractionType}`); throw new Error(`Unknown infraction type: ${infractionType}`); } } diff --git a/packages/racing/application/use-cases/RegisterForRaceUseCase.ts b/packages/racing/application/use-cases/RegisterForRaceUseCase.ts index c13b88915..20eb82eb7 100644 --- a/packages/racing/application/use-cases/RegisterForRaceUseCase.ts +++ b/packages/racing/application/use-cases/RegisterForRaceUseCase.ts @@ -3,6 +3,7 @@ import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repos import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration'; import type { RegisterForRaceCommandDTO } from '../dto/RegisterForRaceCommandDTO'; import type { AsyncUseCase } from '@gridpilot/shared/application'; +import { ILogger } from '@gridpilot/shared/logging/ILogger'; import { BusinessRuleViolationError, PermissionDeniedError, @@ -14,8 +15,9 @@ export class RegisterForRaceUseCase constructor( private readonly registrationRepository: IRaceRegistrationRepository, private readonly membershipRepository: ILeagueMembershipRepository, + private readonly logger: ILogger, ) {} - + /** * Mirrors legacy registerForRace behavior: * - throws if already registered @@ -24,22 +26,26 @@ export class RegisterForRaceUseCase */ async execute(command: RegisterForRaceCommandDTO): Promise { const { raceId, leagueId, driverId } = command; - + this.logger.debug('RegisterForRaceUseCase: executing command', { raceId, leagueId, driverId }); + const alreadyRegistered = await this.registrationRepository.isRegistered(raceId, driverId); if (alreadyRegistered) { + this.logger.warn(`RegisterForRaceUseCase: driver ${driverId} already registered for race ${raceId}`); throw new BusinessRuleViolationError('Already registered for this race'); } - + const membership = await this.membershipRepository.getMembership(leagueId, driverId); if (!membership || membership.status !== 'active') { + this.logger.error(`RegisterForRaceUseCase: driver ${driverId} not an active member of league ${leagueId}`); throw new PermissionDeniedError('NOT_ACTIVE_MEMBER', 'Must be an active league member to register for races'); } - + const registration = RaceRegistration.create({ raceId, driverId, }); - + await this.registrationRepository.register(registration); + this.logger.info(`RegisterForRaceUseCase: driver ${driverId} successfully registered for race ${raceId}`); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryCarRepository.ts b/packages/racing/infrastructure/repositories/InMemoryCarRepository.ts index ca09bfe6e..a9f651500 100644 --- a/packages/racing/infrastructure/repositories/InMemoryCarRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryCarRepository.ts @@ -8,92 +8,191 @@ import { v4 as uuidv4 } from 'uuid'; import { Car, CarClass, CarLicense } from '@gridpilot/racing/domain/entities/Car'; import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryCarRepository implements ICarRepository { private cars: Map; + private readonly logger: ILogger; - constructor(seedData?: Car[]) { + constructor(logger: ILogger, seedData?: Car[]) { + this.logger = logger; this.cars = new Map(); + this.logger.info('InMemoryCarRepository initialized'); + if (seedData) { + this.logger.debug(`Seeding ${seedData.length} cars.`); seedData.forEach(car => { this.cars.set(car.id, car); + this.logger.debug(`Car ${car.id} seeded.`); }); } } async findById(id: string): Promise { - return this.cars.get(id) ?? null; + this.logger.debug(`Attempting to find car with ID: ${id}.`); + try { + const car = this.cars.get(id) ?? null; + if (car) { + this.logger.info(`Successfully found car with ID: ${id}.`); + } else { + this.logger.warn(`Car with ID: ${id} not found.`); + } + return car; + } catch (error) { + this.logger.error(`Error finding car by ID ${id}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.cars.values()); + this.logger.debug('Attempting to find all cars.'); + try { + const cars = Array.from(this.cars.values()); + this.logger.info(`Successfully found ${cars.length} cars.`); + return cars; + } catch (error) { + this.logger.error('Error finding all cars:', error); + throw error; + } } async findByGameId(gameId: string): Promise { - return Array.from(this.cars.values()) - .filter(car => car.gameId === gameId) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Attempting to find cars by game ID: ${gameId}.`); + try { + const cars = Array.from(this.cars.values()) + .filter(car => car.gameId === gameId) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Successfully found ${cars.length} cars for game ID: ${gameId}.`); + return cars; + } catch (error) { + this.logger.error(`Error finding cars by game ID ${gameId}:`, error); + throw error; + } } async findByClass(carClass: CarClass): Promise { - return Array.from(this.cars.values()) - .filter(car => car.carClass === carClass) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Attempting to find cars by class: ${carClass}.`); + try { + const cars = Array.from(this.cars.values()) + .filter(car => car.carClass === carClass) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Successfully found ${cars.length} cars for class: ${carClass}.`); + return cars; + } catch (error) { + this.logger.error(`Error finding cars by class ${carClass}:`, error); + throw error; + } } async findByLicense(license: CarLicense): Promise { - return Array.from(this.cars.values()) - .filter(car => car.license === license) - .sort((a, b) => a.name.localeCompare(b.name)); - } + this.logger.debug(`Attempting to find cars by license: ${license}.`); + try { + const cars = Array.from(this.cars.values()) + .filter(car => car.license === license) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Successfully found ${cars.length} cars for license: ${license}.`); + return cars; + } catch (error) { + this.logger.error(`Error finding cars by license ${license}:`, error); + throw error; + } + } async findByManufacturer(manufacturer: string): Promise { - const lowerManufacturer = manufacturer.toLowerCase(); - return Array.from(this.cars.values()) - .filter(car => car.manufacturer.toLowerCase() === lowerManufacturer) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Attempting to find cars by manufacturer: ${manufacturer}.`); + try { + const lowerManufacturer = manufacturer.toLowerCase(); + const cars = Array.from(this.cars.values()) + .filter(car => car.manufacturer.toLowerCase() === lowerManufacturer) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Successfully found ${cars.length} cars for manufacturer: ${manufacturer}.`); + return cars; + } catch (error) { + this.logger.error(`Error finding cars by manufacturer ${manufacturer}:`, error); + throw error; + } } async searchByName(query: string): Promise { - const lowerQuery = query.toLowerCase(); - return Array.from(this.cars.values()) - .filter(car => - car.name.toLowerCase().includes(lowerQuery) || - car.shortName.toLowerCase().includes(lowerQuery) || - car.manufacturer.toLowerCase().includes(lowerQuery) - ) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Attempting to search cars by name query: ${query}.`); + try { + const lowerQuery = query.toLowerCase(); + const cars = Array.from(this.cars.values()) + .filter(car => + car.name.toLowerCase().includes(lowerQuery) || + car.shortName.toLowerCase().includes(lowerQuery) || + car.manufacturer.toLowerCase().includes(lowerQuery) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Successfully found ${cars.length} cars matching search query: ${query}.`); + return cars; + } catch (error) { + this.logger.error(`Error searching cars by name query ${query}:`, error); + throw error; + } } async create(car: Car): Promise { - if (await this.exists(car.id)) { - throw new Error(`Car with ID ${car.id} already exists`); - } + this.logger.debug(`Attempting to create car: ${car.id}.`); + try { + if (await this.exists(car.id)) { + this.logger.warn(`Car with ID ${car.id} already exists; creation aborted.`); + throw new Error(`Car with ID ${car.id} already exists`); + } - this.cars.set(car.id, car); - return car; + this.cars.set(car.id, car); + this.logger.info(`Car ${car.id} created successfully.`); + return car; + } catch (error) { + this.logger.error(`Error creating car ${car.id}:`, error); + throw error; + } } async update(car: Car): Promise { - if (!await this.exists(car.id)) { - throw new Error(`Car with ID ${car.id} not found`); - } + this.logger.debug(`Attempting to update car with ID: ${car.id}.`); + try { + if (!await this.exists(car.id)) { + this.logger.warn(`Car with ID ${car.id} not found for update; update aborted.`); + throw new Error(`Car with ID ${car.id} not found`); + } - this.cars.set(car.id, car); - return car; + this.cars.set(car.id, car); + this.logger.info(`Car ${car.id} updated successfully.`); + return car; + } catch (error) { + this.logger.error(`Error updating car ${car.id}:`, error); + throw error; + } } async delete(id: string): Promise { - if (!await this.exists(id)) { - throw new Error(`Car with ID ${id} not found`); - } + this.logger.debug(`Attempting to delete car with ID: ${id}.`); + try { + if (!await this.exists(id)) { + this.logger.warn(`Car with ID ${id} not found for deletion; deletion aborted.`); + throw new Error(`Car with ID ${id} not found`); + } - this.cars.delete(id); + this.cars.delete(id); + this.logger.info(`Car ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting car ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.cars.has(id); + this.logger.debug(`Checking existence of car with ID: ${id}.`); + try { + const exists = this.cars.has(id); + this.logger.info(`Car with ID ${id} existence check: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of car with ID ${id}:`, error); + throw error; + } } /** diff --git a/packages/racing/infrastructure/repositories/InMemoryDriverRepository.ts b/packages/racing/infrastructure/repositories/InMemoryDriverRepository.ts index 93b037d55..c72110623 100644 --- a/packages/racing/infrastructure/repositories/InMemoryDriverRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryDriverRepository.ts @@ -8,73 +8,150 @@ import { v4 as uuidv4 } from 'uuid'; 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'; export class InMemoryDriverRepository implements IDriverRepository { private drivers: Map; + private readonly logger: ILogger; - constructor(seedData?: Driver[]) { + constructor(logger: ILogger, seedData?: Driver[]) { + this.logger = logger; + 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}.`); }); } } async findById(id: string): Promise { - return this.drivers.get(id) ?? 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; + } } async findByIRacingId(iracingId: string): Promise { - const driver = Array.from(this.drivers.values()).find( - d => d.iracingId === iracingId - ); - return 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; + } } async findAll(): Promise { - return Array.from(this.drivers.values()); + 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; + } } async create(driver: Driver): Promise { - if (await this.exists(driver.id)) { - throw new Error(`Driver with ID ${driver.id} already exists`); - } + 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)) { - throw new Error(`Driver with iRacing ID ${driver.iracingId} 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); - return driver; + 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; + } } async update(driver: Driver): Promise { - if (!await this.exists(driver.id)) { - throw new Error(`Driver with ID ${driver.id} not found`); - } + 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); - return driver; + 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; + } } async delete(id: string): Promise { - if (!await this.exists(id)) { - throw new Error(`Driver with ID ${id} not found`); - } + 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.drivers.delete(id); + this.drivers.delete(id); + this.logger.info(`Driver ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting driver ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.drivers.has(id); + 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; + } } async existsByIRacingId(iracingId: string): Promise { - return Array.from(this.drivers.values()).some( - d => d.iracingId === iracingId - ); + 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; + } } /** diff --git a/packages/racing/infrastructure/repositories/InMemoryGameRepository.ts b/packages/racing/infrastructure/repositories/InMemoryGameRepository.ts index 0d1c4543c..eacdd173a 100644 --- a/packages/racing/infrastructure/repositories/InMemoryGameRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryGameRepository.ts @@ -6,16 +6,21 @@ import { Game } from '../../domain/entities/Game'; import type { IGameRepository } from '../../domain/repositories/IGameRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryGameRepository implements IGameRepository { private games: Map; + private readonly logger: ILogger; - constructor(seedData?: Game[]) { + constructor(logger: ILogger, seedData?: Game[]) { + this.logger = logger; + 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 @@ -29,33 +34,64 @@ export class InMemoryGameRepository implements IGameRepository { ]; defaultGames.forEach(game => { this.games.set(game.id, game); + this.logger.debug(`Seeded default game: ${game.id}.`); }); } } async findById(id: string): Promise { - return this.games.get(id) ?? 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; + } } async findAll(): Promise { - return Array.from(this.games.values()).sort((a, b) => a.name.localeCompare(b.name)); + 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; + } } /** * Utility method to add a game */ async create(game: Game): Promise { - if (this.games.has(game.id)) { - throw new Error(`Game with ID ${game.id} already exists`); + 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; } - this.games.set(game.id, game); - return game; } /** * Test helper to clear data */ clear(): void { + this.logger.debug('Clearing all games.'); this.games.clear(); + this.logger.info('All games cleared.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts b/packages/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts index 5dc497d78..0a99b84e9 100644 --- a/packages/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts @@ -10,12 +10,16 @@ import type { JoinRequest, } from '@gridpilot/racing/domain/entities/LeagueMembership'; import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository { private membershipsByLeague: Map; private joinRequestsByLeague: Map; + private readonly logger: ILogger; - constructor(seedMemberships?: LeagueMembership[], seedJoinRequests?: JoinRequest[]) { + constructor(logger: ILogger, seedMemberships?: LeagueMembership[], seedJoinRequests?: JoinRequest[]) { + this.logger = logger; + this.logger.info('InMemoryLeagueMembershipRepository initialized.'); this.membershipsByLeague = new Map(); this.joinRequestsByLeague = new Map(); @@ -24,6 +28,7 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo 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}.`); }); } @@ -32,69 +37,143 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo 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}.`); }); } } async getMembership(leagueId: string, driverId: string): Promise { - const list = this.membershipsByLeague.get(leagueId); - if (!list) return null; - return list.find((m) => m.driverId === driverId) ?? 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; + } } async getLeagueMembers(leagueId: string): Promise { - return [...(this.membershipsByLeague.get(leagueId) ?? [])]; + 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; + } } async getJoinRequests(leagueId: string): Promise { - return [...(this.joinRequestsByLeague.get(leagueId) ?? [])]; + 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; + } } async saveMembership(membership: LeagueMembership): Promise { - const list = this.membershipsByLeague.get(membership.leagueId) ?? []; - const existingIndex = list.findIndex( - (m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId, - ); + 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; - } else { - list.push(membership); + 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.membershipsByLeague.set(membership.leagueId, list); - return membership; } async removeMembership(leagueId: string, driverId: string): Promise { - const list = this.membershipsByLeague.get(leagueId); - if (!list) return; + 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); - this.membershipsByLeague.set(leagueId, next); + 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; + } } async saveJoinRequest(request: JoinRequest): Promise { - const list = this.joinRequestsByLeague.get(request.leagueId) ?? []; - const existingIndex = list.findIndex((r) => r.id === request.id); + 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; - } else { - list.push(request); + 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.joinRequestsByLeague.set(request.leagueId, list); - return request; } async removeJoinRequest(requestId: string): Promise { - 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); - break; + 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; } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryLeagueRepository.ts b/packages/racing/infrastructure/repositories/InMemoryLeagueRepository.ts index 065de1cb0..813377790 100644 --- a/packages/racing/infrastructure/repositories/InMemoryLeagueRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryLeagueRepository.ts @@ -8,69 +8,142 @@ 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'; export class InMemoryLeagueRepository implements ILeagueRepository { private leagues: Map; + private readonly logger: ILogger; - constructor(seedData?: League[]) { + constructor(logger: ILogger, seedData?: League[]) { + this.logger = logger; + 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}.`); }); } } async findById(id: string): Promise { - return this.leagues.get(id) ?? 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 { - return Array.from(this.leagues.values()); + 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; + } } async findByOwnerId(ownerId: string): Promise { - return Array.from(this.leagues.values()).filter( - league => league.ownerId === ownerId - ); + 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; + } } async create(league: League): Promise { - if (await this.exists(league.id)) { - throw new Error(`League with ID ${league.id} already exists`); - } + 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); - return league; + 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; + } } async update(league: League): Promise { - if (!await this.exists(league.id)) { - throw new Error(`League with ID ${league.id} not found`); - } + 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); - return league; + 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; + } } async delete(id: string): Promise { - if (!await this.exists(id)) { - throw new Error(`League with ID ${id} not found`); - } + 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.leagues.delete(id); + this.logger.info(`League ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting league ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.leagues.has(id); + 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; + } } async searchByName(query: string): Promise { - const normalizedQuery = query.toLowerCase(); - return Array.from(this.leagues.values()).filter(league => - league.name.toLowerCase().includes(normalizedQuery) - ); + 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; + } } /** diff --git a/packages/racing/infrastructure/repositories/InMemoryLeagueWalletRepository.ts b/packages/racing/infrastructure/repositories/InMemoryLeagueWalletRepository.ts index 2d5b028e0..9dbb16668 100644 --- a/packages/racing/infrastructure/repositories/InMemoryLeagueWalletRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryLeagueWalletRepository.ts @@ -6,49 +6,116 @@ import type { LeagueWallet } from '../../domain/entities/LeagueWallet'; import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository { private wallets: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: LeagueWallet[]) { + this.logger = logger; + this.logger.info('InMemoryLeagueWalletRepository initialized.'); + if (seedData) { + seedData.forEach(wallet => this.wallets.set(wallet.id, wallet)); + this.logger.debug(`Seeded ${seedData.length} league wallets.`); + } + } async findById(id: string): Promise { - return this.wallets.get(id) ?? null; + this.logger.debug(`Finding league wallet by id: ${id}`); + try { + const wallet = this.wallets.get(id) ?? null; + if (wallet) { + this.logger.info(`Found league wallet: ${id}.`); + } else { + this.logger.warn(`League wallet with id ${id} not found.`); + } + return wallet; + } catch (error) { + this.logger.error(`Error finding league wallet by id ${id}:`, error); + throw error; + } } async findByLeagueId(leagueId: string): Promise { - for (const wallet of this.wallets.values()) { - if (wallet.leagueId === leagueId) { - return wallet; + this.logger.debug(`Finding league wallet by league id: ${leagueId}`); + try { + for (const wallet of this.wallets.values()) { + if (wallet.leagueId === leagueId) { + this.logger.info(`Found league wallet for league id: ${leagueId}.`); + return wallet; + } } + this.logger.warn(`No league wallet found for league id: ${leagueId}.`); + return null; + } catch (error) { + this.logger.error(`Error finding league wallet by league id ${leagueId}:`, error); + throw error; } - return null; } async create(wallet: LeagueWallet): Promise { - if (this.wallets.has(wallet.id)) { - throw new Error('LeagueWallet with this ID already exists'); + this.logger.debug(`Creating league wallet: ${wallet.id}`); + try { + if (this.wallets.has(wallet.id)) { + this.logger.warn(`LeagueWallet with ID ${wallet.id} already exists.`); + throw new Error('LeagueWallet with this ID already exists'); + } + this.wallets.set(wallet.id, wallet); + this.logger.info(`LeagueWallet ${wallet.id} created successfully.`); + return wallet; + } catch (error) { + this.logger.error(`Error creating league wallet ${wallet.id}:`, error); + throw error; } - this.wallets.set(wallet.id, wallet); - return wallet; } async update(wallet: LeagueWallet): Promise { - if (!this.wallets.has(wallet.id)) { - throw new Error('LeagueWallet not found'); + this.logger.debug(`Updating league wallet: ${wallet.id}`); + try { + if (!this.wallets.has(wallet.id)) { + this.logger.warn(`LeagueWallet with ID ${wallet.id} not found for update.`); + throw new Error('LeagueWallet not found'); + } + this.wallets.set(wallet.id, wallet); + this.logger.info(`LeagueWallet ${wallet.id} updated successfully.`); + return wallet; + } catch (error) { + this.logger.error(`Error updating league wallet ${wallet.id}:`, error); + throw error; } - this.wallets.set(wallet.id, wallet); - return wallet; } async delete(id: string): Promise { - this.wallets.delete(id); + this.logger.debug(`Deleting league wallet: ${id}`); + try { + if (this.wallets.delete(id)) { + this.logger.info(`LeagueWallet ${id} deleted successfully.`); + } else { + this.logger.warn(`LeagueWallet with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting league wallet ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.wallets.has(id); + this.logger.debug(`Checking existence of league wallet with id: ${id}`); + try { + const exists = this.wallets.has(id); + this.logger.debug(`LeagueWallet ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of league wallet with id ${id}:`, error); + throw error; + } } // Test helper clear(): void { + this.logger.debug('Clearing all league wallets.'); this.wallets.clear(); + this.logger.info('All league wallets cleared.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryLiveryRepository.ts b/packages/racing/infrastructure/repositories/InMemoryLiveryRepository.ts index b5de7eac6..2450c316f 100644 --- a/packages/racing/infrastructure/repositories/InMemoryLiveryRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryLiveryRepository.ts @@ -7,108 +7,253 @@ import type { DriverLivery } from '../../domain/entities/DriverLivery'; import type { LiveryTemplate } from '../../domain/entities/LiveryTemplate'; import type { ILiveryRepository } from '../../domain/repositories/ILiveryRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryLiveryRepository implements ILiveryRepository { private driverLiveries: Map = new Map(); private templates: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedDriverLiveries?: DriverLivery[], seedTemplates?: LiveryTemplate[]) { + this.logger = logger; + this.logger.info('InMemoryLiveryRepository initialized.'); + if (seedDriverLiveries) { + seedDriverLiveries.forEach(livery => this.driverLiveries.set(livery.id, livery)); + this.logger.debug(`Seeded ${seedDriverLiveries.length} driver liveries.`); + } + if (seedTemplates) { + seedTemplates.forEach(template => this.templates.set(template.id, template)); + this.logger.debug(`Seeded ${seedTemplates.length} livery templates.`); + } + } // DriverLivery operations async findDriverLiveryById(id: string): Promise { - return this.driverLiveries.get(id) ?? null; + this.logger.debug(`Finding driver livery by id: ${id}`); + try { + const livery = this.driverLiveries.get(id) ?? null; + if (livery) { + this.logger.info(`Found driver livery: ${id}.`); + } else { + this.logger.warn(`Driver livery with id ${id} not found.`); + } + return livery; + } catch (error) { + this.logger.error(`Error finding driver livery by id ${id}:`, error); + throw error; + } } async findDriverLiveriesByDriverId(driverId: string): Promise { - return Array.from(this.driverLiveries.values()).filter(l => l.driverId === driverId); + this.logger.debug(`Finding driver liveries by driver id: ${driverId}`); + try { + const liveries = Array.from(this.driverLiveries.values()).filter(l => l.driverId === driverId); + this.logger.info(`Found ${liveries.length} driver liveries for driver id: ${driverId}.`); + return liveries; + } catch (error) { + this.logger.error(`Error finding driver liveries by driver id ${driverId}:`, error); + throw error; + } } async findDriverLiveryByDriverAndCar(driverId: string, carId: string): Promise { - for (const livery of this.driverLiveries.values()) { - if (livery.driverId === driverId && livery.carId === carId) { - return livery; + this.logger.debug(`Finding driver livery by driver: ${driverId} and car: ${carId}`); + try { + for (const livery of this.driverLiveries.values()) { + if (livery.driverId === driverId && livery.carId === carId) { + this.logger.info(`Found driver livery for driver: ${driverId}, car: ${carId}.`); + return livery; + } } + this.logger.warn(`Driver livery for driver ${driverId} and car ${carId} not found.`); + return null; + } catch (error) { + this.logger.error(`Error finding driver livery by driver ${driverId}, car ${carId}:`, error); + throw error; } - return null; } async findDriverLiveriesByGameId(gameId: string): Promise { - return Array.from(this.driverLiveries.values()).filter(l => l.gameId === gameId); + this.logger.debug(`Finding driver liveries by game id: ${gameId}`); + try { + const liveries = Array.from(this.driverLiveries.values()).filter(l => l.gameId === gameId); + this.logger.info(`Found ${liveries.length} driver liveries for game id: ${gameId}.`); + return liveries; + } catch (error) { + this.logger.error(`Error finding driver liveries by game id ${gameId}:`, error); + throw error; + } } async findDriverLiveryByDriverAndGame(driverId: string, gameId: string): Promise { - return Array.from(this.driverLiveries.values()).filter( - l => l.driverId === driverId && l.gameId === gameId - ); + this.logger.debug(`Finding driver liveries by driver: ${driverId} and game: ${gameId}`); + try { + const liveries = Array.from(this.driverLiveries.values()).filter( + l => l.driverId === driverId && l.gameId === gameId + ); + this.logger.info(`Found ${liveries.length} driver liveries for driver: ${driverId}, game: ${gameId}.`); + return liveries; + } catch (error) { + this.logger.error(`Error finding driver liveries by driver ${driverId}, game ${gameId}:`, error); + throw error; + } } async createDriverLivery(livery: DriverLivery): Promise { - if (this.driverLiveries.has(livery.id)) { - throw new Error('DriverLivery with this ID already exists'); + this.logger.debug(`Creating driver livery: ${livery.id}`); + try { + if (this.driverLiveries.has(livery.id)) { + this.logger.warn(`DriverLivery with ID ${livery.id} already exists.`); + throw new Error('DriverLivery with this ID already exists'); + } + this.driverLiveries.set(livery.id, livery); + this.logger.info(`DriverLivery ${livery.id} created successfully.`); + return livery; + } catch (error) { + this.logger.error(`Error creating driver livery ${livery.id}:`, error); + throw error; } - this.driverLiveries.set(livery.id, livery); - return livery; } async updateDriverLivery(livery: DriverLivery): Promise { - if (!this.driverLiveries.has(livery.id)) { - throw new Error('DriverLivery not found'); + this.logger.debug(`Updating driver livery: ${livery.id}`); + try { + if (!this.driverLiveries.has(livery.id)) { + this.logger.warn(`DriverLivery with ID ${livery.id} not found for update.`); + throw new Error('DriverLivery not found'); + } + this.driverLiveries.set(livery.id, livery); + this.logger.info(`DriverLivery ${livery.id} updated successfully.`); + return livery; + } catch (error) { + this.logger.error(`Error updating driver livery ${livery.id}:`, error); + throw error; } - this.driverLiveries.set(livery.id, livery); - return livery; } async deleteDriverLivery(id: string): Promise { - this.driverLiveries.delete(id); + this.logger.debug(`Deleting driver livery: ${id}`); + try { + if (this.driverLiveries.delete(id)) { + this.logger.info(`DriverLivery ${id} deleted successfully.`); + } else { + this.logger.warn(`DriverLivery with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting driver livery ${id}:`, error); + throw error; + } } // LiveryTemplate operations async findTemplateById(id: string): Promise { - return this.templates.get(id) ?? null; + this.logger.debug(`Finding livery template by id: ${id}`); + try { + const template = this.templates.get(id) ?? null; + if (template) { + this.logger.info(`Found livery template: ${id}.`); + } else { + this.logger.warn(`Livery template with id ${id} not found.`); + } + return template; + } catch (error) { + this.logger.error(`Error finding livery template by id ${id}:`, error); + throw error; + } } async findTemplatesBySeasonId(seasonId: string): Promise { - return Array.from(this.templates.values()).filter(t => t.seasonId === seasonId); + this.logger.debug(`Finding livery templates by season id: ${seasonId}`); + try { + const templates = Array.from(this.templates.values()).filter(t => t.seasonId === seasonId); + this.logger.info(`Found ${templates.length} livery templates for season id: ${seasonId}.`); + return templates; + } catch (error) { + this.logger.error(`Error finding livery templates by season id ${seasonId}:`, error); + throw error; + } } async findTemplateBySeasonAndCar(seasonId: string, carId: string): Promise { - for (const template of this.templates.values()) { - if (template.seasonId === seasonId && template.carId === carId) { - return template; + this.logger.debug(`Finding livery template by season: ${seasonId} and car: ${carId}`); + try { + for (const template of this.templates.values()) { + if (template.seasonId === seasonId && template.carId === carId) { + this.logger.info(`Found livery template for season: ${seasonId}, car: ${carId}.`); + return template; + } } + this.logger.warn(`Livery template for season ${seasonId} and car ${carId} not found.`); + return null; + } catch (error) { + this.logger.error(`Error finding livery template by season ${seasonId}, car ${carId}:`, error); + throw error; } - return null; } async createTemplate(template: LiveryTemplate): Promise { - if (this.templates.has(template.id)) { - throw new Error('LiveryTemplate with this ID already exists'); + this.logger.debug(`Creating livery template: ${template.id}`); + try { + if (this.templates.has(template.id)) { + this.logger.warn(`LiveryTemplate with ID ${template.id} already exists.`); + throw new Error('LiveryTemplate with this ID already exists'); + } + this.templates.set(template.id, template); + this.logger.info(`LiveryTemplate ${template.id} created successfully.`); + return template; + } catch (error) { + this.logger.error(`Error creating livery template ${template.id}:`, error); + throw error; } - this.templates.set(template.id, template); - return template; } async updateTemplate(template: LiveryTemplate): Promise { - if (!this.templates.has(template.id)) { - throw new Error('LiveryTemplate not found'); + this.logger.debug(`Updating livery template: ${template.id}`); + try { + if (!this.templates.has(template.id)) { + this.logger.warn(`LiveryTemplate with ID ${template.id} not found for update.`); + throw new Error('LiveryTemplate not found'); + } + this.templates.set(template.id, template); + this.logger.info(`LiveryTemplate ${template.id} updated successfully.`); + return template; + } catch (error) { + this.logger.error(`Error updating livery template ${template.id}:`, error); + throw error; } - this.templates.set(template.id, template); - return template; } async deleteTemplate(id: string): Promise { - this.templates.delete(id); + this.logger.debug(`Deleting livery template: ${id}`); + try { + if (this.templates.delete(id)) { + this.logger.info(`LiveryTemplate ${id} deleted successfully.`); + } else { + this.logger.warn(`LiveryTemplate with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting livery template ${id}:`, error); + throw error; + } } // Test helpers clearDriverLiveries(): void { + this.logger.debug('Clearing all driver liveries.'); this.driverLiveries.clear(); + this.logger.info('All driver liveries cleared.'); } clearTemplates(): void { + this.logger.debug('Clearing all livery templates.'); this.templates.clear(); + this.logger.info('All livery templates cleared.'); } clear(): void { + this.logger.debug('Clearing all livery data.'); this.driverLiveries.clear(); this.templates.clear(); + this.logger.info('All livery data cleared.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts b/packages/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts index 32344569a..e914849fd 100644 --- a/packages/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts @@ -6,65 +6,146 @@ import type { Penalty } from '../../domain/entities/Penalty'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryPenaltyRepository implements IPenaltyRepository { private penalties: Map = new Map(); + private readonly logger: ILogger; - constructor(initialPenalties: Penalty[] = []) { + constructor(logger: ILogger, initialPenalties: Penalty[] = []) { + this.logger = logger; + this.logger.info('InMemoryPenaltyRepository initialized.'); initialPenalties.forEach(penalty => { this.penalties.set(penalty.id, penalty); + this.logger.debug(`Seeded penalty: ${penalty.id}`); }); } async findById(id: string): Promise { - return this.penalties.get(id) || null; + this.logger.debug(`Finding penalty by id: ${id}`); + try { + const penalty = this.penalties.get(id) || null; + if (penalty) { + this.logger.info(`Found penalty with id: ${id}.`); + } else { + this.logger.warn(`Penalty with id ${id} not found.`); + } + return penalty; + } catch (error) { + this.logger.error(`Error finding penalty by id ${id}:`, error); + throw error; + } } async findByRaceId(raceId: string): Promise { - return Array.from(this.penalties.values()).filter( - penalty => penalty.raceId === raceId - ); + this.logger.debug(`Finding penalties by race id: ${raceId}`); + try { + const penalties = Array.from(this.penalties.values()).filter( + penalty => penalty.raceId === raceId + ); + this.logger.info(`Found ${penalties.length} penalties for race id: ${raceId}.`); + return penalties; + } catch (error) { + this.logger.error(`Error finding penalties by race id ${raceId}:`, error); + throw error; + } } async findByDriverId(driverId: string): Promise { - return Array.from(this.penalties.values()).filter( - penalty => penalty.driverId === driverId - ); + this.logger.debug(`Finding penalties by driver id: ${driverId}`); + try { + const penalties = Array.from(this.penalties.values()).filter( + penalty => penalty.driverId === driverId + ); + this.logger.info(`Found ${penalties.length} penalties for driver id: ${driverId}.`); + return penalties; + } catch (error) { + this.logger.error(`Error finding penalties by driver id ${driverId}:`, error); + throw error; + } } async findByProtestId(protestId: string): Promise { - return Array.from(this.penalties.values()).filter( - penalty => penalty.protestId === protestId - ); + this.logger.debug(`Finding penalties by protest id: ${protestId}`); + try { + const penalties = Array.from(this.penalties.values()).filter( + penalty => penalty.protestId === protestId + ); + this.logger.info(`Found ${penalties.length} penalties for protest id: ${protestId}.`); + return penalties; + } catch (error) { + this.logger.error(`Error finding penalties by protest id ${protestId}:`, error); + throw error; + } } async findPending(): Promise { - return Array.from(this.penalties.values()).filter( - penalty => penalty.isPending() - ); + this.logger.debug('Finding pending penalties.'); + try { + const penalties = Array.from(this.penalties.values()).filter( + penalty => penalty.isPending() + ); + this.logger.info(`Found ${penalties.length} pending penalties.`); + return penalties; + } catch (error) { + this.logger.error('Error finding pending penalties:', error); + throw error; + } } async findIssuedBy(stewardId: string): Promise { - return Array.from(this.penalties.values()).filter( - penalty => penalty.issuedBy === stewardId - ); + this.logger.debug(`Finding penalties issued by steward: ${stewardId}`); + try { + const penalties = Array.from(this.penalties.values()).filter( + penalty => penalty.issuedBy === stewardId + ); + this.logger.info(`Found ${penalties.length} penalties issued by steward: ${stewardId}.`); + return penalties; + } catch (error) { + this.logger.error(`Error finding penalties issued by steward ${stewardId}:`, error); + throw error; + } } async create(penalty: Penalty): Promise { - if (this.penalties.has(penalty.id)) { - throw new Error(`Penalty with ID ${penalty.id} already exists`); + this.logger.debug(`Creating penalty: ${penalty.id}`); + try { + if (this.penalties.has(penalty.id)) { + this.logger.warn(`Penalty with ID ${penalty.id} already exists.`); + throw new Error(`Penalty with ID ${penalty.id} already exists`); + } + this.penalties.set(penalty.id, penalty); + this.logger.info(`Penalty ${penalty.id} created successfully.`); + } catch (error) { + this.logger.error(`Error creating penalty ${penalty.id}:`, error); + throw error; } - this.penalties.set(penalty.id, penalty); } async update(penalty: Penalty): Promise { - if (!this.penalties.has(penalty.id)) { - throw new Error(`Penalty with ID ${penalty.id} not found`); + this.logger.debug(`Updating penalty: ${penalty.id}`); + try { + if (!this.penalties.has(penalty.id)) { + this.logger.warn(`Penalty with ID ${penalty.id} not found for update.`); + throw new Error(`Penalty with ID ${penalty.id} not found`); + } + this.penalties.set(penalty.id, penalty); + this.logger.info(`Penalty ${penalty.id} updated successfully.`); + } catch (error) { + this.logger.error(`Error updating penalty ${penalty.id}:`, error); + throw error; } - this.penalties.set(penalty.id, penalty); } async exists(id: string): Promise { - return this.penalties.has(id); + this.logger.debug(`Checking existence of penalty with id: ${id}`); + try { + const exists = this.penalties.has(id); + this.logger.debug(`Penalty ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of penalty with id ${id}:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryProtestRepository.ts b/packages/racing/infrastructure/repositories/InMemoryProtestRepository.ts index 42debb00b..75189bce2 100644 --- a/packages/racing/infrastructure/repositories/InMemoryProtestRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryProtestRepository.ts @@ -6,65 +6,146 @@ import type { Protest } from '../../domain/entities/Protest'; import type { IProtestRepository } from '../../domain/repositories/IProtestRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryProtestRepository implements IProtestRepository { private protests: Map = new Map(); + private readonly logger: ILogger; - constructor(initialProtests: Protest[] = []) { + constructor(logger: ILogger, initialProtests: Protest[] = []) { + this.logger = logger; + this.logger.info('InMemoryProtestRepository initialized.'); initialProtests.forEach(protest => { this.protests.set(protest.id, protest); + this.logger.debug(`Seeded protest: ${protest.id}`); }); } async findById(id: string): Promise { - return this.protests.get(id) || 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; + } } async findByRaceId(raceId: string): Promise { - return Array.from(this.protests.values()).filter( - protest => protest.raceId === raceId - ); + 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; + } } async findByProtestingDriverId(driverId: string): Promise { - return Array.from(this.protests.values()).filter( - protest => protest.protestingDriverId === driverId - ); + 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; + } } async findByAccusedDriverId(driverId: string): Promise { - return Array.from(this.protests.values()).filter( - protest => protest.accusedDriverId === driverId - ); + 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; + } } async findPending(): Promise { - return Array.from(this.protests.values()).filter( - protest => protest.isPending() - ); + 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; + } } async findUnderReviewBy(stewardId: string): Promise { - return Array.from(this.protests.values()).filter( - protest => protest.reviewedBy === stewardId && protest.isUnderReview() - ); + 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; + } } async create(protest: Protest): Promise { - if (this.protests.has(protest.id)) { - throw new Error(`Protest with ID ${protest.id} already exists`); + 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.protests.set(protest.id, protest); } async update(protest: Protest): Promise { - if (!this.protests.has(protest.id)) { - throw new Error(`Protest with ID ${protest.id} not found`); + 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.protests.set(protest.id, protest); } async exists(id: string): Promise { - return this.protests.has(id); + 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; + } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryRaceEventRepository.ts b/packages/racing/infrastructure/repositories/InMemoryRaceEventRepository.ts index 381117103..a6ef36197 100644 --- a/packages/racing/infrastructure/repositories/InMemoryRaceEventRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryRaceEventRepository.ts @@ -3,70 +3,178 @@ */ import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository'; import type { RaceEvent } from '../../domain/entities/RaceEvent'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryRaceEventRepository implements IRaceEventRepository { private raceEvents: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: RaceEvent[]) { + this.logger = logger; + this.logger.info('InMemoryRaceEventRepository initialized.'); + if (seedData) { + seedData.forEach(event => this.raceEvents.set(event.id, event)); + this.logger.debug(`Seeded ${seedData.length} race events.`); + } + } async findById(id: string): Promise { - return this.raceEvents.get(id) ?? null; + this.logger.debug(`Finding race event by id: ${id}`); + try { + const event = this.raceEvents.get(id) ?? null; + if (event) { + this.logger.info(`Found race event: ${event.id}`); + } else { + this.logger.warn(`Race event with id ${id} not found.`); + } + return event; + } catch (error) { + this.logger.error(`Error finding race event by id ${id}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.raceEvents.values()); + this.logger.debug('Finding all race events.'); + try { + const events = Array.from(this.raceEvents.values()); + this.logger.info(`Found ${events.length} race events.`); + return events; + } catch (error) { + this.logger.error('Error finding all race events:', error); + throw error; + } } async findBySeasonId(seasonId: string): Promise { - return Array.from(this.raceEvents.values()).filter( - raceEvent => raceEvent.seasonId === seasonId - ); + this.logger.debug(`Finding race events by season id: ${seasonId}`); + try { + const events = Array.from(this.raceEvents.values()).filter( + raceEvent => raceEvent.seasonId === seasonId + ); + this.logger.info(`Found ${events.length} race events for season id: ${seasonId}.`); + return events; + } catch (error) { + this.logger.error(`Error finding race events by season id ${seasonId}:`, error); + throw error; + } } async findByLeagueId(leagueId: string): Promise { - return Array.from(this.raceEvents.values()).filter( - raceEvent => raceEvent.leagueId === leagueId - ); + this.logger.debug(`Finding race events by league id: ${leagueId}`); + try { + const events = Array.from(this.raceEvents.values()).filter( + raceEvent => raceEvent.leagueId === leagueId + ); + this.logger.info(`Found ${events.length} race events for league id: ${leagueId}.`); + return events; + } catch (error) { + this.logger.error(`Error finding race events by league id ${leagueId}:`, error); + throw error; + } } async findByStatus(status: string): Promise { - return Array.from(this.raceEvents.values()).filter( - raceEvent => raceEvent.status === status - ); + this.logger.debug(`Finding race events by status: ${status}`); + try { + const events = Array.from(this.raceEvents.values()).filter( + raceEvent => raceEvent.status === status + ); + this.logger.info(`Found ${events.length} race events with status: ${status}.`); + return events; + } catch (error) { + this.logger.error(`Error finding race events by status ${status}:`, error); + throw error; + } } async findAwaitingStewardingClose(): Promise { - const now = new Date(); - return Array.from(this.raceEvents.values()).filter( - raceEvent => - raceEvent.status === 'awaiting_stewarding' && - raceEvent.stewardingClosesAt && - raceEvent.stewardingClosesAt <= now - ); + this.logger.debug('Finding race events awaiting stewarding close.'); + try { + const now = new Date(); + const events = Array.from(this.raceEvents.values()).filter( + raceEvent => + raceEvent.status === 'awaiting_stewarding' && + raceEvent.stewardingClosesAt && + raceEvent.stewardingClosesAt <= now + ); + this.logger.info(`Found ${events.length} race events awaiting stewarding close.`); + return events; + } catch (error) { + this.logger.error('Error finding race events awaiting stewarding close:', error); + throw error; + } } async create(raceEvent: RaceEvent): Promise { - this.raceEvents.set(raceEvent.id, raceEvent); - return raceEvent; + this.logger.debug(`Creating race event: ${raceEvent.id}`); + try { + this.raceEvents.set(raceEvent.id, raceEvent); + this.logger.info(`Race event ${raceEvent.id} created successfully.`); + return raceEvent; + } catch (error) { + this.logger.error(`Error creating race event ${raceEvent.id}:`, error); + throw error; + } } async update(raceEvent: RaceEvent): Promise { - this.raceEvents.set(raceEvent.id, raceEvent); - return raceEvent; + this.logger.debug(`Updating race event: ${raceEvent.id}`); + try { + if (!this.raceEvents.has(raceEvent.id)) { + this.logger.warn(`Race event with id ${raceEvent.id} not found for update. Creating new.`); + } + this.raceEvents.set(raceEvent.id, raceEvent); + this.logger.info(`Race event ${raceEvent.id} updated successfully.`); + return raceEvent; + } catch (error) { + this.logger.error(`Error updating race event ${raceEvent.id}:`, error); + throw error; + } } async delete(id: string): Promise { - this.raceEvents.delete(id); + this.logger.debug(`Deleting race event: ${id}`); + try { + if (this.raceEvents.delete(id)) { + this.logger.info(`Race event ${id} deleted successfully.`); + } else { + this.logger.warn(`Race event with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting race event ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.raceEvents.has(id); + this.logger.debug(`Checking existence of race event with id: ${id}`); + try { + const exists = this.raceEvents.has(id); + this.logger.debug(`Race event ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of race event with id ${id}:`, error); + throw error; + } } // Test helper methods clear(): void { + this.logger.debug('Clearing all race events.'); this.raceEvents.clear(); + this.logger.info('All race events cleared.'); } getAll(): RaceEvent[] { - return Array.from(this.raceEvents.values()); + this.logger.debug('Getting all race events.'); + try { + const events = Array.from(this.raceEvents.values()); + this.logger.info(`Retrieved ${events.length} race events.`); + return events; + } catch (error) { + this.logger.error(`Error getting all race events:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts b/packages/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts index 25ee21935..2ef44a6a4 100644 --- a/packages/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts @@ -5,6 +5,7 @@ * 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'; @@ -13,12 +14,16 @@ type RaceRegistrationSeed = Pick>; private registrationsByDriver: Map>; + private readonly logger: ILogger; - constructor(seedRegistrations?: RaceRegistrationSeed[]) { + constructor(logger: ILogger, seedRegistrations?: RaceRegistrationSeed[]) { + this.logger = logger; + 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); }); @@ -26,92 +31,148 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo } 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 { + this.logger.info('Checking if driver is registered for race', { raceId, driverId }); const raceSet = this.registrationsByRace.get(raceId); - if (!raceSet) return false; - return raceSet.has(driverId); + 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; } async getRegisteredDrivers(raceId: string): Promise { + this.logger.info('Attempting to fetch registered drivers for race', { raceId }); const raceSet = this.registrationsByRace.get(raceId); - if (!raceSet) return []; - return Array.from(raceSet.values()); + if (!raceSet) { + this.logger.debug('No registered drivers found for race', { raceId }); + return []; + } + 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; } async getRegistrationCount(raceId: string): Promise { + this.logger.info('Attempting to get registration count for race', { raceId }); const raceSet = this.registrationsByRace.get(raceId); - return raceSet ? raceSet.size : 0; + 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; } async register(registration: RaceRegistration): Promise { + 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.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt); + this.logger.info('Driver successfully registered for race', { raceId: registration.raceId, driverId: registration.driverId }); } async withdraw(raceId: string, driverId: string): Promise { + 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.removeFromIndexes(raceId, driverId); + this.logger.info('Driver successfully withdrew from race', { raceId, driverId }); } async getDriverRegistrations(driverId: string): Promise { + this.logger.info('Attempting to fetch registrations for driver', { driverId }); const driverSet = this.registrationsByDriver.get(driverId); - if (!driverSet) return []; - return Array.from(driverSet.values()); + if (!driverSet) { + this.logger.debug('No registrations found for driver', { driverId }); + return []; + } + 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; } async clearRaceRegistrations(raceId: string): Promise { + this.logger.info('Attempting to clear all registrations for race', { raceId }); const raceSet = this.registrationsByRace.get(raceId); - if (!raceSet) return; + 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('Removed race from driver set during race clear', { raceId, driverId }); } this.registrationsByRace.delete(raceId); + this.logger.info('Successfully cleared all registrations for race', { raceId }); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryRaceRepository.ts b/packages/racing/infrastructure/repositories/InMemoryRaceRepository.ts index 7826b8ba9..022f5a630 100644 --- a/packages/racing/infrastructure/repositories/InMemoryRaceRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryRaceRepository.ts @@ -8,97 +8,194 @@ import { v4 as uuidv4 } from 'uuid'; 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'; export class InMemoryRaceRepository implements IRaceRepository { private races: Map; + private readonly logger: ILogger; - constructor(seedData?: Race[]) { + constructor(logger: ILogger, seedData?: Race[]) { + this.logger = logger; + 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}.`); }); } } async findById(id: string): Promise { - return this.races.get(id) ?? 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; + } } async findAll(): Promise { - return Array.from(this.races.values()); + 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; + } } async findByLeagueId(leagueId: string): Promise { - return Array.from(this.races.values()) - .filter(race => race.leagueId === leagueId) - .sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); + 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; + } } async findUpcomingByLeagueId(leagueId: string): Promise { - const now = new Date(); - return 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.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; + } } async findCompletedByLeagueId(leagueId: string): Promise { - return Array.from(this.races.values()) - .filter(race => - race.leagueId === leagueId && - race.status === 'completed' - ) - .sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()); + 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; + } } async findByStatus(status: RaceStatus): Promise { - return Array.from(this.races.values()) - .filter(race => race.status === status) - .sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); + 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; + } } async findByDateRange(startDate: Date, endDate: Date): Promise { - return Array.from(this.races.values()) - .filter(race => - race.scheduledAt >= startDate && - race.scheduledAt <= endDate - ) - .sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); + 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; + } } async create(race: Race): Promise { - if (await this.exists(race.id)) { - throw new Error(`Race with ID ${race.id} already exists`); - } + 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); - return race; + 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; + } } async update(race: Race): Promise { - if (!await this.exists(race.id)) { - throw new Error(`Race with ID ${race.id} not found`); - } + 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); - return race; + 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; + } } async delete(id: string): Promise { - if (!await this.exists(id)) { - throw new Error(`Race with ID ${id} not found`); - } + 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.races.delete(id); + this.logger.info(`Race ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting race ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.races.has(id); + 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; + } } /** diff --git a/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts b/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts index fa6a73b84..9fe6614c8 100644 --- a/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryResultRepository.ts @@ -9,112 +9,221 @@ import { v4 as uuidv4 } from 'uuid'; import { Result } from '@gridpilot/racing/domain/entities/Result'; import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository'; import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryResultRepository implements IResultRepository { private results: Map; private raceRepository: IRaceRepository | null; + private readonly logger: ILogger; - constructor(seedData?: Result[], raceRepository?: IRaceRepository | null) { + constructor(logger: ILogger, seedData?: Result[], raceRepository?: IRaceRepository | null) { + this.logger = logger; + this.logger.info('InMemoryResultRepository initialized.'); this.results = new Map(); this.raceRepository = raceRepository ?? null; if (seedData) { seedData.forEach(result => { this.results.set(result.id, result); + this.logger.debug(`Seeded result: ${result.id}`); }); } } async findById(id: string): Promise { - return this.results.get(id) ?? null; + this.logger.debug(`Finding result by id: ${id}`); + try { + const result = this.results.get(id) ?? null; + if (result) { + this.logger.info(`Found result with id: ${id}.`); + } else { + this.logger.warn(`Result with id ${id} not found.`); + } + return result; + } catch (error) { + this.logger.error(`Error finding result by id ${id}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.results.values()); + this.logger.debug('Finding all results.'); + try { + const results = Array.from(this.results.values()); + this.logger.info(`Found ${results.length} results.`); + return results; + } catch (error) { + this.logger.error('Error finding all results:', error); + throw error; + } } async findByRaceId(raceId: string): Promise { - return Array.from(this.results.values()) - .filter(result => result.raceId === raceId) - .sort((a, b) => a.position - b.position); + this.logger.debug(`Finding results for race id: ${raceId}`); + try { + const results = Array.from(this.results.values()) + .filter(result => result.raceId === raceId) + .sort((a, b) => a.position - b.position); + this.logger.info(`Found ${results.length} results for race id: ${raceId}.`); + return results; + } catch (error) { + this.logger.error(`Error finding results for race id ${raceId}:`, error); + throw error; + } } async findByDriverId(driverId: string): Promise { - return Array.from(this.results.values()) - .filter(result => result.driverId === driverId); + this.logger.debug(`Finding results for driver id: ${driverId}`); + try { + const results = Array.from(this.results.values()) + .filter(result => result.driverId === driverId); + this.logger.info(`Found ${results.length} results for driver id: ${driverId}.`); + return results; + } catch (error) { + this.logger.error(`Error finding results for driver id ${driverId}:`, error); + throw error; + } } async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise { - if (!this.raceRepository) { - return []; + this.logger.debug(`Finding results for driver id: ${driverId} and league id: ${leagueId}`); + try { + if (!this.raceRepository) { + this.logger.warn('Race repository not provided to InMemoryResultRepository. Skipping league-filtered search.'); + return []; + } + + const leagueRaces = await this.raceRepository.findByLeagueId(leagueId); + const leagueRaceIds = new Set(leagueRaces.map(race => race.id)); + this.logger.debug(`Found ${leagueRaces.length} races in league ${leagueId}.`); + + const results = Array.from(this.results.values()) + .filter(result => + result.driverId === driverId && + leagueRaceIds.has(result.raceId) + ); + this.logger.info(`Found ${results.length} results for driver ${driverId} in league ${leagueId}.`); + return results; + } catch (error) { + this.logger.error(`Error finding results for driver ${driverId} and league ${leagueId}:`, error); + throw error; } - - const leagueRaces = await this.raceRepository.findByLeagueId(leagueId); - const leagueRaceIds = new Set(leagueRaces.map(race => race.id)); - - return Array.from(this.results.values()) - .filter(result => - result.driverId === driverId && - leagueRaceIds.has(result.raceId) - ); } async create(result: Result): Promise { - if (await this.exists(result.id)) { - throw new Error(`Result with ID ${result.id} already exists`); - } + this.logger.debug(`Creating result: ${result.id}`); + try { + if (await this.exists(result.id)) { + this.logger.warn(`Result with ID ${result.id} already exists. Throwing error.`); + throw new Error(`Result with ID ${result.id} already exists`); + } - this.results.set(result.id, result); - return result; + this.results.set(result.id, result); + this.logger.info(`Result ${result.id} created successfully.`); + return result; + } catch (error) { + this.logger.error(`Error creating result ${result.id}:`, error); + throw error; + } } async createMany(results: Result[]): Promise { - const created: Result[] = []; - - for (const result of results) { - if (await this.exists(result.id)) { - throw new Error(`Result with ID ${result.id} already exists`); + this.logger.debug(`Creating ${results.length} results.`); + try { + const created: Result[] = []; + + for (const result of results) { + if (await this.exists(result.id)) { + this.logger.warn(`Result with ID ${result.id} already exists. Skipping creation.`); + // In a real system, decide if this should throw or log and skip + continue; + } + this.results.set(result.id, result); + created.push(result); } - this.results.set(result.id, result); - created.push(result); + this.logger.info(`Created ${created.length} results successfully.`); + + return created; + } catch (error) { + this.logger.error(`Error creating many results:`, error); + throw error; } - - return created; } async update(result: Result): Promise { - if (!await this.exists(result.id)) { - throw new Error(`Result with ID ${result.id} not found`); - } + this.logger.debug(`Updating result: ${result.id}`); + try { + if (!await this.exists(result.id)) { + this.logger.warn(`Result with ID ${result.id} not found for update. Throwing error.`); + throw new Error(`Result with ID ${result.id} not found`); + } - this.results.set(result.id, result); - return result; + this.results.set(result.id, result); + this.logger.info(`Result ${result.id} updated successfully.`); + return result; + } catch (error) { + this.logger.error(`Error updating result ${result.id}:`, error); + throw error; + } } async delete(id: string): Promise { - if (!await this.exists(id)) { - throw new Error(`Result with ID ${id} not found`); - } + this.logger.debug(`Deleting result: ${id}`); + try { + if (!await this.exists(id)) { + this.logger.warn(`Result with ID ${id} not found for deletion. Throwing error.`); + throw new Error(`Result with ID ${id} not found`); + } - this.results.delete(id); + this.results.delete(id); + this.logger.info(`Result ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting result ${id}:`, error); + throw error; + } } async deleteByRaceId(raceId: string): Promise { - const raceResults = await this.findByRaceId(raceId); - raceResults.forEach(result => { - this.results.delete(result.id); - }); + this.logger.debug(`Deleting results for race id: ${raceId}`); + try { + const initialCount = this.results.size; + const raceResults = Array.from(this.results.values()).filter( + result => result.raceId === raceId + ); + raceResults.forEach(result => { + this.results.delete(result.id); + }); + this.logger.info(`Deleted ${raceResults.length} results for race id: ${raceId}.`); + } catch (error) { + this.logger.error(`Error deleting results for race id ${raceId}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.results.has(id); + this.logger.debug(`Checking existence of result with id: ${id}`); + try { + const exists = this.results.has(id); + this.logger.debug(`Result ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of result with id ${id}:`, error); + throw error; + } } async existsByRaceId(raceId: string): Promise { - return Array.from(this.results.values()).some( - result => result.raceId === raceId - ); - } + this.logger.debug(`Checking existence of results for race id: ${raceId}`); + try { + const exists = Array.from(this.results.values()).some( + result => result.raceId === raceId + ); + this.logger.debug(`Results for race ${raceId} exist: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of results for race id ${raceId}:`, error); + throw error; + } /** * Utility method to generate a new UUID diff --git a/packages/racing/infrastructure/repositories/InMemoryScoringRepositories.ts b/packages/racing/infrastructure/repositories/InMemoryScoringRepositories.ts index 5dd931eb7..1c69e35b9 100644 --- a/packages/racing/infrastructure/repositories/InMemoryScoringRepositories.ts +++ b/packages/racing/infrastructure/repositories/InMemoryScoringRepositories.ts @@ -13,6 +13,22 @@ import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/r import { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding'; import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType'; import type { ParticipantRef } from '@gridpilot/racing/domain/types/ParticipantRef'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; + +class SilentLogger implements ILogger { + debug(..._args: unknown[]): void { + // console.debug(..._args); + } + info(..._args: unknown[]): void { + // console.info(..._args); + } + warn(..._args: unknown[]): void { + // console.warn(..._args); + } + error(..._args: unknown[]): void { + // console.error(..._args); + } +} export type LeagueScoringPresetPrimaryChampionshipType = | 'driver' @@ -248,70 +264,168 @@ export function getLeagueScoringPresetById( export class InMemoryGameRepository implements IGameRepository { private games: Game[]; + private readonly logger: ILogger; - constructor(seedData?: Game[]) { + constructor(logger: ILogger, seedData?: Game[]) { + this.logger = logger; + this.logger.info('InMemoryGameRepository initialized.'); this.games = seedData ? [...seedData] : []; } async findById(id: string): Promise { - return this.games.find((g) => g.id === id) ?? null; + this.logger.debug(`Finding game by id: ${id}`); + try { + const game = this.games.find((g) => g.id === id) ?? null; + if (game) { + this.logger.info(`Found game: ${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; + } } async findAll(): Promise { - return [...this.games]; + this.logger.debug('Finding all games.'); + try { + const games = [...this.games]; + this.logger.info(`Found ${games.length} games.`); + return games; + } catch (error) { + this.logger.error('Error finding all games:', error); + throw error; + } } seed(game: Game): void { - this.games.push(game); + this.logger.debug(`Seeding game: ${game.id}`); + try { + this.games.push(game); + this.logger.info(`Game ${game.id} seeded successfully.`); + } catch (error) { + this.logger.error(`Error seeding game ${game.id}:`, error); + throw error; + } } } export class InMemorySeasonRepository implements ISeasonRepository { private seasons: Season[]; + private readonly logger: ILogger; - constructor(seedData?: Season[]) { + constructor(logger: ILogger, seedData?: Season[]) { + this.logger = logger; + this.logger.info('InMemorySeasonRepository initialized.'); this.seasons = seedData ? [...seedData] : []; } async findById(id: string): Promise { - return this.seasons.find((s) => s.id === id) ?? null; + this.logger.debug(`Finding season by id: ${id}`); + try { + const season = this.seasons.find((s) => s.id === id) ?? null; + if (season) { + this.logger.info(`Found season: ${season.id}`); + } else { + this.logger.warn(`Season with id ${id} not found.`); + } + return season; + } catch (error) { + this.logger.error(`Error finding season by id ${id}:`, error); + throw error; + } } async findByLeagueId(leagueId: string): Promise { - return this.seasons.filter((s) => s.leagueId === leagueId); + this.logger.debug(`Finding seasons by league id: ${leagueId}`); + try { + const seasons = this.seasons.filter((s) => s.leagueId === leagueId); + this.logger.info(`Found ${seasons.length} seasons for league id: ${leagueId}.`); + return seasons; + } catch (error) { + this.logger.error(`Error finding seasons by league id ${leagueId}:`, error); + throw error; + } } async create(season: Season): Promise { - // Backward-compatible alias for add() - this.seasons.push(season); - return season; + this.logger.debug(`Creating season: ${season.id}`); + try { + // Backward-compatible alias for add() + this.seasons.push(season); + this.logger.info(`Season ${season.id} created successfully.`); + return season; + } catch (error) { + this.logger.error(`Error creating season ${season.id}:`, error); + throw error; + } } async add(season: Season): Promise { - this.seasons.push(season); + this.logger.debug(`Adding season: ${season.id}`); + try { + this.seasons.push(season); + this.logger.info(`Season ${season.id} added successfully.`); + } catch (error) { + this.logger.error(`Error adding season ${season.id}:`, error); + throw error; + } } async update(season: Season): Promise { - const index = this.seasons.findIndex((s) => s.id === season.id); - if (index === -1) { - this.seasons.push(season); - return; + this.logger.debug(`Updating season: ${season.id}`); + try { + const index = this.seasons.findIndex((s) => s.id === season.id); + if (index === -1) { + this.logger.warn(`Season with id ${season.id} not found for update. Adding as new.`); + this.seasons.push(season); + return; + } + this.seasons[index] = season; + this.logger.info(`Season ${season.id} updated successfully.`); + } catch (error) { + this.logger.error(`Error updating season ${season.id}:`, error); + throw error; } - this.seasons[index] = season; } async listByLeague(leagueId: string): Promise { - return this.seasons.filter((s) => s.leagueId === leagueId); + this.logger.debug(`Listing seasons by league id: ${leagueId}`); + try { + const seasons = this.seasons.filter((s) => s.leagueId === leagueId); + this.logger.info(`Found ${seasons.length} seasons for league id: ${leagueId}.`); + return seasons; + } catch (error) { + this.logger.error(`Error listing seasons by league id ${leagueId}:`, error); + throw error; + } } async listActiveByLeague(leagueId: string): Promise { - return this.seasons.filter( - (s) => s.leagueId === leagueId && s.status === 'active', - ); + this.logger.debug(`Listing active seasons by league id: ${leagueId}`); + try { + const seasons = this.seasons.filter( + (s) => s.leagueId === leagueId && s.status === 'active', + ); + this.logger.info(`Found ${seasons.length} active seasons for league id: ${leagueId}.`); + return seasons; + } catch (error) { + this.logger.error(`Error listing active seasons by league id ${leagueId}:`, error); + throw error; + } } seed(season: Season): void { - this.seasons.push(season); + this.logger.debug(`Seeding season: ${season.id}`); + try { + this.seasons.push(season); + this.logger.info(`Season ${season.id} seeded successfully.`); + } catch (error) { + this.logger.error(`Error seeding season ${season.id}:`, error); + throw error; + } } } @@ -319,29 +433,59 @@ export class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepository { private configs: LeagueScoringConfig[]; + private readonly logger: ILogger; - constructor(seedData?: LeagueScoringConfig[]) { + constructor(logger: ILogger, seedData?: LeagueScoringConfig[]) { + this.logger = logger; + this.logger.info('InMemoryLeagueScoringConfigRepository initialized.'); this.configs = seedData ? [...seedData] : []; } async findBySeasonId(seasonId: string): Promise { - return this.configs.find((c) => c.seasonId === seasonId) ?? null; + this.logger.debug(`Finding league scoring config by seasonId: ${seasonId}`); + try { + const config = this.configs.find((c) => c.seasonId === seasonId) ?? null; + if (config) { + this.logger.info(`Found league scoring config for seasonId: ${seasonId}.`); + } else { + this.logger.warn(`League scoring config for seasonId ${seasonId} not found.`); + } + return config; + } catch (error) { + this.logger.error(`Error finding league scoring config for seasonId ${seasonId}:`, error); + throw error; + } } async save(config: LeagueScoringConfig): Promise { - const existingIndex = this.configs.findIndex( - (c) => c.id === config.id, - ); - if (existingIndex >= 0) { - this.configs[existingIndex] = config; - } else { - this.configs.push(config); + this.logger.debug(`Saving league scoring config: ${config.id} for seasonId: ${config.seasonId}`); + try { + const existingIndex = this.configs.findIndex( + (c) => c.id === config.id, + ); + if (existingIndex >= 0) { + this.configs[existingIndex] = config; + this.logger.info(`Updated existing league scoring config: ${config.id}.`); + } else { + this.configs.push(config); + this.logger.info(`Created new league scoring config: ${config.id}.`); + } + return config; + } catch (error) { + this.logger.error(`Error saving league scoring config ${config.id}:`, error); + throw error; } - return config; } seed(config: LeagueScoringConfig): void { - this.configs.push(config); + this.logger.debug(`Seeding league scoring config: ${config.id}`); + try { + this.configs.push(config); + this.logger.info(`League scoring config ${config.id} seeded successfully.`); + } catch (error) { + this.logger.error(`Error seeding league scoring config ${config.id}:`, error); + throw error; + } } } @@ -349,26 +493,63 @@ export class InMemoryChampionshipStandingRepository implements IChampionshipStandingRepository { private standings: ChampionshipStanding[] = []; + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: ChampionshipStanding[]) { + this.logger = logger; + this.logger.info('InMemoryChampionshipStandingRepository initialized.'); + this.standings = seedData ? [...seedData] : []; + } async findBySeasonAndChampionship( seasonId: string, championshipId: string, ): Promise { - return this.standings.filter( - (s) => s.seasonId === seasonId && s.championshipId === championshipId, - ); + this.logger.debug(`Finding championship standings for season: ${seasonId}, championship: ${championshipId}`); + try { + const standings = this.standings.filter( + (s) => s.seasonId === seasonId && s.championshipId === championshipId, + ); + this.logger.info(`Found ${standings.length} championship standings.`); + return standings; + } catch (error) { + this.logger.error(`Error finding championship standings for season ${seasonId}, championship ${championshipId}:`, error); + throw error; + } } async saveAll(standings: ChampionshipStanding[]): Promise { - this.standings = standings; + this.logger.debug(`Saving ${standings.length} championship standings.`); + try { + this.standings = standings; + this.logger.info(`${standings.length} championship standings saved.`); + } catch (error) { + this.logger.error(`Error saving championship standings:`, error); + throw error; + } } seed(standing: ChampionshipStanding): void { - this.standings.push(standing); + this.logger.debug(`Seeding championship standing: ${standing.id}`); + try { + this.standings.push(standing); + this.logger.info(`Championship standing ${standing.id} seeded successfully.`); + } catch (error) { + this.logger.error(`Error seeding championship standing ${standing.id}:`, error); + throw error; + } } getAll(): ChampionshipStanding[] { - return [...this.standings]; + this.logger.debug('Getting all championship standings.'); + try { + const standings = [...this.standings]; + this.logger.info(`Retrieved ${standings.length} championship standings.`); + return standings; + } catch (error) { + this.logger.error(`Error getting all championship standings:`, error); + throw error; + } } } @@ -387,6 +568,8 @@ export function createSprintMainDemoScoringSetup(params: { const seasonId = params.seasonId ?? 'season-sprint-main-demo'; const championshipId = 'driver-champ'; + const logger = new SilentLogger(); + const game = Game.create({ id: 'iracing', name: 'iRacing' }); const season = Season.create({ @@ -410,12 +593,12 @@ export function createSprintMainDemoScoringSetup(params: { seasonId: season.id, }); - const gameRepo = new InMemoryGameRepository([game]); - const seasonRepo = new InMemorySeasonRepository([season]); - const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository([ + const gameRepo = new InMemoryGameRepository(logger, [game]); + const seasonRepo = new InMemorySeasonRepository(logger, [season]); + const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository(logger, [ leagueScoringConfig, ]); - const championshipStandingRepo = new InMemoryChampionshipStandingRepository(); + const championshipStandingRepo = new InMemoryChampionshipStandingRepository(logger); return { gameRepo, diff --git a/packages/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository.ts b/packages/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository.ts index 4a09eac15..8790085cb 100644 --- a/packages/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository.ts @@ -6,67 +6,165 @@ import type { SeasonSponsorship, SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRepository { private sponsorships: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: SeasonSponsorship[]) { + this.logger = logger; + this.logger.info('InMemorySeasonSponsorshipRepository initialized.'); + if (seedData) { + this.seed(seedData); + } + } async findById(id: string): Promise { - return this.sponsorships.get(id) ?? null; + this.logger.debug(`Finding season sponsorship by id: ${id}`); + try { + const sponsorship = this.sponsorships.get(id) ?? null; + if (sponsorship) { + this.logger.info(`Found season sponsorship: ${id}.`); + } else { + this.logger.warn(`Season sponsorship with id ${id} not found.`); + } + return sponsorship; + } catch (error) { + this.logger.error(`Error finding season sponsorship by id ${id}:`, error); + throw error; + } } async findBySeasonId(seasonId: string): Promise { - return Array.from(this.sponsorships.values()).filter(s => s.seasonId === seasonId); + this.logger.debug(`Finding season sponsorships by season id: ${seasonId}`); + try { + const sponsorships = Array.from(this.sponsorships.values()).filter(s => s.seasonId === seasonId); + this.logger.info(`Found ${sponsorships.length} season sponsorships for season id: ${seasonId}.`); + return sponsorships; + } catch (error) { + this.logger.error(`Error finding season sponsorships by season id ${seasonId}:`, error); + throw error; + } } async findByLeagueId(leagueId: string): Promise { - return Array.from(this.sponsorships.values()).filter(s => s.leagueId === leagueId); + this.logger.debug(`Finding season sponsorships by league id: ${leagueId}`); + try { + const sponsorships = Array.from(this.sponsorships.values()).filter(s => s.leagueId === leagueId); + this.logger.info(`Found ${sponsorships.length} season sponsorships for league id: ${leagueId}.`); + return sponsorships; + } catch (error) { + this.logger.error(`Error finding season sponsorships by league id ${leagueId}:`, error); + throw error; + } } async findBySponsorId(sponsorId: string): Promise { - return Array.from(this.sponsorships.values()).filter(s => s.sponsorId === sponsorId); + this.logger.debug(`Finding season sponsorships by sponsor id: ${sponsorId}`); + try { + const sponsorships = Array.from(this.sponsorships.values()).filter(s => s.sponsorId === sponsorId); + this.logger.info(`Found ${sponsorships.length} season sponsorships for sponsor id: ${sponsorId}.`); + return sponsorships; + } catch (error) { + this.logger.error(`Error finding season sponsorships by sponsor id ${sponsorId}:`, error); + throw error; + } } async findBySeasonAndTier(seasonId: string, tier: SponsorshipTier): Promise { - return Array.from(this.sponsorships.values()).filter( - s => s.seasonId === seasonId && s.tier === tier - ); + this.logger.debug(`Finding season sponsorships by season id: ${seasonId} and tier: ${tier}`); + try { + const sponsorships = Array.from(this.sponsorships.values()).filter( + s => s.seasonId === seasonId && s.tier === tier + ); + this.logger.info(`Found ${sponsorships.length} season sponsorships for season id: ${seasonId}, tier: ${tier}.`); + return sponsorships; + } catch (error) { + this.logger.error(`Error finding season sponsorships by season id ${seasonId}, tier ${tier}:`, error); + throw error; + } } async create(sponsorship: SeasonSponsorship): Promise { - if (this.sponsorships.has(sponsorship.id)) { - throw new Error('SeasonSponsorship with this ID already exists'); + this.logger.debug(`Creating season sponsorship: ${sponsorship.id}`); + try { + if (this.sponsorships.has(sponsorship.id)) { + this.logger.warn(`SeasonSponsorship with ID ${sponsorship.id} already exists.`); + throw new Error('SeasonSponsorship with this ID already exists'); + } + this.sponsorships.set(sponsorship.id, sponsorship); + this.logger.info(`SeasonSponsorship ${sponsorship.id} created successfully.`); + return sponsorship; + } catch (error) { + this.logger.error(`Error creating season sponsorship ${sponsorship.id}:`, error); + throw error; } - this.sponsorships.set(sponsorship.id, sponsorship); - return sponsorship; } async update(sponsorship: SeasonSponsorship): Promise { - if (!this.sponsorships.has(sponsorship.id)) { - throw new Error('SeasonSponsorship not found'); + this.logger.debug(`Updating season sponsorship: ${sponsorship.id}`); + try { + if (!this.sponsorships.has(sponsorship.id)) { + this.logger.warn(`SeasonSponsorship with ID ${sponsorship.id} not found for update.`); + throw new Error('SeasonSponsorship not found'); + } + this.sponsorships.set(sponsorship.id, sponsorship); + this.logger.info(`SeasonSponsorship ${sponsorship.id} updated successfully.`); + return sponsorship; + } catch (error) { + this.logger.error(`Error updating season sponsorship ${sponsorship.id}:`, error); + throw error; } - this.sponsorships.set(sponsorship.id, sponsorship); - return sponsorship; } async delete(id: string): Promise { - this.sponsorships.delete(id); + this.logger.debug(`Deleting season sponsorship: ${id}`); + try { + if (this.sponsorships.delete(id)) { + this.logger.info(`SeasonSponsorship ${id} deleted successfully.`); + } else { + this.logger.warn(`SeasonSponsorship with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting season sponsorship ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.sponsorships.has(id); + this.logger.debug(`Checking existence of season sponsorship with id: ${id}`); + try { + const exists = this.sponsorships.has(id); + this.logger.debug(`SeasonSponsorship ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of season sponsorship with id ${id}:`, error); + throw error; + } } /** * Seed initial data */ seed(sponsorships: SeasonSponsorship[]): void { - for (const sponsorship of sponsorships) { - this.sponsorships.set(sponsorship.id, sponsorship); + this.logger.debug(`Seeding ${sponsorships.length} season sponsorships.`); + try { + for (const sponsorship of sponsorships) { + this.sponsorships.set(sponsorship.id, sponsorship); + this.logger.debug(`Seeded season sponsorship: ${sponsorship.id}.`); + } + this.logger.info(`Successfully seeded ${sponsorships.length} season sponsorships.`); + } catch (error) { + this.logger.error(`Error seeding season sponsorships:`, error); + throw error; } } // Test helper clear(): void { + this.logger.debug('Clearing all season sponsorships.'); this.sponsorships.clear(); + this.logger.info('All season sponsorships cleared.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemorySessionRepository.ts b/packages/racing/infrastructure/repositories/InMemorySessionRepository.ts index 941dc0114..29c837cca 100644 --- a/packages/racing/infrastructure/repositories/InMemorySessionRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemorySessionRepository.ts @@ -3,60 +3,158 @@ */ import type { ISessionRepository } from '../../domain/repositories/ISessionRepository'; import type { Session } from '../../domain/entities/Session'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemorySessionRepository implements ISessionRepository { private sessions: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: Session[]) { + this.logger = logger; + this.logger.info('InMemorySessionRepository initialized.'); + if (seedData) { + seedData.forEach(session => this.sessions.set(session.id, session)); + this.logger.debug(`Seeded ${seedData.length} sessions.`); + } + } async findById(id: string): Promise { - return this.sessions.get(id) ?? null; + this.logger.debug(`Finding session by id: ${id}`); + try { + const session = this.sessions.get(id) ?? null; + if (session) { + this.logger.info(`Found session: ${id}.`); + } else { + this.logger.warn(`Session with id ${id} not found.`); + } + return session; + } catch (error) { + this.logger.error(`Error finding session by id ${id}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.sessions.values()); + this.logger.debug('Finding all sessions.'); + try { + const sessions = Array.from(this.sessions.values()); + this.logger.info(`Found ${sessions.length} sessions.`); + return sessions; + } catch (error) { + this.logger.error('Error finding all sessions:', error); + throw error; + } } async findByRaceEventId(raceEventId: string): Promise { - return Array.from(this.sessions.values()).filter( - session => session.raceEventId === raceEventId - ); + this.logger.debug(`Finding sessions by race event id: ${raceEventId}`); + try { + const sessions = Array.from(this.sessions.values()).filter( + session => session.raceEventId === raceEventId + ); + this.logger.info(`Found ${sessions.length} sessions for race event id: ${raceEventId}.`); + return sessions; + } catch (error) { + this.logger.error(`Error finding sessions by race event id ${raceEventId}:`, error); + throw error; + } } async findByLeagueId(leagueId: string): Promise { + this.logger.debug(`Finding sessions by league id: ${leagueId} (not directly supported, returning empty).`); // Sessions don't have leagueId directly - would need to join with RaceEvent // For now, return empty array return []; } async findByStatus(status: string): Promise { - return Array.from(this.sessions.values()).filter( - session => session.status === status - ); + this.logger.debug(`Finding sessions by status: ${status}`); + try { + const sessions = Array.from(this.sessions.values()).filter( + session => session.status === status + ); + this.logger.info(`Found ${sessions.length} sessions with status: ${status}.`); + return sessions; + } catch (error) { + this.logger.error(`Error finding sessions by status ${status}:`, error); + throw error; + } } async create(session: Session): Promise { - this.sessions.set(session.id, session); - return session; + this.logger.debug(`Creating session: ${session.id}`); + try { + if (this.sessions.has(session.id)) { + this.logger.warn(`Session with ID ${session.id} already exists.`); + throw new Error(`Session with ID ${session.id} already exists`); + } + this.sessions.set(session.id, session); + this.logger.info(`Session ${session.id} created successfully.`); + return session; + } catch (error) { + this.logger.error(`Error creating session ${session.id}:`, error); + throw error; + } } async update(session: Session): Promise { - this.sessions.set(session.id, session); - return session; + this.logger.debug(`Updating session: ${session.id}`); + try { + if (!this.sessions.has(session.id)) { + this.logger.warn(`Session with ID ${session.id} not found for update.`); + throw new Error(`Session with ID ${session.id} not found`); + } + this.sessions.set(session.id, session); + this.logger.info(`Session ${session.id} updated successfully.`); + return session; + } catch (error) { + this.logger.error(`Error updating session ${session.id}:`, error); + throw error; + } } async delete(id: string): Promise { - this.sessions.delete(id); + this.logger.debug(`Deleting session: ${id}`); + try { + if (this.sessions.delete(id)) { + this.logger.info(`Session ${id} deleted successfully.`); + } else { + this.logger.warn(`Session with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting session ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.sessions.has(id); + this.logger.debug(`Checking existence of session with id: ${id}`); + try { + const exists = this.sessions.has(id); + this.logger.debug(`Session ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of session with id ${id}:`, error); + throw error; + } } // Test helper methods clear(): void { + this.logger.debug('Clearing all sessions.'); this.sessions.clear(); + this.logger.info('All sessions cleared.'); } getAll(): Session[] { - return Array.from(this.sessions.values()); + this.logger.debug('Getting all sessions.'); + try { + const sessions = Array.from(this.sessions.values()); + this.logger.info(`Retrieved ${sessions.length} sessions.`); + return sessions; + } catch (error) { + this.logger.error(`Error getting all sessions:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemorySponsorRepository.ts b/packages/racing/infrastructure/repositories/InMemorySponsorRepository.ts index 10222ab6b..d0eba93bc 100644 --- a/packages/racing/infrastructure/repositories/InMemorySponsorRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemorySponsorRepository.ts @@ -6,62 +6,144 @@ import type { Sponsor } from '../../domain/entities/Sponsor'; import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemorySponsorRepository implements ISponsorRepository { private sponsors: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: Sponsor[]) { + this.logger = logger; + this.logger.info('InMemorySponsorRepository initialized.'); + if (seedData) { + this.seed(seedData); + } + } async findById(id: string): Promise { - return this.sponsors.get(id) ?? 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; + } } async findAll(): Promise { - return Array.from(this.sponsors.values()); + 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; + } } async findByEmail(email: string): Promise { - for (const sponsor of this.sponsors.values()) { - if (sponsor.contactEmail === email) { - return sponsor; + 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.warn(`Sponsor with email ${email} not found.`); + return null; + } catch (error) { + this.logger.error(`Error finding sponsor by email ${email}:`, error); + throw error; } - return null; } async create(sponsor: Sponsor): Promise { - if (this.sponsors.has(sponsor.id)) { - throw new Error('Sponsor with this ID already exists'); + 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.sponsors.set(sponsor.id, sponsor); - return sponsor; } async update(sponsor: Sponsor): Promise { - if (!this.sponsors.has(sponsor.id)) { - throw new Error('Sponsor not found'); + 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.sponsors.set(sponsor.id, sponsor); - return sponsor; } async delete(id: string): Promise { - this.sponsors.delete(id); + 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; + } } async exists(id: string): Promise { - return this.sponsors.has(id); + 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; + } } /** * Seed initial data */ seed(sponsors: Sponsor[]): void { - for (const sponsor of sponsors) { - this.sponsors.set(sponsor.id, sponsor); + 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.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository.ts b/packages/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository.ts index f57af0eaf..d37258e5a 100644 --- a/packages/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository.ts @@ -5,6 +5,7 @@ import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository'; import { SponsorshipPricing } from '../../domain/value-objects/SponsorshipPricing'; import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; interface StorageKey { entityType: SponsorableEntityType; @@ -13,48 +14,115 @@ interface StorageKey { export class InMemorySponsorshipPricingRepository implements ISponsorshipPricingRepository { private pricings: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>) { + this.logger = logger; + this.logger.info('InMemorySponsorshipPricingRepository initialized.'); + if (seedData) { + this.seed(seedData); + } + } private makeKey(entityType: SponsorableEntityType, entityId: string): string { return `${entityType}:${entityId}`; } async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise { - const key = this.makeKey(entityType, entityId); - const entry = this.pricings.get(key); - return entry?.pricing ?? null; + this.logger.debug(`Finding sponsorship pricing for entity: ${entityType}, ${entityId}`); + try { + const key = this.makeKey(entityType, entityId); + const entry = this.pricings.get(key); + const pricing = entry?.pricing ?? null; + if (pricing) { + this.logger.info(`Found sponsorship pricing for entity: ${entityType}, ${entityId}.`); + } else { + this.logger.warn(`Sponsorship pricing for entity ${entityType}, ${entityId} not found.`); + } + return pricing; + } catch (error) { + this.logger.error(`Error finding sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + throw error; + } } async save(entityType: SponsorableEntityType, entityId: string, pricing: SponsorshipPricing): Promise { - const key = this.makeKey(entityType, entityId); - this.pricings.set(key, { entityType, entityId, pricing }); + this.logger.debug(`Saving sponsorship pricing for entity: ${entityType}, ${entityId}`); + try { + const key = this.makeKey(entityType, entityId); + if (this.pricings.has(key)) { + this.logger.info(`Updating existing sponsorship pricing for entity: ${entityType}, ${entityId}.`); + } else { + this.logger.info(`Creating new sponsorship pricing for entity: ${entityType}, ${entityId}.`); + } + this.pricings.set(key, { entityType, entityId, pricing }); + this.logger.info(`Sponsorship pricing saved for entity: ${entityType}, ${entityId}.`); + } catch (error) { + this.logger.error(`Error saving sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + throw error; + } } async delete(entityType: SponsorableEntityType, entityId: string): Promise { - const key = this.makeKey(entityType, entityId); - this.pricings.delete(key); + this.logger.debug(`Deleting sponsorship pricing for entity: ${entityType}, ${entityId}`); + try { + const key = this.makeKey(entityType, entityId); + if (this.pricings.delete(key)) { + this.logger.info(`Sponsorship pricing deleted for entity: ${entityType}, ${entityId}.`); + } else { + this.logger.warn(`Sponsorship pricing for entity ${entityType}, ${entityId} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + throw error; + } } async exists(entityType: SponsorableEntityType, entityId: string): Promise { - const key = this.makeKey(entityType, entityId); - return this.pricings.has(key); + this.logger.debug(`Checking existence of sponsorship pricing for entity: ${entityType}, ${entityId}`); + try { + const key = this.makeKey(entityType, entityId); + const exists = this.pricings.has(key); + this.logger.debug(`Sponsorship pricing for entity ${entityType}, ${entityId} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of sponsorship pricing for entity ${entityType}, ${entityId}:`, error); + throw error; + } } async findAcceptingApplications(entityType: SponsorableEntityType): Promise> { - return Array.from(this.pricings.values()) - .filter(entry => entry.entityType === entityType && entry.pricing.acceptingApplications) - .map(entry => ({ entityId: entry.entityId, pricing: entry.pricing })); + this.logger.debug(`Finding entities accepting applications for type: ${entityType}`); + try { + const accepting = Array.from(this.pricings.values()) + .filter(entry => entry.entityType === entityType && entry.pricing.acceptingApplications) + .map(entry => ({ entityId: entry.entityId, pricing: entry.pricing })); + this.logger.info(`Found ${accepting.length} entities accepting applications for type: ${entityType}.`); + return accepting; + } catch (error) { + this.logger.error(`Error finding accepting applications for entity type ${entityType}:`, error); + throw error; + } } /** * Seed initial data */ seed(data: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>): void { - for (const entry of data) { - const key = this.makeKey(entry.entityType, entry.entityId); - this.pricings.set(key, entry); + this.logger.debug(`Seeding ${data.length} sponsorship pricing entries.`); + try { + for (const entry of data) { + const key = this.makeKey(entry.entityType, entry.entityId); + this.pricings.set(key, entry); + this.logger.debug(`Seeded pricing for entity ${entry.entityType}, ${entry.entityId}.`); + } + this.logger.info(`Successfully seeded ${data.length} sponsorship pricing entries.`); + } catch (error) { + this.logger.error(`Error seeding sponsorship pricing data:`, error); + throw error; } } @@ -62,6 +130,8 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing * Clear all data (for testing) */ clear(): void { + this.logger.debug('Clearing all sponsorship pricing data.'); this.pricings.clear(); + this.logger.info('All sponsorship pricing data cleared.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository.ts b/packages/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository.ts index be011fb07..d3bd02dfc 100644 --- a/packages/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository.ts @@ -8,100 +8,225 @@ import { type SponsorableEntityType, type SponsorshipRequestStatus } from '../../domain/entities/SponsorshipRequest'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemorySponsorshipRequestRepository implements ISponsorshipRequestRepository { private requests: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: SponsorshipRequest[]) { + this.logger = logger; + this.logger.info('InMemorySponsorshipRequestRepository initialized.'); + if (seedData) { + this.seed(seedData); + } + } async findById(id: string): Promise { - return this.requests.get(id) ?? 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; + } } async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise { - return Array.from(this.requests.values()).filter( - request => request.entityType === entityType && request.entityId === entityId - ); + 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; + } } async findPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise { - return Array.from(this.requests.values()).filter( - request => - request.entityType === entityType && - request.entityId === entityId && - request.status === 'pending' - ); + 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; + } } async findBySponsorId(sponsorId: string): Promise { - return Array.from(this.requests.values()).filter( - request => request.sponsorId === sponsorId - ); + 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; + } } async findByStatus(status: SponsorshipRequestStatus): Promise { - return Array.from(this.requests.values()).filter( - request => request.status === status - ); + 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; + } } async findBySponsorIdAndStatus(sponsorId: string, status: SponsorshipRequestStatus): Promise { - return Array.from(this.requests.values()).filter( - request => request.sponsorId === sponsorId && request.status === status - ); + 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; + } } async hasPendingRequest(sponsorId: string, entityType: SponsorableEntityType, entityId: string): Promise { - return Array.from(this.requests.values()).some( - request => - request.sponsorId === sponsorId && - request.entityType === entityType && - request.entityId === entityId && - request.status === 'pending' - ); + 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; + } } async countPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise { - return Array.from(this.requests.values()).filter( - request => - request.entityType === entityType && - request.entityId === entityId && - request.status === 'pending' - ).length; + 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; + } } async create(request: SponsorshipRequest): Promise { - this.requests.set(request.id, request); - return request; + 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; + } } async update(request: SponsorshipRequest): Promise { - if (!this.requests.has(request.id)) { - throw new Error(`SponsorshipRequest ${request.id} not found`); - } - this.requests.set(request.id, request); - return request; + 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; + } } async delete(id: string): Promise { - this.requests.delete(id); + 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; + } } async exists(id: string): Promise { - return this.requests.has(id); + 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; + } } /** * Seed initial data */ seed(requests: SponsorshipRequest[]): void { - for (const request of requests) { - this.requests.set(request.id, request); - } + 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.requests.clear(); + this.logger.debug('Clearing all sponsorship requests.'); + this.requests.clear(); + this.logger.info('All sponsorship requests cleared.'); } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts b/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts index fc6bf83bb..beab358dc 100644 --- a/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryStandingRepository.ts @@ -10,6 +10,7 @@ import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/ import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository'; import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository'; import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; /** * Points systems presets @@ -31,13 +32,17 @@ export class InMemoryStandingRepository implements IStandingRepository { private resultRepository: IResultRepository | null; private raceRepository: IRaceRepository | null; private leagueRepository: ILeagueRepository | null; + private readonly logger: ILogger; constructor( + logger: ILogger, seedData?: Standing[], resultRepository?: IResultRepository | null, raceRepository?: IRaceRepository | null, leagueRepository?: ILeagueRepository | null ) { + this.logger = logger; + this.logger.info('InMemoryStandingRepository initialized.'); this.standings = new Map(); this.resultRepository = resultRepository ?? null; this.raceRepository = raceRepository ?? null; @@ -47,6 +52,7 @@ export class InMemoryStandingRepository implements IStandingRepository { seedData.forEach(standing => { const key = this.getKey(standing.leagueId, standing.driverId); this.standings.set(key, standing); + this.logger.debug(`Seeded standing for league ${standing.leagueId}, driver ${standing.driverId}.`); }); } } @@ -56,134 +62,235 @@ export class InMemoryStandingRepository implements IStandingRepository { } async findByLeagueId(leagueId: string): Promise { - return Array.from(this.standings.values()) - .filter(standing => standing.leagueId === leagueId) - .sort((a, b) => { - // Sort by position (lower is better) - if (a.position !== b.position) { - return a.position - b.position; - } - // If positions are equal, sort by points (higher is better) - return b.points - a.points; - }); + this.logger.debug(`Finding standings for league id: ${leagueId}`); + try { + const standings = Array.from(this.standings.values()) + .filter(standing => standing.leagueId === leagueId) + .sort((a, b) => { + // Sort by position (lower is better) + if (a.position !== b.position) { + return a.position - b.position; + } + // If positions are equal, sort by points (higher is better) + return b.points - a.points; + }); + this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`); + return standings; + } catch (error) { + this.logger.error(`Error finding standings for league id ${leagueId}:`, error); + throw error; + } } async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise { - const key = this.getKey(leagueId, driverId); - return this.standings.get(key) ?? null; + this.logger.debug(`Finding standing for driver: ${driverId}, league: ${leagueId}`); + try { + const key = this.getKey(leagueId, driverId); + const standing = this.standings.get(key) ?? null; + if (standing) { + this.logger.info(`Found standing for driver: ${driverId}, league: ${leagueId}.`); + } else { + this.logger.warn(`Standing for driver ${driverId}, league ${leagueId} not found.`); + } + return standing; + } catch (error) { + this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.standings.values()); + this.logger.debug('Finding all standings.'); + try { + const standings = Array.from(this.standings.values()); + this.logger.info(`Found ${standings.length} standings.`); + return standings; + } catch (error) { + this.logger.error('Error finding all standings:', error); + throw error; + } } async save(standing: Standing): Promise { - const key = this.getKey(standing.leagueId, standing.driverId); - this.standings.set(key, standing); - return standing; + this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`); + try { + const key = this.getKey(standing.leagueId, standing.driverId); + if (this.standings.has(key)) { + 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}.`); + } + this.standings.set(key, standing); + 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); + throw error; + } } async saveMany(standings: Standing[]): Promise { - standings.forEach(standing => { - const key = this.getKey(standing.leagueId, standing.driverId); - this.standings.set(key, standing); - }); - return standings; + this.logger.debug(`Saving ${standings.length} standings.`); + try { + standings.forEach(standing => { + const key = this.getKey(standing.leagueId, standing.driverId); + this.standings.set(key, standing); + }); + this.logger.info(`${standings.length} standings saved successfully.`); + return standings; + } catch (error) { + this.logger.error(`Error saving many standings:`, error); + throw error; + } } async delete(leagueId: string, driverId: string): Promise { - const key = this.getKey(leagueId, driverId); - this.standings.delete(key); + this.logger.debug(`Deleting standing for league: ${leagueId}, driver: ${driverId}`); + try { + const key = this.getKey(leagueId, driverId); + if (this.standings.delete(key)) { + this.logger.info(`Standing for league ${leagueId}, driver ${driverId} deleted successfully.`); + } else { + 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); + throw error; + } } async deleteByLeagueId(leagueId: string): Promise { - const toDelete = Array.from(this.standings.values()) - .filter(standing => standing.leagueId === leagueId); - - toDelete.forEach(standing => { - const key = this.getKey(standing.leagueId, standing.driverId); - this.standings.delete(key); - }); + this.logger.debug(`Deleting all standings for league id: ${leagueId}`); + try { + const initialCount = Array.from(this.standings.values()).filter(s => s.leagueId === leagueId).length; + const toDelete = Array.from(this.standings.values()) + .filter(standing => standing.leagueId === leagueId); + + toDelete.forEach(standing => { + const key = this.getKey(standing.leagueId, standing.driverId); + this.standings.delete(key); + }); + this.logger.info(`Deleted ${toDelete.length} standings for league id: ${leagueId}.`); + } catch (error) { + this.logger.error(`Error deleting standings by league id ${leagueId}:`, error); + throw error; + } } async exists(leagueId: string, driverId: string): Promise { - const key = this.getKey(leagueId, driverId); - return this.standings.has(key); + this.logger.debug(`Checking existence of standing for league: ${leagueId}, driver: ${driverId}`); + try { + const key = this.getKey(leagueId, driverId); + const exists = this.standings.has(key); + 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); + throw error; + } } async recalculate(leagueId: string): Promise { - if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) { - throw new Error('Cannot recalculate standings: missing required repositories'); - } - - // Get league to determine points system - const league = await this.leagueRepository.findById(leagueId); - if (!league) { - throw new Error(`League with ID ${leagueId} not found`); - } - - // Get points system - const resolvedPointsSystem = - league.settings.customPoints ?? - POINTS_SYSTEMS[league.settings.pointsSystem] ?? - POINTS_SYSTEMS['f1-2024']; - - if (!resolvedPointsSystem) { - throw new Error('No points system configured for league'); - } - - const pointsSystem: Record = resolvedPointsSystem; - - // Get all completed races for the league - const races = await this.raceRepository.findCompletedByLeagueId(leagueId); - - // Get all results for these races - const allResults = await Promise.all( - races.map(race => this.resultRepository!.findByRaceId(race.id)) - ); - const results = allResults.flat(); - - // Calculate standings per driver - const standingsMap = new Map(); - - results.forEach(result => { - let standing = standingsMap.get(result.driverId); - - if (!standing) { - standing = Standing.create({ - leagueId, - driverId: result.driverId, - }); + this.logger.debug(`Recalculating standings for league id: ${leagueId}`); + try { + if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) { + this.logger.error('Cannot recalculate standings: missing required repositories.'); + throw new Error('Cannot recalculate standings: missing required repositories'); } - // Add points from this result - standing = standing.addRaceResult(result.position, pointsSystem); - standingsMap.set(result.driverId, standing); - }); + // 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.`); + throw new Error(`League with ID ${leagueId} not found`); + } + this.logger.debug(`League ${leagueId} found for recalculation.`); - // 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; + // Get points system + const resolvedPointsSystem = + league.settings.customPoints ?? + POINTS_SYSTEMS[league.settings.pointsSystem] ?? + POINTS_SYSTEMS['f1-2024']; + + if (!resolvedPointsSystem) { + this.logger.error(`No points system configured for league ${leagueId}.`); + throw new Error('No points system configured for league'); + } + 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}.`); + + if (races.length === 0) { + this.logger.warn(`No completed races found for league ${leagueId}. Standings will be empty.`); + 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}.`); + const results = await this.resultRepository!.findByRaceId(race.id); + this.logger.debug(`Found ${results.length} results for race ${race.id}.`); + return results; + }) + ); + 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); + + if (!standing) { + standing = Standing.create({ + leagueId, + driverId: result.driverId, + }); + this.logger.debug(`Created new standing for driver ${result.driverId} in league ${leagueId}.`); } - // Tie-breaker: most wins - if (b.wins !== a.wins) { - return b.wins - a.wins; - } - // Tie-breaker: most races completed - return b.racesCompleted - a.racesCompleted; + + // Add points from this result + standing = standing.addRaceResult(result.position, pointsSystem); + standingsMap.set(result.driverId, standing); + this.logger.debug(`Driver ${result.driverId} 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; + } + // 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; }); - // Assign positions - const updatedStandings = sortedStandings.map((standing, index) => - standing.updatePosition(index + 1) - ); + // Save all standings + await this.saveMany(updatedStandings); + this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`); - // Save all standings - await this.saveMany(updatedStandings); - - return updatedStandings; + return updatedStandings; + } catch (error) { + this.logger.error(`Error recalculating standings for league ${leagueId}:`, error); + throw error; + } } /** diff --git a/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts b/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts index 0e7e52803..d15eaa068 100644 --- a/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts @@ -10,12 +10,16 @@ import type { TeamJoinRequest, } from '@gridpilot/racing/domain/types/TeamMembership'; import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryTeamMembershipRepository implements ITeamMembershipRepository { private membershipsByTeam: Map; private joinRequestsByTeam: Map; + private readonly logger: ILogger; - constructor(seedMemberships?: TeamMembership[], seedJoinRequests?: TeamJoinRequest[]) { + constructor(logger: ILogger, seedMemberships?: TeamMembership[], seedJoinRequests?: TeamJoinRequest[]) { + this.logger = logger; + this.logger.info('InMemoryTeamMembershipRepository initialized.'); this.membershipsByTeam = new Map(); this.joinRequestsByTeam = new Map(); @@ -24,6 +28,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito const list = this.membershipsByTeam.get(membership.teamId) ?? []; list.push(membership); this.membershipsByTeam.set(membership.teamId, list); + this.logger.debug(`Seeded membership for team ${membership.teamId}, driver ${membership.driverId}.`); }); } @@ -32,6 +37,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito const list = this.joinRequestsByTeam.get(request.teamId) ?? []; list.push(request); this.joinRequestsByTeam.set(request.teamId, list); + this.logger.debug(`Seeded join request for team ${request.teamId}, driver ${request.driverId}.`); }); } } @@ -41,6 +47,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito if (!list) { list = []; this.membershipsByTeam.set(teamId, list); + this.logger.debug(`Created new membership list for team: ${teamId}`); } return list; } @@ -50,91 +57,177 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito if (!list) { list = []; this.joinRequestsByTeam.set(teamId, list); + this.logger.debug(`Created new join request list for team: ${teamId}`); } return list; } - async getMembership(teamId: string, driverId: string): Promise { - const list = this.membershipsByTeam.get(teamId); - if (!list) return null; - return list.find((m) => m.driverId === driverId) ?? null; +async getMembership(teamId: string, driverId: string): Promise { + this.logger.debug(`[getMembership] Entry - teamId: ${teamId}, driverId: ${driverId}`); + try { + const list = this.membershipsByTeam.get(teamId); + if (!list) { + this.logger.warn(`[getMembership] No membership list found for team: ${teamId}. Returning null.`); + return null; + } + const membership = list.find((m) => m.driverId === driverId) ?? null; + if (membership) { + this.logger.info(`[getMembership] Success - found membership for team: ${teamId}, driver: ${driverId}.`); + } else { + this.logger.info(`[getMembership] Not found - membership for team: ${teamId}, driver: ${driverId}.`); + } + return membership; + } catch (error) { + this.logger.error(`[getMembership] Error getting membership for team ${teamId}, driver ${driverId}:`, error); + throw error; + } } async getActiveMembershipForDriver(driverId: string): Promise { - for (const list of this.membershipsByTeam.values()) { - const membership = list.find( - (m) => m.driverId === driverId && m.status === 'active', - ); - if (membership) { - return membership; + this.logger.debug(`[getActiveMembershipForDriver] Entry - driverId: ${driverId}`); + try { + for (const list of this.membershipsByTeam.values()) { + const membership = list.find( + (m) => m.driverId === driverId && m.status === 'active', + ); + if (membership) { + this.logger.info(`[getActiveMembershipForDriver] Success - found active membership for driver: ${driverId}, team: ${membership.teamId}.`); + return membership; + } } + this.logger.info(`[getActiveMembershipForDriver] Not found - no active membership for driver: ${driverId}.`); + return null; + } catch (error) { + this.logger.error(`[getActiveMembershipForDriver] Error getting active membership for driver ${driverId}:`, error); + throw error; } - return null; } async getTeamMembers(teamId: string): Promise { - return [...(this.membershipsByTeam.get(teamId) ?? [])]; + this.logger.debug(`[getTeamMembers] Entry - teamId: ${teamId}`); + try { + const members = [...(this.membershipsByTeam.get(teamId) ?? [])]; + this.logger.info(`[getTeamMembers] Success - found ${members.length} members for team: ${teamId}.`); + return members; + } catch (error) { + this.logger.error(`[getTeamMembers] Error getting team members for team ${teamId}:`, error); + throw error; + } } async countByTeamId(teamId: string): Promise { - const list = this.membershipsByTeam.get(teamId) ?? []; - return list.filter((m) => m.status === 'active').length; + this.logger.debug(`[countByTeamId] Entry - teamId: ${teamId}`); + try { + const list = this.membershipsByTeam.get(teamId) ?? []; + const count = list.filter((m) => m.status === 'active').length; + this.logger.info(`[countByTeamId] Success - counted ${count} active members for team: ${teamId}.`); + return count; + } catch (error) { + this.logger.error(`[countByTeamId] Error counting members for team ${teamId}:`, error); + throw error; + } } async saveMembership(membership: TeamMembership): Promise { - const list = this.getMembershipList(membership.teamId); - const existingIndex = list.findIndex( - (m) => m.teamId === membership.teamId && m.driverId === membership.driverId, - ); + this.logger.debug(`[saveMembership] Entry - teamId: ${membership.teamId}, driverId: ${membership.driverId}`); + try { + const list = this.getMembershipList(membership.teamId); + const existingIndex = list.findIndex( + (m) => m.teamId === membership.teamId && m.driverId === membership.driverId, + ); - if (existingIndex >= 0) { - list[existingIndex] = membership; - } else { - list.push(membership); + if (existingIndex >= 0) { + list[existingIndex] = membership; + this.logger.info(`[saveMembership] Success - updated existing membership for team: ${membership.teamId}, driver: ${membership.driverId}.`); + } else { + list.push(membership); + this.logger.info(`[saveMembership] Success - created new membership for team: ${membership.teamId}, driver: ${membership.driverId}.`); + } + + this.membershipsByTeam.set(membership.teamId, list); + return membership; + } catch (error) { + this.logger.error(`[saveMembership] Error saving membership for team ${membership.teamId}, driver ${membership.driverId}:`, error); + throw error; } - - this.membershipsByTeam.set(membership.teamId, list); - return membership; } async removeMembership(teamId: string, driverId: string): Promise { - const list = this.membershipsByTeam.get(teamId); - if (!list) { - return; - } - const index = list.findIndex((m) => m.driverId === driverId); - if (index >= 0) { - list.splice(index, 1); - this.membershipsByTeam.set(teamId, list); + this.logger.debug(`[removeMembership] Entry - teamId: ${teamId}, driverId: ${driverId}`); + try { + const list = this.membershipsByTeam.get(teamId); + if (!list) { + this.logger.warn(`[removeMembership] No membership list found for team: ${teamId}. Cannot remove.`); + return; + } + const index = list.findIndex((m) => m.driverId === driverId); + if (index >= 0) { + list.splice(index, 1); + this.membershipsByTeam.set(teamId, list); + this.logger.info(`[removeMembership] Success - removed membership for team: ${teamId}, driver: ${driverId}.`); + } else { + this.logger.info(`[removeMembership] Not found - membership for team: ${teamId}, driver: ${driverId}. Cannot remove.`); + } + } catch (error) { + this.logger.error(`[removeMembership] Error removing membership for team ${teamId}, driver ${driverId}:`, error); + throw error; } } async getJoinRequests(teamId: string): Promise { - return [...(this.joinRequestsByTeam.get(teamId) ?? [])]; + this.logger.debug(`[getJoinRequests] Entry - teamId: ${teamId}`); + try { + const requests = [...(this.joinRequestsByTeam.get(teamId) ?? [])]; + this.logger.info(`[getJoinRequests] Success - found ${requests.length} join requests for team: ${teamId}.`); + return requests; + } catch (error) { + this.logger.error(`[getJoinRequests] Error getting join requests for team ${teamId}:`, error); + throw error; + } } async saveJoinRequest(request: TeamJoinRequest): Promise { - const list = this.getJoinRequestList(request.teamId); - const existingIndex = list.findIndex((r) => r.id === request.id); + this.logger.debug(`[saveJoinRequest] Entry - teamId: ${request.teamId}, driverId: ${request.driverId}, id: ${request.id}`); + try { + const list = this.getJoinRequestList(request.teamId); + const existingIndex = list.findIndex((r) => r.id === request.id); - if (existingIndex >= 0) { - list[existingIndex] = request; - } else { - list.push(request); + if (existingIndex >= 0) { + list[existingIndex] = request; + this.logger.info(`[saveJoinRequest] Success - updated existing join request: ${request.id}.`); + } else { + list.push(request); + this.logger.info(`[saveJoinRequest] Success - created new join request: ${request.id}.`); + } + + this.joinRequestsByTeam.set(request.teamId, list); + return request; + } catch (error) { + this.logger.error(`[saveJoinRequest] Error saving join request ${request.id}:`, error); + throw error; } - - this.joinRequestsByTeam.set(request.teamId, list); - return request; } async removeJoinRequest(requestId: string): Promise { - for (const [teamId, list] of this.joinRequestsByTeam.entries()) { - const index = list.findIndex((r) => r.id === requestId); - if (index >= 0) { - list.splice(index, 1); - this.joinRequestsByTeam.set(teamId, list); - return; + this.logger.debug(`[removeJoinRequest] Entry - requestId: ${requestId}`); + try { + let removed = false; + for (const [teamId, list] of this.joinRequestsByTeam.entries()) { + const index = list.findIndex((r) => r.id === requestId); + if (index >= 0) { + list.splice(index, 1); + this.joinRequestsByTeam.set(teamId, list); + removed = true; + this.logger.info(`[removeJoinRequest] Success - removed join request ${requestId} from team ${teamId}.`); + break; + } } + if (!removed) { + this.logger.warn(`[removeJoinRequest] Not found - join request with ID ${requestId} not found for removal.`); + } + } catch (error) { + this.logger.error(`[removeJoinRequest] Error removing join request ${requestId}:`, error); + throw error; } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryTeamRepository.ts b/packages/racing/infrastructure/repositories/InMemoryTeamRepository.ts index 179cdaea9..ac5216308 100644 --- a/packages/racing/infrastructure/repositories/InMemoryTeamRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryTeamRepository.ts @@ -7,61 +7,126 @@ import type { Team } from '@gridpilot/racing/domain/entities/Team'; import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryTeamRepository implements ITeamRepository { private teams: Map; + private readonly logger: ILogger; - constructor(seedData?: Team[]) { + constructor(logger: ILogger, seedData?: Team[]) { + this.logger = logger; + this.logger.info('InMemoryTeamRepository initialized.'); this.teams = new Map(); if (seedData) { seedData.forEach((team) => { this.teams.set(team.id, team); + this.logger.debug(`Seeded team: ${team.id}.`); }); } } async findById(id: string): Promise { - return this.teams.get(id) ?? null; + this.logger.debug(`Finding team by id: ${id}`); + try { + const team = this.teams.get(id) ?? null; + if (team) { + this.logger.info(`Found team: ${id}.`); + } else { + this.logger.warn(`Team with id ${id} not found.`); + } + return team; + } catch (error) { + this.logger.error(`Error finding team by id ${id}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.teams.values()); + this.logger.debug('Finding all teams.'); + try { + const teams = Array.from(this.teams.values()); + this.logger.info(`Found ${teams.length} teams.`); + return teams; + } catch (error) { + this.logger.error('Error finding all teams:', error); + throw error; + } } async findByLeagueId(leagueId: string): Promise { - return Array.from(this.teams.values()).filter((team) => - team.leagues.includes(leagueId), - ); + this.logger.debug(`Finding teams by league id: ${leagueId}`); + try { + const teams = Array.from(this.teams.values()).filter((team) => + team.leagues.includes(leagueId), + ); + this.logger.info(`Found ${teams.length} teams for league id: ${leagueId}.`); + return teams; + } catch (error) { + this.logger.error(`Error finding teams by league id ${leagueId}:`, error); + throw error; + } } async create(team: Team): Promise { - if (await this.exists(team.id)) { - throw new Error(`Team with ID ${team.id} already exists`); - } + this.logger.debug(`Creating team: ${team.id}`); + try { + if (await this.exists(team.id)) { + this.logger.warn(`Team with ID ${team.id} already exists.`); + throw new Error(`Team with ID ${team.id} already exists`); + } - this.teams.set(team.id, team); - return team; + this.teams.set(team.id, team); + this.logger.info(`Team ${team.id} created successfully.`); + return team; + } catch (error) { + this.logger.error(`Error creating team ${team.id}:`, error); + throw error; + } } async update(team: Team): Promise { - if (!(await this.exists(team.id))) { - throw new Error(`Team with ID ${team.id} not found`); - } + this.logger.debug(`Updating team: ${team.id}`); + try { + if (!(await this.exists(team.id))) { + this.logger.warn(`Team with ID ${team.id} not found for update.`); + throw new Error(`Team with ID ${team.id} not found`); + } - this.teams.set(team.id, team); - return team; + this.teams.set(team.id, team); + this.logger.info(`Team ${team.id} updated successfully.`); + return team; + } catch (error) { + this.logger.error(`Error updating team ${team.id}:`, error); + throw error; + } } async delete(id: string): Promise { - if (!(await this.exists(id))) { - throw new Error(`Team with ID ${id} not found`); - } + this.logger.debug(`Deleting team: ${id}`); + try { + if (!(await this.exists(id))) { + this.logger.warn(`Team with ID ${id} not found for deletion.`); + throw new Error(`Team with ID ${id} not found`); + } - this.teams.delete(id); + this.teams.delete(id); + this.logger.info(`Team ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting team ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.teams.has(id); + this.logger.debug(`Checking existence of team with id: ${id}`); + try { + const exists = this.teams.has(id); + this.logger.debug(`Team ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of team with id ${id}:`, error); + throw error; + } } } \ No newline at end of file diff --git a/packages/racing/infrastructure/repositories/InMemoryTrackRepository.ts b/packages/racing/infrastructure/repositories/InMemoryTrackRepository.ts index dda778a05..fd897cf57 100644 --- a/packages/racing/infrastructure/repositories/InMemoryTrackRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryTrackRepository.ts @@ -8,84 +8,173 @@ import { v4 as uuidv4 } from 'uuid'; import { Track, TrackCategory } from '@gridpilot/racing/domain/entities/Track'; import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryTrackRepository implements ITrackRepository { private tracks: Map; + private readonly logger: ILogger; - constructor(seedData?: Track[]) { + constructor(logger: ILogger, seedData?: Track[]) { + this.logger = logger; + this.logger.info('InMemoryTrackRepository initialized.'); this.tracks = new Map(); if (seedData) { seedData.forEach(track => { this.tracks.set(track.id, track); + this.logger.debug(`Seeded track: ${track.id}.`); }); } } async findById(id: string): Promise { - return this.tracks.get(id) ?? null; + this.logger.debug(`Finding track by id: ${id}`); + try { + const track = this.tracks.get(id) ?? null; + if (track) { + this.logger.info(`Found track: ${id}.`); + } else { + this.logger.warn(`Track with id ${id} not found.`); + } + return track; + } catch (error) { + this.logger.error(`Error finding track by id ${id}:`, error); + throw error; + } } async findAll(): Promise { - return Array.from(this.tracks.values()); + this.logger.debug('Finding all tracks.'); + try { + const tracks = Array.from(this.tracks.values()); + this.logger.info(`Found ${tracks.length} tracks.`); + return tracks; + } catch (error) { + this.logger.error('Error finding all tracks:', error); + throw error; + } } async findByGameId(gameId: string): Promise { - return Array.from(this.tracks.values()) - .filter(track => track.gameId === gameId) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Finding tracks by game id: ${gameId}`); + try { + const tracks = Array.from(this.tracks.values()) + .filter(track => track.gameId === gameId) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Found ${tracks.length} tracks for game id: ${gameId}.`); + return tracks; + } catch (error) { + this.logger.error(`Error finding tracks by game id ${gameId}:`, error); + throw error; + } } async findByCategory(category: TrackCategory): Promise { - return Array.from(this.tracks.values()) - .filter(track => track.category === category) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Finding tracks by category: ${category}`); + try { + const tracks = Array.from(this.tracks.values()) + .filter(track => track.category === category) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Found ${tracks.length} tracks for category: ${category}.`); + return tracks; + } catch (error) { + this.logger.error(`Error finding tracks by category ${category}:`, error); + throw error; + } } async findByCountry(country: string): Promise { - return Array.from(this.tracks.values()) - .filter(track => track.country.toLowerCase() === country.toLowerCase()) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Finding tracks by country: ${country}`); + try { + const tracks = Array.from(this.tracks.values()) + .filter(track => track.country.toLowerCase() === country.toLowerCase()) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Found ${tracks.length} tracks for country: ${country}.`); + return tracks; + } catch (error) { + this.logger.error(`Error finding tracks by country ${country}:`, error); + throw error; + } } async searchByName(query: string): Promise { - const lowerQuery = query.toLowerCase(); - return Array.from(this.tracks.values()) - .filter(track => - track.name.toLowerCase().includes(lowerQuery) || - track.shortName.toLowerCase().includes(lowerQuery) - ) - .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.debug(`Searching tracks by name query: ${query}`); + try { + const lowerQuery = query.toLowerCase(); + const tracks = Array.from(this.tracks.values()) + .filter(track => + track.name.toLowerCase().includes(lowerQuery) || + track.shortName.toLowerCase().includes(lowerQuery) + ) + .sort((a, b) => a.name.localeCompare(b.name)); + this.logger.info(`Found ${tracks.length} tracks matching search query: ${query}.`); + return tracks; + } catch (error) { + this.logger.error(`Error searching tracks by name query ${query}:`, error); + throw error; + } } async create(track: Track): Promise { - if (await this.exists(track.id)) { - throw new Error(`Track with ID ${track.id} already exists`); - } + this.logger.debug(`Creating track: ${track.id}`); + try { + if (await this.exists(track.id)) { + this.logger.warn(`Track with ID ${track.id} already exists.`); + throw new Error(`Track with ID ${track.id} already exists`); + } - this.tracks.set(track.id, track); - return track; + this.tracks.set(track.id, track); + this.logger.info(`Track ${track.id} created successfully.`); + return track; + } catch (error) { + this.logger.error(`Error creating track ${track.id}:`, error); + throw error; + } } async update(track: Track): Promise { - if (!await this.exists(track.id)) { - throw new Error(`Track with ID ${track.id} not found`); - } + this.logger.debug(`Updating track: ${track.id}`); + try { + if (!await this.exists(track.id)) { + this.logger.warn(`Track with ID ${track.id} not found for update.`); + throw new Error(`Track with ID ${track.id} not found`); + } - this.tracks.set(track.id, track); - return track; + this.tracks.set(track.id, track); + this.logger.info(`Track ${track.id} updated successfully.`); + return track; + } catch (error) { + this.logger.error(`Error updating track ${track.id}:`, error); + throw error; + } } async delete(id: string): Promise { - if (!await this.exists(id)) { - throw new Error(`Track with ID ${id} not found`); - } + this.logger.debug(`Deleting track: ${id}`); + try { + if (!await this.exists(id)) { + this.logger.warn(`Track with ID ${id} not found for deletion.`); + throw new Error(`Track with ID ${id} not found`); + } - this.tracks.delete(id); + this.tracks.delete(id); + this.logger.info(`Track ${id} deleted successfully.`); + } catch (error) { + this.logger.error(`Error deleting track ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.tracks.has(id); + this.logger.debug(`Checking existence of track with id: ${id}`); + try { + const exists = this.tracks.has(id); + this.logger.debug(`Track ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of track with id ${id}:`, error); + throw error; + } } /** diff --git a/packages/racing/infrastructure/repositories/InMemoryTransactionRepository.ts b/packages/racing/infrastructure/repositories/InMemoryTransactionRepository.ts index 2cdb2cd23..cb4cae45a 100644 --- a/packages/racing/infrastructure/repositories/InMemoryTransactionRepository.ts +++ b/packages/racing/infrastructure/repositories/InMemoryTransactionRepository.ts @@ -6,48 +6,123 @@ import type { Transaction, TransactionType } from '../../domain/entities/Transaction'; import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export class InMemoryTransactionRepository implements ITransactionRepository { private transactions: Map = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger, seedData?: Transaction[]) { + this.logger = logger; + this.logger.info('InMemoryTransactionRepository initialized.'); + if (seedData) { + seedData.forEach(t => this.transactions.set(t.id, t)); + this.logger.debug(`Seeded ${seedData.length} transactions.`); + } + } async findById(id: string): Promise { - return this.transactions.get(id) ?? null; + this.logger.debug(`Finding transaction by id: ${id}`); + try { + const transaction = this.transactions.get(id) ?? null; + if (transaction) { + this.logger.info(`Found transaction: ${id}.`); + } else { + this.logger.warn(`Transaction with id ${id} not found.`); + } + return transaction; + } catch (error) { + this.logger.error(`Error finding transaction by id ${id}:`, error); + throw error; + } } async findByWalletId(walletId: string): Promise { - return Array.from(this.transactions.values()).filter(t => t.walletId === walletId); + this.logger.debug(`Finding transactions by wallet id: ${walletId}`); + try { + const transactions = Array.from(this.transactions.values()).filter(t => t.walletId === walletId); + this.logger.info(`Found ${transactions.length} transactions for wallet id: ${walletId}.`); + return transactions; + } catch (error) { + this.logger.error(`Error finding transactions by wallet id ${walletId}:`, error); + throw error; + } } async findByType(type: TransactionType): Promise { - return Array.from(this.transactions.values()).filter(t => t.type === type); + this.logger.debug(`Finding transactions by type: ${type}`); + try { + const transactions = Array.from(this.transactions.values()).filter(t => t.type === type); + this.logger.info(`Found ${transactions.length} transactions of type: ${type}.`); + return transactions; + } catch (error) { + this.logger.error(`Error finding transactions by type ${type}:`, error); + throw error; + } } async create(transaction: Transaction): Promise { - if (this.transactions.has(transaction.id)) { - throw new Error('Transaction with this ID already exists'); + this.logger.debug(`Creating transaction: ${transaction.id}`); + try { + if (this.transactions.has(transaction.id)) { + this.logger.warn(`Transaction with ID ${transaction.id} already exists.`); + throw new Error('Transaction with this ID already exists'); + } + this.transactions.set(transaction.id, transaction); + this.logger.info(`Transaction ${transaction.id} created successfully.`); + return transaction; + } catch (error) { + this.logger.error(`Error creating transaction ${transaction.id}:`, error); + throw error; } - this.transactions.set(transaction.id, transaction); - return transaction; } async update(transaction: Transaction): Promise { - if (!this.transactions.has(transaction.id)) { - throw new Error('Transaction not found'); + this.logger.debug(`Updating transaction: ${transaction.id}`); + try { + if (!this.transactions.has(transaction.id)) { + this.logger.warn(`Transaction with ID ${transaction.id} not found for update.`); + throw new Error('Transaction not found'); + } + this.transactions.set(transaction.id, transaction); + this.logger.info(`Transaction ${transaction.id} updated successfully.`); + return transaction; + } catch (error) { + this.logger.error(`Error updating transaction ${transaction.id}:`, error); + throw error; } - this.transactions.set(transaction.id, transaction); - return transaction; } async delete(id: string): Promise { - this.transactions.delete(id); + this.logger.debug(`Deleting transaction: ${id}`); + try { + if (this.transactions.delete(id)) { + this.logger.info(`Transaction ${id} deleted successfully.`); + } else { + this.logger.warn(`Transaction with id ${id} not found for deletion.`); + } + } catch (error) { + this.logger.error(`Error deleting transaction ${id}:`, error); + throw error; + } } async exists(id: string): Promise { - return this.transactions.has(id); + this.logger.debug(`Checking existence of transaction with id: ${id}`); + try { + const exists = this.transactions.has(id); + this.logger.debug(`Transaction ${id} exists: ${exists}.`); + return exists; + } catch (error) { + this.logger.error(`Error checking existence of transaction with id ${id}:`, error); + throw error; + } } // Test helper clear(): void { + this.logger.debug('Clearing all transactions.'); this.transactions.clear(); + this.logger.info('All transactions cleared.'); } } \ No newline at end of file diff --git a/packages/shared/logger/ILogger.ts b/packages/shared/logger/ILogger.ts new file mode 100644 index 000000000..74064a797 --- /dev/null +++ b/packages/shared/logger/ILogger.ts @@ -0,0 +1,6 @@ +export interface ILogger { + debug(message: string, context?: Record): void; + info(message: string, context?: Record): void; + warn(message: string, context?: Record): void; + error(message: string, error?: Error, context?: Record): void; +} \ No newline at end of file diff --git a/packages/shared/logging/ConsoleLogger.ts b/packages/shared/logging/ConsoleLogger.ts new file mode 100644 index 000000000..22f184f84 --- /dev/null +++ b/packages/shared/logging/ConsoleLogger.ts @@ -0,0 +1,20 @@ +import { ILogger } from './ILogger'; + +export class ConsoleLogger implements ILogger { + debug(message: string, ...args: any[]): void { + console.debug(message, ...args); + } + + info(message: string, ...args: any[]): void { + console.info(message, ...args); + } + + warn(message: string, ...args: any[]): void { + console.warn(message, ...args); + } + + error(message: string, ...args: any[]): void { + console.error(message, ...args); + } + +} diff --git a/packages/shared/logging/ILogger.ts b/packages/shared/logging/ILogger.ts new file mode 100644 index 000000000..4ea700273 --- /dev/null +++ b/packages/shared/logging/ILogger.ts @@ -0,0 +1,7 @@ + +export interface ILogger { + debug(message: string, ...args: any[]): void; + info(message: string, ...args: any[]): void; + warn(message: string, ...args: any[]): void; + error(message: string, ...args: any[]): void; +} diff --git a/packages/shared/tests/unit/logging/ConsoleLogger.test.ts b/packages/shared/tests/unit/logging/ConsoleLogger.test.ts new file mode 100644 index 000000000..fbc6eada7 --- /dev/null +++ b/packages/shared/tests/unit/logging/ConsoleLogger.test.ts @@ -0,0 +1,69 @@ +import { vi } from 'vitest'; +import { ConsoleLogger } from '../../../logging/ConsoleLogger'; // Assuming ConsoleLogger is here + +describe('ConsoleLogger', () => { + let logger: ConsoleLogger; + let consoleDebugSpy: vi.SpyInstance; + let consoleInfoSpy: vi.SpyInstance; + let consoleWarnSpy: vi.SpyInstance; + let consoleErrorSpy: vi.SpyInstance; + let consoleLogSpy: vi.SpyInstance; + + beforeEach(() => { + logger = new ConsoleLogger(); + consoleDebugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); + consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleDebugSpy.mockRestore(); + consoleInfoSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + consoleLogSpy.mockRestore(); + }); + + it('should call console.debug with the correct arguments when debug is called', () => { + const message = 'Debug message'; + const context = { key: 'value' }; + logger.debug(message, context); + expect(consoleDebugSpy).toHaveBeenCalledTimes(1); + expect(consoleDebugSpy).toHaveBeenCalledWith(message, context); + }); + + it('should call console.info with the correct arguments when info is called', () => { + const message = 'Info message'; + const context = { key: 'value' }; + logger.info(message, context); + expect(consoleInfoSpy).toHaveBeenCalledTimes(1); + expect(consoleInfoSpy).toHaveBeenCalledWith(message, context); + }); + + it('should call console.warn with the correct arguments when warn is called', () => { + const message = 'Warn message'; + const context = { key: 'value' }; + logger.warn(message, context); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(message, context); + }); + + it('should call console.error with the correct arguments when error is called', () => { + const message = 'Error message'; + const error = new Error('Something went wrong'); + const context = { key: 'value' }; + logger.error(message, error, context); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(message, error, context); + }); + + it('should call console.log with the correct arguments when log is called', () => { + const message = 'Log message'; + const context = { key: 'value' }; + logger.log(message, context); + expect(consoleLogSpy).toHaveBeenCalledTimes(1); + expect(consoleLogSpy).toHaveBeenCalledWith(message, context); + }); +}); \ No newline at end of file diff --git a/packages/social/application/use-cases/GetCurrentUserSocialUseCase.ts b/packages/social/application/use-cases/GetCurrentUserSocialUseCase.ts index 3dc6baff9..986f5e0b2 100644 --- a/packages/social/application/use-cases/GetCurrentUserSocialUseCase.ts +++ b/packages/social/application/use-cases/GetCurrentUserSocialUseCase.ts @@ -1,4 +1,5 @@ import type { AsyncUseCase } from '@gridpilot/shared/application'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository'; import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO'; import type { FriendDTO } from '../dto/FriendDTO'; @@ -22,33 +23,46 @@ export class GetCurrentUserSocialUseCase constructor( private readonly socialGraphRepository: ISocialGraphRepository, public readonly presenter: ICurrentUserSocialPresenter, + private readonly logger: ILogger, ) {} async execute(params: GetCurrentUserSocialParams): Promise { - const { driverId } = params; + this.logger.debug('GetCurrentUserSocialUseCase: Starting execution', { params }); + try { + const { driverId } = params; - const friendsDomain = await this.socialGraphRepository.getFriends(driverId); + this.logger.debug(`GetCurrentUserSocialUseCase: Fetching friends for driverId: ${driverId}`); + const friendsDomain = await this.socialGraphRepository.getFriends(driverId); + this.logger.debug('GetCurrentUserSocialUseCase: Successfully fetched friends from social graph repository', { friendsCount: friendsDomain.length }); + if (friendsDomain.length === 0) { + this.logger.warn(`GetCurrentUserSocialUseCase: No friends found for driverId: ${driverId}`); + } - const friends: FriendDTO[] = friendsDomain.map((friend) => ({ - driverId: friend.id, - displayName: friend.name, - avatarUrl: '', - isOnline: false, - lastSeen: new Date(), - })); + const friends: FriendDTO[] = friendsDomain.map((friend) => ({ + driverId: friend.id, + displayName: friend.name, + avatarUrl: '', + isOnline: false, + lastSeen: new Date(), + })); - const currentUser: CurrentUserSocialDTO = { - driverId, - displayName: '', - avatarUrl: '', - countryCode: '', - }; + const currentUser: CurrentUserSocialDTO = { + driverId, + displayName: '', + avatarUrl: '', + countryCode: '', + }; - const viewModel: CurrentUserSocialViewModel = { - currentUser, - friends, - }; + const viewModel: CurrentUserSocialViewModel = { + currentUser, + friends, + }; - this.presenter.present(viewModel); + this.presenter.present(viewModel); + this.logger.info('GetCurrentUserSocialUseCase: Successfully presented current user social data'); + } catch (error) { + this.logger.error('GetCurrentUserSocialUseCase: Error during execution', { error }); + throw error; + } } } \ No newline at end of file diff --git a/packages/social/application/use-cases/GetUserFeedUseCase.ts b/packages/social/application/use-cases/GetUserFeedUseCase.ts index e44b867f5..e4fd9cbe7 100644 --- a/packages/social/application/use-cases/GetUserFeedUseCase.ts +++ b/packages/social/application/use-cases/GetUserFeedUseCase.ts @@ -6,6 +6,7 @@ import type { IUserFeedPresenter, UserFeedViewModel, } from '../presenters/ISocialPresenters'; +import type { ILogger } from '../../../shared/src/logging/ILogger'; export interface GetUserFeedParams { driverId: string; @@ -17,18 +18,30 @@ export class GetUserFeedUseCase constructor( private readonly feedRepository: IFeedRepository, public readonly presenter: IUserFeedPresenter, + private readonly logger: ILogger, ) {} async execute(params: GetUserFeedParams): Promise { const { driverId, limit } = params; - const items = await this.feedRepository.getFeedForDriver(driverId, limit); - const dtoItems = items.map(mapFeedItemToDTO); + this.logger.debug('Executing GetUserFeedUseCase', { driverId, limit }); - const viewModel: UserFeedViewModel = { - items: dtoItems, - }; + try { + const items = await this.feedRepository.getFeedForDriver(driverId, limit); + this.logger.info('Successfully retrieved user feed', { driverId, itemCount: items.length }); + if (items.length === 0) { + this.logger.warn(`No feed items found for driverId: ${driverId}`); + } + const dtoItems = items.map(mapFeedItemToDTO); - this.presenter.present(viewModel); + const viewModel: UserFeedViewModel = { + items: dtoItems, + }; + + this.presenter.present(viewModel); + } catch (error) { + this.logger.error('Failed to retrieve user feed', error); + throw error; // Re-throw the error so it can be handled upstream + } } } diff --git a/packages/social/infrastructure/inmemory/InMemorySocialAndFeed.ts b/packages/social/infrastructure/inmemory/InMemorySocialAndFeed.ts index b7d4c97cf..e6517d130 100644 --- a/packages/social/infrastructure/inmemory/InMemorySocialAndFeed.ts +++ b/packages/social/infrastructure/inmemory/InMemorySocialAndFeed.ts @@ -2,6 +2,7 @@ import type { Driver } from '@gridpilot/racing/domain/entities/Driver'; import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; +import type { ILogger } from '@gridpilot/shared/logging/ILogger'; export type Friendship = { driverId: string; @@ -18,89 +19,130 @@ export class InMemoryFeedRepository implements IFeedRepository { private readonly feedEvents: FeedItem[]; private readonly friendships: Friendship[]; private readonly driversById: Map; + private readonly logger: ILogger; - constructor(seed: RacingSeedData) { + constructor(logger: ILogger, seed: RacingSeedData) { + this.logger = logger; + this.logger.info('InMemoryFeedRepository initialized.'); this.feedEvents = seed.feedEvents; this.friendships = seed.friendships; this.driversById = new Map(seed.drivers.map((d) => [d.id, d])); } async getFeedForDriver(driverId: string, limit?: number): Promise { - const friendIds = new Set( - this.friendships - .filter((f) => f.driverId === driverId) - .map((f) => f.friendId), - ); + this.logger.debug(`Getting feed for driver: ${driverId}, limit: ${limit}`); + try { + const friendIds = new Set( + this.friendships + .filter((f) => f.driverId === driverId) + .map((f) => f.friendId), + ); - const items = this.feedEvents.filter((item) => { - if (item.actorDriverId && friendIds.has(item.actorDriverId)) { - return true; - } - return false; - }); + const items = this.feedEvents.filter((item) => { + if (item.actorDriverId && friendIds.has(item.actorDriverId)) { + return true; + } + return false; + }); - const sorted = items - .slice() - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + const sorted = items + .slice() + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); - return typeof limit === 'number' ? sorted.slice(0, limit) : sorted; + this.logger.info(`Found ${sorted.length} feed items for driver: ${driverId}.`); + return typeof limit === 'number' ? sorted.slice(0, limit) : sorted; + } catch (error) { + this.logger.error(`Error getting feed for driver ${driverId}:`, error); + throw error; + } } async getGlobalFeed(limit?: number): Promise { - const sorted = this.feedEvents - .slice() - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + this.logger.debug(`Getting global feed, limit: ${limit}`); + try { + const sorted = this.feedEvents + .slice() + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); - return typeof limit === 'number' ? sorted.slice(0, limit) : sorted; + this.logger.info(`Found ${sorted.length} global feed items.`); + return typeof limit === 'number' ? sorted.slice(0, limit) : sorted; + } catch (error) { + this.logger.error(`Error getting global feed:`, error); + throw error; + } } } export class InMemorySocialGraphRepository implements ISocialGraphRepository { private readonly friendships: Friendship[]; private readonly driversById: Map; + private readonly logger: ILogger; - constructor(seed: RacingSeedData) { + constructor(logger: ILogger, seed: RacingSeedData) { + this.logger = logger; + this.logger.info('InMemorySocialGraphRepository initialized.'); this.friendships = seed.friendships; this.driversById = new Map(seed.drivers.map((d) => [d.id, d])); } async getFriendIds(driverId: string): Promise { - return this.friendships - .filter((f) => f.driverId === driverId) - .map((f) => f.friendId); + this.logger.debug(`Getting friend IDs for driver: ${driverId}`); + try { + const friendIds = this.friendships + .filter((f) => f.driverId === driverId) + .map((f) => f.friendId); + this.logger.info(`Found ${friendIds.length} friend IDs for driver: ${driverId}.`); + return friendIds; + } catch (error) { + this.logger.error(`Error getting friend IDs for driver ${driverId}:`, error); + throw error; + } } async getFriends(driverId: string): Promise { - const ids = await this.getFriendIds(driverId); - return ids - .map((id) => this.driversById.get(id)) - .filter((d): d is Driver => Boolean(d)); + this.logger.debug(`Getting friends for driver: ${driverId}`); + try { + const ids = await this.getFriendIds(driverId); + const friends = ids + .map((id) => this.driversById.get(id)) + .filter((d): d is Driver => Boolean(d)); + this.logger.info(`Found ${friends.length} friends for driver: ${driverId}.`); + return friends; + } catch (error) { + this.logger.error(`Error getting friends for driver ${driverId}:`, error); + throw error; + } } async getSuggestedFriends(driverId: string, limit?: number): Promise { - const directFriendIds = new Set(await this.getFriendIds(driverId)); - const suggestions = new Map(); + this.logger.debug(`Getting suggested friends for driver: ${driverId}, limit: ${limit}`); + try { + const directFriendIds = new Set(await this.getFriendIds(driverId)); + const suggestions = new Map(); - for (const friendship of this.friendships) { - if (!directFriendIds.has(friendship.driverId)) continue; - const friendOfFriendId = friendship.friendId; - if (friendOfFriendId === driverId) continue; - if (directFriendIds.has(friendOfFriendId)) continue; + for (const friendship of this.friendships) { + if (!directFriendIds.has(friendship.driverId)) continue; + const friendOfFriendId = friendship.friendId; + if (friendOfFriendId === driverId) continue; + if (directFriendIds.has(friendOfFriendId)) continue; - suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1); + suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1); + } + + const rankedIds = Array.from(suggestions.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([id]) => id); + + const drivers = rankedIds + .map((id) => this.driversById.get(id)) + .filter((d): d is Driver => Boolean(d)); + + const result = typeof limit === 'number' ? drivers.slice(0, limit) : drivers; + this.logger.info(`Found ${result.length} suggested friends for driver: ${driverId}.`); + return result; + } catch (error) { + this.logger.error(`Error getting suggested friends for driver ${driverId}:`, error); + throw error; } - - const rankedIds = Array.from(suggestions.entries()) - .sort((a, b) => b[1] - a[1]) - .map(([id]) => id); - - const drivers = rankedIds - .map((id) => this.driversById.get(id)) - .filter((d): d is Driver => Boolean(d)); - - if (typeof limit === 'number') { - return drivers.slice(0, limit); - } - return drivers; } } \ No newline at end of file diff --git a/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts b/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts index 8b9b2bb43..7086e348b 100644 --- a/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts +++ b/packages/testing-support/src/media/InMemoryAvatarGenerationRepository.ts @@ -5,6 +5,7 @@ import { AvatarGenerationRequest, type AvatarGenerationRequestProps, } from '@gridpilot/media'; +import { ILogger } from '@gridpilot/shared/logging/ILogger'; /** * In-memory implementation of IAvatarGenerationRepository. @@ -13,42 +14,65 @@ import { */ export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepository { private readonly requests = new Map(); + private readonly logger: ILogger; + + constructor(logger: ILogger) { + this.logger = logger; + this.logger.info('InMemoryAvatarGenerationRepository initialized.'); + } async save(request: AvatarGenerationRequest): Promise { + 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.`); } async findById(id: string): Promise { + 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.info(`Avatar generation request with ID: ${id} found.`); return AvatarGenerationRequest.reconstitute(props); } async findByUserId(userId: string): Promise { + this.logger.debug(`Finding avatar generation requests by user ID: ${userId}`); const results: AvatarGenerationRequest[] = []; for (const props of 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()); } async findLatestByUserId(userId: string): Promise { + 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; } 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; } async delete(id: string): Promise { - this.requests.delete(id); + 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.`); + } else { + this.logger.warn(`Attempted to delete non-existent avatar generation request with ID: ${id}.`); + } } } \ No newline at end of file diff --git a/tests/integration/automation/OverlaySyncService.test.ts b/tests/integration/automation/OverlaySyncService.test.ts new file mode 100644 index 000000000..d6f0702c0 --- /dev/null +++ b/tests/integration/automation/OverlaySyncService.test.ts @@ -0,0 +1,104 @@ +import "reflect-metadata"; +import { container } from "tsyringe"; +import { configureDIContainer, resetDIContainer } from "../../../apps/companion/main/di-config"; +import { DI_TOKENS } from "../../../apps/companion/main/di-tokens"; +import { OverlaySyncService } from "@gridpilot/automation/application/services/OverlaySyncService"; +import { LoggerPort } from "@gridpilot/automation/application/ports/LoggerPort"; +import { IAutomationLifecycleEmitter, LifecycleCallback } from "@gridpilot/automation/infrastructure/adapters/IAutomationLifecycleEmitter"; +import { AutomationEventPublisherPort, AutomationEvent } from "@gridpilot/automation/application/ports/AutomationEventPublisherPort"; +import { ConsoleLogAdapter } from "@gridpilot/automation/infrastructure/adapters/logging/ConsoleLogAdapter"; +import { describe, it, expect, beforeEach, afterEach, vi, SpyInstance } from 'vitest'; + +describe("OverlaySyncService Integration with ConsoleLogAdapter", () => { + let consoleErrorSpy: SpyInstance<[message?: any, ...optionalParams: any[]], void>; + let consoleWarnSpy: SpyInstance<[message?: any, ...optionalParams: any[]], void>; + let originalNodeEnv: string | undefined; + + beforeEach(() => { + originalNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + + resetDIContainer(); + configureDIContainer(); + + consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + if (originalNodeEnv !== undefined) { + process.env.NODE_ENV = originalNodeEnv; + } + resetDIContainer(); + }); + + it("should use ConsoleLogAdapter and log messages when OverlaySyncService encounters an error", async () => { + const logger = container.resolve(DI_TOKENS.Logger); + const overlaySyncService = container.resolve(DI_TOKENS.OverlaySyncPort); + + expect(logger).toBeInstanceOf(ConsoleLogAdapter); + + const mockLifecycleEmitter: IAutomationLifecycleEmitter = { + onLifecycle: vi.fn((_cb: LifecycleCallback) => { + throw new Error("Test lifecycle emitter error"); + }), + offLifecycle: vi.fn(), + }; + + const mockPublisher: AutomationEventPublisherPort = { + publish: vi.fn(), + }; + + const serviceWithMockedEmitter = new OverlaySyncService({ + lifecycleEmitter: mockLifecycleEmitter, + publisher: mockPublisher, + logger: logger, + }); + + const action = { id: "test-action-1", label: "Test Action" }; + await serviceWithMockedEmitter.execute(action); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("OverlaySyncService: failed to subscribe to lifecycleEmitter"), + expect.any(Error), + expect.objectContaining({ actionId: action.id }), + ); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it("should use ConsoleLogAdapter and log warn messages when OverlaySyncService fails to publish", async () => { + const logger = container.resolve(DI_TOKENS.Logger); + expect(logger).toBeInstanceOf(ConsoleLogAdapter); + + const mockLifecycleEmitter: IAutomationLifecycleEmitter = { + onLifecycle: vi.fn(), + offLifecycle: vi.fn(), + }; + + const mockPublisher: AutomationEventPublisherPort = { + publish: vi.fn((_event: AutomationEvent) => { + throw new Error("Test publish error"); + }), + }; + + const serviceWithMockedPublisher = new OverlaySyncService({ + lifecycleEmitter: mockLifecycleEmitter, + publisher: mockPublisher, + logger: logger, + }); + + const action = { id: "test-action-2", label: "Test Action" }; + await serviceWithMockedPublisher.execute(action); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("OverlaySyncService: publisher.publish failed"), + expect.objectContaining({ + actionId: action.id, + error: expect.any(Error), + }), + ); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 22b01aeac..bfb6ceac3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ watch: false, environment: 'jsdom', setupFiles: ['tests/setup/vitest.setup.ts'], - include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)', 'packages/**/*.{test,spec}.?(c|m)[jt]s?(x)'], exclude: [ // Do not run library-internal tests from dependencies 'node_modules/**',