This commit is contained in:
2025-12-14 18:11:59 +01:00
parent acc15e8d8d
commit 217337862c
91 changed files with 5919 additions and 1999 deletions

View File

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

View File

@@ -30,7 +30,7 @@ import {
} from '@gridpilot/automation/infrastructure/adapters/automation'; } from '@gridpilot/automation/infrastructure/adapters/automation';
import { MockAutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter'; import { MockAutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter';
import { AutomationEngineAdapter } from '@gridpilot/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter'; 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 { NoOpLogAdapter } from '@gridpilot/automation/infrastructure/adapters/logging/NoOpLogAdapter';
import { import {
loadAutomationConfig, loadAutomationConfig,
@@ -107,7 +107,7 @@ export function configureDIContainer(): void {
container.registerInstance(DI_TOKENS.AutomationMode, automationMode); container.registerInstance(DI_TOKENS.AutomationMode, automationMode);
// Logger (singleton) // 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<LoggerPort>(DI_TOKENS.Logger, logger); container.registerInstance<LoggerPort>(DI_TOKENS.Logger, logger);
// Browser Mode Config Loader (singleton) // Browser Mode Config Loader (singleton)
@@ -120,7 +120,7 @@ export function configureDIContainer(): void {
// Session Repository (singleton) // Session Repository (singleton)
container.register<SessionRepositoryPort>( container.register<SessionRepositoryPort>(
DI_TOKENS.SessionRepository, DI_TOKENS.SessionRepository,
{ useClass: InMemorySessionRepository }, { useFactory: (c) => new InMemorySessionRepository(c.resolve(DI_TOKENS.Logger)) },
{ lifecycle: Lifecycle.Singleton } { lifecycle: Lifecycle.Singleton }
); );
@@ -225,7 +225,8 @@ export function configureDIContainer(): void {
const startAutomationUseCase = new StartAutomationSessionUseCase( const startAutomationUseCase = new StartAutomationSessionUseCase(
automationEngine, automationEngine,
browserAutomation, browserAutomation,
sessionRepository sessionRepository,
logger
); );
container.registerInstance(DI_TOKENS.StartAutomationUseCase, startAutomationUseCase); container.registerInstance(DI_TOKENS.StartAutomationUseCase, startAutomationUseCase);
@@ -235,17 +236,17 @@ export function configureDIContainer(): void {
container.registerInstance( container.registerInstance(
DI_TOKENS.CheckAuthenticationUseCase, DI_TOKENS.CheckAuthenticationUseCase,
new CheckAuthenticationUseCase(authService) new CheckAuthenticationUseCase(authService, logger)
); );
container.registerInstance( container.registerInstance(
DI_TOKENS.InitiateLoginUseCase, DI_TOKENS.InitiateLoginUseCase,
new InitiateLoginUseCase(authService) new InitiateLoginUseCase(authService, logger)
); );
container.registerInstance( container.registerInstance(
DI_TOKENS.ClearSessionUseCase, DI_TOKENS.ClearSessionUseCase,
new ClearSessionUseCase(authService) new ClearSessionUseCase(authService, logger)
); );
container.registerInstance<AuthenticationServicePort>( container.registerInstance<AuthenticationServicePort>(

View File

@@ -11,18 +11,25 @@ import { CookieIdentitySessionAdapter } from '@gridpilot/identity/infrastructure
import { IracingDemoIdentityProviderAdapter } from '@gridpilot/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter'; import { IracingDemoIdentityProviderAdapter } from '@gridpilot/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter';
import { InMemoryUserRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRepository'; import { InMemoryUserRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRepository';
import type { IUserRepository } from '@gridpilot/identity/domain/repositories/IUserRepository'; 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) // Singleton user repository to persist across requests (in-memory demo)
let userRepositoryInstance: IUserRepository | null = null; let userRepositoryInstance: IUserRepository | null = null;
function getUserRepository(): IUserRepository { function getUserRepository(logger: ILogger): IUserRepository {
if (!userRepositoryInstance) { if (!userRepositoryInstance) {
userRepositoryInstance = new InMemoryUserRepository(); userRepositoryInstance = new InMemoryUserRepository(logger);
} }
return userRepositoryInstance; return userRepositoryInstance;
} }
export class InMemoryAuthService implements AuthService { export class InMemoryAuthService implements AuthService {
private readonly logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
async getCurrentSession(): Promise<AuthSession | null> { async getCurrentSession(): Promise<AuthSession | null> {
const sessionPort = new CookieIdentitySessionAdapter(); const sessionPort = new CookieIdentitySessionAdapter();
const useCase = new GetCurrentUserSessionUseCase(sessionPort); const useCase = new GetCurrentUserSessionUseCase(sessionPort);

View File

@@ -1,11 +1,14 @@
import type { AuthService } from './AuthService'; import type { AuthService } from './AuthService';
import { InMemoryAuthService } from './InMemoryAuthService'; import { InMemoryAuthService } from './InMemoryAuthService';
import { getDIContainer } from '../di-container';
let authService: AuthService | null = null; import { DI_TOKENS } from '../di-tokens';
export function getAuthService(): AuthService { export function getAuthService(): AuthService {
if (!authService) { const container = getDIContainer();
authService = new InMemoryAuthService(); if (!container.isRegistered(DI_TOKENS.AuthService)) {
throw new Error(
`${DI_TOKENS.AuthService.description} not registered in DI container.`,
);
} }
return authService; return container.resolve<AuthService>(DI_TOKENS.AuthService);
} }

View File

@@ -37,6 +37,31 @@ import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repos
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
import type { ImageServicePort } from '@gridpilot/media'; 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 // Notifications
import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application'; import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application';
@@ -189,6 +214,9 @@ import {
* Configure the DI container with all bindings for the website application * Configure the DI container with all bindings for the website application
*/ */
export function configureDIContainer(): void { export function configureDIContainer(): void {
// Register the logger
container.registerSingleton<ILogger>(DI_TOKENS.Logger, ConsoleLogger);
const logger = container.resolve<ILogger>(DI_TOKENS.Logger);
// Clear any existing registrations // Clear any existing registrations
container.clearInstances(); container.clearInstances();
@@ -219,19 +247,29 @@ export function configureDIContainer(): void {
// Register repositories // Register repositories
container.registerInstance<IDriverRepository>( container.registerInstance<IDriverRepository>(
DI_TOKENS.DriverRepository, DI_TOKENS.DriverRepository,
new InMemoryDriverRepository(seedData.drivers) new InMemoryDriverRepository(logger, seedData.drivers)
);
container.registerInstance<IPageViewRepository>(
DI_TOKENS.PageViewRepository,
new InMemoryPageViewRepository(logger)
);
container.registerInstance<IEngagementRepository>(
DI_TOKENS.EngagementRepository,
new InMemoryEngagementRepository(logger)
); );
container.registerInstance<ILeagueRepository>( container.registerInstance<ILeagueRepository>(
DI_TOKENS.LeagueRepository, 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<IRaceRepository>(DI_TOKENS.RaceRepository, raceRepository); container.registerInstance<IRaceRepository>(DI_TOKENS.RaceRepository, raceRepository);
// Result repository needs race repository for league-based queries // 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<IResultRepository>(DI_TOKENS.ResultRepository, resultRepository); container.registerInstance<IResultRepository>(DI_TOKENS.ResultRepository, resultRepository);
// Standing repository needs all three for recalculation // Standing repository needs all three for recalculation
@@ -239,6 +277,7 @@ export function configureDIContainer(): void {
container.registerInstance<IStandingRepository>( container.registerInstance<IStandingRepository>(
DI_TOKENS.StandingRepository, DI_TOKENS.StandingRepository,
new InMemoryStandingRepository( new InMemoryStandingRepository(
logger,
seedData.standings, seedData.standings,
resultRepository, resultRepository,
raceRepository, raceRepository,
@@ -293,7 +332,7 @@ export function configureDIContainer(): void {
container.registerInstance<IRaceRegistrationRepository>( container.registerInstance<IRaceRegistrationRepository>(
DI_TOKENS.RaceRegistrationRepository, DI_TOKENS.RaceRegistrationRepository,
new InMemoryRaceRegistrationRepository(seedRaceRegistrations) new InMemoryRaceRegistrationRepository(logger, seedRaceRegistrations)
); );
// Seed penalties and protests // Seed penalties and protests
@@ -449,12 +488,12 @@ export function configureDIContainer(): void {
container.registerInstance<IPenaltyRepository>( container.registerInstance<IPenaltyRepository>(
DI_TOKENS.PenaltyRepository, DI_TOKENS.PenaltyRepository,
new InMemoryPenaltyRepository(seededPenalties) new InMemoryPenaltyRepository(logger, seededPenalties)
); );
container.registerInstance<IProtestRepository>( container.registerInstance<IProtestRepository>(
DI_TOKENS.ProtestRepository, DI_TOKENS.ProtestRepository,
new InMemoryProtestRepository(seededProtests) new InMemoryProtestRepository(logger, seededProtests)
); );
// Scoring repositories // Scoring repositories
@@ -495,17 +534,17 @@ export function configureDIContainer(): void {
container.registerInstance<IGameRepository>( container.registerInstance<IGameRepository>(
DI_TOKENS.GameRepository, DI_TOKENS.GameRepository,
new InMemoryGameRepository([game]) new InMemoryGameRepository(logger, [game])
); );
container.registerInstance<ISeasonRepository>( container.registerInstance<ISeasonRepository>(
DI_TOKENS.SeasonRepository, DI_TOKENS.SeasonRepository,
new InMemorySeasonRepository(seededSeasons) new InMemorySeasonRepository(logger, seededSeasons)
); );
container.registerInstance<ILeagueScoringConfigRepository>( container.registerInstance<ILeagueScoringConfigRepository>(
DI_TOKENS.LeagueScoringConfigRepository, DI_TOKENS.LeagueScoringConfigRepository,
new InMemoryLeagueScoringConfigRepository(seededScoringConfigs) new InMemoryLeagueScoringConfigRepository(logger, seededScoringConfigs)
); );
// League memberships // League memberships
@@ -702,6 +741,7 @@ export function configureDIContainer(): void {
container.registerInstance<ILeagueMembershipRepository>( container.registerInstance<ILeagueMembershipRepository>(
DI_TOKENS.LeagueMembershipRepository, DI_TOKENS.LeagueMembershipRepository,
new InMemoryLeagueMembershipRepository( new InMemoryLeagueMembershipRepository(
logger,
seededMemberships as InMemoryLeagueMembershipSeed, seededMemberships as InMemoryLeagueMembershipSeed,
seededJoinRequests, seededJoinRequests,
) )
@@ -713,6 +753,7 @@ export function configureDIContainer(): void {
container.registerInstance<ITeamRepository>( container.registerInstance<ITeamRepository>(
DI_TOKENS.TeamRepository, DI_TOKENS.TeamRepository,
new InMemoryTeamRepository( new InMemoryTeamRepository(
logger,
seedData.teams.map((t) => ({ seedData.teams.map((t) => ({
id: t.id, id: t.id,
name: t.name, name: t.name,
@@ -728,6 +769,7 @@ export function configureDIContainer(): void {
container.registerInstance<ITeamMembershipRepository>( container.registerInstance<ITeamMembershipRepository>(
DI_TOKENS.TeamMembershipRepository, DI_TOKENS.TeamMembershipRepository,
new InMemoryTeamMembershipRepository( new InMemoryTeamMembershipRepository(
logger,
seedData.memberships seedData.memberships
.filter((m) => m.teamId) .filter((m) => m.teamId)
.map((m) => ({ .map((m) => ({
@@ -743,12 +785,12 @@ export function configureDIContainer(): void {
// Track and Car repositories // Track and Car repositories
container.registerInstance<ITrackRepository>( container.registerInstance<ITrackRepository>(
DI_TOKENS.TrackRepository, DI_TOKENS.TrackRepository,
new InMemoryTrackRepository(DEMO_TRACKS) new InMemoryTrackRepository(logger, DEMO_TRACKS)
); );
container.registerInstance<ICarRepository>( container.registerInstance<ICarRepository>(
DI_TOKENS.CarRepository, DI_TOKENS.CarRepository,
new InMemoryCarRepository(DEMO_CARS) new InMemoryCarRepository(logger, DEMO_CARS)
); );
// Sponsor repositories - seed with demo sponsors // 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); sponsorRepo.seed(seededSponsors);
container.registerInstance<ISponsorRepository>( container.registerInstance<ISponsorRepository>(
DI_TOKENS.SponsorRepository, DI_TOKENS.SponsorRepository,
@@ -781,7 +823,7 @@ export function configureDIContainer(): void {
}), }),
); );
const seasonSponsorshipRepo = new InMemorySeasonSponsorshipRepository(); const seasonSponsorshipRepo = new InMemorySeasonSponsorshipRepository(logger);
seasonSponsorshipRepo.seed(seededSponsorships); seasonSponsorshipRepo.seed(seededSponsorships);
container.registerInstance<ISeasonSponsorshipRepository>( container.registerInstance<ISeasonSponsorshipRepository>(
DI_TOKENS.SeasonSponsorshipRepository, DI_TOKENS.SeasonSponsorshipRepository,
@@ -789,13 +831,13 @@ export function configureDIContainer(): void {
); );
// Sponsorship Request and Pricing repositories // Sponsorship Request and Pricing repositories
const sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(); const sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(logger);
container.registerInstance<ISponsorshipRequestRepository>( container.registerInstance<ISponsorshipRequestRepository>(
DI_TOKENS.SponsorshipRequestRepository, DI_TOKENS.SponsorshipRequestRepository,
sponsorshipRequestRepo sponsorshipRequestRepo
); );
const sponsorshipPricingRepo = new InMemorySponsorshipPricingRepository(); const sponsorshipPricingRepo = new InMemorySponsorshipPricingRepository(logger);
// Seed sponsorship pricings from demo data using domain SponsorshipPricing // Seed sponsorship pricings from demo data using domain SponsorshipPricing
sponsorshipPricingRepo.seed(seedData.sponsorshipPricings ?? []); sponsorshipPricingRepo.seed(seedData.sponsorshipPricings ?? []);
container.registerInstance<ISponsorshipPricingRepository>( container.registerInstance<ISponsorshipPricingRepository>(
@@ -811,12 +853,12 @@ export function configureDIContainer(): void {
// Social repositories // Social repositories
container.registerInstance<IFeedRepository>( container.registerInstance<IFeedRepository>(
DI_TOKENS.FeedRepository, DI_TOKENS.FeedRepository,
new InMemoryFeedRepository(seedData) new InMemoryFeedRepository(logger, seedData)
); );
container.registerInstance<ISocialGraphRepository>( container.registerInstance<ISocialGraphRepository>(
DI_TOKENS.SocialRepository, DI_TOKENS.SocialRepository,
new InMemorySocialGraphRepository(seedData) new InMemorySocialGraphRepository(logger, seedData)
); );
// Image service // Image service
@@ -828,12 +870,12 @@ export function configureDIContainer(): void {
// Notification repositories // Notification repositories
container.registerInstance<INotificationRepository>( container.registerInstance<INotificationRepository>(
DI_TOKENS.NotificationRepository, DI_TOKENS.NotificationRepository,
new InMemoryNotificationRepository() new InMemoryNotificationRepository(logger)
); );
container.registerInstance<INotificationPreferenceRepository>( container.registerInstance<INotificationPreferenceRepository>(
DI_TOKENS.NotificationPreferenceRepository, DI_TOKENS.NotificationPreferenceRepository,
new InMemoryNotificationPreferenceRepository() new InMemoryNotificationPreferenceRepository(logger)
); );
const notificationGatewayRegistry = new NotificationGatewayRegistry([ const notificationGatewayRegistry = new NotificationGatewayRegistry([
@@ -889,26 +931,39 @@ export function configureDIContainer(): void {
const notificationPreferenceRepository = container.resolve<INotificationPreferenceRepository>(DI_TOKENS.NotificationPreferenceRepository); const notificationPreferenceRepository = container.resolve<INotificationPreferenceRepository>(DI_TOKENS.NotificationPreferenceRepository);
const feedRepository = container.resolve<IFeedRepository>(DI_TOKENS.FeedRepository); const feedRepository = container.resolve<IFeedRepository>(DI_TOKENS.FeedRepository);
const socialRepository = container.resolve<ISocialGraphRepository>(DI_TOKENS.SocialRepository); const socialRepository = container.resolve<ISocialGraphRepository>(DI_TOKENS.SocialRepository);
const pageViewRepository = container.resolve<IPageViewRepository>(DI_TOKENS.PageViewRepository);
const engagementRepository = container.resolve<IEngagementRepository>(DI_TOKENS.EngagementRepository);
const logger = container.resolve<ILogger>(DI_TOKENS.Logger);
// Register use cases - Racing // Register use cases - Racing
container.registerInstance( container.registerInstance(
DI_TOKENS.JoinLeagueUseCase, 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( container.registerInstance(
DI_TOKENS.RegisterForRaceUseCase, DI_TOKENS.RegisterForRaceUseCase,
new RegisterForRaceUseCase(raceRegistrationRepository, leagueMembershipRepository) new RegisterForRaceUseCase(raceRegistrationRepository, leagueMembershipRepository, logger)
); );
container.registerInstance( container.registerInstance(
DI_TOKENS.WithdrawFromRaceUseCase, DI_TOKENS.WithdrawFromRaceUseCase,
new WithdrawFromRaceUseCase(raceRegistrationRepository) new WithdrawFromRaceUseCase(raceRegistrationRepository, logger)
); );
container.registerInstance( container.registerInstance(
DI_TOKENS.CancelRaceUseCase, DI_TOKENS.CancelRaceUseCase,
new CancelRaceUseCase(raceRepository) new CancelRaceUseCase(raceRepository, logger)
); );
container.registerInstance( container.registerInstance(
@@ -918,7 +973,8 @@ export function configureDIContainer(): void {
raceRegistrationRepository, raceRegistrationRepository,
resultRepository, resultRepository,
standingRepository, standingRepository,
driverRatingProvider driverRatingProvider,
logger
) )
); );
@@ -928,7 +984,8 @@ export function configureDIContainer(): void {
leagueRepository, leagueRepository,
seasonRepository, seasonRepository,
leagueScoringConfigRepository, leagueScoringConfigRepository,
leagueScoringPresetProvider leagueScoringPresetProvider,
logger
) )
); );
@@ -945,7 +1002,7 @@ export function configureDIContainer(): void {
container.registerInstance( container.registerInstance(
DI_TOKENS.JoinTeamUseCase, DI_TOKENS.JoinTeamUseCase,
new JoinTeamUseCase(teamRepository, teamMembershipRepository) new JoinTeamUseCase(teamRepository, teamMembershipRepository, logger)
); );
container.registerInstance( container.registerInstance(
@@ -955,7 +1012,7 @@ export function configureDIContainer(): void {
container.registerInstance( container.registerInstance(
DI_TOKENS.ApproveTeamJoinRequestUseCase, DI_TOKENS.ApproveTeamJoinRequestUseCase,
new ApproveTeamJoinRequestUseCase(teamMembershipRepository) new ApproveTeamJoinRequestUseCase(teamMembershipRepository, logger)
); );
container.registerInstance( container.registerInstance(
@@ -985,7 +1042,8 @@ export function configureDIContainer(): void {
penaltyRepository, penaltyRepository,
protestRepository, protestRepository,
raceRepository, raceRepository,
leagueMembershipRepository leagueMembershipRepository,
logger
) )
); );
@@ -994,7 +1052,8 @@ export function configureDIContainer(): void {
new QuickPenaltyUseCase( new QuickPenaltyUseCase(
penaltyRepository, penaltyRepository,
raceRepository, raceRepository,
leagueMembershipRepository leagueMembershipRepository,
logger
) )
); );
@@ -1014,13 +1073,14 @@ export function configureDIContainer(): void {
new SendNotificationUseCase( new SendNotificationUseCase(
notificationRepository, notificationRepository,
notificationPreferenceRepository, notificationPreferenceRepository,
notificationGatewayRegistry notificationGatewayRegistry,
logger
) )
); );
container.registerInstance( container.registerInstance(
DI_TOKENS.MarkNotificationReadUseCase, DI_TOKENS.MarkNotificationReadUseCase,
new MarkNotificationReadUseCase(notificationRepository) new MarkNotificationReadUseCase(notificationRepository, logger)
); );
// Register queries - Racing // Register queries - Racing
@@ -1144,7 +1204,8 @@ export function configureDIContainer(): void {
raceRepository, raceRepository,
resultRepository, resultRepository,
driverRatingProvider, driverRatingProvider,
leagueStatsPresenter leagueStatsPresenter,
logger
) )
); );
@@ -1155,7 +1216,7 @@ export function configureDIContainer(): void {
container.registerInstance( container.registerInstance(
DI_TOKENS.GetAllRacesPageDataUseCase, DI_TOKENS.GetAllRacesPageDataUseCase,
new GetAllRacesPageDataUseCase(raceRepository, leagueRepository) new GetAllRacesPageDataUseCase(raceRepository, leagueRepository, logger)
); );
const imageService = container.resolve<ImageServicePort>(DI_TOKENS.ImageService); const imageService = container.resolve<ImageServicePort>(DI_TOKENS.ImageService);
@@ -1194,7 +1255,8 @@ export function configureDIContainer(): void {
resultRepository, resultRepository,
driverRepository, driverRepository,
standingRepository, standingRepository,
importRaceResultsPresenter importRaceResultsPresenter,
logger
) )
); );
@@ -1324,7 +1386,7 @@ export function configureDIContainer(): void {
const allTeamsPresenter = new AllTeamsPresenter(); const allTeamsPresenter = new AllTeamsPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetAllTeamsUseCase, DI_TOKENS.GetAllTeamsUseCase,
new GetAllTeamsUseCase(teamRepository, teamMembershipRepository), new GetAllTeamsUseCase(teamRepository, teamMembershipRepository, logger),
); );
container.registerInstance( container.registerInstance(
@@ -1335,7 +1397,7 @@ export function configureDIContainer(): void {
const teamMembersPresenter = new TeamMembersPresenter(); const teamMembersPresenter = new TeamMembersPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetTeamMembersUseCase, DI_TOKENS.GetTeamMembersUseCase,
new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, teamMembersPresenter), new GetTeamMembersUseCase(teamMembershipRepository, driverRepository, imageService, logger, teamMembersPresenter),
); );
const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter(); const teamJoinRequestsPresenter = new TeamJoinRequestsPresenter();
@@ -1345,14 +1407,15 @@ export function configureDIContainer(): void {
teamMembershipRepository, teamMembershipRepository,
driverRepository, driverRepository,
imageService, imageService,
teamJoinRequestsPresenter, logger,
teamJoinRequestsPresenter
), ),
); );
const driverTeamPresenter = new DriverTeamPresenter(); const driverTeamPresenter = new DriverTeamPresenter();
container.registerInstance( container.registerInstance(
DI_TOKENS.GetDriverTeamUseCase, DI_TOKENS.GetDriverTeamUseCase,
new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, driverTeamPresenter) new GetDriverTeamUseCase(teamRepository, teamMembershipRepository, logger, driverTeamPresenter)
); );
// Register queries - Stewarding // Register queries - Stewarding
@@ -1371,7 +1434,7 @@ export function configureDIContainer(): void {
// Register queries - Notifications // Register queries - Notifications
container.registerInstance( container.registerInstance(
DI_TOKENS.GetUnreadNotificationsUseCase, DI_TOKENS.GetUnreadNotificationsUseCase,
new GetUnreadNotificationsUseCase(notificationRepository) new GetUnreadNotificationsUseCase(notificationRepository, logger)
); );
// Register use cases - Sponsors // Register use cases - Sponsors
@@ -1421,7 +1484,8 @@ export function configureDIContainer(): void {
sponsorshipPricingRepository, sponsorshipPricingRepository,
sponsorshipRequestRepository, sponsorshipRequestRepository,
seasonSponsorshipRepository, seasonSponsorshipRepository,
entitySponsorshipPricingPresenter entitySponsorshipPricingPresenter,
logger
) )
); );
@@ -1430,7 +1494,8 @@ export function configureDIContainer(): void {
new ApplyForSponsorshipUseCase( new ApplyForSponsorshipUseCase(
sponsorshipRequestRepository, sponsorshipRequestRepository,
sponsorshipPricingRepository, sponsorshipPricingRepository,
sponsorRepository sponsorRepository,
logger
) )
); );
@@ -1440,6 +1505,7 @@ export function configureDIContainer(): void {
sponsorshipRequestRepository, sponsorshipRequestRepository,
seasonSponsorshipRepository, seasonSponsorshipRepository,
seasonRepository, seasonRepository,
logger
) )
); );

View File

@@ -28,6 +28,17 @@ export const DI_TOKENS = {
SeasonSponsorshipRepository: Symbol.for('ISeasonSponsorshipRepository'), SeasonSponsorshipRepository: Symbol.for('ISeasonSponsorshipRepository'),
SponsorshipRequestRepository: Symbol.for('ISponsorshipRequestRepository'), SponsorshipRequestRepository: Symbol.for('ISponsorshipRequestRepository'),
SponsorshipPricingRepository: Symbol.for('ISponsorshipPricingRepository'), 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 // Providers
LeagueScoringPresetProvider: Symbol.for('LeagueScoringPresetProvider'), LeagueScoringPresetProvider: Symbol.for('LeagueScoringPresetProvider'),
@@ -36,6 +47,12 @@ export const DI_TOKENS = {
// Services // Services
ImageService: Symbol.for('ImageServicePort'), ImageService: Symbol.for('ImageServicePort'),
NotificationGatewayRegistry: Symbol.for('NotificationGatewayRegistry'), 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 // Use Cases - Racing
JoinLeagueUseCase: Symbol.for('JoinLeagueUseCase'), JoinLeagueUseCase: Symbol.for('JoinLeagueUseCase'),

View File

@@ -6,6 +6,7 @@
*/ */
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '@gridpilot/shared/application';
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository'; import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository';
@@ -47,56 +48,108 @@ export class GetEntityAnalyticsQuery
constructor( constructor(
private readonly pageViewRepository: IPageViewRepository, private readonly pageViewRepository: IPageViewRepository,
private readonly engagementRepository: IEngagementRepository, private readonly engagementRepository: IEngagementRepository,
private readonly snapshotRepository: IAnalyticsSnapshotRepository private readonly snapshotRepository: IAnalyticsSnapshotRepository,
private readonly logger: ILogger
) {} ) {}
async execute(input: GetEntityAnalyticsInput): Promise<EntityAnalyticsOutput> { async execute(input: GetEntityAnalyticsInput): Promise<EntityAnalyticsOutput> {
this.logger.debug(`Executing GetEntityAnalyticsQuery with input: ${JSON.stringify(input)}`);
const period = input.period ?? 'weekly'; const period = input.period ?? 'weekly';
const now = new Date(); const now = new Date();
const since = input.since ?? this.getPeriodStartDate(now, period); const since = input.since ?? this.getPeriodStartDate(now, period);
this.logger.debug(`Calculated period: ${period}, now: ${now.toISOString()}, since: ${since.toISOString()}`);
// Get current metrics // Get current metrics
const totalPageViews = await this.pageViewRepository.countByEntityId( let totalPageViews = 0;
input.entityType, try {
input.entityId, totalPageViews = await this.pageViewRepository.countByEntityId(
since 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( let uniqueVisitors = 0;
input.entityType, try {
input.entityId, uniqueVisitors = await this.pageViewRepository.countUniqueVisitors(
since 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( let sponsorClicks = 0;
input.entityId, try {
since 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) // 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 // Determine trust indicator
const trustIndicator = this.determineTrustIndicator(totalPageViews, uniqueVisitors, engagementScore); const trustIndicator = this.determineTrustIndicator(totalPageViews, uniqueVisitors, engagementScore);
this.logger.debug(`Trust indicator for entity ${input.entityId}: ${trustIndicator}`);
// Calculate exposure value (for sponsor ROI) // Calculate exposure value (for sponsor ROI)
const exposureValue = this.calculateExposureValue(totalPageViews, uniqueVisitors, sponsorClicks); const exposureValue = this.calculateExposureValue(totalPageViews, uniqueVisitors, sponsorClicks);
this.logger.debug(`Exposure value for entity ${input.entityId}: ${exposureValue}`);
// Get previous period for trends // Get previous period for trends
const previousPeriodStart = this.getPreviousPeriodStart(since, period); const previousPeriodStart = this.getPreviousPeriodStart(since, period);
const previousPageViews = await this.pageViewRepository.countByEntityId( this.logger.debug(`Previous period start: ${previousPeriodStart.toISOString()}`);
input.entityType,
input.entityId,
previousPeriodStart
) - totalPageViews;
const previousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors( let previousPageViews = 0;
input.entityType, try {
input.entityId, const fullPreviousPageViews = await this.pageViewRepository.countByEntityId(
previousPeriodStart input.entityType,
) - uniqueVisitors; 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, entityType: input.entityType,
entityId: input.entityId, entityId: input.entityId,
summary: { summary: {
@@ -118,9 +171,12 @@ export class GetEntityAnalyticsQuery
label: this.formatPeriodLabel(since, now), label: this.formatPeriodLabel(since, now),
}, },
}; };
this.logger.info(`Successfully retrieved analytics for entity ${input.entityId}.`);
return result;
} }
private getPeriodStartDate(now: Date, period: SnapshotPeriod): Date { private getPeriodStartDate(now: Date, period: SnapshotPeriod): Date {
this.logger.debug(`Calculating period start date for "${period}" from ${now.toISOString()}`);
const start = new Date(now); const start = new Date(now);
switch (period) { switch (period) {
case 'daily': case 'daily':
@@ -133,10 +189,12 @@ export class GetEntityAnalyticsQuery
start.setMonth(start.getMonth() - 1); start.setMonth(start.getMonth() - 1);
break; break;
} }
this.logger.debug(`Period start date calculated: ${start.toISOString()}`);
return start; return start;
} }
private getPreviousPeriodStart(currentStart: Date, period: SnapshotPeriod): Date { 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); const start = new Date(currentStart);
switch (period) { switch (period) {
case 'daily': case 'daily':
@@ -149,13 +207,23 @@ export class GetEntityAnalyticsQuery
start.setMonth(start.getMonth() - 1); start.setMonth(start.getMonth() - 1);
break; break;
} }
this.logger.debug(`Previous period start date calculated: ${start.toISOString()}`);
return start; return start;
} }
private async calculateEngagementScore(entityId: string, since: Date): Promise<number> { private async calculateEngagementScore(entityId: string, since: Date): Promise<number> {
// Base engagement from sponsor interactions this.logger.debug(`Calculating engagement score for entity ${entityId} since ${since.toISOString()}`);
const sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(entityId, since); let sponsorClicks = 0;
return sponsorClicks * 10; // Weighted score 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( private determineTrustIndicator(
@@ -163,8 +231,10 @@ export class GetEntityAnalyticsQuery
uniqueVisitors: number, uniqueVisitors: number,
engagementScore: number engagementScore: number
): 'high' | 'medium' | 'low' { ): 'high' | 'medium' | 'low' {
this.logger.debug(`Determining trust indicator with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, engagementScore: ${engagementScore}`);
const engagementRate = pageViews > 0 ? engagementScore / pageViews : 0; const engagementRate = pageViews > 0 ? engagementScore / pageViews : 0;
const returningVisitorRate = pageViews > 0 ? (pageViews - uniqueVisitors) / 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) { if (engagementRate > 0.1 && returningVisitorRate > 0.3) {
return 'high'; return 'high';
@@ -180,20 +250,33 @@ export class GetEntityAnalyticsQuery
uniqueVisitors: number, uniqueVisitors: number,
sponsorClicks: number sponsorClicks: number
): number { ): number {
this.logger.debug(`Calculating exposure value with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, sponsorClicks: ${sponsorClicks}`);
// Simple exposure value calculation (could be monetized) // 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 { private calculatePercentageChange(previous: number, current: number): number {
if (previous === 0) return current > 0 ? 100 : 0; this.logger.debug(`Calculating percentage change from previous: ${previous} to current: ${current}`);
return Math.round(((current - previous) / previous) * 100); 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 { 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', { const formatter = new Intl.DateTimeFormat('en-US', {
month: 'short', month: 'short',
day: 'numeric', 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;
} }
} }

View File

@@ -5,6 +5,7 @@
*/ */
import type { AsyncUseCase } from '@gridpilot/shared/application'; 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 { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent';
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
@@ -25,31 +26,41 @@ export interface RecordEngagementOutput {
export class RecordEngagementUseCase export class RecordEngagementUseCase
implements AsyncUseCase<RecordEngagementInput, RecordEngagementOutput> { implements AsyncUseCase<RecordEngagementInput, RecordEngagementOutput> {
constructor(private readonly engagementRepository: IEngagementRepository) {} constructor(
private readonly engagementRepository: IEngagementRepository,
private readonly logger: ILogger,
) {}
async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> { async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
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<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = { const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
id: eventId, id: eventId,
action: input.action, action: input.action,
entityType: input.entityType, entityType: input.entityType,
entityId: input.entityId, entityId: input.entityId,
actorType: input.actorType, actorType: input.actorType,
sessionId: input.sessionId, sessionId: input.sessionId,
}; };
const event = EngagementEvent.create({ const event = EngagementEvent.create({
...baseProps, ...baseProps,
...(input.actorId !== undefined ? { actorId: input.actorId } : {}), ...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
...(input.metadata !== undefined ? { metadata: input.metadata } : {}), ...(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 { return {
eventId, eventId,
engagementWeight: event.getEngagementWeight(), engagementWeight: event.getEngagementWeight(),
}; };
} catch (error) {
this.logger.error('Error recording engagement', error, { input });
throw error;
}
} }
} }

View File

@@ -5,6 +5,7 @@
*/ */
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
import { PageView } from '../../domain/entities/PageView'; import { PageView } from '../../domain/entities/PageView';
import type { EntityType, VisitorType } from '../../domain/types/PageView'; import type { EntityType, VisitorType } from '../../domain/types/PageView';
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
@@ -26,29 +27,38 @@ export interface RecordPageViewOutput {
export class RecordPageViewUseCase export class RecordPageViewUseCase
implements AsyncUseCase<RecordPageViewInput, RecordPageViewOutput> { implements AsyncUseCase<RecordPageViewInput, RecordPageViewOutput> {
constructor(private readonly pageViewRepository: IPageViewRepository) {} constructor(
private readonly pageViewRepository: IPageViewRepository,
private readonly logger: ILogger,
) {}
async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> { async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
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<Parameters<typeof PageView.create>[0], 'timestamp'> = { const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
id: pageViewId, id: pageViewId,
entityType: input.entityType, entityType: input.entityType,
entityId: input.entityId, entityId: input.entityId,
visitorType: input.visitorType, visitorType: input.visitorType,
sessionId: input.sessionId, sessionId: input.sessionId,
}; };
const pageView = PageView.create({ const pageView = PageView.create({
...baseProps, ...baseProps,
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}), ...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
...(input.referrer !== undefined ? { referrer: input.referrer } : {}), ...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}), ...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
...(input.country !== undefined ? { country: input.country } : {}), ...(input.country !== undefined ? { country: input.country } : {}),
}); });
await this.pageViewRepository.save(pageView); await this.pageViewRepository.save(pageView);
this.logger.info('Page view recorded successfully', { pageViewId, input });
return { pageViewId }; return { pageViewId };
} catch (error) {
this.logger.error('Error recording page view', error, { input });
throw error;
}
} }
} }

View File

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

View File

@@ -6,22 +6,56 @@
import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository'; import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository';
import { AnalyticsSnapshot, type SnapshotPeriod, type SnapshotEntityType } from '../../domain/entities/AnalyticsSnapshot'; import { AnalyticsSnapshot, type SnapshotPeriod, type SnapshotEntityType } from '../../domain/entities/AnalyticsSnapshot';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRepository { export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRepository {
private snapshots: Map<string, AnalyticsSnapshot> = new Map(); private snapshots: Map<string, AnalyticsSnapshot> = new Map();
private readonly logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
this.logger.info('InMemoryAnalyticsSnapshotRepository initialized.');
}
async save(snapshot: AnalyticsSnapshot): Promise<void> { async save(snapshot: AnalyticsSnapshot): Promise<void> {
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<AnalyticsSnapshot | null> { async findById(id: string): Promise<AnalyticsSnapshot | null> {
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<AnalyticsSnapshot[]> { async findByEntity(entityType: SnapshotEntityType, entityId: string): Promise<AnalyticsSnapshot[]> {
return Array.from(this.snapshots.values()).filter( this.logger.debug(`Finding AnalyticsSnapshots by Entity: ${entityType}, ${entityId}`);
s => s.entityType === entityType && s.entityId === 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( async findByPeriod(
@@ -31,13 +65,25 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe
startDate: Date, startDate: Date,
endDate: Date endDate: Date
): Promise<AnalyticsSnapshot | null> { ): Promise<AnalyticsSnapshot | null> {
return Array.from(this.snapshots.values()).find( this.logger.debug(`Finding AnalyticsSnapshot by Period for entity ${entityId}, period ${period}, from ${startDate.toISOString()} to ${endDate.toISOString()}`);
s => s.entityType === entityType && try {
s.entityId === entityId && const snapshot = Array.from(this.snapshots.values()).find(
s.period === period && s => s.entityType === entityType &&
s.startDate >= startDate && s.entityId === entityId &&
s.endDate <= endDate s.period === period &&
) ?? null; 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( async findLatest(
@@ -45,11 +91,23 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe
entityId: string, entityId: string,
period: SnapshotPeriod period: SnapshotPeriod
): Promise<AnalyticsSnapshot | null> { ): Promise<AnalyticsSnapshot | null> {
const matching = Array.from(this.snapshots.values()) this.logger.debug(`Finding latest AnalyticsSnapshot for entity ${entityId}, period ${period}`);
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period) try {
.sort((a, b) => b.endDate.getTime() - a.endDate.getTime()); const matching = Array.from(this.snapshots.values())
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period)
return matching[0] ?? null; .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( async getHistoricalSnapshots(
@@ -58,10 +116,18 @@ export class InMemoryAnalyticsSnapshotRepository implements IAnalyticsSnapshotRe
period: SnapshotPeriod, period: SnapshotPeriod,
limit: number limit: number
): Promise<AnalyticsSnapshot[]> { ): Promise<AnalyticsSnapshot[]> {
return Array.from(this.snapshots.values()) this.logger.debug(`Getting historical AnalyticsSnapshots for entity ${entityId}, period ${period}, limit ${limit}`);
.filter(s => s.entityType === entityType && s.entityId === entityId && s.period === period) try {
.sort((a, b) => b.endDate.getTime() - a.endDate.getTime()) const snapshots = Array.from(this.snapshots.values())
.slice(0, limit); .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 // Helper for testing

View File

@@ -6,59 +6,135 @@
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent'; import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryEngagementRepository implements IEngagementRepository { export class InMemoryEngagementRepository implements IEngagementRepository {
private events: Map<string, EngagementEvent> = new Map(); private events: Map<string, EngagementEvent> = new Map();
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
this.logger.info('InMemoryEngagementRepository initialized.');
}
async save(event: EngagementEvent): Promise<void> { async save(event: EngagementEvent): Promise<void> {
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<EngagementEvent | null> { async findById(id: string): Promise<EngagementEvent | null> {
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<EngagementEvent[]> { async findByEntityId(entityType: EngagementEntityType, entityId: string): Promise<EngagementEvent[]> {
return Array.from(this.events.values()).filter( this.logger.debug(`Attempting to find engagement events for entityType: ${entityType}, entityId: ${entityId}`);
e => e.entityType === entityType && e.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<EngagementEvent[]> { async findByAction(action: EngagementAction): Promise<EngagementEvent[]> {
return Array.from(this.events.values()).filter( this.logger.debug(`Attempting to find engagement events by action: ${action}`);
e => e.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<EngagementEvent[]> { async findByDateRange(startDate: Date, endDate: Date): Promise<EngagementEvent[]> {
return Array.from(this.events.values()).filter( this.logger.debug(`Attempting to find engagement events by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
e => e.timestamp >= startDate && e.timestamp <= endDate 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<number> { async countByAction(action: EngagementAction, entityId?: string, since?: Date): Promise<number> {
return Array.from(this.events.values()).filter( this.logger.debug(`Attempting to count engagement events for action: ${action}, entityId: ${entityId}, since: ${since?.toISOString()}`);
e => e.action === action && try {
(!entityId || e.entityId === entityId) && const count = Array.from(this.events.values()).filter(
(!since || e.timestamp >= since) e => e.action === action &&
).length; (!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<number> { async getSponsorClicksForEntity(entityId: string, since?: Date): Promise<number> {
return Array.from(this.events.values()).filter( this.logger.debug(`Attempting to get sponsor clicks for entity ID: ${entityId}, since: ${since?.toISOString()}`);
e => e.entityId === entityId && try {
(e.action === 'click_sponsor_logo' || e.action === 'click_sponsor_url') && const count = Array.from(this.events.values()).filter(
(!since || e.timestamp >= since) e => e.entityId === entityId &&
).length; (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 // Helper for testing
clear(): void { clear(): void {
this.logger.debug('Clearing all engagement events.');
this.events.clear(); this.events.clear();
this.logger.info('All engagement events cleared.');
} }
// Helper for seeding demo data // Helper for seeding demo data
seed(events: EngagementEvent[]): void { 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;
}
} }
} }

View File

@@ -6,65 +6,139 @@
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
import { PageView, type EntityType } from '../../domain/entities/PageView'; import { PageView, type EntityType } from '../../domain/entities/PageView';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryPageViewRepository implements IPageViewRepository { export class InMemoryPageViewRepository implements IPageViewRepository {
private pageViews: Map<string, PageView> = new Map(); private pageViews: Map<string, PageView> = new Map();
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
this.logger.info('InMemoryPageViewRepository initialized.');
}
async save(pageView: PageView): Promise<void> { async save(pageView: PageView): Promise<void> {
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<PageView | null> { async findById(id: string): Promise<PageView | null> {
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<PageView[]> { async findByEntityId(entityType: EntityType, entityId: string): Promise<PageView[]> {
return Array.from(this.pageViews.values()).filter( this.logger.debug(`Attempting to find page views for entityType: ${entityType}, entityId: ${entityId}`);
pv => pv.entityType === entityType && pv.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<PageView[]> { async findByDateRange(startDate: Date, endDate: Date): Promise<PageView[]> {
return Array.from(this.pageViews.values()).filter( this.logger.debug(`Attempting to find page views by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
pv => pv.timestamp >= startDate && pv.timestamp <= endDate 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<PageView[]> { async findBySession(sessionId: string): Promise<PageView[]> {
return Array.from(this.pageViews.values()).filter( this.logger.debug(`Attempting to find page views by session ID: ${sessionId}`);
pv => pv.sessionId === 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<number> { async countByEntityId(entityType: EntityType, entityId: string, since?: Date): Promise<number> {
return Array.from(this.pageViews.values()).filter( this.logger.debug(`Attempting to count page views for entityType: ${entityType}, entityId: ${entityId}, since: ${since?.toISOString()}`);
pv => pv.entityType === entityType && try {
pv.entityId === entityId && const count = Array.from(this.pageViews.values()).filter(
(!since || pv.timestamp >= since) pv => pv.entityType === entityType &&
).length; 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<number> { async countUniqueVisitors(entityType: EntityType, entityId: string, since?: Date): Promise<number> {
const visitors = new Set<string>(); this.logger.debug(`Attempting to count unique visitors for entityType: ${entityType}, entityId: ${entityId}, since: ${since?.toISOString()}`);
Array.from(this.pageViews.values()) try {
.filter( const visitors = new Set<string>();
pv => pv.entityType === entityType && Array.from(this.pageViews.values())
pv.entityId === entityId && .filter(
(!since || pv.timestamp >= since) pv => pv.entityType === entityType &&
) pv.entityId === entityId &&
.forEach(pv => { (!since || pv.timestamp >= since)
visitors.add(pv.visitorId ?? pv.sessionId); )
}); .forEach(pv => {
return visitors.size; 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 // Helper for testing
clear(): void { clear(): void {
this.logger.debug('Clearing all page views.');
this.pageViews.clear(); this.pageViews.clear();
this.logger.info('All page views cleared.');
} }
// Helper for seeding demo data // Helper for seeding demo data
seed(pageViews: PageView[]): void { 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;
}
} }
} }

View File

@@ -0,0 +1,7 @@
export interface ILogger {
debug(message: string, context?: Record<string, any>): void;
info(message: string, context?: Record<string, any>): void;
warn(message: string, context?: Record<string, any>): void;
error(message: string, error?: Error, context?: Record<string, any>): void;
verbose?(message: string, context?: Record<string, any>): void;
}

View File

@@ -1,10 +1,11 @@
import type { LogLevel } from './LoggerLogLevel'; import type { LogLevel } from './LoggerLogLevel';
import type { LogContext } from './LoggerContext'; import type { LogContext } from './LoggerContext';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
/** /**
* LoggerPort - Port interface for application-layer logging. * LoggerPort - Port interface for application-layer logging.
*/ */
export interface LoggerPort { export interface LoggerPort extends ILogger {
debug(message: string, context?: LogContext): void; debug(message: string, context?: LogContext): void;
info(message: string, context?: LogContext): void; info(message: string, context?: LogContext): void;
warn(message: string, context?: LogContext): void; warn(message: string, context?: LogContext): void;

View File

@@ -1,4 +1,5 @@
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState'; import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import { Result } from '../../../shared/result/Result'; import { Result } from '../../../shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { SessionLifetime } from '../../domain/value-objects/SessionLifetime'; import { SessionLifetime } from '../../domain/value-objects/SessionLifetime';
@@ -16,6 +17,7 @@ import type { SessionValidatorPort } from '../ports/SessionValidatorPort';
*/ */
export class CheckAuthenticationUseCase { export class CheckAuthenticationUseCase {
constructor( constructor(
private readonly logger: ILogger,
private readonly authService: AuthenticationServicePort, private readonly authService: AuthenticationServicePort,
private readonly sessionValidator?: SessionValidatorPort private readonly sessionValidator?: SessionValidatorPort
) {} ) {}
@@ -30,63 +32,88 @@ export class CheckAuthenticationUseCase {
requireServerValidation?: boolean; requireServerValidation?: boolean;
verifyPageContent?: boolean; verifyPageContent?: boolean;
}): Promise<Result<AuthenticationState>> { }): Promise<Result<AuthenticationState>> {
// Step 1: File-based validation (fast) this.logger.debug('Executing CheckAuthenticationUseCase', { options });
const fileResult = await this.authService.checkSession(); try {
if (fileResult.isErr()) { // Step 1: File-based validation (fast)
return fileResult; this.logger.debug('Performing file-based authentication check.');
} const fileResult = await this.authService.checkSession();
if (fileResult.isErr()) {
const fileState = fileResult.unwrap(); this.logger.error('File-based authentication check failed.', { error: fileResult.unwrapErr() });
return fileResult;
// 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.info('File-based authentication check succeeded.');
const expiry = expiryResult.unwrap(); const fileState = fileResult.unwrap();
if (expiry !== null) { this.logger.debug(`File-based authentication state: ${fileState}`);
try {
const sessionLifetime = new SessionLifetime(expiry); // Step 2: Check session expiry if authenticated
if (sessionLifetime.isExpired()) { 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); 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 // Step 3: Optional page content verification
if (options?.verifyPageContent && fileState === AuthenticationState.AUTHENTICATED) { if (options?.verifyPageContent && fileState === AuthenticationState.AUTHENTICATED) {
const pageResult = await this.authService.verifyPageAuthentication(); this.logger.debug('Performing optional page content verification.');
const pageResult = await this.authService.verifyPageAuthentication();
if (pageResult.isOk()) {
const browserState = pageResult.unwrap(); if (pageResult.isOk()) {
// If cookies valid but page shows login UI, session is expired const browserState = pageResult.unwrap();
if (!browserState.isFullyAuthenticated()) { // If cookies valid but page shows login UI, session is expired
return Result.ok(AuthenticationState.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);
} }
} }

View File

@@ -1,5 +1,6 @@
import { Result } from '../../../shared/result/Result'; import { Result } from '../../../shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use case for clearing the user's session (logout). * 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. * the user out. The next automation attempt will require re-authentication.
*/ */
export class ClearSessionUseCase { export class ClearSessionUseCase {
constructor(private readonly authService: AuthenticationServicePort) {} constructor(
private readonly authService: AuthenticationServicePort,
private readonly logger: ILogger, // Inject ILogger
) {}
/** /**
* Execute the session clearing. * Execute the session clearing.
@@ -16,6 +20,28 @@ export class ClearSessionUseCase {
* @returns Result indicating success or failure * @returns Result indicating success or failure
*/ */
async execute(): Promise<Result<void>> { async execute(): Promise<Result<void>> {
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);
}
} }
} }

View File

@@ -1,24 +1,30 @@
import { Result } from '../../../shared/result/Result'; import { Result } from '../../../shared/result/Result';
import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult'; import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort'; import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export class CompleteRaceCreationUseCase { export class CompleteRaceCreationUseCase {
constructor(private readonly checkoutService: CheckoutServicePort) {} constructor(private readonly checkoutService: CheckoutServicePort, private readonly logger: ILogger) {}
async execute(sessionId: string): Promise<Result<RaceCreationResult>> { async execute(sessionId: string): Promise<Result<RaceCreationResult>> {
this.logger.debug(`Attempting to complete race creation for session ID: ${sessionId}`);
if (!sessionId || sessionId.trim() === '') { if (!sessionId || sessionId.trim() === '') {
this.logger.error('Session ID is required for completing race creation.');
return Result.err(new Error('Session ID is required')); return Result.err(new Error('Session ID is required'));
} }
const infoResult = await this.checkoutService.extractCheckoutInfo(); const infoResult = await this.checkoutService.extractCheckoutInfo();
if (infoResult.isErr()) { if (infoResult.isErr()) {
this.logger.error(`Failed to extract checkout info: ${infoResult.unwrapErr().message}`);
return Result.err(infoResult.unwrapErr()); return Result.err(infoResult.unwrapErr());
} }
const info = infoResult.unwrap(); const info = infoResult.unwrap();
this.logger.debug(`Extracted checkout information: ${JSON.stringify(info)}`);
if (!info.price) { 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')); return Result.err(new Error('Could not extract price from checkout page'));
} }
@@ -29,9 +35,11 @@ export class CompleteRaceCreationUseCase {
timestamp: new Date(), timestamp: new Date(),
}); });
this.logger.info(`Race creation completed successfully for session ID: ${sessionId}`);
return Result.ok(raceCreationResult); return Result.ok(raceCreationResult);
} catch (error) { } catch (error) {
const err = error instanceof Error ? error : new Error('Unknown 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); return Result.err(err);
} }
} }

View File

@@ -1,4 +1,5 @@
import { Result } from '../../../shared/result/Result'; import { Result } from '../../../shared/result/Result';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort'; import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort'; import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort';
import { CheckoutStateEnum } from '../../domain/value-objects/CheckoutState'; import { CheckoutStateEnum } from '../../domain/value-objects/CheckoutState';
@@ -14,26 +15,36 @@ export class ConfirmCheckoutUseCase {
constructor( constructor(
private readonly checkoutService: CheckoutServicePort, private readonly checkoutService: CheckoutServicePort,
private readonly confirmationPort: CheckoutConfirmationPort private readonly confirmationPort: CheckoutConfirmationPort,
private readonly logger: ILogger,
) {} ) {}
async execute(sessionMetadata?: SessionMetadata): Promise<Result<void>> { async execute(sessionMetadata?: SessionMetadata): Promise<Result<void>> {
this.logger.debug('Executing ConfirmCheckoutUseCase', { sessionMetadata });
const infoResult = await this.checkoutService.extractCheckoutInfo(); const infoResult = await this.checkoutService.extractCheckoutInfo();
if (infoResult.isErr()) { if (infoResult.isErr()) {
this.logger.error('Failed to extract checkout info', { error: infoResult.unwrapErr() });
return Result.err(infoResult.unwrapErr()); return Result.err(infoResult.unwrapErr());
} }
const info = infoResult.unwrap(); const info = infoResult.unwrap();
this.logger.info('Extracted checkout info', { state: info.state.getValue(), price: info.price });
if (info.state.getValue() === CheckoutStateEnum.INSUFFICIENT_FUNDS) { 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')); return Result.err(new Error('Insufficient funds to complete checkout'));
} }
if (!info.price) { 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')); 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 // Request confirmation via port with full checkout context
const confirmationResult = await this.confirmationPort.requestCheckoutConfirmation({ const confirmationResult = await this.confirmationPort.requestCheckoutConfirmation({
price: info.price, price: info.price,
@@ -47,19 +58,31 @@ export class ConfirmCheckoutUseCase {
}); });
if (confirmationResult.isErr()) { if (confirmationResult.isErr()) {
this.logger.error('Checkout confirmation failed', { error: confirmationResult.unwrapErr() });
return Result.err(confirmationResult.unwrapErr()); return Result.err(confirmationResult.unwrapErr());
} }
const confirmation = confirmationResult.unwrap(); const confirmation = confirmationResult.unwrap();
this.logger.info('Checkout confirmation received', { confirmation });
if (confirmation.isCancelled()) { if (confirmation.isCancelled()) {
this.logger.error('Checkout cancelled by user');
return Result.err(new Error('Checkout cancelled by user')); return Result.err(new Error('Checkout cancelled by user'));
} }
if (confirmation.isTimeout()) { if (confirmation.isTimeout()) {
this.logger.error('Checkout confirmation timeout');
return Result.err(new 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;
} }
} }

View File

@@ -1,5 +1,6 @@
import { Result } from '../../../shared/result/Result'; import { Result } from '../../../shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { ILogger } from '../../../shared/logger/ILogger';
/** /**
* Use case for initiating the manual login flow. * Use case for initiating the manual login flow.
@@ -9,7 +10,10 @@ import type { AuthenticationServicePort } from '../ports/AuthenticationServicePo
* indicating successful login. * indicating successful login.
*/ */
export class InitiateLoginUseCase { export class InitiateLoginUseCase {
constructor(private readonly authService: AuthenticationServicePort) {} constructor(
private readonly authService: AuthenticationServicePort,
private readonly logger: ILogger,
) {}
/** /**
* Execute the login flow. * Execute the login flow.
@@ -18,6 +22,18 @@ export class InitiateLoginUseCase {
* @returns Result indicating success (login complete) or failure (cancelled/timeout) * @returns Result indicating success (login complete) or failure (cancelled/timeout)
*/ */
async execute(): Promise<Result<void>> { async execute(): Promise<Result<void>> {
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.');
}
} }
} }

View File

@@ -1,4 +1,5 @@
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import { AutomationSession } from '../../domain/entities/AutomationSession'; import { AutomationSession } from '../../domain/entities/AutomationSession';
import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig'; import type { HostedSessionConfig } from '../../domain/types/HostedSessionConfig';
import { AutomationEnginePort } from '../ports/AutomationEnginePort'; import { AutomationEnginePort } from '../ports/AutomationEnginePort';
@@ -11,18 +12,26 @@ export class StartAutomationSessionUseCase
constructor( constructor(
private readonly automationEngine: AutomationEnginePort, private readonly automationEngine: AutomationEnginePort,
private readonly browserAutomation: IBrowserAutomation, private readonly browserAutomation: IBrowserAutomation,
private readonly sessionRepository: SessionRepositoryPort private readonly sessionRepository: SessionRepositoryPort,
private readonly logger: ILogger
) {} ) {}
async execute(config: HostedSessionConfig): Promise<SessionDTO> { async execute(config: HostedSessionConfig): Promise<SessionDTO> {
this.logger.debug('Starting automation session execution', { config });
const session = AutomationSession.create(config); const session = AutomationSession.create(config);
this.logger.info(`Automation session created with ID: ${session.id}`);
const validationResult = await this.automationEngine.validateConfiguration(config); const validationResult = await this.automationEngine.validateConfiguration(config);
if (!validationResult.isValid) { 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); throw new Error(validationResult.error);
} }
this.logger.debug('Automation session configuration validated successfully.');
await this.sessionRepository.save(session); await this.sessionRepository.save(session);
this.logger.info(`Automation session with ID: ${session.id} saved to repository.`);
const dto: SessionDTO = { const dto: SessionDTO = {
sessionId: session.id, sessionId: session.id,
@@ -34,6 +43,7 @@ export class StartAutomationSessionUseCase
...(session.errorMessage ? { errorMessage: session.errorMessage } : {}), ...(session.errorMessage ? { errorMessage: session.errorMessage } : {}),
}; };
this.logger.debug('Automation session executed successfully, returning DTO.', { dto });
return dto; return dto;
} }
} }

View File

@@ -1,6 +1,7 @@
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort'; import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { Result } from '../../../shared/result/Result'; import { Result } from '../../../shared/result/Result';
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState'; import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use case for verifying browser shows authenticated page state. * Use case for verifying browser shows authenticated page state.
@@ -8,22 +9,27 @@ import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAu
*/ */
export class VerifyAuthenticatedPageUseCase { export class VerifyAuthenticatedPageUseCase {
constructor( constructor(
private readonly authService: AuthenticationServicePort private readonly authService: AuthenticationServicePort,
private readonly logger: ILogger,
) {} ) {}
async execute(): Promise<Result<BrowserAuthenticationState>> { async execute(): Promise<Result<BrowserAuthenticationState>> {
this.logger.debug('Executing VerifyAuthenticatedPageUseCase');
try { try {
const result = await this.authService.verifyPageAuthentication(); const result = await this.authService.verifyPageAuthentication();
if (result.isErr()) { if (result.isErr()) {
const error = result.error ?? new Error('Page verification failed'); const error = result.error ?? new Error('Page verification failed');
this.logger.error(`Page verification failed: ${error.message}`, error);
return Result.err<BrowserAuthenticationState>(error); return Result.err<BrowserAuthenticationState>(error);
} }
const browserState = result.unwrap(); const browserState = result.unwrap();
this.logger.info('Successfully verified authenticated page state.');
return Result.ok<BrowserAuthenticationState>(browserState); return Result.ok<BrowserAuthenticationState>(browserState);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Page verification failed unexpectedly: ${message}`, error);
return Result.err<BrowserAuthenticationState>(new Error(`Page verification failed: ${message}`)); return Result.err<BrowserAuthenticationState>(new Error(`Page verification failed: ${message}`));
} }
} }

View File

@@ -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<void> {
// No-op for console logger as it's synchronous
return Promise.resolve();
}
}

View File

@@ -1,7 +1,8 @@
import type { LoggerPort } from '../../../application/ports/LoggerPort'; import type { LoggerPort } from '../../../application/ports/LoggerPort';
import type { LogContext } from '../../../application/ports/LoggerContext'; 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 {} debug(_message: string, _context?: LogContext): void {}
info(_message: string, _context?: LogContext): void {} info(_message: string, _context?: LogContext): void {}

View File

@@ -2,6 +2,7 @@ import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerP
import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext'; import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext';
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel'; import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig'; import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = { const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 10, debug: 10,
@@ -20,7 +21,7 @@ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
* *
* This provides structured JSON logging to stdout with the same interface. * 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 config: LoggingEnvironmentConfig;
private readonly baseContext: LogContext; private readonly baseContext: LogContext;
private readonly levelPriority: number; private readonly levelPriority: number;

View File

@@ -14,17 +14,22 @@ import {
} from '../../domain/entities/Achievement'; } from '../../domain/entities/Achievement';
import { UserAchievement } from '../../domain/entities/UserAchievement'; import { UserAchievement } from '../../domain/entities/UserAchievement';
import type { IAchievementRepository } from '../../domain/repositories/IAchievementRepository'; import type { IAchievementRepository } from '../../domain/repositories/IAchievementRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryAchievementRepository implements IAchievementRepository { export class InMemoryAchievementRepository implements IAchievementRepository {
private achievements: Map<string, Achievement> = new Map(); private achievements: Map<string, Achievement> = new Map();
private userAchievements: Map<string, UserAchievement> = new Map(); private userAchievements: Map<string, UserAchievement> = new Map();
private readonly logger: ILogger;
constructor() { constructor(logger: ILogger) {
this.logger = logger;
this.logger.info('InMemoryAchievementRepository initialized.');
// Seed with predefined achievements // Seed with predefined achievements
this.seedAchievements(); this.seedAchievements();
} }
private seedAchievements(): void { private seedAchievements(): void {
this.logger.debug('Seeding predefined achievements.');
const allAchievements = [ const allAchievements = [
...DRIVER_ACHIEVEMENTS, ...DRIVER_ACHIEVEMENTS,
...STEWARD_ACHIEVEMENTS, ...STEWARD_ACHIEVEMENTS,
@@ -35,136 +40,252 @@ export class InMemoryAchievementRepository implements IAchievementRepository {
for (const props of allAchievements) { for (const props of allAchievements) {
const achievement = Achievement.create(props); const achievement = Achievement.create(props);
this.achievements.set(achievement.id, achievement); 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 // Achievement operations
async findAchievementById(id: string): Promise<Achievement | null> { async findAchievementById(id: string): Promise<Achievement | null> {
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<Achievement[]> { async findAllAchievements(): Promise<Achievement[]> {
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<Achievement[]> { async findAchievementsByCategory(category: AchievementCategory): Promise<Achievement[]> {
return Array.from(this.achievements.values()) this.logger.debug(`Finding achievements by category: ${category}`);
.filter(a => a.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<Achievement> { async createAchievement(achievement: Achievement): Promise<Achievement> {
if (this.achievements.has(achievement.id)) { this.logger.debug(`Creating achievement: ${achievement.id}`);
throw new Error('Achievement with this ID already exists'); 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 // UserAchievement operations
async findUserAchievementById(id: string): Promise<UserAchievement | null> { async findUserAchievementById(id: string): Promise<UserAchievement | null> {
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<UserAchievement[]> { async findUserAchievementsByUserId(userId: string): Promise<UserAchievement[]> {
return Array.from(this.userAchievements.values()) this.logger.debug(`Finding user achievements by user id: ${userId}`);
.filter(ua => ua.userId === 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( async findUserAchievementByUserAndAchievement(
userId: string, userId: string,
achievementId: string achievementId: string
): Promise<UserAchievement | null> { ): Promise<UserAchievement | null> {
for (const ua of this.userAchievements.values()) { this.logger.debug(`Finding user achievement for user: ${userId}, achievement: ${achievementId}`);
if (ua.userId === userId && ua.achievementId === achievementId) { try {
return ua; 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<boolean> { async hasUserEarnedAchievement(userId: string, achievementId: string): Promise<boolean> {
const ua = await this.findUserAchievementByUserAndAchievement(userId, achievementId); this.logger.debug(`Checking if user ${userId} earned achievement ${achievementId}`);
return ua !== null && ua.isComplete(); 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<UserAchievement> { async createUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
if (this.userAchievements.has(userAchievement.id)) { this.logger.debug(`Creating user achievement: ${userAchievement.id}`);
throw new Error('UserAchievement with this ID already exists'); 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<UserAchievement> { async updateUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
if (!this.userAchievements.has(userAchievement.id)) { this.logger.debug(`Updating user achievement: ${userAchievement.id}`);
throw new Error('UserAchievement not found'); 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 // Stats
async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> { async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> {
const userStats = new Map<string, { points: number; count: number }>(); this.logger.debug(`Getting achievement leaderboard with limit: ${limit}`);
try {
const userStats = new Map<string, { points: number; count: number }>();
for (const ua of this.userAchievements.values()) { for (const ua of this.userAchievements.values()) {
if (!ua.isComplete()) continue; if (!ua.isComplete()) continue;
const achievement = this.achievements.get(ua.achievementId); const achievement = this.achievements.get(ua.achievementId);
if (!achievement) continue; if (!achievement) {
this.logger.warn(`Achievement ${ua.achievementId} not found while building leaderboard.`);
continue;
}
const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 }; const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 };
userStats.set(ua.userId, { userStats.set(ua.userId, {
points: existing.points + achievement.points, points: existing.points + achievement.points,
count: existing.count + 1, 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<{ async getUserAchievementStats(userId: string): Promise<{
total: number; total: number;
points: number; points: number;
byCategory: Record<AchievementCategory, number> byCategory: Record<AchievementCategory, number>
}> { }> {
const userAchievements = await this.findUserAchievementsByUserId(userId); this.logger.debug(`Getting achievement stats for user: ${userId}`);
const completedAchievements = userAchievements.filter(ua => ua.isComplete()); 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<AchievementCategory, number> = { const byCategory: Record<AchievementCategory, number> = {
driver: 0, driver: 0,
steward: 0, steward: 0,
admin: 0, admin: 0,
community: 0, community: 0,
}; };
let points = 0; let points = 0;
for (const ua of completedAchievements) { for (const ua of completedAchievements) {
const achievement = this.achievements.get(ua.achievementId); const achievement = this.achievements.get(ua.achievementId);
if (achievement) { if (achievement) {
points += achievement.points; points += achievement.points;
byCategory[achievement.category]++; byCategory[achievement.category]++;
} else {
this.logger.warn(`Achievement ${ua.achievementId} not found while calculating user stats for user ${userId}.`);
}
} }
}
return { const stats = {
total: completedAchievements.length, total: completedAchievements.length,
points, points,
byCategory, 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 // Test helpers
clearUserAchievements(): void { clearUserAchievements(): void {
this.logger.debug('Clearing all user achievements.');
this.userAchievements.clear(); this.userAchievements.clear();
this.logger.info('All user achievements cleared.');
} }
clear(): void { clear(): void {
this.logger.debug('Clearing all achievement data.');
this.achievements.clear(); this.achievements.clear();
this.userAchievements.clear(); this.userAchievements.clear();
this.logger.info('All achievement data cleared.');
} }
} }

View File

@@ -7,42 +7,117 @@
import type { ISponsorAccountRepository } from '../../domain/repositories/ISponsorAccountRepository'; import type { ISponsorAccountRepository } from '../../domain/repositories/ISponsorAccountRepository';
import type { SponsorAccount } from '../../domain/entities/SponsorAccount'; import type { SponsorAccount } from '../../domain/entities/SponsorAccount';
import type { UserId } from '../../domain/value-objects/UserId'; import type { UserId } from '../../domain/value-objects/UserId';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemorySponsorAccountRepository implements ISponsorAccountRepository { export class InMemorySponsorAccountRepository implements ISponsorAccountRepository {
private accounts: Map<string, SponsorAccount> = new Map(); private accounts: Map<string, SponsorAccount> = 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<void> { async save(account: SponsorAccount): Promise<void> {
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<SponsorAccount | null> { async findById(id: UserId): Promise<SponsorAccount | null> {
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<SponsorAccount | null> { async findBySponsorId(sponsorId: string): Promise<SponsorAccount | null> {
return Array.from(this.accounts.values()).find( this.logger.debug(`Finding sponsor account by sponsor id: ${sponsorId}`);
a => a.getSponsorId() === sponsorId try {
) ?? null; 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<SponsorAccount | null> { async findByEmail(email: string): Promise<SponsorAccount | null> {
const normalizedEmail = email.toLowerCase().trim(); this.logger.debug(`Finding sponsor account by email: ${email}`);
return Array.from(this.accounts.values()).find( try {
a => a.getEmail().toLowerCase() === normalizedEmail const normalizedEmail = email.toLowerCase().trim();
) ?? null; 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<void> { async delete(id: UserId): Promise<void> {
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 // Helper for testing
clear(): void { clear(): void {
this.logger.debug('Clearing all sponsor accounts.');
this.accounts.clear(); this.accounts.clear();
this.logger.info('All sponsor accounts cleared.');
} }
// Helper for seeding demo data // Helper for seeding demo data
seed(accounts: SponsorAccount[]): void { 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;
}
} }
} }

View File

@@ -6,60 +6,148 @@
import { UserRating } from '../../domain/value-objects/UserRating'; import { UserRating } from '../../domain/value-objects/UserRating';
import type { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository'; import type { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryUserRatingRepository implements IUserRatingRepository { export class InMemoryUserRatingRepository implements IUserRatingRepository {
private ratings: Map<string, UserRating> = new Map(); private ratings: Map<string, UserRating> = 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<UserRating | null> { async findByUserId(userId: string): Promise<UserRating | null> {
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<UserRating[]> { async findByUserIds(userIds: string[]): Promise<UserRating[]> {
const results: UserRating[] = []; this.logger.debug(`Finding user ratings for user ids: ${userIds.join(', ')}`);
for (const userId of userIds) { try {
const rating = this.ratings.get(userId); const results: UserRating[] = [];
if (rating) { for (const userId of userIds) {
results.push(rating); 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<UserRating> { async save(rating: UserRating): Promise<UserRating> {
this.ratings.set(rating.userId, rating); this.logger.debug(`Saving user rating for user id: ${rating.userId}`);
return rating; 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<UserRating[]> { async getTopDrivers(limit: number): Promise<UserRating[]> {
return Array.from(this.ratings.values()) this.logger.debug(`Getting top ${limit} drivers.`);
.filter(r => r.driver.sampleSize > 0) try {
.sort((a, b) => b.driver.value - a.driver.value) const topDrivers = Array.from(this.ratings.values())
.slice(0, limit); .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<UserRating[]> { async getTopTrusted(limit: number): Promise<UserRating[]> {
return Array.from(this.ratings.values()) this.logger.debug(`Getting top ${limit} trusted users.`);
.filter(r => r.trust.sampleSize > 0) try {
.sort((a, b) => b.trust.value - a.trust.value) const topTrusted = Array.from(this.ratings.values())
.slice(0, limit); .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<UserRating[]> { async getEligibleStewards(): Promise<UserRating[]> {
return Array.from(this.ratings.values()) this.logger.debug('Getting eligible stewards.');
.filter(r => r.canBeSteward()); 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<UserRating[]> { async findByDriverTier(tier: 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite'): Promise<UserRating[]> {
return Array.from(this.ratings.values()) this.logger.debug(`Finding user ratings by driver tier: ${tier}`);
.filter(r => r.getDriverTier() === 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<void> { async delete(userId: string): Promise<void> {
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 // Test helper
clear(): void { clear(): void {
this.logger.debug('Clearing all user ratings.');
this.ratings.clear(); this.ratings.clear();
this.logger.info('All user ratings cleared.');
} }
} }

View File

@@ -5,52 +5,113 @@
*/ */
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryUserRepository implements IUserRepository { export class InMemoryUserRepository implements IUserRepository {
private users: Map<string, StoredUser> = new Map(); private users: Map<string, StoredUser> = new Map();
private emailIndex: Map<string, string> = new Map(); // email -> userId private emailIndex: Map<string, string> = 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) { for (const user of initialUsers) {
this.users.set(user.id, user); this.users.set(user.id, user);
this.emailIndex.set(user.email.toLowerCase(), user.id); this.emailIndex.set(user.email.toLowerCase(), user.id);
this.logger.debug(`Seeded user: ${user.id} (${user.email}).`);
} }
} }
async findByEmail(email: string): Promise<StoredUser | null> { async findByEmail(email: string): Promise<StoredUser | null> {
const userId = this.emailIndex.get(email.toLowerCase()); this.logger.debug(`Finding user by email: ${email}`);
if (!userId) return null; try {
return this.users.get(userId) ?? null; 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<StoredUser | null> { async findById(id: string): Promise<StoredUser | null> {
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<StoredUser> { async create(user: StoredUser): Promise<StoredUser> {
if (this.emailIndex.has(user.email.toLowerCase())) { this.logger.debug(`Creating user: ${user.id} with email: ${user.email}`);
throw new Error('Email already exists'); 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<StoredUser> { async update(user: StoredUser): Promise<StoredUser> {
const existing = this.users.get(user.id); this.logger.debug(`Updating user: ${user.id} with email: ${user.email}`);
if (!existing) { try {
throw new Error('User not found'); 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<boolean> { async emailExists(email: string): Promise<boolean> {
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;
}
} }
} }

View File

@@ -1,11 +1,4 @@
/** import type { AsyncUseCase, ILogger } from '@gridpilot/shared/application';
* 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 { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository'; import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
import type { FaceValidationPort } from '../ports/FaceValidationPort'; import type { FaceValidationPort } from '../ports/FaceValidationPort';
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort'; import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
@@ -32,89 +25,126 @@ export class RequestAvatarGenerationUseCase
private readonly avatarRepository: IAvatarGenerationRepository, private readonly avatarRepository: IAvatarGenerationRepository,
private readonly faceValidation: FaceValidationPort, private readonly faceValidation: FaceValidationPort,
private readonly avatarGeneration: AvatarGenerationPort, private readonly avatarGeneration: AvatarGenerationPort,
private readonly logger: ILogger,
) {} ) {}
async execute(command: RequestAvatarGenerationCommand): Promise<RequestAvatarGenerationResult> { async execute(command: RequestAvatarGenerationCommand): Promise<RequestAvatarGenerationResult> {
// Create the generation request this.logger.debug(
const requestId = this.generateId(); `Executing RequestAvatarGenerationUseCase for userId: ${command.userId}`,
const request = AvatarGenerationRequest.create({ command,
id: requestId, );
userId: command.userId,
facePhotoUrl: `data:image/jpeg;base64,${command.facePhotoData}`,
suitColor: command.suitColor,
...(command.style ? { style: command.style } : {}),
});
// Mark as validating try {
request.markAsValidating(); // Create the generation request
await this.avatarRepository.save(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 this.logger.info(`Avatar generation request created with ID: ${requestId}`);
const validationResult = await this.faceValidation.validateFacePhoto(command.facePhotoData);
// Mark as validating
if (!validationResult.isValid) { request.markAsValidating();
request.fail(validationResult.errorMessage || 'Face validation failed');
await this.avatarRepository.save(request); 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 { return {
requestId, requestId,
status: 'failed', status: 'completed',
errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face', 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 { private generateId(): string {

View File

@@ -4,7 +4,7 @@
* Allows a user to select one of the generated avatars as their profile avatar. * 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'; import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
export interface SelectAvatarCommand { export interface SelectAvatarCommand {
@@ -23,12 +23,16 @@ export class SelectAvatarUseCase
implements AsyncUseCase<SelectAvatarCommand, SelectAvatarResult> { implements AsyncUseCase<SelectAvatarCommand, SelectAvatarResult> {
constructor( constructor(
private readonly avatarRepository: IAvatarGenerationRepository, private readonly avatarRepository: IAvatarGenerationRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: SelectAvatarCommand): Promise<SelectAvatarResult> { async execute(command: SelectAvatarCommand): Promise<SelectAvatarResult> {
this.logger.debug(`Executing SelectAvatarUseCase for userId: ${command.userId}, requestId: ${command.requestId}, avatarIndex: ${command.avatarIndex}`);
const request = await this.avatarRepository.findById(command.requestId); const request = await this.avatarRepository.findById(command.requestId);
if (!request) { if (!request) {
this.logger.info(`Avatar generation request not found for requestId: ${command.requestId}`);
return { return {
success: false, success: false,
errorMessage: 'Avatar generation request not found', errorMessage: 'Avatar generation request not found',
@@ -36,6 +40,7 @@ export class SelectAvatarUseCase
} }
if (request.userId !== command.userId) { if (request.userId !== command.userId) {
this.logger.info(`Permission denied for userId: ${command.userId} to select avatar for requestId: ${command.requestId}`);
return { return {
success: false, success: false,
errorMessage: 'You do not have permission to select this avatar', errorMessage: 'You do not have permission to select this avatar',
@@ -43,6 +48,7 @@ export class SelectAvatarUseCase
} }
if (request.status !== 'completed') { if (request.status !== 'completed') {
this.logger.info(`Avatar generation not completed for requestId: ${command.requestId}, current status: ${request.status}`);
return { return {
success: false, success: false,
errorMessage: 'Avatar generation is not yet complete', errorMessage: 'Avatar generation is not yet complete',
@@ -59,8 +65,10 @@ export class SelectAvatarUseCase
? { success: true, selectedAvatarUrl } ? { success: true, selectedAvatarUrl }
: { success: true }; : { success: true };
this.logger.info(`Avatar selected successfully for userId: ${command.userId}, requestId: ${command.requestId}, selectedAvatarUrl: ${selectedAvatarUrl}`);
return result; return result;
} catch (error) { } 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 { return {
success: false, success: false,
errorMessage: error instanceof Error ? error.message : 'Failed to select avatar', errorMessage: error instanceof Error ? error.message : 'Failed to select avatar',

View File

@@ -5,6 +5,7 @@
*/ */
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { Notification } from '../../domain/entities/Notification'; import type { Notification } from '../../domain/entities/Notification';
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
@@ -16,15 +17,27 @@ export interface UnreadNotificationsResult {
export class GetUnreadNotificationsUseCase implements AsyncUseCase<string, UnreadNotificationsResult> { export class GetUnreadNotificationsUseCase implements AsyncUseCase<string, UnreadNotificationsResult> {
constructor( constructor(
private readonly notificationRepository: INotificationRepository, private readonly notificationRepository: INotificationRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(recipientId: string): Promise<UnreadNotificationsResult> { async execute(recipientId: string): Promise<UnreadNotificationsResult> {
const notifications = await this.notificationRepository.findUnreadByRecipientId(recipientId); this.logger.debug(`Attempting to retrieve unread notifications for recipient ID: ${recipientId}`);
try {
return { const notifications = await this.notificationRepository.findUnreadByRecipientId(recipientId);
notifications, this.logger.info(`Successfully retrieved ${notifications.length} unread notifications for recipient ID: ${recipientId}`);
totalCount: notifications.length,
}; 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;
}
} }
} }

View File

@@ -7,6 +7,7 @@
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError'; import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface MarkNotificationReadCommand { export interface MarkNotificationReadCommand {
notificationId: string; notificationId: string;
@@ -16,25 +17,36 @@ export interface MarkNotificationReadCommand {
export class MarkNotificationReadUseCase implements AsyncUseCase<MarkNotificationReadCommand, void> { export class MarkNotificationReadUseCase implements AsyncUseCase<MarkNotificationReadCommand, void> {
constructor( constructor(
private readonly notificationRepository: INotificationRepository, private readonly notificationRepository: INotificationRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: MarkNotificationReadCommand): Promise<void> { async execute(command: MarkNotificationReadCommand): Promise<void> {
const notification = await this.notificationRepository.findById(command.notificationId); this.logger.debug(`Attempting to mark notification ${command.notificationId} as read for recipient ${command.recipientId}`);
try {
if (!notification) { const notification = await this.notificationRepository.findById(command.notificationId);
throw new NotificationDomainError('Notification not found');
} if (!notification) {
this.logger.warn(`Notification not found for ID: ${command.notificationId}`);
throw new NotificationDomainError('Notification not found');
}
if (notification.recipientId !== command.recipientId) { if (notification.recipientId !== command.recipientId) {
throw new NotificationDomainError('Cannot mark another user\'s notification as read'); 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()) { if (!notification.isUnread()) {
return; // Already read, nothing to do this.logger.info(`Notification ${command.notificationId} is already read. Skipping update.`);
} return; // Already read, nothing to do
}
const updatedNotification = notification.markAsRead(); const updatedNotification = notification.markAsRead();
await this.notificationRepository.update(updatedNotification); 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;
}
} }
} }

View File

@@ -5,6 +5,7 @@
*/ */
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
import { NotificationPreference } from '../../domain/entities/NotificationPreference'; import { NotificationPreference } from '../../domain/entities/NotificationPreference';
import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference'; import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository'; import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
@@ -17,10 +18,19 @@ import { NotificationDomainError } from '../../domain/errors/NotificationDomainE
export class GetNotificationPreferencesQuery implements AsyncUseCase<string, NotificationPreference> { export class GetNotificationPreferencesQuery implements AsyncUseCase<string, NotificationPreference> {
constructor( constructor(
private readonly preferenceRepository: INotificationPreferenceRepository, private readonly preferenceRepository: INotificationPreferenceRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(driverId: string): Promise<NotificationPreference> { async execute(driverId: string): Promise<NotificationPreference> {
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<UpdateChannelPreferenceCommand, void> { export class UpdateChannelPreferenceUseCase implements AsyncUseCase<UpdateChannelPreferenceCommand, void> {
constructor( constructor(
private readonly preferenceRepository: INotificationPreferenceRepository, private readonly preferenceRepository: INotificationPreferenceRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: UpdateChannelPreferenceCommand): Promise<void> { async execute(command: UpdateChannelPreferenceCommand): Promise<void> {
const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); this.logger.debug(`Updating channel preference for driver: ${command.driverId}, channel: ${command.channel}, preference: ${command.preference}`);
const updated = preferences.updateChannel(command.channel, command.preference); try {
await this.preferenceRepository.save(updated); 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<UpdateTypePreferenceCommand, void> { export class UpdateTypePreferenceUseCase implements AsyncUseCase<UpdateTypePreferenceCommand, void> {
constructor( constructor(
private readonly preferenceRepository: INotificationPreferenceRepository, private readonly preferenceRepository: INotificationPreferenceRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: UpdateTypePreferenceCommand): Promise<void> { async execute(command: UpdateTypePreferenceCommand): Promise<void> {
const preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId); this.logger.debug(`Updating type preference for driver: ${command.driverId}, type: ${command.type}, preference: ${command.preference}`);
const updated = preferences.updateTypePreference(command.type, command.preference); try {
await this.preferenceRepository.save(updated); 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<UpdateQuietHoursCommand, void> { export class UpdateQuietHoursUseCase implements AsyncUseCase<UpdateQuietHoursCommand, void> {
constructor( constructor(
private readonly preferenceRepository: INotificationPreferenceRepository, private readonly preferenceRepository: INotificationPreferenceRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: UpdateQuietHoursCommand): Promise<void> { async execute(command: UpdateQuietHoursCommand): Promise<void> {
// Validate hours if provided this.logger.debug(`Updating quiet hours for driver: ${command.driverId}, startHour: ${command.startHour}, endHour: ${command.endHour}`);
if (command.startHour !== undefined && (command.startHour < 0 || command.startHour > 23)) { try {
throw new NotificationDomainError('Start hour must be between 0 and 23'); // Validate hours if provided
} if (command.startHour !== undefined && (command.startHour < 0 || command.startHour > 23)) {
if (command.endHour !== undefined && (command.endHour < 0 || command.endHour > 23)) { this.logger.warn(`Invalid start hour provided for driver: ${command.driverId}. startHour: ${command.startHour}`);
throw new NotificationDomainError('End hour must be between 0 and 23'); 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 preferences = await this.preferenceRepository.getOrCreateDefault(command.driverId);
const updated = preferences.updateQuietHours(command.startHour, command.endHour); const updated = preferences.updateQuietHours(command.startHour, command.endHour);
await this.preferenceRepository.save(updated); 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;
}
} }
} }

View File

@@ -7,6 +7,7 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import { Notification } from '../../domain/entities/Notification'; import { Notification } from '../../domain/entities/Notification';
import type { NotificationData } from '../../domain/entities/Notification'; import type { NotificationData } from '../../domain/entities/Notification';
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
@@ -48,11 +49,17 @@ export class SendNotificationUseCase implements AsyncUseCase<SendNotificationCom
private readonly notificationRepository: INotificationRepository, private readonly notificationRepository: INotificationRepository,
private readonly preferenceRepository: INotificationPreferenceRepository, private readonly preferenceRepository: INotificationPreferenceRepository,
private readonly gatewayRegistry: INotificationGatewayRegistry, private readonly gatewayRegistry: INotificationGatewayRegistry,
) {} private readonly logger: ILogger,
) {
this.logger.debug('SendNotificationUseCase initialized.');
}
async execute(command: SendNotificationCommand): Promise<SendNotificationResult> { async execute(command: SendNotificationCommand): Promise<SendNotificationResult> {
// Get recipient's preferences this.logger.debug('Executing SendNotificationUseCase', { command });
const preferences = await this.preferenceRepository.getOrCreateDefault(command.recipientId); 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 // Check if this notification type is enabled
if (!preferences.isTypeEnabled(command.type)) { if (!preferences.isTypeEnabled(command.type)) {

View File

@@ -6,36 +6,79 @@
import { NotificationPreference } from '../../domain/entities/NotificationPreference'; import { NotificationPreference } from '../../domain/entities/NotificationPreference';
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository'; import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryNotificationPreferenceRepository implements INotificationPreferenceRepository { export class InMemoryNotificationPreferenceRepository implements INotificationPreferenceRepository {
private preferences: Map<string, NotificationPreference> = new Map(); private preferences: Map<string, NotificationPreference> = 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 => { initialPreferences.forEach(pref => {
this.preferences.set(pref.driverId, pref); this.preferences.set(pref.driverId, pref);
this.logger.debug(`Seeded preference for driver: ${pref.driverId}`);
}); });
} }
async findByDriverId(driverId: string): Promise<NotificationPreference | null> { async findByDriverId(driverId: string): Promise<NotificationPreference | null> {
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<void> { async save(preference: NotificationPreference): Promise<void> {
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<void> { async delete(driverId: string): Promise<void> {
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<NotificationPreference> { async getOrCreateDefault(driverId: string): Promise<NotificationPreference> {
const existing = this.preferences.get(driverId); this.logger.debug(`Getting or creating default notification preference for driver: ${driverId}`);
if (existing) { try {
return existing; 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.logger.info(`Creating default preference for driver: ${driverId}.`);
this.preferences.set(driverId, defaultPreference); const defaultPreference = NotificationPreference.createDefault(driverId);
return defaultPreference; 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;
}
} }
} }

View File

@@ -7,77 +7,169 @@
import { Notification } from '../../domain/entities/Notification'; import { Notification } from '../../domain/entities/Notification';
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository'; import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
import type { NotificationType } from '../../domain/types/NotificationTypes'; import type { NotificationType } from '../../domain/types/NotificationTypes';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryNotificationRepository implements INotificationRepository { export class InMemoryNotificationRepository implements INotificationRepository {
private notifications: Map<string, Notification> = new Map(); private notifications: Map<string, Notification> = 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 => { initialNotifications.forEach(notification => {
this.notifications.set(notification.id, notification); this.notifications.set(notification.id, notification);
this.logger.debug(`Seeded notification: ${notification.id}`);
}); });
} }
async findById(id: string): Promise<Notification | null> { async findById(id: string): Promise<Notification | null> {
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<Notification[]> { async findByRecipientId(recipientId: string): Promise<Notification[]> {
return Array.from(this.notifications.values()) this.logger.debug(`Finding notifications for recipient ID: ${recipientId}`);
.filter(n => n.recipientId === recipientId) try {
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); 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<Notification[]> { async findUnreadByRecipientId(recipientId: string): Promise<Notification[]> {
return Array.from(this.notifications.values()) this.logger.debug(`Finding unread notifications for recipient ID: ${recipientId}`);
.filter(n => n.recipientId === recipientId && n.isUnread()) try {
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); 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<Notification[]> { async findByRecipientIdAndType(recipientId: string, type: NotificationType): Promise<Notification[]> {
return Array.from(this.notifications.values()) this.logger.debug(`Finding notifications for recipient ID: ${recipientId}, type: ${type}`);
.filter(n => n.recipientId === recipientId && n.type === type) try {
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); 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<number> { async countUnreadByRecipientId(recipientId: string): Promise<number> {
return Array.from(this.notifications.values()) this.logger.debug(`Counting unread notifications for recipient ID: ${recipientId}`);
.filter(n => n.recipientId === recipientId && n.isUnread()) try {
.length; 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<void> { async create(notification: Notification): Promise<void> {
if (this.notifications.has(notification.id)) { this.logger.debug(`Creating notification: ${notification.id}`);
throw new Error(`Notification with ID ${notification.id} already exists`); 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<void> { async update(notification: Notification): Promise<void> {
if (!this.notifications.has(notification.id)) { this.logger.debug(`Updating notification: ${notification.id}`);
throw new Error(`Notification with ID ${notification.id} not found`); 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<void> { async delete(id: string): Promise<void> {
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<void> { async deleteAllByRecipientId(recipientId: string): Promise<void> {
const toDelete = Array.from(this.notifications.values()) this.logger.debug(`Deleting all notifications for recipient ID: ${recipientId}`);
.filter(n => n.recipientId === recipientId) try {
.map(n => n.id); const initialCount = this.notifications.size;
const toDelete = Array.from(this.notifications.values())
toDelete.forEach(id => this.notifications.delete(id)); .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<void> { async markAllAsReadByRecipientId(recipientId: string): Promise<void> {
const toUpdate = Array.from(this.notifications.values()) this.logger.debug(`Marking all notifications as read for recipient ID: ${recipientId}`);
.filter(n => n.recipientId === recipientId && n.isUnread()); try {
const toUpdate = Array.from(this.notifications.values())
toUpdate.forEach(n => { .filter(n => n.recipientId === recipientId && n.isUnread());
const updated = n.markAsRead();
this.notifications.set(updated.id, updated); 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;
}
} }
} }

View File

@@ -5,6 +5,7 @@
* This creates an active sponsorship and notifies the sponsor. * 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 { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository'; import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository'; import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
@@ -31,56 +32,73 @@ export class AcceptSponsorshipRequestUseCase
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository, private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly seasonRepository: ISeasonRepository, private readonly seasonRepository: ISeasonRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(dto: AcceptSponsorshipRequestDTO): Promise<AcceptSponsorshipRequestResultDTO> { async execute(dto: AcceptSponsorshipRequestDTO): Promise<AcceptSponsorshipRequestResultDTO> {
// Find the request this.logger.debug(`Attempting to accept sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, respondedBy: dto.respondedBy });
const request = await this.sponsorshipRequestRepo.findById(dto.requestId); try {
if (!request) { // Find the request
throw new Error('Sponsorship request not found'); const request = await this.sponsorshipRequestRepo.findById(dto.requestId);
} if (!request) {
this.logger.warn(`Sponsorship request not found: ${dto.requestId}`, { requestId: dto.requestId });
if (!request.isPending()) { throw new Error('Sponsorship request not found');
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');
} }
const sponsorship = SeasonSponsorship.create({ if (!request.isPending()) {
id: sponsorshipId, this.logger.warn(`Cannot accept a ${request.status} sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, status: request.status });
seasonId: season.id, throw new Error(`Cannot accept a ${request.status} sponsorship request`);
leagueId: season.leagueId, }
sponsorId: request.sponsorId,
tier: request.tier, this.logger.info(`Sponsorship request ${dto.requestId} found and is pending. Proceeding with acceptance.`, { requestId: dto.requestId });
pricing: request.offeredAmount,
status: 'active', // Accept the request
}); const acceptedRequest = request.accept(dto.respondedBy);
await this.seasonSponsorshipRepo.create(sponsorship); 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,
};
} }
} }

View File

@@ -16,6 +16,7 @@ import {
EntityNotFoundError, EntityNotFoundError,
BusinessRuleViolationError, BusinessRuleViolationError,
} from '../errors/RacingApplicationError'; } from '../errors/RacingApplicationError';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface ApplyForSponsorshipDTO { export interface ApplyForSponsorshipDTO {
sponsorId: string; sponsorId: string;
@@ -40,22 +41,28 @@ export class ApplyForSponsorshipUseCase
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository, private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
private readonly sponsorRepo: ISponsorRepository, private readonly sponsorRepo: ISponsorRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(dto: ApplyForSponsorshipDTO): Promise<ApplyForSponsorshipResultDTO> { async execute(dto: ApplyForSponsorshipDTO): Promise<ApplyForSponsorshipResultDTO> {
this.logger.debug('Attempting to apply for sponsorship', { dto });
// Validate sponsor exists // Validate sponsor exists
const sponsor = await this.sponsorRepo.findById(dto.sponsorId); const sponsor = await this.sponsorRepo.findById(dto.sponsorId);
if (!sponsor) { if (!sponsor) {
this.logger.error('Sponsor not found', { sponsorId: dto.sponsorId });
throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId }); throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId });
} }
// Check if entity accepts sponsorship applications // Check if entity accepts sponsorship applications
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId); const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
if (!pricing) { 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'); throw new BusinessRuleViolationError('This entity has not set up sponsorship pricing');
} }
if (!pricing.acceptingApplications) { if (!pricing.acceptingApplications) {
this.logger.warn('Entity not accepting sponsorship applications', { entityType: dto.entityType, entityId: dto.entityId });
throw new BusinessRuleViolationError( throw new BusinessRuleViolationError(
'This entity is not currently accepting sponsorship applications', 'This entity is not currently accepting sponsorship applications',
); );
@@ -64,6 +71,7 @@ export class ApplyForSponsorshipUseCase
// Check if the requested tier slot is available // Check if the requested tier slot is available
const slotAvailable = pricing.isSlotAvailable(dto.tier); const slotAvailable = pricing.isSlotAvailable(dto.tier);
if (!slotAvailable) { if (!slotAvailable) {
this.logger.warn(`No ${dto.tier} sponsorship slots are available for entity ${dto.entityId}`);
throw new BusinessRuleViolationError( throw new BusinessRuleViolationError(
`No ${dto.tier} sponsorship slots are available`, `No ${dto.tier} sponsorship slots are available`,
); );
@@ -76,6 +84,7 @@ export class ApplyForSponsorshipUseCase
dto.entityId, dto.entityId,
); );
if (hasPending) { 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( throw new BusinessRuleViolationError(
'You already have a pending sponsorship request for this entity', 'You already have a pending sponsorship request for this entity',
); );
@@ -84,6 +93,7 @@ export class ApplyForSponsorshipUseCase
// Validate offered amount meets minimum price // Validate offered amount meets minimum price
const minPrice = pricing.getPrice(dto.tier); const minPrice = pricing.getPrice(dto.tier);
if (minPrice && dto.offeredAmount < minPrice.amount) { 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( throw new BusinessRuleViolationError(
`Offered amount must be at least ${minPrice.format()}`, `Offered amount must be at least ${minPrice.format()}`,
); );

View File

@@ -12,6 +12,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface ApplyPenaltyCommand { export interface ApplyPenaltyCommand {
raceId: string; raceId: string;
@@ -31,57 +32,73 @@ export class ApplyPenaltyUseCase
private readonly protestRepository: IProtestRepository, private readonly protestRepository: IProtestRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: ApplyPenaltyCommand): Promise<{ penaltyId: string }> { async execute(command: ApplyPenaltyCommand): Promise<{ penaltyId: string }> {
// Validate race exists this.logger.debug('ApplyPenaltyUseCase: Executing with command', command);
const race = await this.raceRepository.findById(command.raceId); try {
if (!race) { // Validate race exists
throw new Error('Race not found'); const race = await this.raceRepository.findById(command.raceId);
} if (!race) {
this.logger.warn(`ApplyPenaltyUseCase: Race with ID ${command.raceId} not found.`);
// Validate steward has authority (owner or admin of the league) throw new Error('Race not found');
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');
} }
if (protest.status !== 'upheld') { this.logger.debug(`ApplyPenaltyUseCase: Race ${race.id} found.`);
throw new Error('Can only create penalties for upheld protests');
// 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) { this.logger.debug(`ApplyPenaltyUseCase: Steward ${command.stewardId} has authority.`);
throw new Error('Protest is not for this race');
// 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 };
} }
} }

View File

@@ -1,3 +1,4 @@
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository'; import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { import type {
TeamMembership, TeamMembership,
@@ -12,24 +13,31 @@ export class ApproveTeamJoinRequestUseCase
implements AsyncUseCase<ApproveTeamJoinRequestCommandDTO, void> { implements AsyncUseCase<ApproveTeamJoinRequestCommandDTO, void> {
constructor( constructor(
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: ApproveTeamJoinRequestCommandDTO): Promise<void> { async execute(command: ApproveTeamJoinRequestCommandDTO): Promise<void> {
const { requestId } = command; 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, // There is no repository method to look up a single request by ID,
// so we rely on the repository implementation to surface all relevant try {
// requests via getJoinRequests and search by ID here. // There is no repository method to look up a single request by ID,
const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests( // so we rely on the repository implementation to surface all relevant
// For the in-memory fake used in tests, the teamId argument is ignored // requests via getJoinRequests and search by ID here.
// and all requests are returned. const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests(
'' as string, // For the in-memory fake used in tests, the teamId argument is ignored
); // and all requests are returned.'
const request = allRequests.find((r) => r.id === requestId); '' as string,
);
const request = allRequests.find((r) => r.id === requestId);
if (!request) { if (!request) {
throw new Error('Join request not found'); this.logger.warn(`Team join request with ID ${requestId} not found`);
} throw new Error('Join request not found');
}
const membership: TeamMembership = { const membership: TeamMembership = {
teamId: request.teamId, teamId: request.teamId,
@@ -40,6 +48,14 @@ export class ApproveTeamJoinRequestUseCase
}; };
await this.membershipRepository.saveMembership(membership); 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); 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;
}
} }
} }

View File

@@ -1,5 +1,6 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use Case: CancelRaceUseCase * Use Case: CancelRaceUseCase
@@ -18,17 +19,26 @@ export class CancelRaceUseCase
implements AsyncUseCase<CancelRaceCommandDTO, void> { implements AsyncUseCase<CancelRaceCommandDTO, void> {
constructor( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: CancelRaceCommandDTO): Promise<void> { async execute(command: CancelRaceCommandDTO): Promise<void> {
const { raceId } = command; const { raceId } = command;
this.logger.debug(`[CancelRaceUseCase] Executing for raceId: ${raceId}`);
const race = await this.raceRepository.findById(raceId); try {
if (!race) { const race = await this.raceRepository.findById(raceId);
throw new Error('Race not found'); 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);
} }
} }

View File

@@ -6,6 +6,7 @@ import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import { Result } from '../../domain/entities/Result'; import { Result } from '../../domain/entities/Result';
import { Standing } from '../../domain/entities/Standing'; import { Standing } from '../../domain/entities/Standing';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use Case: CompleteRaceUseCase * Use Case: CompleteRaceUseCase
@@ -30,39 +31,55 @@ export class CompleteRaceUseCase
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly standingRepository: IStandingRepository, private readonly standingRepository: IStandingRepository,
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
private readonly logger: ILogger,
) {} ) {}
async execute(command: CompleteRaceCommandDTO): Promise<void> { async execute(command: CompleteRaceCommandDTO): Promise<void> {
this.logger.debug(`Executing CompleteRaceUseCase for raceId: ${command.raceId}`);
const { raceId } = command; const { raceId } = command;
const race = await this.raceRepository.findById(raceId); try {
if (!race) { const race = await this.raceRepository.findById(raceId);
throw new Error('Race not found'); 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( private generateRaceResults(
@@ -70,6 +87,7 @@ export class CompleteRaceUseCase
driverIds: string[], driverIds: string[],
driverRatings: Map<string, number> driverRatings: Map<string, number>
): Result[] { ): Result[] {
this.logger.debug(`Generating race results for race ${raceId} with ${driverIds.length} drivers.`);
// Create driver performance data // Create driver performance data
const driverPerformances = driverIds.map(driverId => ({ const driverPerformances = driverIds.map(driverId => ({
driverId, driverId,
@@ -83,6 +101,7 @@ export class CompleteRaceUseCase
const perfB = b.rating + (b.randomFactor * 200); const perfB = b.rating + (b.randomFactor * 200);
return perfB - perfA; // Higher performance first 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) // Generate qualifying results for start positions (similar but different from race results)
const qualiPerformances = driverPerformances.map(p => ({ const qualiPerformances = driverPerformances.map(p => ({
@@ -94,6 +113,7 @@ export class CompleteRaceUseCase
const perfB = b.rating + (b.randomFactor * 150); const perfB = b.rating + (b.randomFactor * 150);
return perfB - perfA; return perfB - perfA;
}); });
this.logger.debug(`Qualifying performances generated for race ${raceId}.`);
// Generate results // Generate results
const results: Result[] = []; const results: Result[] = [];
@@ -123,11 +143,13 @@ export class CompleteRaceUseCase
}) })
); );
} }
this.logger.debug(`Individual results created for race ${raceId}.`);
return results; return results;
} }
private async updateStandings(leagueId: string, results: Result[]): Promise<void> { private async updateStandings(leagueId: string, results: Result[]): Promise<void> {
this.logger.debug(`Updating standings for league ${leagueId} with ${results.length} results.`);
// Group results by driver // Group results by driver
const resultsByDriver = new Map<string, Result[]>(); const resultsByDriver = new Map<string, Result[]>();
for (const result of results) { for (const result of results) {
@@ -135,6 +157,7 @@ export class CompleteRaceUseCase
existing.push(result); existing.push(result);
resultsByDriver.set(result.driverId, existing); resultsByDriver.set(result.driverId, existing);
} }
this.logger.debug(`Results grouped by driver for league ${leagueId}.`);
// Update or create standings for each driver // Update or create standings for each driver
for (const [driverId, driverResults] of resultsByDriver) { for (const [driverId, driverResults] of resultsByDriver) {
@@ -145,6 +168,9 @@ export class CompleteRaceUseCase
leagueId, leagueId,
driverId, 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) // 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); 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}.`);
} }
} }

View File

@@ -8,6 +8,7 @@ import { Standing } from '../../domain/entities/Standing';
import { RaceResultGenerator } from '../utils/RaceResultGenerator'; import { RaceResultGenerator } from '../utils/RaceResultGenerator';
import { RatingUpdateService } from '@gridpilot/identity/domain/services/RatingUpdateService'; import { RatingUpdateService } from '@gridpilot/identity/domain/services/RatingUpdateService';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Enhanced CompleteRaceUseCase that includes rating updates * Enhanced CompleteRaceUseCase that includes rating updates
@@ -25,42 +26,65 @@ export class CompleteRaceUseCaseWithRatings
private readonly standingRepository: IStandingRepository, private readonly standingRepository: IStandingRepository,
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
private readonly ratingUpdateService: RatingUpdateService, private readonly ratingUpdateService: RatingUpdateService,
private readonly logger: ILogger,
) {} ) {}
async execute(command: CompleteRaceCommandDTO): Promise<void> { async execute(command: CompleteRaceCommandDTO): Promise<void> {
const { raceId } = command; const { raceId } = command;
this.logger.debug(`Attempting to complete race with ID: ${raceId}`);
const race = await this.raceRepository.findById(raceId); try {
if (!race) { const race = await this.raceRepository.findById(raceId);
throw new Error('Race not found'); 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<void> { private async updateStandings(leagueId: string, results: Result[]): Promise<void> {
@@ -105,4 +129,4 @@ export class CompleteRaceUseCaseWithRatings
await this.ratingUpdateService.updateDriverRatingsAfterRace(driverResults); await this.ratingUpdateService.updateDriverRatingsAfterRace(driverResults);
} }
} }

View File

@@ -6,6 +6,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository'; import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig'; import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { import type {
LeagueScoringPresetProvider, LeagueScoringPresetProvider,
LeagueScoringPresetDTO, LeagueScoringPresetDTO,
@@ -61,107 +62,136 @@ export class CreateLeagueWithSeasonAndScoringUseCase
async execute( async execute(
command: CreateLeagueWithSeasonAndScoringCommand, command: CreateLeagueWithSeasonAndScoringCommand,
): Promise<CreateLeagueWithSeasonAndScoringResultDTO> { ): Promise<CreateLeagueWithSeasonAndScoringResultDTO> {
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({ const league = League.create({
id: leagueId, id: leagueId,
name: command.name, name: command.name,
description: command.description ?? '', description: command.description ?? '',
ownerId: command.ownerId, ownerId: command.ownerId,
settings: { settings: {
// Presets are attached at scoring-config level; league settings use a stable points system id. pointsSystem: 'custom',
pointsSystem: 'custom', ...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}),
...(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 seasonId = uuidv4();
const season = Season.create({ this.logger.debug(`Generated seasonId: ${seasonId}`);
id: seasonId, const season = Season.create({
leagueId: league.id, id: seasonId,
gameId: command.gameId, leagueId: league.id,
name: `${command.name} Season 1`, gameId: command.gameId,
year: new Date().getFullYear(), name: `${command.name} Season 1`,
order: 1, year: new Date().getFullYear(),
status: 'active', order: 1,
startDate: new Date(), status: 'active',
endDate: new Date(), 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 presetId = command.scoringPresetId ?? 'club-default';
const preset: LeagueScoringPresetDTO | undefined = this.logger.debug(`Attempting to retrieve scoring preset: ${presetId}`);
this.presetProvider.getPresetById(presetId); const preset: LeagueScoringPresetDTO | undefined =
this.presetProvider.getPresetById(presetId);
if (!preset) { if (!preset) {
throw new Error(`Unknown scoring preset: ${presetId}`); 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 { private validate(command: CreateLeagueWithSeasonAndScoringCommand): void {
this.logger.debug('Validating CreateLeagueWithSeasonAndScoringCommand', { command });
if (!command.name || command.name.trim().length === 0) { 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'); throw new Error('League name is required');
} }
if (!command.ownerId || command.ownerId.trim().length === 0) { 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'); throw new Error('League ownerId is required');
} }
if (!command.gameId || command.gameId.trim().length === 0) { if (!command.gameId || command.gameId.trim().length === 0) {
this.logger.warn('Validation failed: gameId is required', { command });
throw new Error('gameId is required'); throw new Error('gameId is required');
} }
if (!command.visibility) { if (!command.visibility) {
this.logger.warn('Validation failed: visibility is required', { command });
throw new Error('visibility is required'); throw new Error('visibility is required');
} }
if (command.maxDrivers !== undefined && command.maxDrivers <= 0) { 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'); throw new Error('maxDrivers must be greater than 0 when provided');
} }
// Validate visibility-specific constraints
const visibility = LeagueVisibility.fromString(command.visibility); const visibility = LeagueVisibility.fromString(command.visibility);
if (visibility.isRanked()) { if (visibility.isRanked()) {
// Ranked (public) leagues require minimum 10 drivers for competitive integrity
const driverCount = command.maxDrivers ?? 0; const driverCount = command.maxDrivers ?? 0;
if (driverCount < MIN_RANKED_LEAGUE_DRIVERS) { 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( throw new Error(
`Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. ` + `Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. ` +
`Current setting: ${driverCount}. ` + `Current setting: ${driverCount}. ` +
@@ -169,5 +199,6 @@ export class CreateLeagueWithSeasonAndScoringUseCase
); );
} }
} }
this.logger.debug('Validation successful.');
} }
} }

View File

@@ -1,5 +1,6 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { import type {
IAllRacesPagePresenter, IAllRacesPagePresenter,
AllRacesPageResultDTO, AllRacesPageResultDTO,
@@ -7,59 +8,68 @@ import type {
AllRacesListItemViewModel, AllRacesListItemViewModel,
AllRacesFilterOptionsViewModel, AllRacesFilterOptionsViewModel,
} from '../presenters/IAllRacesPagePresenter'; } from '../presenters/IAllRacesPagePresenter';
import type { UseCase } from '@gridpilot/shared/application'; import type { UseCase } => '@gridpilot/shared/application';
export class GetAllRacesPageDataUseCase export class GetAllRacesPageDataUseCase
implements UseCase<void, AllRacesPageResultDTO, AllRacesPageViewModel, IAllRacesPagePresenter> { implements UseCase<void, AllRacesPageResultDTO, AllRacesPageViewModel, IAllRacesPagePresenter> {
constructor( constructor(
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository, private readonly leagueRepository: ILeagueRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(_input: void, presenter: IAllRacesPagePresenter): Promise<void> { async execute(_input: void, presenter: IAllRacesPagePresenter): Promise<void> {
const [allRaces, allLeagues] = await Promise.all([ this.logger.debug('Executing GetAllRacesPageDataUseCase');
this.raceRepository.findAll(), try {
this.leagueRepository.findAll(), 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 const races: AllRacesListItemViewModel[] = allRaces
.slice() .slice()
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()) .sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
.map((race) => ({ .map((race) => ({
id: race.id, id: race.id,
track: race.track, track: race.track,
car: race.car, car: race.car,
scheduledAt: race.scheduledAt.toISOString(), scheduledAt: race.scheduledAt.toISOString(),
status: race.status, status: race.status,
leagueId: race.leagueId, leagueId: race.leagueId,
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League', leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
strengthOfField: race.strengthOfField ?? null, strengthOfField: race.strengthOfField ?? null,
})); }));
const uniqueLeagues = new Map<string, { id: string; name: string }>(); const uniqueLeagues = new Map<string, { id: string; name: string }>();
for (const league of allLeagues) { for (const league of allLeagues) {
uniqueLeagues.set(league.id, { id: league.id, name: league.name }); 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);
} }
} }

View File

@@ -6,6 +6,7 @@ import type {
} from '../presenters/IAllTeamsPresenter'; } from '../presenters/IAllTeamsPresenter';
import type { UseCase } from '@gridpilot/shared/application'; import type { UseCase } from '@gridpilot/shared/application';
import type { Team } from '../../domain/entities/Team'; import type { Team } from '../../domain/entities/Team';
import { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use Case for retrieving all teams. * Use Case for retrieving all teams.
@@ -17,33 +18,44 @@ export class GetAllTeamsUseCase
constructor( constructor(
private readonly teamRepository: ITeamRepository, private readonly teamRepository: ITeamRepository,
private readonly teamMembershipRepository: ITeamMembershipRepository, private readonly teamMembershipRepository: ITeamMembershipRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(_input: void, presenter: IAllTeamsPresenter): Promise<void> { async execute(_input: void, presenter: IAllTeamsPresenter): Promise<void> {
this.logger.debug('Executing GetAllTeamsUseCase');
presenter.reset(); 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( const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all(
teams.map(async (team) => { teams.map(async (team) => {
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id); const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
return { return {
id: team.id, id: team.id,
name: team.name, name: team.name,
tag: team.tag, tag: team.tag,
description: team.description, description: team.description,
ownerId: team.ownerId, ownerId: team.ownerId,
leagues: [...team.leagues], leagues: [...team.leagues],
createdAt: team.createdAt, createdAt: team.createdAt,
memberCount, memberCount,
}; };
}), }),
); );
const dto: AllTeamsResultDTO = { const dto: AllTeamsResultDTO = {
teams: enrichedTeams, 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
}
} }
} }

View File

@@ -6,6 +6,7 @@ import type {
DriverTeamViewModel, DriverTeamViewModel,
} from '../presenters/IDriverTeamPresenter'; } from '../presenters/IDriverTeamPresenter';
import type { UseCase } from '@gridpilot/shared/application'; import type { UseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use Case for retrieving a driver's team. * Use Case for retrieving a driver's team.
@@ -17,23 +18,29 @@ export class GetDriverTeamUseCase
constructor( constructor(
private readonly teamRepository: ITeamRepository, private readonly teamRepository: ITeamRepository,
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
private readonly logger: ILogger,
// Kept for backward compatibility; callers must pass their own presenter. // Kept for backward compatibility; callers must pass their own presenter.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public readonly presenter: IDriverTeamPresenter, public readonly presenter: IDriverTeamPresenter,
) {} ) {}
async execute(input: { driverId: string }, presenter: IDriverTeamPresenter): Promise<void> { async execute(input: { driverId: string }, presenter: IDriverTeamPresenter): Promise<void> {
this.logger.debug(`Executing GetDriverTeamUseCase for driverId: ${input.driverId}`);
presenter.reset(); presenter.reset();
const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId); const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId);
if (!membership) { if (!membership) {
this.logger.warn(`No active membership found for driverId: ${input.driverId}`);
return; return;
} }
this.logger.debug(`Found membership for driverId: ${input.driverId}, teamId: ${membership.teamId}`);
const team = await this.teamRepository.findById(membership.teamId); const team = await this.teamRepository.findById(membership.teamId);
if (!team) { if (!team) {
this.logger.error(`Team not found for teamId: ${membership.teamId}`);
return; return;
} }
this.logger.debug(`Found team for teamId: ${team.id}, name: ${team.name}`);
const dto: DriverTeamResultDTO = { const dto: DriverTeamResultDTO = {
team, team,
@@ -42,5 +49,6 @@ export class GetDriverTeamUseCase
}; };
presenter.present(dto); presenter.present(dto);
this.logger.info(`Successfully presented driver team for driverId: ${input.driverId}`);
} }
} }

View File

@@ -12,6 +12,7 @@ import type { SponsorableEntityType } from '../../domain/entities/SponsorshipReq
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter'; import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface GetEntitySponsorshipPricingDTO { export interface GetEntitySponsorshipPricingDTO {
entityType: SponsorableEntityType; entityType: SponsorableEntityType;
@@ -46,74 +47,115 @@ export class GetEntitySponsorshipPricingUseCase
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository, private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository, private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly presenter: IEntitySponsorshipPricingPresenter, private readonly presenter: IEntitySponsorshipPricingPresenter,
private readonly logger: ILogger,
) {} ) {}
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> { async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> {
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId); this.logger.debug(
`Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
if (!pricing) { { dto },
this.presenter.present(null);
return;
}
// 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;
// Count filled slots (for seasons, check SeasonSponsorship table) try {
let filledMainSlots = 0; const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
let filledSecondarySlots = 0;
if (dto.entityType === 'season') { if (!pricing) {
const sponsorships = await this.seasonSponsorshipRepo.findBySeasonId(dto.entityId); this.logger.warn(
const activeSponsorships = sponsorships.filter(s => s.isActive()); `No pricing found for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Presenting null.`,
filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length; { dto },
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length; );
} this.presenter.present(null);
return;
}
const result: GetEntitySponsorshipPricingResultDTO = { this.logger.debug(`Found pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, { pricing });
entityType: dto.entityType,
entityId: dto.entityId,
acceptingApplications: pricing.acceptingApplications,
...(pricing.customRequirements !== undefined
? { customRequirements: pricing.customRequirements }
: {}),
};
if (pricing.mainSlot) { // Count pending requests by tier
const mainMaxSlots = pricing.mainSlot.maxSlots; const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity(
result.mainSlot = { dto.entityType,
tier: 'main', dto.entityId,
price: pricing.mainSlot.price.amount, );
currency: pricing.mainSlot.price.currency, const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length;
formattedPrice: pricing.mainSlot.price.format(), const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length;
benefits: pricing.mainSlot.benefits,
available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots, this.logger.debug(
maxSlots: mainMaxSlots, `Pending requests counts: main=${pendingMainCount}, secondary=${pendingSecondaryCount}`,
filledSlots: filledMainSlots, );
pendingRequests: pendingMainCount,
// 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) { if (pricing.mainSlot) {
const secondaryMaxSlots = pricing.secondarySlots.maxSlots; const mainMaxSlots = pricing.mainSlot.maxSlots;
result.secondarySlot = { result.mainSlot = {
tier: 'secondary', tier: 'main',
price: pricing.secondarySlots.price.amount, price: pricing.mainSlot.price.amount,
currency: pricing.secondarySlots.price.currency, currency: pricing.mainSlot.price.currency,
formattedPrice: pricing.secondarySlots.price.format(), formattedPrice: pricing.mainSlot.price.format(),
benefits: pricing.secondarySlots.benefits, benefits: pricing.mainSlot.benefits,
available: pricing.secondarySlots.available && filledSecondarySlots < secondaryMaxSlots, available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots,
maxSlots: secondaryMaxSlots, maxSlots: mainMaxSlots,
filledSlots: filledSecondarySlots, filledSlots: filledMainSlots,
pendingRequests: pendingSecondaryCount, 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;
}
} }
} }

View File

@@ -5,10 +5,11 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'; 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 { DriverRatingProvider } from '../ports/DriverRatingProvider';
import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter'; import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import { ILogger } from '../../../shared/src/logging/ILogger';
import { import {
AverageStrengthOfFieldCalculator, AverageStrengthOfFieldCalculator,
type StrengthOfFieldCalculator, type StrengthOfFieldCalculator,
@@ -31,55 +32,78 @@ export class GetLeagueStatsUseCase
private readonly resultRepository: IResultRepository, private readonly resultRepository: IResultRepository,
private readonly driverRatingProvider: DriverRatingProvider, private readonly driverRatingProvider: DriverRatingProvider,
public readonly presenter: ILeagueStatsPresenter, public readonly presenter: ILeagueStatsPresenter,
private readonly logger: ILogger,
sofCalculator?: StrengthOfFieldCalculator, sofCalculator?: StrengthOfFieldCalculator,
) { ) {
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator(); this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
} }
async execute(params: GetLeagueStatsUseCaseParams): Promise<void> { async execute(params: GetLeagueStatsUseCaseParams): Promise<void> {
this.logger.debug(
`Executing GetLeagueStatsUseCase with params: ${JSON.stringify(params)}`,
);
const { leagueId } = params; const { leagueId } = params;
const league = await this.leagueRepository.findById(leagueId); try {
if (!league) { const league = await this.leagueRepository.findById(leagueId);
throw new Error(`League ${leagueId} not found`); if (!league) {
} this.logger.error(`League ${leagueId} not found`);
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;
} }
// Otherwise calculate from results const races = await this.raceRepository.findByLeagueId(leagueId);
const results = await this.resultRepository.findByRaceId(race.id); const completedRaces = races.filter(r => r.status === 'completed');
if (results.length === 0) continue; 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); // Calculate SOF for each completed race
const ratings = this.driverRatingProvider.getRatings(driverIds); const sofValues: number[] = [];
const driverRatings = driverIds
.filter(id => ratings.has(id))
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
const sof = this.sofCalculator.calculate(driverRatings); for (const race of completedRaces) {
if (sof !== null) { // Use stored SOF if available
sofValues.push(sof); 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( this.presenter.present(
leagueId, leagueId,
races.length, races.length,
completedRaces.length, completedRaces.length,
scheduledRaces.length, scheduledRaces.length,
sofValues sofValues,
); );
this.logger.info(`Successfully presented league statistics for league ${leagueId}.`);
} catch (error) {
this.logger.error(`Error in GetLeagueStatsUseCase: ${error.message}`);
throw error;
}
} }
} }

View File

@@ -7,6 +7,7 @@ import type {
TeamJoinRequestsViewModel, TeamJoinRequestsViewModel,
} from '../presenters/ITeamJoinRequestsPresenter'; } from '../presenters/ITeamJoinRequestsPresenter';
import type { UseCase } from '@gridpilot/shared/application'; import type { UseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use Case for retrieving team join requests. * Use Case for retrieving team join requests.
@@ -19,33 +20,44 @@ export class GetTeamJoinRequestsUseCase
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
private readonly driverRepository: IDriverRepository, private readonly driverRepository: IDriverRepository,
private readonly imageService: IImageServicePort, private readonly imageService: IImageServicePort,
private readonly logger: ILogger,
// Kept for backward compatibility; callers must pass their own presenter. // Kept for backward compatibility; callers must pass their own presenter.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public readonly presenter: ITeamJoinRequestsPresenter, public readonly presenter: ITeamJoinRequestsPresenter,
) {} ) {}
async execute(input: { teamId: string }, presenter: ITeamJoinRequestsPresenter): Promise<void> { async execute(input: { teamId: string }, presenter: ITeamJoinRequestsPresenter): Promise<void> {
this.logger.debug('Executing GetTeamJoinRequestsUseCase', { teamId: input.teamId });
presenter.reset(); 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<string, string> = {}; const driverNames: Record<string, string> = {};
const avatarUrls: Record<string, string> = {}; const avatarUrls: Record<string, string> = {};
for (const request of requests) { for (const request of requests) {
const driver = await this.driverRepository.findById(request.driverId); const driver = await this.driverRepository.findById(request.driverId);
if (driver) { if (driver) {
driverNames[request.driverId] = driver.name; 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);
} }
} }

View File

@@ -7,6 +7,7 @@ import type {
TeamMembersViewModel, TeamMembersViewModel,
} from '../presenters/ITeamMembersPresenter'; } from '../presenters/ITeamMembersPresenter';
import type { UseCase } from '@gridpilot/shared/application'; import type { UseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
/** /**
* Use Case for retrieving team members. * Use Case for retrieving team members.
@@ -19,33 +20,45 @@ export class GetTeamMembersUseCase
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
private readonly driverRepository: IDriverRepository, private readonly driverRepository: IDriverRepository,
private readonly imageService: IImageServicePort, private readonly imageService: IImageServicePort,
private readonly logger: ILogger,
// Kept for backward compatibility; callers must pass their own presenter. // Kept for backward compatibility; callers must pass their own presenter.
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public readonly presenter: ITeamMembersPresenter, public readonly presenter: ITeamMembersPresenter,
) {} ) {}
async execute(input: { teamId: string }, presenter: ITeamMembersPresenter): Promise<void> { async execute(input: { teamId: string }, presenter: ITeamMembersPresenter): Promise<void> {
this.logger.debug(`Executing GetTeamMembersUseCase for teamId: ${input.teamId}`);
presenter.reset(); 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<string, string> = {}; const driverNames: Record<string, string> = {};
const avatarUrls: Record<string, string> = {}; const avatarUrls: Record<string, string> = {};
for (const membership of memberships) { for (const membership of memberships) {
const driver = await this.driverRepository.findById(membership.driverId); this.logger.debug(`Processing membership for driverId: ${membership.driverId}`);
if (driver) { const driver = await this.driverRepository.findById(membership.driverId);
driverNames[membership.driverId] = driver.name; 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);
} }
} }

View File

@@ -13,6 +13,7 @@ import type {
IImportRaceResultsPresenter, IImportRaceResultsPresenter,
ImportRaceResultsSummaryViewModel, ImportRaceResultsSummaryViewModel,
} from '../presenters/IImportRaceResultsPresenter'; } from '../presenters/IImportRaceResultsPresenter';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface ImportRaceResultDTO { export interface ImportRaceResultDTO {
id: string; id: string;
@@ -39,53 +40,72 @@ export class ImportRaceResultsUseCase
private readonly driverRepository: IDriverRepository, private readonly driverRepository: IDriverRepository,
private readonly standingRepository: IStandingRepository, private readonly standingRepository: IStandingRepository,
public readonly presenter: IImportRaceResultsPresenter, public readonly presenter: IImportRaceResultsPresenter,
private readonly logger: ILogger,
) {} ) {}
async execute(params: ImportRaceResultsParams): Promise<void> { async execute(params: ImportRaceResultsParams): Promise<void> {
this.logger.debug('ImportRaceResultsUseCase:execute', { params });
const { raceId, results } = params; const { raceId, results } = params;
const race = await this.raceRepository.findById(raceId); try {
if (!race) { const race = await this.raceRepository.findById(raceId);
throw new EntityNotFoundError({ entity: 'race', id: 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);
} }
} }

View File

@@ -1,3 +1,4 @@
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { import type {
ILeagueMembershipRepository, ILeagueMembershipRepository,
} from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
@@ -11,7 +12,10 @@ import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO';
import { BusinessRuleViolationError } from '../errors/RacingApplicationError'; import { BusinessRuleViolationError } from '../errors/RacingApplicationError';
export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, LeagueMembership> { export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, LeagueMembership> {
constructor(private readonly membershipRepository: ILeagueMembershipRepository) {} constructor(
private readonly membershipRepository: ILeagueMembershipRepository,
private readonly logger: ILogger,
) {}
/** /**
* Joins a driver to a league as an active member. * Joins a driver to a league as an active member.
@@ -21,20 +25,31 @@ export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, Lea
* - Creates a new active membership with role "member" and current timestamp. * - Creates a new active membership with role "member" and current timestamp.
*/ */
async execute(command: JoinLeagueCommandDTO): Promise<LeagueMembership> { async execute(command: JoinLeagueCommandDTO): Promise<LeagueMembership> {
this.logger.debug('Attempting to join league', { command });
const { leagueId, driverId } = command; const { leagueId, driverId } = command;
const existing = await this.membershipRepository.getMembership(leagueId, driverId); try {
if (existing) { const existing = await this.membershipRepository.getMembership(leagueId, driverId);
throw new BusinessRuleViolationError('Already a member or have a pending request'); 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);
} }
} }

View File

@@ -7,6 +7,7 @@ import type {
} from '../../domain/types/TeamMembership'; } from '../../domain/types/TeamMembership';
import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO'; import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import { import {
BusinessRuleViolationError, BusinessRuleViolationError,
EntityNotFoundError, EntityNotFoundError,
@@ -16,36 +17,50 @@ export class JoinTeamUseCase implements AsyncUseCase<JoinTeamCommandDTO, void> {
constructor( constructor(
private readonly teamRepository: ITeamRepository, private readonly teamRepository: ITeamRepository,
private readonly membershipRepository: ITeamMembershipRepository, private readonly membershipRepository: ITeamMembershipRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: JoinTeamCommandDTO): Promise<void> { async execute(command: JoinTeamCommandDTO): Promise<void> {
this.logger.debug('Attempting to join team', { command });
const { teamId, driverId } = command; const { teamId, driverId } = command;
const existingActive = await this.membershipRepository.getActiveMembershipForDriver( try {
driverId, const existingActive = await this.membershipRepository.getActiveMembershipForDriver(
); driverId,
if (existingActive) { );
throw new BusinessRuleViolationError('Driver already belongs to a team'); 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);
} }
} }

View File

@@ -11,6 +11,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface QuickPenaltyCommand { export interface QuickPenaltyCommand {
raceId: string; raceId: string;
@@ -27,50 +28,60 @@ export class QuickPenaltyUseCase
private readonly penaltyRepository: IPenaltyRepository, private readonly penaltyRepository: IPenaltyRepository,
private readonly raceRepository: IRaceRepository, private readonly raceRepository: IRaceRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository, private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly logger: ILogger,
) {} ) {}
async execute(command: QuickPenaltyCommand): Promise<{ penaltyId: string }> { async execute(command: QuickPenaltyCommand): Promise<{ penaltyId: string }> {
// Validate race exists this.logger.debug('Executing QuickPenaltyUseCase', { command });
const race = await this.raceRepository.findById(command.raceId); try {
if (!race) { // Validate race exists
throw new Error('Race not found'); 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( private mapInfractionToPenalty(
@@ -132,6 +143,7 @@ export class QuickPenaltyUseCase
}; };
default: default:
this.logger.error(`Unknown infraction type: ${infractionType}`);
throw new Error(`Unknown infraction type: ${infractionType}`); throw new Error(`Unknown infraction type: ${infractionType}`);
} }
} }

View File

@@ -3,6 +3,7 @@ import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repos
import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration'; import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
import type { RegisterForRaceCommandDTO } from '../dto/RegisterForRaceCommandDTO'; import type { RegisterForRaceCommandDTO } from '../dto/RegisterForRaceCommandDTO';
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
import { import {
BusinessRuleViolationError, BusinessRuleViolationError,
PermissionDeniedError, PermissionDeniedError,
@@ -14,8 +15,9 @@ export class RegisterForRaceUseCase
constructor( constructor(
private readonly registrationRepository: IRaceRegistrationRepository, private readonly registrationRepository: IRaceRegistrationRepository,
private readonly membershipRepository: ILeagueMembershipRepository, private readonly membershipRepository: ILeagueMembershipRepository,
private readonly logger: ILogger,
) {} ) {}
/** /**
* Mirrors legacy registerForRace behavior: * Mirrors legacy registerForRace behavior:
* - throws if already registered * - throws if already registered
@@ -24,22 +26,26 @@ export class RegisterForRaceUseCase
*/ */
async execute(command: RegisterForRaceCommandDTO): Promise<void> { async execute(command: RegisterForRaceCommandDTO): Promise<void> {
const { raceId, leagueId, driverId } = command; const { raceId, leagueId, driverId } = command;
this.logger.debug('RegisterForRaceUseCase: executing command', { raceId, leagueId, driverId });
const alreadyRegistered = await this.registrationRepository.isRegistered(raceId, driverId); const alreadyRegistered = await this.registrationRepository.isRegistered(raceId, driverId);
if (alreadyRegistered) { if (alreadyRegistered) {
this.logger.warn(`RegisterForRaceUseCase: driver ${driverId} already registered for race ${raceId}`);
throw new BusinessRuleViolationError('Already registered for this race'); throw new BusinessRuleViolationError('Already registered for this race');
} }
const membership = await this.membershipRepository.getMembership(leagueId, driverId); const membership = await this.membershipRepository.getMembership(leagueId, driverId);
if (!membership || membership.status !== 'active') { 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'); throw new PermissionDeniedError('NOT_ACTIVE_MEMBER', 'Must be an active league member to register for races');
} }
const registration = RaceRegistration.create({ const registration = RaceRegistration.create({
raceId, raceId,
driverId, driverId,
}); });
await this.registrationRepository.register(registration); await this.registrationRepository.register(registration);
this.logger.info(`RegisterForRaceUseCase: driver ${driverId} successfully registered for race ${raceId}`);
} }
} }

View File

@@ -8,92 +8,191 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Car, CarClass, CarLicense } from '@gridpilot/racing/domain/entities/Car'; import { Car, CarClass, CarLicense } from '@gridpilot/racing/domain/entities/Car';
import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository'; import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryCarRepository implements ICarRepository { export class InMemoryCarRepository implements ICarRepository {
private cars: Map<string, Car>; private cars: Map<string, Car>;
private readonly logger: ILogger;
constructor(seedData?: Car[]) { constructor(logger: ILogger, seedData?: Car[]) {
this.logger = logger;
this.cars = new Map(); this.cars = new Map();
this.logger.info('InMemoryCarRepository initialized');
if (seedData) { if (seedData) {
this.logger.debug(`Seeding ${seedData.length} cars.`);
seedData.forEach(car => { seedData.forEach(car => {
this.cars.set(car.id, car); this.cars.set(car.id, car);
this.logger.debug(`Car ${car.id} seeded.`);
}); });
} }
} }
async findById(id: string): Promise<Car | null> { async findById(id: string): Promise<Car | null> {
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<Car[]> { async findAll(): Promise<Car[]> {
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<Car[]> { async findByGameId(gameId: string): Promise<Car[]> {
return Array.from(this.cars.values()) this.logger.debug(`Attempting to find cars by game ID: ${gameId}.`);
.filter(car => car.gameId === gameId) try {
.sort((a, b) => a.name.localeCompare(b.name)); 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<Car[]> { async findByClass(carClass: CarClass): Promise<Car[]> {
return Array.from(this.cars.values()) this.logger.debug(`Attempting to find cars by class: ${carClass}.`);
.filter(car => car.carClass === carClass) try {
.sort((a, b) => a.name.localeCompare(b.name)); 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<Car[]> { async findByLicense(license: CarLicense): Promise<Car[]> {
return Array.from(this.cars.values()) this.logger.debug(`Attempting to find cars by license: ${license}.`);
.filter(car => car.license === license) try {
.sort((a, b) => a.name.localeCompare(b.name)); 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<Car[]> { async findByManufacturer(manufacturer: string): Promise<Car[]> {
const lowerManufacturer = manufacturer.toLowerCase(); this.logger.debug(`Attempting to find cars by manufacturer: ${manufacturer}.`);
return Array.from(this.cars.values()) try {
.filter(car => car.manufacturer.toLowerCase() === lowerManufacturer) const lowerManufacturer = manufacturer.toLowerCase();
.sort((a, b) => a.name.localeCompare(b.name)); 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<Car[]> { async searchByName(query: string): Promise<Car[]> {
const lowerQuery = query.toLowerCase(); this.logger.debug(`Attempting to search cars by name query: ${query}.`);
return Array.from(this.cars.values()) try {
.filter(car => const lowerQuery = query.toLowerCase();
car.name.toLowerCase().includes(lowerQuery) || const cars = Array.from(this.cars.values())
car.shortName.toLowerCase().includes(lowerQuery) || .filter(car =>
car.manufacturer.toLowerCase().includes(lowerQuery) car.name.toLowerCase().includes(lowerQuery) ||
) car.shortName.toLowerCase().includes(lowerQuery) ||
.sort((a, b) => a.name.localeCompare(b.name)); 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<Car> { async create(car: Car): Promise<Car> {
if (await this.exists(car.id)) { this.logger.debug(`Attempting to create car: ${car.id}.`);
throw new Error(`Car with ID ${car.id} already exists`); 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); this.cars.set(car.id, car);
return 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<Car> { async update(car: Car): Promise<Car> {
if (!await this.exists(car.id)) { this.logger.debug(`Attempting to update car with ID: ${car.id}.`);
throw new Error(`Car with ID ${car.id} not found`); 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); this.cars.set(car.id, car);
return 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<void> { async delete(id: string): Promise<void> {
if (!await this.exists(id)) { this.logger.debug(`Attempting to delete car with ID: ${id}.`);
throw new Error(`Car with ID ${id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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;
}
} }
/** /**

View File

@@ -8,73 +8,150 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Driver } from '@gridpilot/racing/domain/entities/Driver'; import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository'; import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryDriverRepository implements IDriverRepository { export class InMemoryDriverRepository implements IDriverRepository {
private drivers: Map<string, Driver>; private drivers: Map<string, Driver>;
private readonly logger: ILogger;
constructor(seedData?: Driver[]) { constructor(logger: ILogger, seedData?: Driver[]) {
this.logger = logger;
this.logger.info('InMemoryDriverRepository initialized.');
this.drivers = new Map(); this.drivers = new Map();
if (seedData) { if (seedData) {
seedData.forEach(driver => { seedData.forEach(driver => {
this.drivers.set(driver.id, driver); this.drivers.set(driver.id, driver);
this.logger.debug(`Seeded driver: ${driver.id}.`);
}); });
} }
} }
async findById(id: string): Promise<Driver | null> { async findById(id: string): Promise<Driver | null> {
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<Driver | null> { async findByIRacingId(iracingId: string): Promise<Driver | null> {
const driver = Array.from(this.drivers.values()).find( this.logger.debug(`Finding driver by iRacing id: ${iracingId}`);
d => d.iracingId === iracingId try {
); const driver = Array.from(this.drivers.values()).find(
return driver ?? null; 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<Driver[]> { async findAll(): Promise<Driver[]> {
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<Driver> { async create(driver: Driver): Promise<Driver> {
if (await this.exists(driver.id)) { this.logger.debug(`Creating driver: ${driver.id}`);
throw new Error(`Driver with ID ${driver.id} already exists`); 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)) { if (await this.existsByIRacingId(driver.iracingId)) {
throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`); this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`);
} throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`);
}
this.drivers.set(driver.id, driver); this.drivers.set(driver.id, driver);
return 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<Driver> { async update(driver: Driver): Promise<Driver> {
if (!await this.exists(driver.id)) { this.logger.debug(`Updating driver: ${driver.id}`);
throw new Error(`Driver with ID ${driver.id} not found`); try {
} if (!await this.exists(driver.id)) {
this.logger.warn(`Driver with ID ${driver.id} not found for update.`);
throw new Error(`Driver with ID ${driver.id} not found`);
}
this.drivers.set(driver.id, driver); this.drivers.set(driver.id, driver);
return 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<void> { async delete(id: string): Promise<void> {
if (!await this.exists(id)) { this.logger.debug(`Deleting driver: ${id}`);
throw new Error(`Driver with ID ${id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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<boolean> { async existsByIRacingId(iracingId: string): Promise<boolean> {
return Array.from(this.drivers.values()).some( this.logger.debug(`Checking existence of driver with iRacing id: ${iracingId}`);
d => d.iracingId === 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;
}
} }
/** /**

View File

@@ -6,16 +6,21 @@
import { Game } from '../../domain/entities/Game'; import { Game } from '../../domain/entities/Game';
import type { IGameRepository } from '../../domain/repositories/IGameRepository'; import type { IGameRepository } from '../../domain/repositories/IGameRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryGameRepository implements IGameRepository { export class InMemoryGameRepository implements IGameRepository {
private games: Map<string, Game>; private games: Map<string, Game>;
private readonly logger: ILogger;
constructor(seedData?: Game[]) { constructor(logger: ILogger, seedData?: Game[]) {
this.logger = logger;
this.logger.info('InMemoryGameRepository initialized.');
this.games = new Map(); this.games = new Map();
if (seedData) { if (seedData) {
seedData.forEach(game => { seedData.forEach(game => {
this.games.set(game.id, game); this.games.set(game.id, game);
this.logger.debug(`Seeded game: ${game.id}.`);
}); });
} else { } else {
// Default seed data for common sim racing games // Default seed data for common sim racing games
@@ -29,33 +34,64 @@ export class InMemoryGameRepository implements IGameRepository {
]; ];
defaultGames.forEach(game => { defaultGames.forEach(game => {
this.games.set(game.id, game); this.games.set(game.id, game);
this.logger.debug(`Seeded default game: ${game.id}.`);
}); });
} }
} }
async findById(id: string): Promise<Game | null> { async findById(id: string): Promise<Game | null> {
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<Game[]> { async findAll(): Promise<Game[]> {
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 * Utility method to add a game
*/ */
async create(game: Game): Promise<Game> { async create(game: Game): Promise<Game> {
if (this.games.has(game.id)) { this.logger.debug(`Creating game: ${game.id}`);
throw new Error(`Game with ID ${game.id} already exists`); 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 * Test helper to clear data
*/ */
clear(): void { clear(): void {
this.logger.debug('Clearing all games.');
this.games.clear(); this.games.clear();
this.logger.info('All games cleared.');
} }
} }

View File

@@ -10,12 +10,16 @@ import type {
JoinRequest, JoinRequest,
} from '@gridpilot/racing/domain/entities/LeagueMembership'; } from '@gridpilot/racing/domain/entities/LeagueMembership';
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository'; import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository { export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
private membershipsByLeague: Map<string, LeagueMembership[]>; private membershipsByLeague: Map<string, LeagueMembership[]>;
private joinRequestsByLeague: Map<string, JoinRequest[]>; private joinRequestsByLeague: Map<string, JoinRequest[]>;
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.membershipsByLeague = new Map();
this.joinRequestsByLeague = new Map(); this.joinRequestsByLeague = new Map();
@@ -24,6 +28,7 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo
const list = this.membershipsByLeague.get(membership.leagueId) ?? []; const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
list.push(membership); list.push(membership);
this.membershipsByLeague.set(membership.leagueId, list); 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) ?? []; const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
list.push(request); list.push(request);
this.joinRequestsByLeague.set(request.leagueId, list); 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<LeagueMembership | null> { async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
const list = this.membershipsByLeague.get(leagueId); this.logger.debug(`Getting membership for league: ${leagueId}, driver: ${driverId}`);
if (!list) return null; try {
return list.find((m) => m.driverId === driverId) ?? null; 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<LeagueMembership[]> { async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
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<JoinRequest[]> { async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
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<LeagueMembership> { async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
const list = this.membershipsByLeague.get(membership.leagueId) ?? []; this.logger.debug(`Saving membership for league: ${membership.leagueId}, driver: ${membership.driverId}`);
const existingIndex = list.findIndex( try {
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId, const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
); const existingIndex = list.findIndex(
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
);
if (existingIndex >= 0) { if (existingIndex >= 0) {
list[existingIndex] = membership; list[existingIndex] = membership;
} else { this.logger.info(`Updated existing membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
list.push(membership); } 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<void> { async removeMembership(leagueId: string, driverId: string): Promise<void> {
const list = this.membershipsByLeague.get(leagueId); this.logger.debug(`Removing membership for league: ${leagueId}, driver: ${driverId}`);
if (!list) return; 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); const next = list.filter((m) => m.driverId !== driverId);
this.membershipsByLeague.set(leagueId, next); 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<JoinRequest> { async saveJoinRequest(request: JoinRequest): Promise<JoinRequest> {
const list = this.joinRequestsByLeague.get(request.leagueId) ?? []; this.logger.debug(`Saving join request for league: ${request.leagueId}, driver: ${request.driverId}, id: ${request.id}`);
const existingIndex = list.findIndex((r) => r.id === request.id); try {
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
const existingIndex = list.findIndex((r) => r.id === request.id);
if (existingIndex >= 0) { if (existingIndex >= 0) {
list[existingIndex] = request; list[existingIndex] = request;
} else { this.logger.info(`Updated existing join request: ${request.id}.`);
list.push(request); } 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<void> { async removeJoinRequest(requestId: string): Promise<void> {
for (const [leagueId, requests] of this.joinRequestsByLeague.entries()) { this.logger.debug(`Removing join request with ID: ${requestId}`);
const next = requests.filter((r) => r.id !== requestId); try {
if (next.length !== requests.length) { let removed = false;
this.joinRequestsByLeague.set(leagueId, next); for (const [leagueId, requests] of this.joinRequestsByLeague.entries()) {
break; 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;
} }
} }
} }

View File

@@ -8,69 +8,142 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { League } from '@gridpilot/racing/domain/entities/League'; import { League } from '@gridpilot/racing/domain/entities/League';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryLeagueRepository implements ILeagueRepository { export class InMemoryLeagueRepository implements ILeagueRepository {
private leagues: Map<string, League>; private leagues: Map<string, League>;
private readonly logger: ILogger;
constructor(seedData?: League[]) { constructor(logger: ILogger, seedData?: League[]) {
this.logger = logger;
this.logger.info('InMemoryLeagueRepository initialized.');
this.leagues = new Map(); this.leagues = new Map();
if (seedData) { if (seedData) {
seedData.forEach(league => { seedData.forEach(league => {
this.leagues.set(league.id, league); this.leagues.set(league.id, league);
this.logger.debug(`Seeded league: ${league.id}.`);
}); });
} }
} }
async findById(id: string): Promise<League | null> { async findById(id: string): Promise<League | null> {
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<League[]> { async findAll(): Promise<League[]> {
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<League[]> { async findByOwnerId(ownerId: string): Promise<League[]> {
return Array.from(this.leagues.values()).filter( this.logger.debug(`Finding leagues by owner id: ${ownerId}`);
league => league.ownerId === 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<League> { async create(league: League): Promise<League> {
if (await this.exists(league.id)) { this.logger.debug(`Creating league: ${league.id}`);
throw new Error(`League with ID ${league.id} already exists`); try {
} if (await this.exists(league.id)) {
this.logger.warn(`League with ID ${league.id} already exists.`);
throw new Error(`League with ID ${league.id} already exists`);
}
this.leagues.set(league.id, league); this.leagues.set(league.id, league);
return 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<League> { async update(league: League): Promise<League> {
if (!await this.exists(league.id)) { this.logger.debug(`Updating league: ${league.id}`);
throw new Error(`League with ID ${league.id} not found`); try {
} if (!await this.exists(league.id)) {
this.logger.warn(`League with ID ${league.id} not found for update.`);
throw new Error(`League with ID ${league.id} not found`);
}
this.leagues.set(league.id, league); this.leagues.set(league.id, league);
return 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<void> { async delete(id: string): Promise<void> {
if (!await this.exists(id)) { this.logger.debug(`Deleting league: ${id}`);
throw new Error(`League with ID ${id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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<League[]> { async searchByName(query: string): Promise<League[]> {
const normalizedQuery = query.toLowerCase(); this.logger.debug(`Searching leagues by name query: ${query}`);
return Array.from(this.leagues.values()).filter(league => try {
league.name.toLowerCase().includes(normalizedQuery) 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;
}
} }
/** /**

View File

@@ -6,49 +6,116 @@
import type { LeagueWallet } from '../../domain/entities/LeagueWallet'; import type { LeagueWallet } from '../../domain/entities/LeagueWallet';
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository'; import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository { export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository {
private wallets: Map<string, LeagueWallet> = new Map(); private wallets: Map<string, LeagueWallet> = 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<LeagueWallet | null> { async findById(id: string): Promise<LeagueWallet | null> {
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<LeagueWallet | null> { async findByLeagueId(leagueId: string): Promise<LeagueWallet | null> {
for (const wallet of this.wallets.values()) { this.logger.debug(`Finding league wallet by league id: ${leagueId}`);
if (wallet.leagueId === leagueId) { try {
return wallet; 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<LeagueWallet> { async create(wallet: LeagueWallet): Promise<LeagueWallet> {
if (this.wallets.has(wallet.id)) { this.logger.debug(`Creating league wallet: ${wallet.id}`);
throw new Error('LeagueWallet with this ID already exists'); 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<LeagueWallet> { async update(wallet: LeagueWallet): Promise<LeagueWallet> {
if (!this.wallets.has(wallet.id)) { this.logger.debug(`Updating league wallet: ${wallet.id}`);
throw new Error('LeagueWallet not found'); 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 // Test helper
clear(): void { clear(): void {
this.logger.debug('Clearing all league wallets.');
this.wallets.clear(); this.wallets.clear();
this.logger.info('All league wallets cleared.');
} }
} }

View File

@@ -7,108 +7,253 @@
import type { DriverLivery } from '../../domain/entities/DriverLivery'; import type { DriverLivery } from '../../domain/entities/DriverLivery';
import type { LiveryTemplate } from '../../domain/entities/LiveryTemplate'; import type { LiveryTemplate } from '../../domain/entities/LiveryTemplate';
import type { ILiveryRepository } from '../../domain/repositories/ILiveryRepository'; import type { ILiveryRepository } from '../../domain/repositories/ILiveryRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryLiveryRepository implements ILiveryRepository { export class InMemoryLiveryRepository implements ILiveryRepository {
private driverLiveries: Map<string, DriverLivery> = new Map(); private driverLiveries: Map<string, DriverLivery> = new Map();
private templates: Map<string, LiveryTemplate> = new Map(); private templates: Map<string, LiveryTemplate> = 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 // DriverLivery operations
async findDriverLiveryById(id: string): Promise<DriverLivery | null> { async findDriverLiveryById(id: string): Promise<DriverLivery | null> {
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<DriverLivery[]> { async findDriverLiveriesByDriverId(driverId: string): Promise<DriverLivery[]> {
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<DriverLivery | null> { async findDriverLiveryByDriverAndCar(driverId: string, carId: string): Promise<DriverLivery | null> {
for (const livery of this.driverLiveries.values()) { this.logger.debug(`Finding driver livery by driver: ${driverId} and car: ${carId}`);
if (livery.driverId === driverId && livery.carId === carId) { try {
return livery; 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<DriverLivery[]> { async findDriverLiveriesByGameId(gameId: string): Promise<DriverLivery[]> {
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<DriverLivery[]> { async findDriverLiveryByDriverAndGame(driverId: string, gameId: string): Promise<DriverLivery[]> {
return Array.from(this.driverLiveries.values()).filter( this.logger.debug(`Finding driver liveries by driver: ${driverId} and game: ${gameId}`);
l => l.driverId === driverId && l.gameId === 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<DriverLivery> { async createDriverLivery(livery: DriverLivery): Promise<DriverLivery> {
if (this.driverLiveries.has(livery.id)) { this.logger.debug(`Creating driver livery: ${livery.id}`);
throw new Error('DriverLivery with this ID already exists'); 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<DriverLivery> { async updateDriverLivery(livery: DriverLivery): Promise<DriverLivery> {
if (!this.driverLiveries.has(livery.id)) { this.logger.debug(`Updating driver livery: ${livery.id}`);
throw new Error('DriverLivery not found'); 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<void> { async deleteDriverLivery(id: string): Promise<void> {
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 // LiveryTemplate operations
async findTemplateById(id: string): Promise<LiveryTemplate | null> { async findTemplateById(id: string): Promise<LiveryTemplate | null> {
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<LiveryTemplate[]> { async findTemplatesBySeasonId(seasonId: string): Promise<LiveryTemplate[]> {
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<LiveryTemplate | null> { async findTemplateBySeasonAndCar(seasonId: string, carId: string): Promise<LiveryTemplate | null> {
for (const template of this.templates.values()) { this.logger.debug(`Finding livery template by season: ${seasonId} and car: ${carId}`);
if (template.seasonId === seasonId && template.carId === carId) { try {
return template; 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<LiveryTemplate> { async createTemplate(template: LiveryTemplate): Promise<LiveryTemplate> {
if (this.templates.has(template.id)) { this.logger.debug(`Creating livery template: ${template.id}`);
throw new Error('LiveryTemplate with this ID already exists'); 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<LiveryTemplate> { async updateTemplate(template: LiveryTemplate): Promise<LiveryTemplate> {
if (!this.templates.has(template.id)) { this.logger.debug(`Updating livery template: ${template.id}`);
throw new Error('LiveryTemplate not found'); 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<void> { async deleteTemplate(id: string): Promise<void> {
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 // Test helpers
clearDriverLiveries(): void { clearDriverLiveries(): void {
this.logger.debug('Clearing all driver liveries.');
this.driverLiveries.clear(); this.driverLiveries.clear();
this.logger.info('All driver liveries cleared.');
} }
clearTemplates(): void { clearTemplates(): void {
this.logger.debug('Clearing all livery templates.');
this.templates.clear(); this.templates.clear();
this.logger.info('All livery templates cleared.');
} }
clear(): void { clear(): void {
this.logger.debug('Clearing all livery data.');
this.driverLiveries.clear(); this.driverLiveries.clear();
this.templates.clear(); this.templates.clear();
this.logger.info('All livery data cleared.');
} }
} }

View File

@@ -6,65 +6,146 @@
import type { Penalty } from '../../domain/entities/Penalty'; import type { Penalty } from '../../domain/entities/Penalty';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository'; import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryPenaltyRepository implements IPenaltyRepository { export class InMemoryPenaltyRepository implements IPenaltyRepository {
private penalties: Map<string, Penalty> = new Map(); private penalties: Map<string, Penalty> = 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 => { initialPenalties.forEach(penalty => {
this.penalties.set(penalty.id, penalty); this.penalties.set(penalty.id, penalty);
this.logger.debug(`Seeded penalty: ${penalty.id}`);
}); });
} }
async findById(id: string): Promise<Penalty | null> { async findById(id: string): Promise<Penalty | null> {
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<Penalty[]> { async findByRaceId(raceId: string): Promise<Penalty[]> {
return Array.from(this.penalties.values()).filter( this.logger.debug(`Finding penalties by race id: ${raceId}`);
penalty => penalty.raceId === 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<Penalty[]> { async findByDriverId(driverId: string): Promise<Penalty[]> {
return Array.from(this.penalties.values()).filter( this.logger.debug(`Finding penalties by driver id: ${driverId}`);
penalty => penalty.driverId === 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<Penalty[]> { async findByProtestId(protestId: string): Promise<Penalty[]> {
return Array.from(this.penalties.values()).filter( this.logger.debug(`Finding penalties by protest id: ${protestId}`);
penalty => penalty.protestId === 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<Penalty[]> { async findPending(): Promise<Penalty[]> {
return Array.from(this.penalties.values()).filter( this.logger.debug('Finding pending penalties.');
penalty => penalty.isPending() 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<Penalty[]> { async findIssuedBy(stewardId: string): Promise<Penalty[]> {
return Array.from(this.penalties.values()).filter( this.logger.debug(`Finding penalties issued by steward: ${stewardId}`);
penalty => penalty.issuedBy === 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<void> { async create(penalty: Penalty): Promise<void> {
if (this.penalties.has(penalty.id)) { this.logger.debug(`Creating penalty: ${penalty.id}`);
throw new Error(`Penalty with ID ${penalty.id} already exists`); 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<void> { async update(penalty: Penalty): Promise<void> {
if (!this.penalties.has(penalty.id)) { this.logger.debug(`Updating penalty: ${penalty.id}`);
throw new Error(`Penalty with ID ${penalty.id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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;
}
} }
} }

View File

@@ -6,65 +6,146 @@
import type { Protest } from '../../domain/entities/Protest'; import type { Protest } from '../../domain/entities/Protest';
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository'; import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryProtestRepository implements IProtestRepository { export class InMemoryProtestRepository implements IProtestRepository {
private protests: Map<string, Protest> = new Map(); private protests: Map<string, Protest> = 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 => { initialProtests.forEach(protest => {
this.protests.set(protest.id, protest); this.protests.set(protest.id, protest);
this.logger.debug(`Seeded protest: ${protest.id}`);
}); });
} }
async findById(id: string): Promise<Protest | null> { async findById(id: string): Promise<Protest | null> {
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<Protest[]> { async findByRaceId(raceId: string): Promise<Protest[]> {
return Array.from(this.protests.values()).filter( this.logger.debug(`Finding protests by race id: ${raceId}`);
protest => protest.raceId === 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<Protest[]> { async findByProtestingDriverId(driverId: string): Promise<Protest[]> {
return Array.from(this.protests.values()).filter( this.logger.debug(`Finding protests by protesting driver id: ${driverId}`);
protest => protest.protestingDriverId === 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<Protest[]> { async findByAccusedDriverId(driverId: string): Promise<Protest[]> {
return Array.from(this.protests.values()).filter( this.logger.debug(`Finding protests by accused driver id: ${driverId}`);
protest => protest.accusedDriverId === 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<Protest[]> { async findPending(): Promise<Protest[]> {
return Array.from(this.protests.values()).filter( this.logger.debug('Finding pending protests.');
protest => protest.isPending() 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<Protest[]> { async findUnderReviewBy(stewardId: string): Promise<Protest[]> {
return Array.from(this.protests.values()).filter( this.logger.debug(`Finding protests under review by steward: ${stewardId}`);
protest => protest.reviewedBy === stewardId && protest.isUnderReview() 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<void> { async create(protest: Protest): Promise<void> {
if (this.protests.has(protest.id)) { this.logger.debug(`Creating protest: ${protest.id}`);
throw new Error(`Protest with ID ${protest.id} already exists`); 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<void> { async update(protest: Protest): Promise<void> {
if (!this.protests.has(protest.id)) { this.logger.debug(`Updating protest: ${protest.id}`);
throw new Error(`Protest with ID ${protest.id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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;
}
} }
} }

View File

@@ -3,70 +3,178 @@
*/ */
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository'; import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
import type { RaceEvent } from '../../domain/entities/RaceEvent'; import type { RaceEvent } from '../../domain/entities/RaceEvent';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryRaceEventRepository implements IRaceEventRepository { export class InMemoryRaceEventRepository implements IRaceEventRepository {
private raceEvents: Map<string, RaceEvent> = new Map(); private raceEvents: Map<string, RaceEvent> = 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<RaceEvent | null> { async findById(id: string): Promise<RaceEvent | null> {
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<RaceEvent[]> { async findAll(): Promise<RaceEvent[]> {
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<RaceEvent[]> { async findBySeasonId(seasonId: string): Promise<RaceEvent[]> {
return Array.from(this.raceEvents.values()).filter( this.logger.debug(`Finding race events by season id: ${seasonId}`);
raceEvent => raceEvent.seasonId === 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<RaceEvent[]> { async findByLeagueId(leagueId: string): Promise<RaceEvent[]> {
return Array.from(this.raceEvents.values()).filter( this.logger.debug(`Finding race events by league id: ${leagueId}`);
raceEvent => raceEvent.leagueId === 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<RaceEvent[]> { async findByStatus(status: string): Promise<RaceEvent[]> {
return Array.from(this.raceEvents.values()).filter( this.logger.debug(`Finding race events by status: ${status}`);
raceEvent => raceEvent.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<RaceEvent[]> { async findAwaitingStewardingClose(): Promise<RaceEvent[]> {
const now = new Date(); this.logger.debug('Finding race events awaiting stewarding close.');
return Array.from(this.raceEvents.values()).filter( try {
raceEvent => const now = new Date();
raceEvent.status === 'awaiting_stewarding' && const events = Array.from(this.raceEvents.values()).filter(
raceEvent.stewardingClosesAt && raceEvent =>
raceEvent.stewardingClosesAt <= now 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<RaceEvent> { async create(raceEvent: RaceEvent): Promise<RaceEvent> {
this.raceEvents.set(raceEvent.id, raceEvent); this.logger.debug(`Creating race event: ${raceEvent.id}`);
return raceEvent; 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<RaceEvent> { async update(raceEvent: RaceEvent): Promise<RaceEvent> {
this.raceEvents.set(raceEvent.id, raceEvent); this.logger.debug(`Updating race event: ${raceEvent.id}`);
return raceEvent; 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 // Test helper methods
clear(): void { clear(): void {
this.logger.debug('Clearing all race events.');
this.raceEvents.clear(); this.raceEvents.clear();
this.logger.info('All race events cleared.');
} }
getAll(): RaceEvent[] { 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;
}
} }
} }

View File

@@ -5,6 +5,7 @@
* Stores race registrations in Maps keyed by raceId and driverId. * 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 { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository'; import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
@@ -13,12 +14,16 @@ type RaceRegistrationSeed = Pick<RaceRegistration, 'raceId' | 'driverId' | 'regi
export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository { export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
private registrationsByRace: Map<string, Set<string>>; private registrationsByRace: Map<string, Set<string>>;
private registrationsByDriver: Map<string, Set<string>>; private registrationsByDriver: Map<string, Set<string>>;
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.registrationsByRace = new Map();
this.registrationsByDriver = new Map(); this.registrationsByDriver = new Map();
if (seedRegistrations) { if (seedRegistrations) {
this.logger.debug('Seeding with initial registrations', { count: seedRegistrations.length });
seedRegistrations.forEach((registration) => { seedRegistrations.forEach((registration) => {
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt); 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 { 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); let raceSet = this.registrationsByRace.get(raceId);
if (!raceSet) { if (!raceSet) {
raceSet = new Set(); raceSet = new Set();
this.registrationsByRace.set(raceId, raceSet); this.registrationsByRace.set(raceId, raceSet);
this.logger.debug('Created new race set as none existed', { raceId });
} }
raceSet.add(driverId); raceSet.add(driverId);
this.logger.debug('Added driver to race set', { raceId, driverId });
let driverSet = this.registrationsByDriver.get(driverId); let driverSet = this.registrationsByDriver.get(driverId);
if (!driverSet) { if (!driverSet) {
driverSet = new Set(); driverSet = new Set();
this.registrationsByDriver.set(driverId, driverSet); this.registrationsByDriver.set(driverId, driverSet);
this.logger.debug('Created new driver set as none existed', { driverId });
} }
driverSet.add(raceId); 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 { 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); const raceSet = this.registrationsByRace.get(raceId);
if (raceSet) { if (raceSet) {
raceSet.delete(driverId); raceSet.delete(driverId);
this.logger.debug('Removed driver from race set', { raceId, driverId });
if (raceSet.size === 0) { if (raceSet.size === 0) {
this.registrationsByRace.delete(raceId); 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); const driverSet = this.registrationsByDriver.get(driverId);
if (driverSet) { if (driverSet) {
driverSet.delete(raceId); driverSet.delete(raceId);
this.logger.debug('Removed race from driver set', { raceId, driverId });
if (driverSet.size === 0) { if (driverSet.size === 0) {
this.registrationsByDriver.delete(driverId); this.registrationsByDriver.delete(driverId);
this.logger.debug('Deleted driver set as it is now empty', { driverId });
} }
} else {
this.logger.warn('Driver set not found during removal, potential inconsistency', { driverId });
} }
this.logger.info('Successfully removed race registration from indexes', { raceId, driverId });
} }
async isRegistered(raceId: string, driverId: string): Promise<boolean> { async isRegistered(raceId: string, driverId: string): Promise<boolean> {
this.logger.info('Checking if driver is registered for race', { raceId, driverId });
const raceSet = this.registrationsByRace.get(raceId); const raceSet = this.registrationsByRace.get(raceId);
if (!raceSet) return false; if (!raceSet) {
return raceSet.has(driverId); 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<string[]> { async getRegisteredDrivers(raceId: string): Promise<string[]> {
this.logger.info('Attempting to fetch registered drivers for race', { raceId });
const raceSet = this.registrationsByRace.get(raceId); const raceSet = this.registrationsByRace.get(raceId);
if (!raceSet) return []; if (!raceSet) {
return Array.from(raceSet.values()); 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<number> { async getRegistrationCount(raceId: string): Promise<number> {
this.logger.info('Attempting to get registration count for race', { raceId });
const raceSet = this.registrationsByRace.get(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<void> { async register(registration: RaceRegistration): Promise<void> {
this.logger.info('Attempting to register driver for race', { raceId: registration.raceId, driverId: registration.driverId });
const alreadyRegistered = await this.isRegistered(registration.raceId, registration.driverId); const alreadyRegistered = await this.isRegistered(registration.raceId, registration.driverId);
if (alreadyRegistered) { 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'); throw new Error('Already registered for this race');
} }
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt); 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<void> { async withdraw(raceId: string, driverId: string): Promise<void> {
this.logger.info('Attempting to withdraw driver from race', { raceId, driverId });
const alreadyRegistered = await this.isRegistered(raceId, driverId); const alreadyRegistered = await this.isRegistered(raceId, driverId);
if (!alreadyRegistered) { if (!alreadyRegistered) {
this.logger.warn('Driver not registered for race, withdrawal aborted', { raceId, driverId });
throw new Error('Not registered for this race'); throw new Error('Not registered for this race');
} }
this.removeFromIndexes(raceId, driverId); this.removeFromIndexes(raceId, driverId);
this.logger.info('Driver successfully withdrew from race', { raceId, driverId });
} }
async getDriverRegistrations(driverId: string): Promise<string[]> { async getDriverRegistrations(driverId: string): Promise<string[]> {
this.logger.info('Attempting to fetch registrations for driver', { driverId });
const driverSet = this.registrationsByDriver.get(driverId); const driverSet = this.registrationsByDriver.get(driverId);
if (!driverSet) return []; if (!driverSet) {
return Array.from(driverSet.values()); 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<void> { async clearRaceRegistrations(raceId: string): Promise<void> {
this.logger.info('Attempting to clear all registrations for race', { raceId });
const raceSet = this.registrationsByRace.get(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()) { for (const driverId of raceSet.values()) {
const driverSet = this.registrationsByDriver.get(driverId); const driverSet = this.registrationsByDriver.get(driverId);
if (driverSet) { if (driverSet) {
driverSet.delete(raceId); driverSet.delete(raceId);
if (driverSet.size === 0) { if (driverSet.size === 0) {
this.registrationsByDriver.delete(driverId); 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.registrationsByRace.delete(raceId);
this.logger.info('Successfully cleared all registrations for race', { raceId });
} }
} }

View File

@@ -8,97 +8,194 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race'; import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository'; import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryRaceRepository implements IRaceRepository { export class InMemoryRaceRepository implements IRaceRepository {
private races: Map<string, Race>; private races: Map<string, Race>;
private readonly logger: ILogger;
constructor(seedData?: Race[]) { constructor(logger: ILogger, seedData?: Race[]) {
this.logger = logger;
this.logger.info('InMemoryRaceRepository initialized.');
this.races = new Map(); this.races = new Map();
if (seedData) { if (seedData) {
seedData.forEach(race => { seedData.forEach(race => {
this.races.set(race.id, race); this.races.set(race.id, race);
this.logger.debug(`Seeded race: ${race.id}.`);
}); });
} }
} }
async findById(id: string): Promise<Race | null> { async findById(id: string): Promise<Race | null> {
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<Race[]> { async findAll(): Promise<Race[]> {
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<Race[]> { async findByLeagueId(leagueId: string): Promise<Race[]> {
return Array.from(this.races.values()) this.logger.debug(`Finding races by league id: ${leagueId}`);
.filter(race => race.leagueId === leagueId) try {
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); 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<Race[]> { async findUpcomingByLeagueId(leagueId: string): Promise<Race[]> {
const now = new Date(); this.logger.debug(`Finding upcoming races by league id: ${leagueId}`);
return Array.from(this.races.values()) try {
.filter(race => const now = new Date();
race.leagueId === leagueId && const races = Array.from(this.races.values())
race.status === 'scheduled' && .filter(race =>
race.scheduledAt > now race.leagueId === leagueId &&
) race.status === 'scheduled' &&
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); 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<Race[]> { async findCompletedByLeagueId(leagueId: string): Promise<Race[]> {
return Array.from(this.races.values()) this.logger.debug(`Finding completed races by league id: ${leagueId}`);
.filter(race => try {
race.leagueId === leagueId && const races = Array.from(this.races.values())
race.status === 'completed' .filter(race =>
) race.leagueId === leagueId &&
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime()); 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<Race[]> { async findByStatus(status: RaceStatus): Promise<Race[]> {
return Array.from(this.races.values()) this.logger.debug(`Finding races by status: ${status}`);
.filter(race => race.status === status) try {
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); 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<Race[]> { async findByDateRange(startDate: Date, endDate: Date): Promise<Race[]> {
return Array.from(this.races.values()) this.logger.debug(`Finding races by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
.filter(race => try {
race.scheduledAt >= startDate && const races = Array.from(this.races.values())
race.scheduledAt <= endDate .filter(race =>
) race.scheduledAt >= startDate &&
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); 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<Race> { async create(race: Race): Promise<Race> {
if (await this.exists(race.id)) { this.logger.debug(`Creating race: ${race.id}`);
throw new Error(`Race with ID ${race.id} already exists`); try {
} if (await this.exists(race.id)) {
this.logger.warn(`Race with ID ${race.id} already exists.`);
throw new Error(`Race with ID ${race.id} already exists`);
}
this.races.set(race.id, race); this.races.set(race.id, race);
return 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<Race> { async update(race: Race): Promise<Race> {
if (!await this.exists(race.id)) { this.logger.debug(`Updating race: ${race.id}`);
throw new Error(`Race with ID ${race.id} not found`); try {
} if (!await this.exists(race.id)) {
this.logger.warn(`Race with ID ${race.id} not found for update.`);
throw new Error(`Race with ID ${race.id} not found`);
}
this.races.set(race.id, race); this.races.set(race.id, race);
return 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<void> { async delete(id: string): Promise<void> {
if (!await this.exists(id)) { this.logger.debug(`Deleting race: ${id}`);
throw new Error(`Race with ID ${id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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;
}
} }
/** /**

View File

@@ -9,112 +9,221 @@ import { v4 as uuidv4 } from 'uuid';
import { Result } from '@gridpilot/racing/domain/entities/Result'; import { Result } from '@gridpilot/racing/domain/entities/Result';
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository'; import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository'; import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryResultRepository implements IResultRepository { export class InMemoryResultRepository implements IResultRepository {
private results: Map<string, Result>; private results: Map<string, Result>;
private raceRepository: IRaceRepository | null; 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.results = new Map();
this.raceRepository = raceRepository ?? null; this.raceRepository = raceRepository ?? null;
if (seedData) { if (seedData) {
seedData.forEach(result => { seedData.forEach(result => {
this.results.set(result.id, result); this.results.set(result.id, result);
this.logger.debug(`Seeded result: ${result.id}`);
}); });
} }
} }
async findById(id: string): Promise<Result | null> { async findById(id: string): Promise<Result | null> {
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<Result[]> { async findAll(): Promise<Result[]> {
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<Result[]> { async findByRaceId(raceId: string): Promise<Result[]> {
return Array.from(this.results.values()) this.logger.debug(`Finding results for race id: ${raceId}`);
.filter(result => result.raceId === raceId) try {
.sort((a, b) => a.position - b.position); 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<Result[]> { async findByDriverId(driverId: string): Promise<Result[]> {
return Array.from(this.results.values()) this.logger.debug(`Finding results for driver id: ${driverId}`);
.filter(result => result.driverId === 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<Result[]> { async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Result[]> {
if (!this.raceRepository) { this.logger.debug(`Finding results for driver id: ${driverId} and league id: ${leagueId}`);
return []; 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<Result> { async create(result: Result): Promise<Result> {
if (await this.exists(result.id)) { this.logger.debug(`Creating result: ${result.id}`);
throw new Error(`Result with ID ${result.id} already exists`); 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); this.results.set(result.id, result);
return 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<Result[]> { async createMany(results: Result[]): Promise<Result[]> {
const created: Result[] = []; this.logger.debug(`Creating ${results.length} results.`);
try {
for (const result of results) { const created: Result[] = [];
if (await this.exists(result.id)) {
throw new Error(`Result with ID ${result.id} already exists`); 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); this.logger.info(`Created ${created.length} results successfully.`);
created.push(result);
return created;
} catch (error) {
this.logger.error(`Error creating many results:`, error);
throw error;
} }
return created;
} }
async update(result: Result): Promise<Result> { async update(result: Result): Promise<Result> {
if (!await this.exists(result.id)) { this.logger.debug(`Updating result: ${result.id}`);
throw new Error(`Result with ID ${result.id} not found`); 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); this.results.set(result.id, result);
return 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<void> { async delete(id: string): Promise<void> {
if (!await this.exists(id)) { this.logger.debug(`Deleting result: ${id}`);
throw new Error(`Result with ID ${id} not found`); 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<void> { async deleteByRaceId(raceId: string): Promise<void> {
const raceResults = await this.findByRaceId(raceId); this.logger.debug(`Deleting results for race id: ${raceId}`);
raceResults.forEach(result => { try {
this.results.delete(result.id); 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<boolean> { async exists(id: string): Promise<boolean> {
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<boolean> { async existsByRaceId(raceId: string): Promise<boolean> {
return Array.from(this.results.values()).some( this.logger.debug(`Checking existence of results for race id: ${raceId}`);
result => result.raceId === 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 * Utility method to generate a new UUID

View File

@@ -13,6 +13,22 @@ import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/r
import { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding'; import { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding';
import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType'; import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType';
import type { ParticipantRef } from '@gridpilot/racing/domain/types/ParticipantRef'; 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 = export type LeagueScoringPresetPrimaryChampionshipType =
| 'driver' | 'driver'
@@ -248,70 +264,168 @@ export function getLeagueScoringPresetById(
export class InMemoryGameRepository implements IGameRepository { export class InMemoryGameRepository implements IGameRepository {
private games: Game[]; 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] : []; this.games = seedData ? [...seedData] : [];
} }
async findById(id: string): Promise<Game | null> { async findById(id: string): Promise<Game | null> {
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<Game[]> { async findAll(): Promise<Game[]> {
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 { 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 { export class InMemorySeasonRepository implements ISeasonRepository {
private seasons: Season[]; 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] : []; this.seasons = seedData ? [...seedData] : [];
} }
async findById(id: string): Promise<Season | null> { async findById(id: string): Promise<Season | null> {
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<Season[]> { async findByLeagueId(leagueId: string): Promise<Season[]> {
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<Season> { async create(season: Season): Promise<Season> {
// Backward-compatible alias for add() this.logger.debug(`Creating season: ${season.id}`);
this.seasons.push(season); try {
return season; // 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<void> { async add(season: Season): Promise<void> {
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<void> { async update(season: Season): Promise<void> {
const index = this.seasons.findIndex((s) => s.id === season.id); this.logger.debug(`Updating season: ${season.id}`);
if (index === -1) { try {
this.seasons.push(season); const index = this.seasons.findIndex((s) => s.id === season.id);
return; 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<Season[]> { async listByLeague(leagueId: string): Promise<Season[]> {
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<Season[]> { async listActiveByLeague(leagueId: string): Promise<Season[]> {
return this.seasons.filter( this.logger.debug(`Listing active seasons by league id: ${leagueId}`);
(s) => s.leagueId === leagueId && s.status === 'active', 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 { 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 implements ILeagueScoringConfigRepository
{ {
private configs: LeagueScoringConfig[]; 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] : []; this.configs = seedData ? [...seedData] : [];
} }
async findBySeasonId(seasonId: string): Promise<LeagueScoringConfig | null> { async findBySeasonId(seasonId: string): Promise<LeagueScoringConfig | null> {
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<LeagueScoringConfig> { async save(config: LeagueScoringConfig): Promise<LeagueScoringConfig> {
const existingIndex = this.configs.findIndex( this.logger.debug(`Saving league scoring config: ${config.id} for seasonId: ${config.seasonId}`);
(c) => c.id === config.id, try {
); const existingIndex = this.configs.findIndex(
if (existingIndex >= 0) { (c) => c.id === config.id,
this.configs[existingIndex] = config; );
} else { if (existingIndex >= 0) {
this.configs.push(config); 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 { 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 implements IChampionshipStandingRepository
{ {
private standings: ChampionshipStanding[] = []; 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( async findBySeasonAndChampionship(
seasonId: string, seasonId: string,
championshipId: string, championshipId: string,
): Promise<ChampionshipStanding[]> { ): Promise<ChampionshipStanding[]> {
return this.standings.filter( this.logger.debug(`Finding championship standings for season: ${seasonId}, championship: ${championshipId}`);
(s) => s.seasonId === seasonId && s.championshipId === 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<void> { async saveAll(standings: ChampionshipStanding[]): Promise<void> {
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 { 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[] { 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 seasonId = params.seasonId ?? 'season-sprint-main-demo';
const championshipId = 'driver-champ'; const championshipId = 'driver-champ';
const logger = new SilentLogger();
const game = Game.create({ id: 'iracing', name: 'iRacing' }); const game = Game.create({ id: 'iracing', name: 'iRacing' });
const season = Season.create({ const season = Season.create({
@@ -410,12 +593,12 @@ export function createSprintMainDemoScoringSetup(params: {
seasonId: season.id, seasonId: season.id,
}); });
const gameRepo = new InMemoryGameRepository([game]); const gameRepo = new InMemoryGameRepository(logger, [game]);
const seasonRepo = new InMemorySeasonRepository([season]); const seasonRepo = new InMemorySeasonRepository(logger, [season]);
const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository([ const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository(logger, [
leagueScoringConfig, leagueScoringConfig,
]); ]);
const championshipStandingRepo = new InMemoryChampionshipStandingRepository(); const championshipStandingRepo = new InMemoryChampionshipStandingRepository(logger);
return { return {
gameRepo, gameRepo,

View File

@@ -6,67 +6,165 @@
import type { SeasonSponsorship, SponsorshipTier } from '../../domain/entities/SeasonSponsorship'; import type { SeasonSponsorship, SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository'; import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRepository { export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRepository {
private sponsorships: Map<string, SeasonSponsorship> = new Map(); private sponsorships: Map<string, SeasonSponsorship> = 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<SeasonSponsorship | null> { async findById(id: string): Promise<SeasonSponsorship | null> {
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<SeasonSponsorship[]> { async findBySeasonId(seasonId: string): Promise<SeasonSponsorship[]> {
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<SeasonSponsorship[]> { async findByLeagueId(leagueId: string): Promise<SeasonSponsorship[]> {
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<SeasonSponsorship[]> { async findBySponsorId(sponsorId: string): Promise<SeasonSponsorship[]> {
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<SeasonSponsorship[]> { async findBySeasonAndTier(seasonId: string, tier: SponsorshipTier): Promise<SeasonSponsorship[]> {
return Array.from(this.sponsorships.values()).filter( this.logger.debug(`Finding season sponsorships by season id: ${seasonId} and tier: ${tier}`);
s => s.seasonId === seasonId && s.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<SeasonSponsorship> { async create(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship> {
if (this.sponsorships.has(sponsorship.id)) { this.logger.debug(`Creating season sponsorship: ${sponsorship.id}`);
throw new Error('SeasonSponsorship with this ID already exists'); 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<SeasonSponsorship> { async update(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship> {
if (!this.sponsorships.has(sponsorship.id)) { this.logger.debug(`Updating season sponsorship: ${sponsorship.id}`);
throw new Error('SeasonSponsorship not found'); 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 initial data
*/ */
seed(sponsorships: SeasonSponsorship[]): void { seed(sponsorships: SeasonSponsorship[]): void {
for (const sponsorship of sponsorships) { this.logger.debug(`Seeding ${sponsorships.length} season sponsorships.`);
this.sponsorships.set(sponsorship.id, sponsorship); 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 // Test helper
clear(): void { clear(): void {
this.logger.debug('Clearing all season sponsorships.');
this.sponsorships.clear(); this.sponsorships.clear();
this.logger.info('All season sponsorships cleared.');
} }
} }

View File

@@ -3,60 +3,158 @@
*/ */
import type { ISessionRepository } from '../../domain/repositories/ISessionRepository'; import type { ISessionRepository } from '../../domain/repositories/ISessionRepository';
import type { Session } from '../../domain/entities/Session'; import type { Session } from '../../domain/entities/Session';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemorySessionRepository implements ISessionRepository { export class InMemorySessionRepository implements ISessionRepository {
private sessions: Map<string, Session> = new Map(); private sessions: Map<string, Session> = 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<Session | null> { async findById(id: string): Promise<Session | null> {
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<Session[]> { async findAll(): Promise<Session[]> {
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<Session[]> { async findByRaceEventId(raceEventId: string): Promise<Session[]> {
return Array.from(this.sessions.values()).filter( this.logger.debug(`Finding sessions by race event id: ${raceEventId}`);
session => session.raceEventId === 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<Session[]> { async findByLeagueId(leagueId: string): Promise<Session[]> {
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 // Sessions don't have leagueId directly - would need to join with RaceEvent
// For now, return empty array // For now, return empty array
return []; return [];
} }
async findByStatus(status: string): Promise<Session[]> { async findByStatus(status: string): Promise<Session[]> {
return Array.from(this.sessions.values()).filter( this.logger.debug(`Finding sessions by status: ${status}`);
session => session.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<Session> { async create(session: Session): Promise<Session> {
this.sessions.set(session.id, session); this.logger.debug(`Creating session: ${session.id}`);
return session; 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<Session> { async update(session: Session): Promise<Session> {
this.sessions.set(session.id, session); this.logger.debug(`Updating session: ${session.id}`);
return session; 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 // Test helper methods
clear(): void { clear(): void {
this.logger.debug('Clearing all sessions.');
this.sessions.clear(); this.sessions.clear();
this.logger.info('All sessions cleared.');
} }
getAll(): Session[] { 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;
}
} }
} }

View File

@@ -6,62 +6,144 @@
import type { Sponsor } from '../../domain/entities/Sponsor'; import type { Sponsor } from '../../domain/entities/Sponsor';
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository'; import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemorySponsorRepository implements ISponsorRepository { export class InMemorySponsorRepository implements ISponsorRepository {
private sponsors: Map<string, Sponsor> = new Map(); private sponsors: Map<string, Sponsor> = 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<Sponsor | null> { async findById(id: string): Promise<Sponsor | null> {
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<Sponsor[]> { async findAll(): Promise<Sponsor[]> {
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<Sponsor | null> { async findByEmail(email: string): Promise<Sponsor | null> {
for (const sponsor of this.sponsors.values()) { this.logger.debug(`Finding sponsor by email: ${email}`);
if (sponsor.contactEmail === email) { try {
return sponsor; 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<Sponsor> { async create(sponsor: Sponsor): Promise<Sponsor> {
if (this.sponsors.has(sponsor.id)) { this.logger.debug(`Creating sponsor: ${sponsor.id}`);
throw new Error('Sponsor with this ID already exists'); 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<Sponsor> { async update(sponsor: Sponsor): Promise<Sponsor> {
if (!this.sponsors.has(sponsor.id)) { this.logger.debug(`Updating sponsor: ${sponsor.id}`);
throw new Error('Sponsor not found'); 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 initial data
*/ */
seed(sponsors: Sponsor[]): void { seed(sponsors: Sponsor[]): void {
for (const sponsor of sponsors) { this.logger.debug(`Seeding ${sponsors.length} sponsors.`);
this.sponsors.set(sponsor.id, sponsor); 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 // Test helper
clear(): void { clear(): void {
this.logger.debug('Clearing all sponsors.');
this.sponsors.clear(); this.sponsors.clear();
this.logger.info('All sponsors cleared.');
} }
} }

View File

@@ -5,6 +5,7 @@
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository'; import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
import { SponsorshipPricing } from '../../domain/value-objects/SponsorshipPricing'; import { SponsorshipPricing } from '../../domain/value-objects/SponsorshipPricing';
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest'; import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
interface StorageKey { interface StorageKey {
entityType: SponsorableEntityType; entityType: SponsorableEntityType;
@@ -13,48 +14,115 @@ interface StorageKey {
export class InMemorySponsorshipPricingRepository implements ISponsorshipPricingRepository { export class InMemorySponsorshipPricingRepository implements ISponsorshipPricingRepository {
private pricings: Map<string, { entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }> = new Map(); private pricings: Map<string, { entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }> = 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 { private makeKey(entityType: SponsorableEntityType, entityId: string): string {
return `${entityType}:${entityId}`; return `${entityType}:${entityId}`;
} }
async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipPricing | null> { async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipPricing | null> {
const key = this.makeKey(entityType, entityId); this.logger.debug(`Finding sponsorship pricing for entity: ${entityType}, ${entityId}`);
const entry = this.pricings.get(key); try {
return entry?.pricing ?? null; 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<void> { async save(entityType: SponsorableEntityType, entityId: string, pricing: SponsorshipPricing): Promise<void> {
const key = this.makeKey(entityType, entityId); this.logger.debug(`Saving sponsorship pricing for entity: ${entityType}, ${entityId}`);
this.pricings.set(key, { entityType, entityId, pricing }); 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<void> { async delete(entityType: SponsorableEntityType, entityId: string): Promise<void> {
const key = this.makeKey(entityType, entityId); this.logger.debug(`Deleting sponsorship pricing for entity: ${entityType}, ${entityId}`);
this.pricings.delete(key); 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<boolean> { async exists(entityType: SponsorableEntityType, entityId: string): Promise<boolean> {
const key = this.makeKey(entityType, entityId); this.logger.debug(`Checking existence of sponsorship pricing for entity: ${entityType}, ${entityId}`);
return this.pricings.has(key); 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<Array<{ async findAcceptingApplications(entityType: SponsorableEntityType): Promise<Array<{
entityId: string; entityId: string;
pricing: SponsorshipPricing; pricing: SponsorshipPricing;
}>> { }>> {
return Array.from(this.pricings.values()) this.logger.debug(`Finding entities accepting applications for type: ${entityType}`);
.filter(entry => entry.entityType === entityType && entry.pricing.acceptingApplications) try {
.map(entry => ({ entityId: entry.entityId, pricing: entry.pricing })); 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 initial data
*/ */
seed(data: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>): void { seed(data: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>): void {
for (const entry of data) { this.logger.debug(`Seeding ${data.length} sponsorship pricing entries.`);
const key = this.makeKey(entry.entityType, entry.entityId); try {
this.pricings.set(key, entry); 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 all data (for testing)
*/ */
clear(): void { clear(): void {
this.logger.debug('Clearing all sponsorship pricing data.');
this.pricings.clear(); this.pricings.clear();
this.logger.info('All sponsorship pricing data cleared.');
} }
} }

View File

@@ -8,100 +8,225 @@ import {
type SponsorableEntityType, type SponsorableEntityType,
type SponsorshipRequestStatus type SponsorshipRequestStatus
} from '../../domain/entities/SponsorshipRequest'; } from '../../domain/entities/SponsorshipRequest';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemorySponsorshipRequestRepository implements ISponsorshipRequestRepository { export class InMemorySponsorshipRequestRepository implements ISponsorshipRequestRepository {
private requests: Map<string, SponsorshipRequest> = new Map(); private requests: Map<string, SponsorshipRequest> = new Map();
private readonly logger: ILogger;
constructor(logger: ILogger, seedData?: SponsorshipRequest[]) {
this.logger = logger;
this.logger.info('InMemorySponsorshipRequestRepository initialized.');
if (seedData) {
this.seed(seedData);
}
}
async findById(id: string): Promise<SponsorshipRequest | null> { async findById(id: string): Promise<SponsorshipRequest | null> {
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<SponsorshipRequest[]> { async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
return Array.from(this.requests.values()).filter( this.logger.debug(`Finding sponsorship requests by entity: ${entityType}, ${entityId}`);
request => request.entityType === entityType && request.entityId === 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<SponsorshipRequest[]> { async findPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
return Array.from(this.requests.values()).filter( this.logger.debug(`Finding pending sponsorship requests by entity: ${entityType}, ${entityId}`);
request => try {
request.entityType === entityType && const requests = Array.from(this.requests.values()).filter(
request.entityId === entityId && request =>
request.status === 'pending' 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<SponsorshipRequest[]> { async findBySponsorId(sponsorId: string): Promise<SponsorshipRequest[]> {
return Array.from(this.requests.values()).filter( this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId}`);
request => request.sponsorId === 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<SponsorshipRequest[]> { async findByStatus(status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
return Array.from(this.requests.values()).filter( this.logger.debug(`Finding sponsorship requests by status: ${status}`);
request => request.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<SponsorshipRequest[]> { async findBySponsorIdAndStatus(sponsorId: string, status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
return Array.from(this.requests.values()).filter( this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId} and status: ${status}`);
request => request.sponsorId === sponsorId && request.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<boolean> { async hasPendingRequest(sponsorId: string, entityType: SponsorableEntityType, entityId: string): Promise<boolean> {
return Array.from(this.requests.values()).some( this.logger.debug(`Checking for pending request from sponsor: ${sponsorId} for entity: ${entityType}, ${entityId}`);
request => try {
request.sponsorId === sponsorId && const has = Array.from(this.requests.values()).some(
request.entityType === entityType && request =>
request.entityId === entityId && request.sponsorId === sponsorId &&
request.status === 'pending' 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<number> { async countPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<number> {
return Array.from(this.requests.values()).filter( this.logger.debug(`Counting pending requests for entity: ${entityType}, ${entityId}`);
request => try {
request.entityType === entityType && const count = Array.from(this.requests.values()).filter(
request.entityId === entityId && request =>
request.status === 'pending' request.entityType === entityType &&
).length; 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<SponsorshipRequest> { async create(request: SponsorshipRequest): Promise<SponsorshipRequest> {
this.requests.set(request.id, request); this.logger.debug(`Creating sponsorship request: ${request.id}`);
return request; 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<SponsorshipRequest> { async update(request: SponsorshipRequest): Promise<SponsorshipRequest> {
if (!this.requests.has(request.id)) { this.logger.debug(`Updating sponsorship request: ${request.id}`);
throw new Error(`SponsorshipRequest ${request.id} not found`); try {
} if (!this.requests.has(request.id)) {
this.requests.set(request.id, request); this.logger.warn(`SponsorshipRequest ${request.id} not found for update.`);
return request; 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 initial data
*/ */
seed(requests: SponsorshipRequest[]): void { seed(requests: SponsorshipRequest[]): void {
for (const request of requests) { this.logger.debug(`Seeding ${requests.length} sponsorship requests.`);
this.requests.set(request.id, request); 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 all data (for testing)
*/ */
clear(): void { clear(): void {
this.requests.clear(); this.logger.debug('Clearing all sponsorship requests.');
this.requests.clear();
this.logger.info('All sponsorship requests cleared.');
} }
} }

View File

@@ -10,6 +10,7 @@ import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository'; import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository'; import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository'; import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
/** /**
* Points systems presets * Points systems presets
@@ -31,13 +32,17 @@ export class InMemoryStandingRepository implements IStandingRepository {
private resultRepository: IResultRepository | null; private resultRepository: IResultRepository | null;
private raceRepository: IRaceRepository | null; private raceRepository: IRaceRepository | null;
private leagueRepository: ILeagueRepository | null; private leagueRepository: ILeagueRepository | null;
private readonly logger: ILogger;
constructor( constructor(
logger: ILogger,
seedData?: Standing[], seedData?: Standing[],
resultRepository?: IResultRepository | null, resultRepository?: IResultRepository | null,
raceRepository?: IRaceRepository | null, raceRepository?: IRaceRepository | null,
leagueRepository?: ILeagueRepository | null leagueRepository?: ILeagueRepository | null
) { ) {
this.logger = logger;
this.logger.info('InMemoryStandingRepository initialized.');
this.standings = new Map(); this.standings = new Map();
this.resultRepository = resultRepository ?? null; this.resultRepository = resultRepository ?? null;
this.raceRepository = raceRepository ?? null; this.raceRepository = raceRepository ?? null;
@@ -47,6 +52,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
seedData.forEach(standing => { seedData.forEach(standing => {
const key = this.getKey(standing.leagueId, standing.driverId); const key = this.getKey(standing.leagueId, standing.driverId);
this.standings.set(key, standing); 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<Standing[]> { async findByLeagueId(leagueId: string): Promise<Standing[]> {
return Array.from(this.standings.values()) this.logger.debug(`Finding standings for league id: ${leagueId}`);
.filter(standing => standing.leagueId === leagueId) try {
.sort((a, b) => { const standings = Array.from(this.standings.values())
// Sort by position (lower is better) .filter(standing => standing.leagueId === leagueId)
if (a.position !== b.position) { .sort((a, b) => {
return a.position - b.position; // Sort by position (lower is better)
} if (a.position !== b.position) {
// If positions are equal, sort by points (higher is better) return a.position - b.position;
return b.points - a.points; }
}); // 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<Standing | null> { async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Standing | null> {
const key = this.getKey(leagueId, driverId); this.logger.debug(`Finding standing for driver: ${driverId}, league: ${leagueId}`);
return this.standings.get(key) ?? null; 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<Standing[]> { async findAll(): Promise<Standing[]> {
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<Standing> { async save(standing: Standing): Promise<Standing> {
const key = this.getKey(standing.leagueId, standing.driverId); this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`);
this.standings.set(key, standing); try {
return standing; 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<Standing[]> { async saveMany(standings: Standing[]): Promise<Standing[]> {
standings.forEach(standing => { this.logger.debug(`Saving ${standings.length} standings.`);
const key = this.getKey(standing.leagueId, standing.driverId); try {
this.standings.set(key, standing); standings.forEach(standing => {
}); const key = this.getKey(standing.leagueId, standing.driverId);
return standings; 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<void> { async delete(leagueId: string, driverId: string): Promise<void> {
const key = this.getKey(leagueId, driverId); this.logger.debug(`Deleting standing for league: ${leagueId}, driver: ${driverId}`);
this.standings.delete(key); 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<void> { async deleteByLeagueId(leagueId: string): Promise<void> {
const toDelete = Array.from(this.standings.values()) this.logger.debug(`Deleting all standings for league id: ${leagueId}`);
.filter(standing => standing.leagueId === leagueId); try {
const initialCount = Array.from(this.standings.values()).filter(s => s.leagueId === leagueId).length;
toDelete.forEach(standing => { const toDelete = Array.from(this.standings.values())
const key = this.getKey(standing.leagueId, standing.driverId); .filter(standing => standing.leagueId === leagueId);
this.standings.delete(key);
}); 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<boolean> { async exists(leagueId: string, driverId: string): Promise<boolean> {
const key = this.getKey(leagueId, driverId); this.logger.debug(`Checking existence of standing for league: ${leagueId}, driver: ${driverId}`);
return this.standings.has(key); 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<Standing[]> { async recalculate(leagueId: string): Promise<Standing[]> {
if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) { this.logger.debug(`Recalculating standings for league id: ${leagueId}`);
throw new Error('Cannot recalculate standings: missing required repositories'); try {
} if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) {
this.logger.error('Cannot recalculate standings: missing required repositories.');
// Get league to determine points system throw new Error('Cannot recalculate standings: missing required repositories');
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<number, number> = 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<string, Standing>();
results.forEach(result => {
let standing = standingsMap.get(result.driverId);
if (!standing) {
standing = Standing.create({
leagueId,
driverId: result.driverId,
});
} }
// Add points from this result // Get league to determine points system
standing = standing.addRaceResult(result.position, pointsSystem); const league = await this.leagueRepository.findById(leagueId);
standingsMap.set(result.driverId, standing); 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 // Get points system
const sortedStandings = Array.from(standingsMap.values()) const resolvedPointsSystem =
.sort((a, b) => { league.settings.customPoints ??
if (b.points !== a.points) { POINTS_SYSTEMS[league.settings.pointsSystem] ??
return b.points - a.points; 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<string, Standing>();
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) { // Add points from this result
return b.wins - a.wins; standing = standing.addRaceResult(result.position, pointsSystem);
} standingsMap.set(result.driverId, standing);
// Tie-breaker: most races completed this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`);
return b.racesCompleted - a.racesCompleted; });
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 // Save all standings
const updatedStandings = sortedStandings.map((standing, index) => await this.saveMany(updatedStandings);
standing.updatePosition(index + 1) this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`);
);
// Save all standings return updatedStandings;
await this.saveMany(updatedStandings); } catch (error) {
this.logger.error(`Error recalculating standings for league ${leagueId}:`, error);
return updatedStandings; throw error;
}
} }
/** /**

View File

@@ -10,12 +10,16 @@ import type {
TeamJoinRequest, TeamJoinRequest,
} from '@gridpilot/racing/domain/types/TeamMembership'; } from '@gridpilot/racing/domain/types/TeamMembership';
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository'; import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryTeamMembershipRepository implements ITeamMembershipRepository { export class InMemoryTeamMembershipRepository implements ITeamMembershipRepository {
private membershipsByTeam: Map<string, TeamMembership[]>; private membershipsByTeam: Map<string, TeamMembership[]>;
private joinRequestsByTeam: Map<string, TeamJoinRequest[]>; private joinRequestsByTeam: Map<string, TeamJoinRequest[]>;
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.membershipsByTeam = new Map();
this.joinRequestsByTeam = new Map(); this.joinRequestsByTeam = new Map();
@@ -24,6 +28,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito
const list = this.membershipsByTeam.get(membership.teamId) ?? []; const list = this.membershipsByTeam.get(membership.teamId) ?? [];
list.push(membership); list.push(membership);
this.membershipsByTeam.set(membership.teamId, list); 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) ?? []; const list = this.joinRequestsByTeam.get(request.teamId) ?? [];
list.push(request); list.push(request);
this.joinRequestsByTeam.set(request.teamId, list); 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) { if (!list) {
list = []; list = [];
this.membershipsByTeam.set(teamId, list); this.membershipsByTeam.set(teamId, list);
this.logger.debug(`Created new membership list for team: ${teamId}`);
} }
return list; return list;
} }
@@ -50,91 +57,177 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito
if (!list) { if (!list) {
list = []; list = [];
this.joinRequestsByTeam.set(teamId, list); this.joinRequestsByTeam.set(teamId, list);
this.logger.debug(`Created new join request list for team: ${teamId}`);
} }
return list; return list;
} }
async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> { async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> {
const list = this.membershipsByTeam.get(teamId); this.logger.debug(`[getMembership] Entry - teamId: ${teamId}, driverId: ${driverId}`);
if (!list) return null; try {
return list.find((m) => m.driverId === driverId) ?? null; 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<TeamMembership | null> { async getActiveMembershipForDriver(driverId: string): Promise<TeamMembership | null> {
for (const list of this.membershipsByTeam.values()) { this.logger.debug(`[getActiveMembershipForDriver] Entry - driverId: ${driverId}`);
const membership = list.find( try {
(m) => m.driverId === driverId && m.status === 'active', for (const list of this.membershipsByTeam.values()) {
); const membership = list.find(
if (membership) { (m) => m.driverId === driverId && m.status === 'active',
return membership; );
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<TeamMembership[]> { async getTeamMembers(teamId: string): Promise<TeamMembership[]> {
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<number> { async countByTeamId(teamId: string): Promise<number> {
const list = this.membershipsByTeam.get(teamId) ?? []; this.logger.debug(`[countByTeamId] Entry - teamId: ${teamId}`);
return list.filter((m) => m.status === 'active').length; 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<TeamMembership> { async saveMembership(membership: TeamMembership): Promise<TeamMembership> {
const list = this.getMembershipList(membership.teamId); this.logger.debug(`[saveMembership] Entry - teamId: ${membership.teamId}, driverId: ${membership.driverId}`);
const existingIndex = list.findIndex( try {
(m) => m.teamId === membership.teamId && m.driverId === membership.driverId, const list = this.getMembershipList(membership.teamId);
); const existingIndex = list.findIndex(
(m) => m.teamId === membership.teamId && m.driverId === membership.driverId,
);
if (existingIndex >= 0) { if (existingIndex >= 0) {
list[existingIndex] = membership; list[existingIndex] = membership;
} else { this.logger.info(`[saveMembership] Success - updated existing membership for team: ${membership.teamId}, driver: ${membership.driverId}.`);
list.push(membership); } 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<void> { async removeMembership(teamId: string, driverId: string): Promise<void> {
const list = this.membershipsByTeam.get(teamId); this.logger.debug(`[removeMembership] Entry - teamId: ${teamId}, driverId: ${driverId}`);
if (!list) { try {
return; const list = this.membershipsByTeam.get(teamId);
} if (!list) {
const index = list.findIndex((m) => m.driverId === driverId); this.logger.warn(`[removeMembership] No membership list found for team: ${teamId}. Cannot remove.`);
if (index >= 0) { return;
list.splice(index, 1); }
this.membershipsByTeam.set(teamId, list); 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<TeamJoinRequest[]> { async getJoinRequests(teamId: string): Promise<TeamJoinRequest[]> {
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<TeamJoinRequest> { async saveJoinRequest(request: TeamJoinRequest): Promise<TeamJoinRequest> {
const list = this.getJoinRequestList(request.teamId); this.logger.debug(`[saveJoinRequest] Entry - teamId: ${request.teamId}, driverId: ${request.driverId}, id: ${request.id}`);
const existingIndex = list.findIndex((r) => r.id === request.id); try {
const list = this.getJoinRequestList(request.teamId);
const existingIndex = list.findIndex((r) => r.id === request.id);
if (existingIndex >= 0) { if (existingIndex >= 0) {
list[existingIndex] = request; list[existingIndex] = request;
} else { this.logger.info(`[saveJoinRequest] Success - updated existing join request: ${request.id}.`);
list.push(request); } 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<void> { async removeJoinRequest(requestId: string): Promise<void> {
for (const [teamId, list] of this.joinRequestsByTeam.entries()) { this.logger.debug(`[removeJoinRequest] Entry - requestId: ${requestId}`);
const index = list.findIndex((r) => r.id === requestId); try {
if (index >= 0) { let removed = false;
list.splice(index, 1); for (const [teamId, list] of this.joinRequestsByTeam.entries()) {
this.joinRequestsByTeam.set(teamId, list); const index = list.findIndex((r) => r.id === requestId);
return; 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;
} }
} }
} }

View File

@@ -7,61 +7,126 @@
import type { Team } from '@gridpilot/racing/domain/entities/Team'; import type { Team } from '@gridpilot/racing/domain/entities/Team';
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository'; import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryTeamRepository implements ITeamRepository { export class InMemoryTeamRepository implements ITeamRepository {
private teams: Map<string, Team>; private teams: Map<string, Team>;
private readonly logger: ILogger;
constructor(seedData?: Team[]) { constructor(logger: ILogger, seedData?: Team[]) {
this.logger = logger;
this.logger.info('InMemoryTeamRepository initialized.');
this.teams = new Map(); this.teams = new Map();
if (seedData) { if (seedData) {
seedData.forEach((team) => { seedData.forEach((team) => {
this.teams.set(team.id, team); this.teams.set(team.id, team);
this.logger.debug(`Seeded team: ${team.id}.`);
}); });
} }
} }
async findById(id: string): Promise<Team | null> { async findById(id: string): Promise<Team | null> {
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<Team[]> { async findAll(): Promise<Team[]> {
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<Team[]> { async findByLeagueId(leagueId: string): Promise<Team[]> {
return Array.from(this.teams.values()).filter((team) => this.logger.debug(`Finding teams by league id: ${leagueId}`);
team.leagues.includes(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<Team> { async create(team: Team): Promise<Team> {
if (await this.exists(team.id)) { this.logger.debug(`Creating team: ${team.id}`);
throw new Error(`Team with ID ${team.id} already exists`); 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); this.teams.set(team.id, team);
return 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<Team> { async update(team: Team): Promise<Team> {
if (!(await this.exists(team.id))) { this.logger.debug(`Updating team: ${team.id}`);
throw new Error(`Team with ID ${team.id} not found`); 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); this.teams.set(team.id, team);
return 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<void> { async delete(id: string): Promise<void> {
if (!(await this.exists(id))) { this.logger.debug(`Deleting team: ${id}`);
throw new Error(`Team with ID ${id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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;
}
} }
} }

View File

@@ -8,84 +8,173 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Track, TrackCategory } from '@gridpilot/racing/domain/entities/Track'; import { Track, TrackCategory } from '@gridpilot/racing/domain/entities/Track';
import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository'; import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryTrackRepository implements ITrackRepository { export class InMemoryTrackRepository implements ITrackRepository {
private tracks: Map<string, Track>; private tracks: Map<string, Track>;
private readonly logger: ILogger;
constructor(seedData?: Track[]) { constructor(logger: ILogger, seedData?: Track[]) {
this.logger = logger;
this.logger.info('InMemoryTrackRepository initialized.');
this.tracks = new Map(); this.tracks = new Map();
if (seedData) { if (seedData) {
seedData.forEach(track => { seedData.forEach(track => {
this.tracks.set(track.id, track); this.tracks.set(track.id, track);
this.logger.debug(`Seeded track: ${track.id}.`);
}); });
} }
} }
async findById(id: string): Promise<Track | null> { async findById(id: string): Promise<Track | null> {
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<Track[]> { async findAll(): Promise<Track[]> {
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<Track[]> { async findByGameId(gameId: string): Promise<Track[]> {
return Array.from(this.tracks.values()) this.logger.debug(`Finding tracks by game id: ${gameId}`);
.filter(track => track.gameId === gameId) try {
.sort((a, b) => a.name.localeCompare(b.name)); 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<Track[]> { async findByCategory(category: TrackCategory): Promise<Track[]> {
return Array.from(this.tracks.values()) this.logger.debug(`Finding tracks by category: ${category}`);
.filter(track => track.category === category) try {
.sort((a, b) => a.name.localeCompare(b.name)); 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<Track[]> { async findByCountry(country: string): Promise<Track[]> {
return Array.from(this.tracks.values()) this.logger.debug(`Finding tracks by country: ${country}`);
.filter(track => track.country.toLowerCase() === country.toLowerCase()) try {
.sort((a, b) => a.name.localeCompare(b.name)); 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<Track[]> { async searchByName(query: string): Promise<Track[]> {
const lowerQuery = query.toLowerCase(); this.logger.debug(`Searching tracks by name query: ${query}`);
return Array.from(this.tracks.values()) try {
.filter(track => const lowerQuery = query.toLowerCase();
track.name.toLowerCase().includes(lowerQuery) || const tracks = Array.from(this.tracks.values())
track.shortName.toLowerCase().includes(lowerQuery) .filter(track =>
) track.name.toLowerCase().includes(lowerQuery) ||
.sort((a, b) => a.name.localeCompare(b.name)); 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<Track> { async create(track: Track): Promise<Track> {
if (await this.exists(track.id)) { this.logger.debug(`Creating track: ${track.id}`);
throw new Error(`Track with ID ${track.id} already exists`); 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); this.tracks.set(track.id, track);
return 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<Track> { async update(track: Track): Promise<Track> {
if (!await this.exists(track.id)) { this.logger.debug(`Updating track: ${track.id}`);
throw new Error(`Track with ID ${track.id} not found`); 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); this.tracks.set(track.id, track);
return 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<void> { async delete(id: string): Promise<void> {
if (!await this.exists(id)) { this.logger.debug(`Deleting track: ${id}`);
throw new Error(`Track with ID ${id} not found`); 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<boolean> { async exists(id: string): Promise<boolean> {
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;
}
} }
/** /**

View File

@@ -6,48 +6,123 @@
import type { Transaction, TransactionType } from '../../domain/entities/Transaction'; import type { Transaction, TransactionType } from '../../domain/entities/Transaction';
import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository'; import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryTransactionRepository implements ITransactionRepository { export class InMemoryTransactionRepository implements ITransactionRepository {
private transactions: Map<string, Transaction> = new Map(); private transactions: Map<string, Transaction> = 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<Transaction | null> { async findById(id: string): Promise<Transaction | null> {
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<Transaction[]> { async findByWalletId(walletId: string): Promise<Transaction[]> {
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<Transaction[]> { async findByType(type: TransactionType): Promise<Transaction[]> {
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<Transaction> { async create(transaction: Transaction): Promise<Transaction> {
if (this.transactions.has(transaction.id)) { this.logger.debug(`Creating transaction: ${transaction.id}`);
throw new Error('Transaction with this ID already exists'); 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<Transaction> { async update(transaction: Transaction): Promise<Transaction> {
if (!this.transactions.has(transaction.id)) { this.logger.debug(`Updating transaction: ${transaction.id}`);
throw new Error('Transaction not found'); 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<void> { async delete(id: string): Promise<void> {
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<boolean> { async exists(id: string): Promise<boolean> {
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 // Test helper
clear(): void { clear(): void {
this.logger.debug('Clearing all transactions.');
this.transactions.clear(); this.transactions.clear();
this.logger.info('All transactions cleared.');
} }
} }

View File

@@ -0,0 +1,6 @@
export interface ILogger {
debug(message: string, context?: Record<string, any>): void;
info(message: string, context?: Record<string, any>): void;
warn(message: string, context?: Record<string, any>): void;
error(message: string, error?: Error, context?: Record<string, any>): void;
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository'; import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO'; import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO';
import type { FriendDTO } from '../dto/FriendDTO'; import type { FriendDTO } from '../dto/FriendDTO';
@@ -22,33 +23,46 @@ export class GetCurrentUserSocialUseCase
constructor( constructor(
private readonly socialGraphRepository: ISocialGraphRepository, private readonly socialGraphRepository: ISocialGraphRepository,
public readonly presenter: ICurrentUserSocialPresenter, public readonly presenter: ICurrentUserSocialPresenter,
private readonly logger: ILogger,
) {} ) {}
async execute(params: GetCurrentUserSocialParams): Promise<void> { async execute(params: GetCurrentUserSocialParams): Promise<void> {
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) => ({ const friends: FriendDTO[] = friendsDomain.map((friend) => ({
driverId: friend.id, driverId: friend.id,
displayName: friend.name, displayName: friend.name,
avatarUrl: '', avatarUrl: '',
isOnline: false, isOnline: false,
lastSeen: new Date(), lastSeen: new Date(),
})); }));
const currentUser: CurrentUserSocialDTO = { const currentUser: CurrentUserSocialDTO = {
driverId, driverId,
displayName: '', displayName: '',
avatarUrl: '', avatarUrl: '',
countryCode: '', countryCode: '',
}; };
const viewModel: CurrentUserSocialViewModel = { const viewModel: CurrentUserSocialViewModel = {
currentUser, currentUser,
friends, 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;
}
} }
} }

View File

@@ -6,6 +6,7 @@ import type {
IUserFeedPresenter, IUserFeedPresenter,
UserFeedViewModel, UserFeedViewModel,
} from '../presenters/ISocialPresenters'; } from '../presenters/ISocialPresenters';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface GetUserFeedParams { export interface GetUserFeedParams {
driverId: string; driverId: string;
@@ -17,18 +18,30 @@ export class GetUserFeedUseCase
constructor( constructor(
private readonly feedRepository: IFeedRepository, private readonly feedRepository: IFeedRepository,
public readonly presenter: IUserFeedPresenter, public readonly presenter: IUserFeedPresenter,
private readonly logger: ILogger,
) {} ) {}
async execute(params: GetUserFeedParams): Promise<void> { async execute(params: GetUserFeedParams): Promise<void> {
const { driverId, limit } = params; const { driverId, limit } = params;
const items = await this.feedRepository.getFeedForDriver(driverId, limit); this.logger.debug('Executing GetUserFeedUseCase', { driverId, limit });
const dtoItems = items.map(mapFeedItemToDTO);
const viewModel: UserFeedViewModel = { try {
items: dtoItems, 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
}
} }
} }

View File

@@ -2,6 +2,7 @@ import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem'; import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem';
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export type Friendship = { export type Friendship = {
driverId: string; driverId: string;
@@ -18,89 +19,130 @@ export class InMemoryFeedRepository implements IFeedRepository {
private readonly feedEvents: FeedItem[]; private readonly feedEvents: FeedItem[];
private readonly friendships: Friendship[]; private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>; private readonly driversById: Map<string, Driver>;
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.feedEvents = seed.feedEvents;
this.friendships = seed.friendships; this.friendships = seed.friendships;
this.driversById = new Map(seed.drivers.map((d) => [d.id, d])); this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
} }
async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> { async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> {
const friendIds = new Set( this.logger.debug(`Getting feed for driver: ${driverId}, limit: ${limit}`);
this.friendships try {
.filter((f) => f.driverId === driverId) const friendIds = new Set(
.map((f) => f.friendId), this.friendships
); .filter((f) => f.driverId === driverId)
.map((f) => f.friendId),
);
const items = this.feedEvents.filter((item) => { const items = this.feedEvents.filter((item) => {
if (item.actorDriverId && friendIds.has(item.actorDriverId)) { if (item.actorDriverId && friendIds.has(item.actorDriverId)) {
return true; return true;
} }
return false; return false;
}); });
const sorted = items const sorted = items
.slice() .slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); .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<FeedItem[]> { async getGlobalFeed(limit?: number): Promise<FeedItem[]> {
const sorted = this.feedEvents this.logger.debug(`Getting global feed, limit: ${limit}`);
.slice() try {
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 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 { export class InMemorySocialGraphRepository implements ISocialGraphRepository {
private readonly friendships: Friendship[]; private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>; private readonly driversById: Map<string, Driver>;
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.friendships = seed.friendships;
this.driversById = new Map(seed.drivers.map((d) => [d.id, d])); this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
} }
async getFriendIds(driverId: string): Promise<string[]> { async getFriendIds(driverId: string): Promise<string[]> {
return this.friendships this.logger.debug(`Getting friend IDs for driver: ${driverId}`);
.filter((f) => f.driverId === driverId) try {
.map((f) => f.friendId); 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<Driver[]> { async getFriends(driverId: string): Promise<Driver[]> {
const ids = await this.getFriendIds(driverId); this.logger.debug(`Getting friends for driver: ${driverId}`);
return ids try {
.map((id) => this.driversById.get(id)) const ids = await this.getFriendIds(driverId);
.filter((d): d is Driver => Boolean(d)); 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<Driver[]> { async getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]> {
const directFriendIds = new Set(await this.getFriendIds(driverId)); this.logger.debug(`Getting suggested friends for driver: ${driverId}, limit: ${limit}`);
const suggestions = new Map<string, number>(); try {
const directFriendIds = new Set(await this.getFriendIds(driverId));
const suggestions = new Map<string, number>();
for (const friendship of this.friendships) { for (const friendship of this.friendships) {
if (!directFriendIds.has(friendship.driverId)) continue; if (!directFriendIds.has(friendship.driverId)) continue;
const friendOfFriendId = friendship.friendId; const friendOfFriendId = friendship.friendId;
if (friendOfFriendId === driverId) continue; if (friendOfFriendId === driverId) continue;
if (directFriendIds.has(friendOfFriendId)) 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;
} }
} }

View File

@@ -5,6 +5,7 @@ import {
AvatarGenerationRequest, AvatarGenerationRequest,
type AvatarGenerationRequestProps, type AvatarGenerationRequestProps,
} from '@gridpilot/media'; } from '@gridpilot/media';
import { ILogger } from '@gridpilot/shared/logging/ILogger';
/** /**
* In-memory implementation of IAvatarGenerationRepository. * In-memory implementation of IAvatarGenerationRepository.
@@ -13,42 +14,65 @@ import {
*/ */
export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepository { export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepository {
private readonly requests = new Map<string, AvatarGenerationRequestProps>(); private readonly requests = new Map<string, AvatarGenerationRequestProps>();
private readonly logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
this.logger.info('InMemoryAvatarGenerationRepository initialized.');
}
async save(request: AvatarGenerationRequest): Promise<void> { async save(request: AvatarGenerationRequest): Promise<void> {
this.logger.debug(`Saving avatar generation request with ID: ${request.id}`);
this.requests.set(request.id, request.toProps()); this.requests.set(request.id, request.toProps());
this.logger.info(`Avatar generation request with ID: ${request.id} saved successfully.`);
} }
async findById(id: string): Promise<AvatarGenerationRequest | null> { async findById(id: string): Promise<AvatarGenerationRequest | null> {
this.logger.debug(`Finding avatar generation request by ID: ${id}`);
const props = this.requests.get(id); const props = this.requests.get(id);
if (!props) { if (!props) {
this.logger.info(`Avatar generation request with ID: ${id} not found.`);
return null; return null;
} }
this.logger.info(`Avatar generation request with ID: ${id} found.`);
return AvatarGenerationRequest.reconstitute(props); return AvatarGenerationRequest.reconstitute(props);
} }
async findByUserId(userId: string): Promise<AvatarGenerationRequest[]> { async findByUserId(userId: string): Promise<AvatarGenerationRequest[]> {
this.logger.debug(`Finding avatar generation requests by user ID: ${userId}`);
const results: AvatarGenerationRequest[] = []; const results: AvatarGenerationRequest[] = [];
for (const props of this.requests.values()) { for (const props of this.requests.values()) {
if (props.userId === userId) { if (props.userId === userId) {
results.push(AvatarGenerationRequest.reconstitute(props)); 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()); return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
} }
async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> { async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> {
this.logger.debug(`Finding latest avatar generation request for user ID: ${userId}`);
const userRequests = await this.findByUserId(userId); const userRequests = await this.findByUserId(userId);
if (userRequests.length === 0) { if (userRequests.length === 0) {
this.logger.info(`No avatar generation requests found for user ID: ${userId}.`);
return null; return null;
} }
const latest = userRequests[0]; const latest = userRequests[0];
if (!latest) { if (!latest) {
this.logger.info(`No latest avatar generation request found for user ID: ${userId}.`);
return null; return null;
} }
this.logger.info(`Latest avatar generation request found for user ID: ${userId}, ID: ${latest.id}.`);
return latest; return latest;
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
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}.`);
}
} }
} }

View File

@@ -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<LoggerPort>(DI_TOKENS.Logger);
const overlaySyncService = container.resolve<OverlaySyncService>(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<LoggerPort>(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();
});
});

View File

@@ -7,7 +7,7 @@ export default defineConfig({
watch: false, watch: false,
environment: 'jsdom', environment: 'jsdom',
setupFiles: ['tests/setup/vitest.setup.ts'], 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: [ exclude: [
// Do not run library-internal tests from dependencies // Do not run library-internal tests from dependencies
'node_modules/**', 'node_modules/**',