wip
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* This creates an active sponsorship and notifies the sponsor.
|
||||
*/
|
||||
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
@@ -31,56 +32,73 @@ export class AcceptSponsorshipRequestUseCase
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(dto: AcceptSponsorshipRequestDTO): Promise<AcceptSponsorshipRequestResultDTO> {
|
||||
// Find the request
|
||||
const request = await this.sponsorshipRequestRepo.findById(dto.requestId);
|
||||
if (!request) {
|
||||
throw new Error('Sponsorship request not found');
|
||||
}
|
||||
|
||||
if (!request.isPending()) {
|
||||
throw new Error(`Cannot accept a ${request.status} sponsorship request`);
|
||||
}
|
||||
|
||||
// Accept the request
|
||||
const acceptedRequest = request.accept(dto.respondedBy);
|
||||
await this.sponsorshipRequestRepo.update(acceptedRequest);
|
||||
|
||||
// If this is a season sponsorship, create the SeasonSponsorship record
|
||||
let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
if (request.entityType === 'season') {
|
||||
const season = await this.seasonRepository.findById(request.entityId);
|
||||
if (!season) {
|
||||
throw new Error('Season not found for sponsorship request');
|
||||
this.logger.debug(`Attempting to accept sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, respondedBy: dto.respondedBy });
|
||||
try {
|
||||
// Find the request
|
||||
const request = await this.sponsorshipRequestRepo.findById(dto.requestId);
|
||||
if (!request) {
|
||||
this.logger.warn(`Sponsorship request not found: ${dto.requestId}`, { requestId: dto.requestId });
|
||||
throw new Error('Sponsorship request not found');
|
||||
}
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: sponsorshipId,
|
||||
seasonId: season.id,
|
||||
leagueId: season.leagueId,
|
||||
sponsorId: request.sponsorId,
|
||||
tier: request.tier,
|
||||
pricing: request.offeredAmount,
|
||||
status: 'active',
|
||||
});
|
||||
await this.seasonSponsorshipRepo.create(sponsorship);
|
||||
if (!request.isPending()) {
|
||||
this.logger.warn(`Cannot accept a ${request.status} sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, status: request.status });
|
||||
throw new Error(`Cannot accept a ${request.status} sponsorship request`);
|
||||
}
|
||||
|
||||
this.logger.info(`Sponsorship request ${dto.requestId} found and is pending. Proceeding with acceptance.`, { requestId: dto.requestId });
|
||||
|
||||
// Accept the request
|
||||
const acceptedRequest = request.accept(dto.respondedBy);
|
||||
await this.sponsorshipRequestRepo.update(acceptedRequest);
|
||||
this.logger.debug(`Sponsorship request ${dto.requestId} accepted and updated in repository.`, { requestId: dto.requestId });
|
||||
|
||||
// If this is a season sponsorship, create the SeasonSponsorship record
|
||||
let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
if (request.entityType === 'season') {
|
||||
this.logger.debug(`Sponsorship request ${dto.requestId} is for a season. Creating SeasonSponsorship record.`, { requestId: dto.requestId, entityType: request.entityType });
|
||||
const season = await this.seasonRepository.findById(request.entityId);
|
||||
if (!season) {
|
||||
this.logger.warn(`Season not found for sponsorship request ${dto.requestId} and entityId ${request.entityId}`, { requestId: dto.requestId, entityId: request.entityId });
|
||||
throw new Error('Season not found for sponsorship request');
|
||||
}
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: sponsorshipId,
|
||||
seasonId: season.id,
|
||||
leagueId: season.leagueId,
|
||||
sponsorId: request.sponsorId,
|
||||
tier: request.tier,
|
||||
pricing: request.offeredAmount,
|
||||
status: 'active',
|
||||
});
|
||||
await this.seasonSponsorshipRepo.create(sponsorship);
|
||||
this.logger.info(`Season sponsorship ${sponsorshipId} created for request ${dto.requestId}.`, { sponsorshipId, requestId: dto.requestId });
|
||||
}
|
||||
|
||||
// TODO: In a real implementation, we would:
|
||||
// 1. Create notification for the sponsor
|
||||
// 2. Process payment
|
||||
// 3. Update wallet balances
|
||||
|
||||
this.logger.info(`Sponsorship request ${acceptedRequest.id} successfully accepted.`, { requestId: acceptedRequest.id, sponsorshipId });
|
||||
|
||||
return {
|
||||
requestId: acceptedRequest.id,
|
||||
sponsorshipId,
|
||||
status: 'accepted',
|
||||
acceptedAt: acceptedRequest.respondedAt!,
|
||||
platformFee: acceptedRequest.getPlatformFee().amount,
|
||||
netAmount: acceptedRequest.getNetAmount().amount,
|
||||
};
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to accept sponsorship request ${dto.requestId}: ${error.message}`, { requestId: dto.requestId, error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
}
|
||||
|
||||
// TODO: In a real implementation, we would:
|
||||
// 1. Create notification for the sponsor
|
||||
// 2. Process payment
|
||||
// 3. Update wallet balances
|
||||
|
||||
return {
|
||||
requestId: acceptedRequest.id,
|
||||
sponsorshipId,
|
||||
status: 'accepted',
|
||||
acceptedAt: acceptedRequest.respondedAt!,
|
||||
platformFee: acceptedRequest.getPlatformFee().amount,
|
||||
netAmount: acceptedRequest.getNetAmount().amount,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
EntityNotFoundError,
|
||||
BusinessRuleViolationError,
|
||||
} from '../errors/RacingApplicationError';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
export interface ApplyForSponsorshipDTO {
|
||||
sponsorId: string;
|
||||
@@ -40,22 +41,28 @@ export class ApplyForSponsorshipUseCase
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
||||
private readonly sponsorRepo: ISponsorRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(dto: ApplyForSponsorshipDTO): Promise<ApplyForSponsorshipResultDTO> {
|
||||
this.logger.debug('Attempting to apply for sponsorship', { dto });
|
||||
|
||||
// Validate sponsor exists
|
||||
const sponsor = await this.sponsorRepo.findById(dto.sponsorId);
|
||||
if (!sponsor) {
|
||||
this.logger.error('Sponsor not found', { sponsorId: dto.sponsorId });
|
||||
throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId });
|
||||
}
|
||||
|
||||
// Check if entity accepts sponsorship applications
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
if (!pricing) {
|
||||
this.logger.warn('Sponsorship pricing not set up for this entity', { entityType: dto.entityType, entityId: dto.entityId });
|
||||
throw new BusinessRuleViolationError('This entity has not set up sponsorship pricing');
|
||||
}
|
||||
|
||||
if (!pricing.acceptingApplications) {
|
||||
this.logger.warn('Entity not accepting sponsorship applications', { entityType: dto.entityType, entityId: dto.entityId });
|
||||
throw new BusinessRuleViolationError(
|
||||
'This entity is not currently accepting sponsorship applications',
|
||||
);
|
||||
@@ -64,6 +71,7 @@ export class ApplyForSponsorshipUseCase
|
||||
// Check if the requested tier slot is available
|
||||
const slotAvailable = pricing.isSlotAvailable(dto.tier);
|
||||
if (!slotAvailable) {
|
||||
this.logger.warn(`No ${dto.tier} sponsorship slots are available for entity ${dto.entityId}`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`No ${dto.tier} sponsorship slots are available`,
|
||||
);
|
||||
@@ -76,6 +84,7 @@ export class ApplyForSponsorshipUseCase
|
||||
dto.entityId,
|
||||
);
|
||||
if (hasPending) {
|
||||
this.logger.warn('Sponsor already has a pending request for this entity', { sponsorId: dto.sponsorId, entityType: dto.entityType, entityId: dto.entityId });
|
||||
throw new BusinessRuleViolationError(
|
||||
'You already have a pending sponsorship request for this entity',
|
||||
);
|
||||
@@ -84,6 +93,7 @@ export class ApplyForSponsorshipUseCase
|
||||
// Validate offered amount meets minimum price
|
||||
const minPrice = pricing.getPrice(dto.tier);
|
||||
if (minPrice && dto.offeredAmount < minPrice.amount) {
|
||||
this.logger.warn(`Offered amount ${dto.offeredAmount} is less than minimum ${minPrice.amount} for entity ${dto.entityId}, tier ${dto.tier}`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`Offered amount must be at least ${minPrice.format()}`,
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
export interface ApplyPenaltyCommand {
|
||||
raceId: string;
|
||||
@@ -31,57 +32,73 @@ export class ApplyPenaltyUseCase
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: ApplyPenaltyCommand): Promise<{ penaltyId: string }> {
|
||||
// Validate race exists
|
||||
const race = await this.raceRepository.findById(command.raceId);
|
||||
if (!race) {
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
|
||||
// Validate steward has authority (owner or admin of the league)
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
|
||||
const stewardMembership = memberships.find(
|
||||
m => m.driverId === command.stewardId && m.status === 'active'
|
||||
);
|
||||
|
||||
if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) {
|
||||
throw new Error('Only league owners and admins can apply penalties');
|
||||
}
|
||||
|
||||
// If linked to a protest, validate the protest exists and is upheld
|
||||
if (command.protestId) {
|
||||
const protest = await this.protestRepository.findById(command.protestId);
|
||||
if (!protest) {
|
||||
throw new Error('Protest not found');
|
||||
this.logger.debug('ApplyPenaltyUseCase: Executing with command', command);
|
||||
try {
|
||||
// Validate race exists
|
||||
const race = await this.raceRepository.findById(command.raceId);
|
||||
if (!race) {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Race with ID ${command.raceId} not found.`);
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
if (protest.status !== 'upheld') {
|
||||
throw new Error('Can only create penalties for upheld protests');
|
||||
this.logger.debug(`ApplyPenaltyUseCase: Race ${race.id} found.`);
|
||||
|
||||
// Validate steward has authority (owner or admin of the league)
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
|
||||
const stewardMembership = memberships.find(
|
||||
m => m.driverId === command.stewardId && m.status === 'active'
|
||||
);
|
||||
|
||||
if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Steward ${command.stewardId} does not have authority for league ${race.leagueId}.`);
|
||||
throw new Error('Only league owners and admins can apply penalties');
|
||||
}
|
||||
if (protest.raceId !== command.raceId) {
|
||||
throw new Error('Protest is not for this race');
|
||||
this.logger.debug(`ApplyPenaltyUseCase: Steward ${command.stewardId} has authority.`);
|
||||
|
||||
// If linked to a protest, validate the protest exists and is upheld
|
||||
if (command.protestId) {
|
||||
const protest = await this.protestRepository.findById(command.protestId);
|
||||
if (!protest) {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Protest with ID ${command.protestId} not found.`);
|
||||
throw new Error('Protest not found');
|
||||
}
|
||||
if (protest.status !== 'upheld') {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Protest ${protest.id} is not upheld. Status: ${protest.status}`);
|
||||
throw new Error('Can only create penalties for upheld protests');
|
||||
}
|
||||
if (protest.raceId !== command.raceId) {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Protest ${protest.id} is for race ${protest.raceId}, not ${command.raceId}.`);
|
||||
throw new Error('Protest is not for this race');
|
||||
}
|
||||
this.logger.debug(`ApplyPenaltyUseCase: Protest ${protest.id} is valid and upheld.`);
|
||||
}
|
||||
|
||||
// Create the penalty
|
||||
const penalty = Penalty.create({
|
||||
id: randomUUID(),
|
||||
leagueId: race.leagueId,
|
||||
raceId: command.raceId,
|
||||
driverId: command.driverId,
|
||||
type: command.type,
|
||||
...(command.value !== undefined ? { value: command.value } : {}),
|
||||
reason: command.reason,
|
||||
...(command.protestId !== undefined ? { protestId: command.protestId } : {}),
|
||||
issuedBy: command.stewardId,
|
||||
status: 'pending',
|
||||
issuedAt: new Date(),
|
||||
...(command.notes !== undefined ? { notes: command.notes } : {}),
|
||||
});
|
||||
|
||||
await this.penaltyRepository.create(penalty);
|
||||
this.logger.info(`ApplyPenaltyUseCase: Successfully applied penalty ${penalty.id} for driver ${command.driverId} in race ${command.raceId}.`);
|
||||
|
||||
return { penaltyId: penalty.id };
|
||||
} catch (error) {
|
||||
this.logger.error('ApplyPenaltyUseCase: Failed to apply penalty', { command, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create the penalty
|
||||
const penalty = Penalty.create({
|
||||
id: randomUUID(),
|
||||
leagueId: race.leagueId,
|
||||
raceId: command.raceId,
|
||||
driverId: command.driverId,
|
||||
type: command.type,
|
||||
...(command.value !== undefined ? { value: command.value } : {}),
|
||||
reason: command.reason,
|
||||
...(command.protestId !== undefined ? { protestId: command.protestId } : {}),
|
||||
issuedBy: command.stewardId,
|
||||
status: 'pending',
|
||||
issuedAt: new Date(),
|
||||
...(command.notes !== undefined ? { notes: command.notes } : {}),
|
||||
});
|
||||
|
||||
await this.penaltyRepository.create(penalty);
|
||||
|
||||
return { penaltyId: penalty.id };
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type {
|
||||
TeamMembership,
|
||||
@@ -12,24 +13,31 @@ export class ApproveTeamJoinRequestUseCase
|
||||
implements AsyncUseCase<ApproveTeamJoinRequestCommandDTO, void> {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: ApproveTeamJoinRequestCommandDTO): Promise<void> {
|
||||
const { requestId } = command;
|
||||
this.logger.debug(
|
||||
`Attempting to approve team join request with ID: ${requestId}`,
|
||||
);
|
||||
|
||||
// There is no repository method to look up a single request by ID,
|
||||
// so we rely on the repository implementation to surface all relevant
|
||||
// requests via getJoinRequests and search by ID here.
|
||||
const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests(
|
||||
// For the in-memory fake used in tests, the teamId argument is ignored
|
||||
// and all requests are returned.
|
||||
'' as string,
|
||||
);
|
||||
const request = allRequests.find((r) => r.id === requestId);
|
||||
try {
|
||||
// There is no repository method to look up a single request by ID,
|
||||
// so we rely on the repository implementation to surface all relevant
|
||||
// requests via getJoinRequests and search by ID here.
|
||||
const allRequests: TeamJoinRequest[] = await this.membershipRepository.getJoinRequests(
|
||||
// For the in-memory fake used in tests, the teamId argument is ignored
|
||||
// and all requests are returned.'
|
||||
'' as string,
|
||||
);
|
||||
const request = allRequests.find((r) => r.id === requestId);
|
||||
|
||||
if (!request) {
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
if (!request) {
|
||||
this.logger.warn(`Team join request with ID ${requestId} not found`);
|
||||
throw new Error('Join request not found');
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId: request.teamId,
|
||||
@@ -40,6 +48,14 @@ export class ApproveTeamJoinRequestUseCase
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
this.logger.info(
|
||||
`Team membership created for driver ${request.driverId} in team ${request.teamId} from request ${requestId}`,
|
||||
);
|
||||
await this.membershipRepository.removeJoinRequest(requestId);
|
||||
this.logger.info(`Team join request with ID ${requestId} removed`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to approve team join request ${requestId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Use Case: CancelRaceUseCase
|
||||
@@ -18,17 +19,26 @@ export class CancelRaceUseCase
|
||||
implements AsyncUseCase<CancelRaceCommandDTO, void> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: CancelRaceCommandDTO): Promise<void> {
|
||||
const { raceId } = command;
|
||||
this.logger.debug(`[CancelRaceUseCase] Executing for raceId: ${raceId}`);
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
throw new Error('Race not found');
|
||||
try {
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
this.logger.warn(`[CancelRaceUseCase] Race with ID ${raceId} not found.`);
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
|
||||
const cancelledRace = race.cancel();
|
||||
await this.raceRepository.update(cancelledRace);
|
||||
this.logger.info(`[CancelRaceUseCase] Race ${raceId} cancelled successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`[CancelRaceUseCase] Error cancelling race ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const cancelledRace = race.cancel();
|
||||
await this.raceRepository.update(cancelledRace);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
||||
import { Result } from '../../domain/entities/Result';
|
||||
import { Standing } from '../../domain/entities/Standing';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Use Case: CompleteRaceUseCase
|
||||
@@ -30,39 +31,55 @@ export class CompleteRaceUseCase
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: CompleteRaceCommandDTO): Promise<void> {
|
||||
this.logger.debug(`Executing CompleteRaceUseCase for raceId: ${command.raceId}`);
|
||||
const { raceId } = command;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
throw new Error('Race not found');
|
||||
try {
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
this.logger.error(`Race with id ${raceId} not found.`);
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
this.logger.debug(`Race ${raceId} found. Status: ${race.status}`);
|
||||
|
||||
// Get registered drivers for this race
|
||||
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
||||
if (registeredDriverIds.length === 0) {
|
||||
this.logger.warn(`No registered drivers found for race ${raceId}.`);
|
||||
throw new Error('Cannot complete race with no registered drivers');
|
||||
}
|
||||
this.logger.info(`${registeredDriverIds.length} drivers registered for race ${raceId}. Generating results.`);
|
||||
|
||||
// Get driver ratings
|
||||
const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds);
|
||||
this.logger.debug(`Driver ratings fetched for ${registeredDriverIds.length} drivers.`);
|
||||
|
||||
// Generate realistic race results
|
||||
const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings);
|
||||
this.logger.debug(`Generated ${results.length} race results for race ${raceId}.`);
|
||||
|
||||
// Save results
|
||||
for (const result of results) {
|
||||
await this.resultRepository.create(result);
|
||||
}
|
||||
this.logger.info(`Persisted ${results.length} race results for race ${raceId}.`);
|
||||
|
||||
// Update standings
|
||||
await this.updateStandings(race.leagueId, results);
|
||||
this.logger.info(`Standings updated for league ${race.leagueId}.`);
|
||||
|
||||
// Complete the race
|
||||
const completedRace = race.complete();
|
||||
await this.raceRepository.update(completedRace);
|
||||
this.logger.info(`Race ${raceId} successfully completed and updated.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to complete race ${raceId}: ${error.message}`, error as Error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Get registered drivers for this race
|
||||
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
||||
if (registeredDriverIds.length === 0) {
|
||||
throw new Error('Cannot complete race with no registered drivers');
|
||||
}
|
||||
|
||||
// Get driver ratings
|
||||
const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds);
|
||||
|
||||
// Generate realistic race results
|
||||
const results = this.generateRaceResults(raceId, registeredDriverIds, driverRatings);
|
||||
|
||||
// Save results
|
||||
for (const result of results) {
|
||||
await this.resultRepository.create(result);
|
||||
}
|
||||
|
||||
// Update standings
|
||||
await this.updateStandings(race.leagueId, results);
|
||||
|
||||
// Complete the race
|
||||
const completedRace = race.complete();
|
||||
await this.raceRepository.update(completedRace);
|
||||
}
|
||||
|
||||
private generateRaceResults(
|
||||
@@ -70,6 +87,7 @@ export class CompleteRaceUseCase
|
||||
driverIds: string[],
|
||||
driverRatings: Map<string, number>
|
||||
): Result[] {
|
||||
this.logger.debug(`Generating race results for race ${raceId} with ${driverIds.length} drivers.`);
|
||||
// Create driver performance data
|
||||
const driverPerformances = driverIds.map(driverId => ({
|
||||
driverId,
|
||||
@@ -83,6 +101,7 @@ export class CompleteRaceUseCase
|
||||
const perfB = b.rating + (b.randomFactor * 200);
|
||||
return perfB - perfA; // Higher performance first
|
||||
});
|
||||
this.logger.debug(`Driver performances sorted for race ${raceId}.`);
|
||||
|
||||
// Generate qualifying results for start positions (similar but different from race results)
|
||||
const qualiPerformances = driverPerformances.map(p => ({
|
||||
@@ -94,6 +113,7 @@ export class CompleteRaceUseCase
|
||||
const perfB = b.rating + (b.randomFactor * 150);
|
||||
return perfB - perfA;
|
||||
});
|
||||
this.logger.debug(`Qualifying performances generated for race ${raceId}.`);
|
||||
|
||||
// Generate results
|
||||
const results: Result[] = [];
|
||||
@@ -123,11 +143,13 @@ export class CompleteRaceUseCase
|
||||
})
|
||||
);
|
||||
}
|
||||
this.logger.debug(`Individual results created for race ${raceId}.`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async updateStandings(leagueId: string, results: Result[]): Promise<void> {
|
||||
this.logger.debug(`Updating standings for league ${leagueId} with ${results.length} results.`);
|
||||
// Group results by driver
|
||||
const resultsByDriver = new Map<string, Result[]>();
|
||||
for (const result of results) {
|
||||
@@ -135,6 +157,7 @@ export class CompleteRaceUseCase
|
||||
existing.push(result);
|
||||
resultsByDriver.set(result.driverId, existing);
|
||||
}
|
||||
this.logger.debug(`Results grouped by driver for league ${leagueId}.`);
|
||||
|
||||
// Update or create standings for each driver
|
||||
for (const [driverId, driverResults] of resultsByDriver) {
|
||||
@@ -145,6 +168,9 @@ export class CompleteRaceUseCase
|
||||
leagueId,
|
||||
driverId,
|
||||
});
|
||||
this.logger.debug(`Created new standing for driver ${driverId} in league ${leagueId}.`);
|
||||
} else {
|
||||
this.logger.debug(`Found existing standing for driver ${driverId} in league ${leagueId}.`);
|
||||
}
|
||||
|
||||
// Add all results for this driver (should be just one for this race)
|
||||
@@ -155,6 +181,8 @@ export class CompleteRaceUseCase
|
||||
}
|
||||
|
||||
await this.standingRepository.save(standing);
|
||||
this.logger.debug(`Standing saved for driver ${driverId} in league ${leagueId}.`);
|
||||
}
|
||||
this.logger.info(`Standings update complete for league ${leagueId}.`);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { Standing } from '../../domain/entities/Standing';
|
||||
import { RaceResultGenerator } from '../utils/RaceResultGenerator';
|
||||
import { RatingUpdateService } from '@gridpilot/identity/domain/services/RatingUpdateService';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Enhanced CompleteRaceUseCase that includes rating updates
|
||||
@@ -25,42 +26,65 @@ export class CompleteRaceUseCaseWithRatings
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
private readonly ratingUpdateService: RatingUpdateService,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: CompleteRaceCommandDTO): Promise<void> {
|
||||
const { raceId } = command;
|
||||
this.logger.debug(`Attempting to complete race with ID: ${raceId}`);
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
throw new Error('Race not found');
|
||||
try {
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
this.logger.error(`Race not found for ID: ${raceId}`);
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
this.logger.debug(`Found race: ${race.id}`);
|
||||
|
||||
// Get registered drivers for this race
|
||||
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
||||
if (registeredDriverIds.length === 0) {
|
||||
this.logger.warn(`No registered drivers for race ID: ${raceId}. Cannot complete race.`);
|
||||
throw new Error('Cannot complete race with no registered drivers');
|
||||
}
|
||||
this.logger.debug(`Found ${registeredDriverIds.length} registered drivers for race ID: ${raceId}`);
|
||||
|
||||
// Get driver ratings
|
||||
this.logger.debug('Fetching driver ratings...');
|
||||
const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds);
|
||||
this.logger.debug('Driver ratings fetched.');
|
||||
|
||||
// Generate realistic race results
|
||||
this.logger.debug('Generating race results...');
|
||||
const results = RaceResultGenerator.generateRaceResults(raceId, registeredDriverIds, driverRatings);
|
||||
this.logger.info(`Generated ${results.length} race results for race ID: ${raceId}`);
|
||||
|
||||
// Save results
|
||||
this.logger.debug('Saving race results...');
|
||||
for (const result of results) {
|
||||
await this.resultRepository.create(result);
|
||||
}
|
||||
this.logger.info('Race results saved successfully.');
|
||||
|
||||
// Update standings
|
||||
this.logger.debug(`Updating standings for league ID: ${race.leagueId}`);
|
||||
await this.updateStandings(race.leagueId, results);
|
||||
this.logger.info('Standings updated successfully.');
|
||||
|
||||
// Update driver ratings based on performance
|
||||
this.logger.debug('Updating driver ratings...');
|
||||
await this.updateDriverRatings(results, registeredDriverIds.length);
|
||||
this.logger.info('Driver ratings updated successfully.');
|
||||
|
||||
// Complete the race
|
||||
this.logger.debug(`Marking race ID: ${raceId} as complete...`);
|
||||
const completedRace = race.complete();
|
||||
await this.raceRepository.update(completedRace);
|
||||
this.logger.info(`Race ID: ${raceId} completed successfully.`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error completing race ${raceId}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Get registered drivers for this race
|
||||
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
||||
if (registeredDriverIds.length === 0) {
|
||||
throw new Error('Cannot complete race with no registered drivers');
|
||||
}
|
||||
|
||||
// Get driver ratings
|
||||
const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds);
|
||||
|
||||
// Generate realistic race results
|
||||
const results = RaceResultGenerator.generateRaceResults(raceId, registeredDriverIds, driverRatings);
|
||||
|
||||
// Save results
|
||||
for (const result of results) {
|
||||
await this.resultRepository.create(result);
|
||||
}
|
||||
|
||||
// Update standings
|
||||
await this.updateStandings(race.leagueId, results);
|
||||
|
||||
// Update driver ratings based on performance
|
||||
await this.updateDriverRatings(results, registeredDriverIds.length);
|
||||
|
||||
// Complete the race
|
||||
const completedRace = race.complete();
|
||||
await this.raceRepository.update(completedRace);
|
||||
}
|
||||
|
||||
private async updateStandings(leagueId: string, results: Result[]): Promise<void> {
|
||||
@@ -105,4 +129,4 @@ export class CompleteRaceUseCaseWithRatings
|
||||
|
||||
await this.ratingUpdateService.updateDriverRatingsAfterRace(driverResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import type {
|
||||
LeagueScoringPresetProvider,
|
||||
LeagueScoringPresetDTO,
|
||||
@@ -61,107 +62,136 @@ export class CreateLeagueWithSeasonAndScoringUseCase
|
||||
async execute(
|
||||
command: CreateLeagueWithSeasonAndScoringCommand,
|
||||
): Promise<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({
|
||||
id: leagueId,
|
||||
name: command.name,
|
||||
description: command.description ?? '',
|
||||
ownerId: command.ownerId,
|
||||
settings: {
|
||||
// Presets are attached at scoring-config level; league settings use a stable points system id.
|
||||
pointsSystem: 'custom',
|
||||
...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}),
|
||||
},
|
||||
});
|
||||
const league = League.create({
|
||||
id: leagueId,
|
||||
name: command.name,
|
||||
description: command.description ?? '',
|
||||
ownerId: command.ownerId,
|
||||
settings: {
|
||||
pointsSystem: 'custom',
|
||||
...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
await this.leagueRepository.create(league);
|
||||
await this.leagueRepository.create(league);
|
||||
this.logger.info(`League ${league.name} (${league.id}) created successfully.`);
|
||||
|
||||
const seasonId = uuidv4();
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
gameId: command.gameId,
|
||||
name: `${command.name} Season 1`,
|
||||
year: new Date().getFullYear(),
|
||||
order: 1,
|
||||
status: 'active',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
});
|
||||
const seasonId = uuidv4();
|
||||
this.logger.debug(`Generated seasonId: ${seasonId}`);
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
gameId: command.gameId,
|
||||
name: `${command.name} Season 1`,
|
||||
year: new Date().getFullYear(),
|
||||
order: 1,
|
||||
status: 'active',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
});
|
||||
|
||||
await this.seasonRepository.create(season);
|
||||
await this.seasonRepository.create(season);
|
||||
this.logger.info(`Season ${season.name} (${season.id}) created for league ${league.id}.`);
|
||||
|
||||
const presetId = command.scoringPresetId ?? 'club-default';
|
||||
const preset: LeagueScoringPresetDTO | undefined =
|
||||
this.presetProvider.getPresetById(presetId);
|
||||
const presetId = command.scoringPresetId ?? 'club-default';
|
||||
this.logger.debug(`Attempting to retrieve scoring preset: ${presetId}`);
|
||||
const preset: LeagueScoringPresetDTO | undefined =
|
||||
this.presetProvider.getPresetById(presetId);
|
||||
|
||||
if (!preset) {
|
||||
throw new Error(`Unknown scoring preset: ${presetId}`);
|
||||
if (!preset) {
|
||||
this.logger.error(`Unknown scoring preset: ${presetId}`);
|
||||
throw new Error(`Unknown scoring preset: ${presetId}`);
|
||||
}
|
||||
this.logger.info(`Scoring preset ${preset.name} (${preset.id}) retrieved.`);
|
||||
|
||||
|
||||
const scoringConfig: LeagueScoringConfig = {
|
||||
id: uuidv4(),
|
||||
seasonId,
|
||||
scoringPresetId: preset.id,
|
||||
championships: [],
|
||||
};
|
||||
|
||||
const fullConfigFactory = (await import(
|
||||
'../../infrastructure/repositories/InMemoryScoringRepositories'
|
||||
)) as typeof import('../../infrastructure/repositories/InMemoryScoringRepositories');
|
||||
|
||||
const presetFromInfra = fullConfigFactory.getLeagueScoringPresetById(
|
||||
preset.id,
|
||||
);
|
||||
if (!presetFromInfra) {
|
||||
this.logger.error(`Preset registry missing preset: ${preset.id}`);
|
||||
throw new Error(`Preset registry missing preset: ${preset.id}`);
|
||||
}
|
||||
this.logger.debug(`Preset from infrastructure retrieved for ${preset.id}.`);
|
||||
|
||||
const infraConfig = presetFromInfra.createConfig({ seasonId });
|
||||
const finalConfig: LeagueScoringConfig = {
|
||||
...infraConfig,
|
||||
scoringPresetId: preset.id,
|
||||
};
|
||||
|
||||
await this.leagueScoringConfigRepository.save(finalConfig);
|
||||
this.logger.info(`Scoring configuration saved for season ${seasonId}.`);
|
||||
|
||||
const result = {
|
||||
leagueId: league.id,
|
||||
seasonId,
|
||||
scoringPresetId: preset.id,
|
||||
scoringPresetName: preset.name,
|
||||
};
|
||||
this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result });
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', {
|
||||
command,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
const scoringConfig: LeagueScoringConfig = {
|
||||
id: uuidv4(),
|
||||
seasonId,
|
||||
scoringPresetId: preset.id,
|
||||
championships: [],
|
||||
};
|
||||
|
||||
// For the initial alpha slice, we keep using the preset's config shape from the in-memory registry.
|
||||
// The preset registry is responsible for building the full LeagueScoringConfig; we only attach the preset id here.
|
||||
const fullConfigFactory = (await import(
|
||||
'../../infrastructure/repositories/InMemoryScoringRepositories'
|
||||
)) as typeof import('../../infrastructure/repositories/InMemoryScoringRepositories');
|
||||
|
||||
const presetFromInfra = fullConfigFactory.getLeagueScoringPresetById(
|
||||
preset.id,
|
||||
);
|
||||
if (!presetFromInfra) {
|
||||
throw new Error(`Preset registry missing preset: ${preset.id}`);
|
||||
}
|
||||
|
||||
const infraConfig = presetFromInfra.createConfig({ seasonId });
|
||||
const finalConfig: LeagueScoringConfig = {
|
||||
...infraConfig,
|
||||
scoringPresetId: preset.id,
|
||||
};
|
||||
|
||||
await this.leagueScoringConfigRepository.save(finalConfig);
|
||||
|
||||
return {
|
||||
leagueId: league.id,
|
||||
seasonId,
|
||||
scoringPresetId: preset.id,
|
||||
scoringPresetName: preset.name,
|
||||
};
|
||||
}
|
||||
|
||||
private validate(command: CreateLeagueWithSeasonAndScoringCommand): void {
|
||||
this.logger.debug('Validating CreateLeagueWithSeasonAndScoringCommand', { command });
|
||||
if (!command.name || command.name.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: League name is required', { command });
|
||||
throw new Error('League name is required');
|
||||
}
|
||||
if (!command.ownerId || command.ownerId.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: League ownerId is required', { command });
|
||||
throw new Error('League ownerId is required');
|
||||
}
|
||||
if (!command.gameId || command.gameId.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: gameId is required', { command });
|
||||
throw new Error('gameId is required');
|
||||
}
|
||||
if (!command.visibility) {
|
||||
this.logger.warn('Validation failed: visibility is required', { command });
|
||||
throw new Error('visibility is required');
|
||||
}
|
||||
if (command.maxDrivers !== undefined && command.maxDrivers <= 0) {
|
||||
this.logger.warn('Validation failed: maxDrivers must be greater than 0 when provided', { command });
|
||||
throw new Error('maxDrivers must be greater than 0 when provided');
|
||||
}
|
||||
|
||||
// Validate visibility-specific constraints
|
||||
const visibility = LeagueVisibility.fromString(command.visibility);
|
||||
|
||||
if (visibility.isRanked()) {
|
||||
// Ranked (public) leagues require minimum 10 drivers for competitive integrity
|
||||
const driverCount = command.maxDrivers ?? 0;
|
||||
if (driverCount < MIN_RANKED_LEAGUE_DRIVERS) {
|
||||
this.logger.warn(
|
||||
`Validation failed: Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. Current setting: ${driverCount}.`,
|
||||
{ command }
|
||||
);
|
||||
throw new Error(
|
||||
`Ranked leagues require at least ${MIN_RANKED_LEAGUE_DRIVERS} drivers. ` +
|
||||
`Current setting: ${driverCount}. ` +
|
||||
@@ -169,5 +199,6 @@ export class CreateLeagueWithSeasonAndScoringUseCase
|
||||
);
|
||||
}
|
||||
}
|
||||
this.logger.debug('Validation successful.');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import type {
|
||||
IAllRacesPagePresenter,
|
||||
AllRacesPageResultDTO,
|
||||
@@ -7,59 +8,68 @@ import type {
|
||||
AllRacesListItemViewModel,
|
||||
AllRacesFilterOptionsViewModel,
|
||||
} from '../presenters/IAllRacesPagePresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { UseCase } => '@gridpilot/shared/application';
|
||||
|
||||
export class GetAllRacesPageDataUseCase
|
||||
implements UseCase<void, AllRacesPageResultDTO, AllRacesPageViewModel, IAllRacesPagePresenter> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(_input: void, presenter: IAllRacesPagePresenter): Promise<void> {
|
||||
const [allRaces, allLeagues] = await Promise.all([
|
||||
this.raceRepository.findAll(),
|
||||
this.leagueRepository.findAll(),
|
||||
]);
|
||||
this.logger.debug('Executing GetAllRacesPageDataUseCase');
|
||||
try {
|
||||
const [allRaces, allLeagues] = await Promise.all([
|
||||
this.raceRepository.findAll(),
|
||||
this.leagueRepository.findAll(),
|
||||
]);
|
||||
this.logger.info(`Found ${allRaces.length} races and ${allLeagues.length} leagues.`);
|
||||
|
||||
const leagueMap = new Map(allLeagues.map((league) => [league.id, league.name]));
|
||||
const leagueMap = new Map(allLeagues.map((league) => [league.id, league.name]));
|
||||
|
||||
const races: AllRacesListItemViewModel[] = allRaces
|
||||
.slice()
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
|
||||
.map((race) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
status: race.status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
|
||||
strengthOfField: race.strengthOfField ?? null,
|
||||
}));
|
||||
const races: AllRacesListItemViewModel[] = allRaces
|
||||
.slice()
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime())
|
||||
.map((race) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
status: race.status,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
|
||||
strengthOfField: race.strengthOfField ?? null,
|
||||
}));
|
||||
|
||||
const uniqueLeagues = new Map<string, { id: string; name: string }>();
|
||||
for (const league of allLeagues) {
|
||||
uniqueLeagues.set(league.id, { id: league.id, name: league.name });
|
||||
const uniqueLeagues = new Map<string, { id: string; name: string }>();
|
||||
for (const league of allLeagues) {
|
||||
uniqueLeagues.set(league.id, { id: league.id, name: league.name });
|
||||
}
|
||||
|
||||
const filters: AllRacesFilterOptionsViewModel = {
|
||||
statuses: [
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'scheduled', label: 'Scheduled' },
|
||||
{ value: 'running', label: 'Live' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
],
|
||||
leagues: Array.from(uniqueLeagues.values()),
|
||||
};
|
||||
|
||||
const viewModel: AllRacesPageViewModel = {
|
||||
races,
|
||||
filters,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(viewModel);
|
||||
this.logger.debug('Successfully presented all races page data.');
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing GetAllRacesPageDataUseCase', { error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const filters: AllRacesFilterOptionsViewModel = {
|
||||
statuses: [
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'scheduled', label: 'Scheduled' },
|
||||
{ value: 'running', label: 'Live' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
],
|
||||
leagues: Array.from(uniqueLeagues.values()),
|
||||
};
|
||||
|
||||
const viewModel: AllRacesPageViewModel = {
|
||||
races,
|
||||
filters,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(viewModel);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from '../presenters/IAllTeamsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
@@ -17,33 +18,44 @@ export class GetAllTeamsUseCase
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(_input: void, presenter: IAllTeamsPresenter): Promise<void> {
|
||||
this.logger.debug('Executing GetAllTeamsUseCase');
|
||||
presenter.reset();
|
||||
|
||||
const teams = await this.teamRepository.findAll();
|
||||
try {
|
||||
const teams = await this.teamRepository.findAll();
|
||||
if (teams.length === 0) {
|
||||
this.logger.warn('No teams found.');
|
||||
}
|
||||
|
||||
const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: [...team.leagues],
|
||||
createdAt: team.createdAt,
|
||||
memberCount,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const enrichedTeams: AllTeamsResultDTO['teams'] = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: [...team.leagues],
|
||||
createdAt: team.createdAt,
|
||||
memberCount,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const dto: AllTeamsResultDTO = {
|
||||
teams: enrichedTeams,
|
||||
};
|
||||
const dto: AllTeamsResultDTO = {
|
||||
teams: enrichedTeams,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
presenter.present(dto);
|
||||
this.logger.info('Successfully retrieved all teams.');
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving all teams:', error);
|
||||
throw error; // Re-throw the error after logging
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
DriverTeamViewModel,
|
||||
} from '../presenters/IDriverTeamPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a driver's team.
|
||||
@@ -17,23 +18,29 @@ export class GetDriverTeamUseCase
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: IDriverTeamPresenter,
|
||||
) {}
|
||||
|
||||
async execute(input: { driverId: string }, presenter: IDriverTeamPresenter): Promise<void> {
|
||||
this.logger.debug(`Executing GetDriverTeamUseCase for driverId: ${input.driverId}`);
|
||||
presenter.reset();
|
||||
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId);
|
||||
if (!membership) {
|
||||
this.logger.warn(`No active membership found for driverId: ${input.driverId}`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug(`Found membership for driverId: ${input.driverId}, teamId: ${membership.teamId}`);
|
||||
|
||||
const team = await this.teamRepository.findById(membership.teamId);
|
||||
if (!team) {
|
||||
this.logger.error(`Team not found for teamId: ${membership.teamId}`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug(`Found team for teamId: ${team.id}, name: ${team.name}`);
|
||||
|
||||
const dto: DriverTeamResultDTO = {
|
||||
team,
|
||||
@@ -42,5 +49,6 @@ export class GetDriverTeamUseCase
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
this.logger.info(`Successfully presented driver team for driverId: ${input.driverId}`);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type { SponsorableEntityType } from '../../domain/entities/SponsorshipReq
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
export interface GetEntitySponsorshipPricingDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -46,74 +47,115 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
private readonly presenter: IEntitySponsorshipPricingPresenter,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> {
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
|
||||
if (!pricing) {
|
||||
this.presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Count pending requests by tier
|
||||
const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
this.logger.debug(
|
||||
`Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
|
||||
{ dto },
|
||||
);
|
||||
const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length;
|
||||
const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length;
|
||||
|
||||
// Count filled slots (for seasons, check SeasonSponsorship table)
|
||||
let filledMainSlots = 0;
|
||||
let filledSecondarySlots = 0;
|
||||
try {
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
|
||||
if (dto.entityType === 'season') {
|
||||
const sponsorships = await this.seasonSponsorshipRepo.findBySeasonId(dto.entityId);
|
||||
const activeSponsorships = sponsorships.filter(s => s.isActive());
|
||||
filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length;
|
||||
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
|
||||
}
|
||||
if (!pricing) {
|
||||
this.logger.warn(
|
||||
`No pricing found for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Presenting null.`,
|
||||
{ dto },
|
||||
);
|
||||
this.presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const result: GetEntitySponsorshipPricingResultDTO = {
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
acceptingApplications: pricing.acceptingApplications,
|
||||
...(pricing.customRequirements !== undefined
|
||||
? { customRequirements: pricing.customRequirements }
|
||||
: {}),
|
||||
};
|
||||
this.logger.debug(`Found pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, { pricing });
|
||||
|
||||
if (pricing.mainSlot) {
|
||||
const mainMaxSlots = pricing.mainSlot.maxSlots;
|
||||
result.mainSlot = {
|
||||
tier: 'main',
|
||||
price: pricing.mainSlot.price.amount,
|
||||
currency: pricing.mainSlot.price.currency,
|
||||
formattedPrice: pricing.mainSlot.price.format(),
|
||||
benefits: pricing.mainSlot.benefits,
|
||||
available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots,
|
||||
maxSlots: mainMaxSlots,
|
||||
filledSlots: filledMainSlots,
|
||||
pendingRequests: pendingMainCount,
|
||||
// Count pending requests by tier
|
||||
const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId,
|
||||
);
|
||||
const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length;
|
||||
const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length;
|
||||
|
||||
this.logger.debug(
|
||||
`Pending requests counts: main=${pendingMainCount}, secondary=${pendingSecondaryCount}`,
|
||||
);
|
||||
|
||||
// Count filled slots (for seasons, check SeasonSponsorship table)
|
||||
let filledMainSlots = 0;
|
||||
let filledSecondarySlots = 0;
|
||||
|
||||
if (dto.entityType === 'season') {
|
||||
const sponsorships = await this.seasonSponsorshipRepo.findBySeasonId(dto.entityId);
|
||||
const activeSponsorships = sponsorships.filter(s => s.isActive());
|
||||
filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length;
|
||||
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
|
||||
this.logger.debug(
|
||||
`Filled slots for season: main=${filledMainSlots}, secondary=${filledSecondarySlots}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result: GetEntitySponsorshipPricingResultDTO = {
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
acceptingApplications: pricing.acceptingApplications,
|
||||
...(pricing.customRequirements !== undefined
|
||||
? { customRequirements: pricing.customRequirements }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (pricing.secondarySlots) {
|
||||
const secondaryMaxSlots = pricing.secondarySlots.maxSlots;
|
||||
result.secondarySlot = {
|
||||
tier: 'secondary',
|
||||
price: pricing.secondarySlots.price.amount,
|
||||
currency: pricing.secondarySlots.price.currency,
|
||||
formattedPrice: pricing.secondarySlots.price.format(),
|
||||
benefits: pricing.secondarySlots.benefits,
|
||||
available: pricing.secondarySlots.available && filledSecondarySlots < secondaryMaxSlots,
|
||||
maxSlots: secondaryMaxSlots,
|
||||
filledSlots: filledSecondarySlots,
|
||||
pendingRequests: pendingSecondaryCount,
|
||||
};
|
||||
}
|
||||
if (pricing.mainSlot) {
|
||||
const mainMaxSlots = pricing.mainSlot.maxSlots;
|
||||
result.mainSlot = {
|
||||
tier: 'main',
|
||||
price: pricing.mainSlot.price.amount,
|
||||
currency: pricing.mainSlot.price.currency,
|
||||
formattedPrice: pricing.mainSlot.price.format(),
|
||||
benefits: pricing.mainSlot.benefits,
|
||||
available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots,
|
||||
maxSlots: mainMaxSlots,
|
||||
filledSlots: filledMainSlots,
|
||||
pendingRequests: pendingMainCount,
|
||||
};
|
||||
this.logger.debug(`Main slot pricing information processed`, { mainSlot: result.mainSlot });
|
||||
}
|
||||
|
||||
this.presenter.present(result);
|
||||
if (pricing.secondarySlots) {
|
||||
const secondaryMaxSlots = pricing.secondarySlots.maxSlots;
|
||||
result.secondarySlot = {
|
||||
tier: 'secondary',
|
||||
price: pricing.secondarySlots.price.amount,
|
||||
currency: pricing.secondarySlots.price.currency,
|
||||
formattedPrice: pricing.secondarySlots.price.format(),
|
||||
benefits: pricing.secondarySlots.benefits,
|
||||
available:
|
||||
pricing.secondarySlots.available && filledSecondarySlots < secondaryMaxSlots,
|
||||
maxSlots: secondaryMaxSlots,
|
||||
filledSlots: filledSecondarySlots,
|
||||
pendingRequests: pendingSecondaryCount,
|
||||
};
|
||||
this.logger.debug(`Secondary slot pricing information processed`, {
|
||||
secondarySlot: result.secondarySlot,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Successfully retrieved and processed entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
|
||||
{ result },
|
||||
);
|
||||
this.presenter.present(result);
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = 'An unknown error occurred';
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
this.logger.error(
|
||||
`Failed to get entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Error: ${errorMessage}`,
|
||||
{ error, dto },
|
||||
);
|
||||
// Re-throw the error or present an error state if the presenter supports it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IResultRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
||||
import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import {
|
||||
AverageStrengthOfFieldCalculator,
|
||||
type StrengthOfFieldCalculator,
|
||||
@@ -31,55 +32,78 @@ export class GetLeagueStatsUseCase
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
public readonly presenter: ILeagueStatsPresenter,
|
||||
private readonly logger: ILogger,
|
||||
sofCalculator?: StrengthOfFieldCalculator,
|
||||
) {
|
||||
this.sofCalculator = sofCalculator ?? new AverageStrengthOfFieldCalculator();
|
||||
}
|
||||
|
||||
async execute(params: GetLeagueStatsUseCaseParams): Promise<void> {
|
||||
this.logger.debug(
|
||||
`Executing GetLeagueStatsUseCase with params: ${JSON.stringify(params)}`,
|
||||
);
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League ${leagueId} not found`);
|
||||
}
|
||||
|
||||
const races = await this.raceRepository.findByLeagueId(leagueId);
|
||||
const completedRaces = races.filter(r => r.status === 'completed');
|
||||
const scheduledRaces = races.filter(r => r.status === 'scheduled');
|
||||
|
||||
// Calculate SOF for each completed race
|
||||
const sofValues: number[] = [];
|
||||
|
||||
for (const race of completedRaces) {
|
||||
// Use stored SOF if available
|
||||
if (race.strengthOfField) {
|
||||
sofValues.push(race.strengthOfField);
|
||||
continue;
|
||||
try {
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
this.logger.error(`League ${leagueId} not found`);
|
||||
throw new Error(`League ${leagueId} not found`);
|
||||
}
|
||||
|
||||
// Otherwise calculate from results
|
||||
const results = await this.resultRepository.findByRaceId(race.id);
|
||||
if (results.length === 0) continue;
|
||||
const races = await this.raceRepository.findByLeagueId(leagueId);
|
||||
const completedRaces = races.filter(r => r.status === 'completed');
|
||||
const scheduledRaces = races.filter(r => r.status === 'scheduled');
|
||||
this.logger.info(
|
||||
`Found ${races.length} races for league ${leagueId}: ${completedRaces.length} completed, ${scheduledRaces.length} scheduled. `,
|
||||
);
|
||||
|
||||
const driverIds = results.map(r => r.driverId);
|
||||
const ratings = this.driverRatingProvider.getRatings(driverIds);
|
||||
const driverRatings = driverIds
|
||||
.filter(id => ratings.has(id))
|
||||
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
|
||||
// Calculate SOF for each completed race
|
||||
const sofValues: number[] = [];
|
||||
|
||||
const sof = this.sofCalculator.calculate(driverRatings);
|
||||
if (sof !== null) {
|
||||
sofValues.push(sof);
|
||||
for (const race of completedRaces) {
|
||||
// Use stored SOF if available
|
||||
if (race.strengthOfField) {
|
||||
this.logger.debug(
|
||||
`Using stored Strength of Field for race ${race.id}: ${race.strengthOfField}`,
|
||||
);
|
||||
sofValues.push(race.strengthOfField);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise calculate from results
|
||||
const results = await this.resultRepository.findByRaceId(race.id);
|
||||
if (results.length === 0) {
|
||||
this.logger.debug(`No results found for race ${race.id}. Skipping SOF calculation.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const driverIds = results.map(r => r.driverId);
|
||||
const ratings = this.driverRatingProvider.getRatings(driverIds);
|
||||
const driverRatings = driverIds
|
||||
.filter(id => ratings.has(id))
|
||||
.map(id => ({ driverId: id, rating: ratings.get(id)! }));
|
||||
|
||||
const sof = this.sofCalculator.calculate(driverRatings);
|
||||
if (sof !== null) {
|
||||
this.logger.debug(`Calculated Strength of Field for race ${race.id}: ${sof}`);
|
||||
sofValues.push(sof);
|
||||
} else {
|
||||
this.logger.warn(`Could not calculate Strength of Field for race ${race.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.presenter.present(
|
||||
leagueId,
|
||||
races.length,
|
||||
completedRaces.length,
|
||||
scheduledRaces.length,
|
||||
sofValues
|
||||
);
|
||||
this.presenter.present(
|
||||
leagueId,
|
||||
races.length,
|
||||
completedRaces.length,
|
||||
scheduledRaces.length,
|
||||
sofValues,
|
||||
);
|
||||
this.logger.info(`Successfully presented league statistics for league ${leagueId}.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in GetLeagueStatsUseCase: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
TeamJoinRequestsViewModel,
|
||||
} from '../presenters/ITeamJoinRequestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team join requests.
|
||||
@@ -19,33 +20,44 @@ export class GetTeamJoinRequestsUseCase
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
private readonly logger: ILogger,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: ITeamJoinRequestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(input: { teamId: string }, presenter: ITeamJoinRequestsPresenter): Promise<void> {
|
||||
this.logger.debug('Executing GetTeamJoinRequestsUseCase', { teamId: input.teamId });
|
||||
presenter.reset();
|
||||
|
||||
const requests = await this.membershipRepository.getJoinRequests(input.teamId);
|
||||
try {
|
||||
const requests = await this.membershipRepository.getJoinRequests(input.teamId);
|
||||
this.logger.info('Successfully retrieved team join requests', { teamId: input.teamId, count: requests.length });
|
||||
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
for (const request of requests) {
|
||||
const driver = await this.driverRepository.findById(request.driverId);
|
||||
if (driver) {
|
||||
driverNames[request.driverId] = driver.name;
|
||||
for (const request of requests) {
|
||||
const driver = await this.driverRepository.findById(request.driverId);
|
||||
if (driver) {
|
||||
driverNames[request.driverId] = driver.name;
|
||||
} else {
|
||||
this.logger.warn(`Driver not found for ID: ${request.driverId} during join request processing.`);
|
||||
}
|
||||
avatarUrls[request.driverId] = this.imageService.getDriverAvatar(request.driverId);
|
||||
this.logger.debug('Processed driver details for join request', { driverId: request.driverId });
|
||||
}
|
||||
avatarUrls[request.driverId] = this.imageService.getDriverAvatar(request.driverId);
|
||||
|
||||
const dto: TeamJoinRequestsResultDTO = {
|
||||
requests,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving team join requests', { teamId: input.teamId, error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const dto: TeamJoinRequestsResultDTO = {
|
||||
requests,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
TeamMembersViewModel,
|
||||
} from '../presenters/ITeamMembersPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team members.
|
||||
@@ -19,33 +20,45 @@ export class GetTeamMembersUseCase
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
private readonly logger: ILogger,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: ITeamMembersPresenter,
|
||||
) {}
|
||||
|
||||
async execute(input: { teamId: string }, presenter: ITeamMembersPresenter): Promise<void> {
|
||||
this.logger.debug(`Executing GetTeamMembersUseCase for teamId: ${input.teamId}`);
|
||||
presenter.reset();
|
||||
|
||||
const memberships = await this.membershipRepository.getTeamMembers(input.teamId);
|
||||
try {
|
||||
const memberships = await this.membershipRepository.getTeamMembers(input.teamId);
|
||||
this.logger.info(`Found ${memberships.length} memberships for teamId: ${input.teamId}`);
|
||||
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
for (const membership of memberships) {
|
||||
const driver = await this.driverRepository.findById(membership.driverId);
|
||||
if (driver) {
|
||||
driverNames[membership.driverId] = driver.name;
|
||||
for (const membership of memberships) {
|
||||
this.logger.debug(`Processing membership for driverId: ${membership.driverId}`);
|
||||
const driver = await this.driverRepository.findById(membership.driverId);
|
||||
if (driver) {
|
||||
driverNames[membership.driverId] = driver.name;
|
||||
} else {
|
||||
this.logger.warn(`Driver with ID ${membership.driverId} not found while fetching team members for team ${input.teamId}.`);
|
||||
}
|
||||
avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId);
|
||||
}
|
||||
avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId);
|
||||
|
||||
const dto: TeamMembersResultDTO = {
|
||||
memberships,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
this.logger.info(`Successfully presented team members for teamId: ${input.teamId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in GetTeamMembersUseCase for teamId: ${input.teamId}, error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const dto: TeamMembersResultDTO = {
|
||||
memberships,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
IImportRaceResultsPresenter,
|
||||
ImportRaceResultsSummaryViewModel,
|
||||
} from '../presenters/IImportRaceResultsPresenter';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
export interface ImportRaceResultDTO {
|
||||
id: string;
|
||||
@@ -39,53 +40,72 @@ export class ImportRaceResultsUseCase
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
public readonly presenter: IImportRaceResultsPresenter,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(params: ImportRaceResultsParams): Promise<void> {
|
||||
this.logger.debug('ImportRaceResultsUseCase:execute', { params });
|
||||
const { raceId, results } = params;
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
throw new EntityNotFoundError({ entity: 'race', id: raceId });
|
||||
try {
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
this.logger.warn(`ImportRaceResultsUseCase: Race with ID ${raceId} not found.`);
|
||||
throw new EntityNotFoundError({ entity: 'race', id: raceId });
|
||||
}
|
||||
this.logger.debug(`ImportRaceResultsUseCase: Race ${raceId} found.`);
|
||||
|
||||
const league = await this.leagueRepository.findById(race.leagueId);
|
||||
if (!league) {
|
||||
this.logger.warn(`ImportRaceResultsUseCase: League with ID ${race.leagueId} not found for race ${raceId}.`);
|
||||
throw new EntityNotFoundError({ entity: 'league', id: race.leagueId });
|
||||
}
|
||||
this.logger.debug(`ImportRaceResultsUseCase: League ${league.id} found.`);
|
||||
|
||||
const existing = await this.resultRepository.existsByRaceId(raceId);
|
||||
if (existing) {
|
||||
this.logger.warn(`ImportRaceResultsUseCase: Results already exist for race ID: ${raceId}.`);
|
||||
throw new BusinessRuleViolationError('Results already exist for this race');
|
||||
}
|
||||
this.logger.debug(`ImportRaceResultsUseCase: No existing results for race ${raceId}.`);
|
||||
|
||||
// Lookup drivers by iracingId and create results with driver.id
|
||||
const entities = await Promise.all(
|
||||
results.map(async (dto) => {
|
||||
const driver = await this.driverRepository.findByIRacingId(dto.driverId);
|
||||
if (!driver) {
|
||||
this.logger.warn(`ImportRaceResultsUseCase: Driver with iRacing ID ${dto.driverId} not found for race ${raceId}.`);
|
||||
throw new BusinessRuleViolationError(`Driver with iRacing ID ${dto.driverId} not found`);
|
||||
}
|
||||
return Result.create({
|
||||
id: dto.id,
|
||||
raceId: dto.raceId,
|
||||
driverId: driver.id,
|
||||
position: dto.position,
|
||||
fastestLap: dto.fastestLap,
|
||||
incidents: dto.incidents,
|
||||
startPosition: dto.startPosition,
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.logger.debug('ImportRaceResultsUseCase:entities created', { count: entities.length });
|
||||
|
||||
await this.resultRepository.createMany(entities);
|
||||
this.logger.info('ImportRaceResultsUseCase:race results created', { raceId });
|
||||
|
||||
await this.standingRepository.recalculate(league.id);
|
||||
this.logger.info('ImportRaceResultsUseCase:standings recalculated', { leagueId: league.id });
|
||||
|
||||
const viewModel: ImportRaceResultsSummaryViewModel = {
|
||||
importedCount: results.length,
|
||||
standingsRecalculated: true,
|
||||
};
|
||||
this.logger.debug('ImportRaceResultsUseCase:presenting view model', { viewModel });
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
} catch (error) {
|
||||
this.logger.error('ImportRaceResultsUseCase:execution error', { error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const league = await this.leagueRepository.findById(race.leagueId);
|
||||
if (!league) {
|
||||
throw new EntityNotFoundError({ entity: 'league', id: race.leagueId });
|
||||
}
|
||||
|
||||
const existing = await this.resultRepository.existsByRaceId(raceId);
|
||||
if (existing) {
|
||||
throw new BusinessRuleViolationError('Results already exist for this race');
|
||||
}
|
||||
|
||||
// Lookup drivers by iracingId and create results with driver.id
|
||||
const entities = await Promise.all(
|
||||
results.map(async (dto) => {
|
||||
const driver = await this.driverRepository.findByIRacingId(dto.driverId);
|
||||
if (!driver) {
|
||||
throw new BusinessRuleViolationError(`Driver with iRacing ID ${dto.driverId} not found`);
|
||||
}
|
||||
return Result.create({
|
||||
id: dto.id,
|
||||
raceId: dto.raceId,
|
||||
driverId: driver.id,
|
||||
position: dto.position,
|
||||
fastestLap: dto.fastestLap,
|
||||
incidents: dto.incidents,
|
||||
startPosition: dto.startPosition,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
await this.resultRepository.createMany(entities);
|
||||
await this.standingRepository.recalculate(league.id);
|
||||
|
||||
const viewModel: ImportRaceResultsSummaryViewModel = {
|
||||
importedCount: results.length,
|
||||
standingsRecalculated: true,
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import type {
|
||||
ILeagueMembershipRepository,
|
||||
} from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
@@ -11,7 +12,10 @@ import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO';
|
||||
import { BusinessRuleViolationError } from '../errors/RacingApplicationError';
|
||||
|
||||
export class JoinLeagueUseCase implements AsyncUseCase<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.
|
||||
@@ -21,20 +25,31 @@ export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, Lea
|
||||
* - Creates a new active membership with role "member" and current timestamp.
|
||||
*/
|
||||
async execute(command: JoinLeagueCommandDTO): Promise<LeagueMembership> {
|
||||
this.logger.debug('Attempting to join league', { command });
|
||||
const { leagueId, driverId } = command;
|
||||
|
||||
const existing = await this.membershipRepository.getMembership(leagueId, driverId);
|
||||
if (existing) {
|
||||
throw new BusinessRuleViolationError('Already a member or have a pending request');
|
||||
try {
|
||||
const existing = await this.membershipRepository.getMembership(leagueId, driverId);
|
||||
if (existing) {
|
||||
this.logger.warn('Driver already a member or has pending request', { leagueId, driverId });
|
||||
throw new BusinessRuleViolationError('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const membership = LeagueMembership.create({
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member' as MembershipRole,
|
||||
status: 'active' as MembershipStatus,
|
||||
});
|
||||
|
||||
const savedMembership = await this.membershipRepository.saveMembership(membership);
|
||||
this.logger.info('Successfully joined league', { membershipId: savedMembership.id });
|
||||
return savedMembership;
|
||||
} catch (error) {
|
||||
if (!(error instanceof BusinessRuleViolationError)) {
|
||||
this.logger.error('Failed to join league due to an unexpected error', { command, error: error.message });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const membership = LeagueMembership.create({
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member' as MembershipRole,
|
||||
status: 'active' as MembershipStatus,
|
||||
});
|
||||
|
||||
return this.membershipRepository.saveMembership(membership);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from '../../domain/types/TeamMembership';
|
||||
import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
import {
|
||||
BusinessRuleViolationError,
|
||||
EntityNotFoundError,
|
||||
@@ -16,36 +17,50 @@ export class JoinTeamUseCase implements AsyncUseCase<JoinTeamCommandDTO, void> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: JoinTeamCommandDTO): Promise<void> {
|
||||
this.logger.debug('Attempting to join team', { command });
|
||||
const { teamId, driverId } = command;
|
||||
|
||||
const existingActive = await this.membershipRepository.getActiveMembershipForDriver(
|
||||
driverId,
|
||||
);
|
||||
if (existingActive) {
|
||||
throw new BusinessRuleViolationError('Driver already belongs to a team');
|
||||
try {
|
||||
const existingActive = await this.membershipRepository.getActiveMembershipForDriver(
|
||||
driverId,
|
||||
);
|
||||
if (existingActive) {
|
||||
this.logger.warn('Driver already belongs to a team', { driverId, teamId });
|
||||
throw new BusinessRuleViolationError('Driver already belongs to a team');
|
||||
}
|
||||
|
||||
const existingMembership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
if (existingMembership) {
|
||||
this.logger.warn('Driver already has a pending or active membership request', { driverId, teamId });
|
||||
throw new BusinessRuleViolationError('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
if (!team) {
|
||||
this.logger.error('Team not found', { entity: 'team', id: teamId });
|
||||
throw new EntityNotFoundError({ entity: 'team', id: teamId });
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
this.logger.info('Driver successfully joined team', { driverId, teamId });
|
||||
} catch (error) {
|
||||
if (error instanceof BusinessRuleViolationError || error instanceof EntityNotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error('Failed to join team due to an unexpected error', { error, command });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingMembership = await this.membershipRepository.getMembership(teamId, driverId);
|
||||
if (existingMembership) {
|
||||
throw new BusinessRuleViolationError('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(teamId);
|
||||
if (!team) {
|
||||
throw new EntityNotFoundError({ entity: 'team', id: teamId });
|
||||
}
|
||||
|
||||
const membership: TeamMembership = {
|
||||
teamId,
|
||||
driverId,
|
||||
role: 'driver' as TeamRole,
|
||||
status: 'active' as TeamMembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.membershipRepository.saveMembership(membership);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
||||
|
||||
export interface QuickPenaltyCommand {
|
||||
raceId: string;
|
||||
@@ -27,50 +28,60 @@ export class QuickPenaltyUseCase
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async execute(command: QuickPenaltyCommand): Promise<{ penaltyId: string }> {
|
||||
// Validate race exists
|
||||
const race = await this.raceRepository.findById(command.raceId);
|
||||
if (!race) {
|
||||
throw new Error('Race not found');
|
||||
this.logger.debug('Executing QuickPenaltyUseCase', { command });
|
||||
try {
|
||||
// Validate race exists
|
||||
const race = await this.raceRepository.findById(command.raceId);
|
||||
if (!race) {
|
||||
this.logger.warn('Race not found', { raceId: command.raceId });
|
||||
throw new Error('Race not found');
|
||||
}
|
||||
|
||||
// Validate admin has authority
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
|
||||
const adminMembership = memberships.find(
|
||||
m => m.driverId === command.adminId && m.status === 'active'
|
||||
);
|
||||
|
||||
if (!adminMembership || (adminMembership.role !== 'owner' && adminMembership.role !== 'admin')) {
|
||||
this.logger.warn('Unauthorized admin attempting to issue penalty', { adminId: command.adminId, leagueId: race.leagueId });
|
||||
throw new Error('Only league owners and admins can issue penalties');
|
||||
}
|
||||
|
||||
// Map infraction + severity to penalty type and value
|
||||
const { type, value, reason } = this.mapInfractionToPenalty(
|
||||
command.infractionType,
|
||||
command.severity
|
||||
);
|
||||
|
||||
// Create the penalty
|
||||
const penalty = Penalty.create({
|
||||
id: randomUUID(),
|
||||
leagueId: race.leagueId,
|
||||
raceId: command.raceId,
|
||||
driverId: command.driverId,
|
||||
type,
|
||||
...(value !== undefined ? { value } : {}),
|
||||
reason,
|
||||
issuedBy: command.adminId,
|
||||
status: 'applied', // Quick penalties are applied immediately
|
||||
issuedAt: new Date(),
|
||||
appliedAt: new Date(),
|
||||
...(command.notes !== undefined ? { notes: command.notes } : {}),
|
||||
});
|
||||
|
||||
await this.penaltyRepository.create(penalty);
|
||||
|
||||
this.logger.info('Quick penalty applied successfully', { penaltyId: penalty.id, raceId: command.raceId, driverId: command.driverId });
|
||||
return { penaltyId: penalty.id };
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to apply quick penalty', { command, error: error.message });
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Validate admin has authority
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
|
||||
const adminMembership = memberships.find(
|
||||
m => m.driverId === command.adminId && m.status === 'active'
|
||||
);
|
||||
|
||||
if (!adminMembership || (adminMembership.role !== 'owner' && adminMembership.role !== 'admin')) {
|
||||
throw new Error('Only league owners and admins can issue penalties');
|
||||
}
|
||||
|
||||
// Map infraction + severity to penalty type and value
|
||||
const { type, value, reason } = this.mapInfractionToPenalty(
|
||||
command.infractionType,
|
||||
command.severity
|
||||
);
|
||||
|
||||
// Create the penalty
|
||||
const penalty = Penalty.create({
|
||||
id: randomUUID(),
|
||||
leagueId: race.leagueId,
|
||||
raceId: command.raceId,
|
||||
driverId: command.driverId,
|
||||
type,
|
||||
...(value !== undefined ? { value } : {}),
|
||||
reason,
|
||||
issuedBy: command.adminId,
|
||||
status: 'applied', // Quick penalties are applied immediately
|
||||
issuedAt: new Date(),
|
||||
appliedAt: new Date(),
|
||||
...(command.notes !== undefined ? { notes: command.notes } : {}),
|
||||
});
|
||||
|
||||
await this.penaltyRepository.create(penalty);
|
||||
|
||||
return { penaltyId: penalty.id };
|
||||
}
|
||||
|
||||
private mapInfractionToPenalty(
|
||||
@@ -132,6 +143,7 @@ export class QuickPenaltyUseCase
|
||||
};
|
||||
|
||||
default:
|
||||
this.logger.error(`Unknown infraction type: ${infractionType}`);
|
||||
throw new Error(`Unknown infraction type: ${infractionType}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repos
|
||||
import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import type { RegisterForRaceCommandDTO } from '../dto/RegisterForRaceCommandDTO';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import {
|
||||
BusinessRuleViolationError,
|
||||
PermissionDeniedError,
|
||||
@@ -14,8 +15,9 @@ export class RegisterForRaceUseCase
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
private readonly membershipRepository: ILeagueMembershipRepository,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
* Mirrors legacy registerForRace behavior:
|
||||
* - throws if already registered
|
||||
@@ -24,22 +26,26 @@ export class RegisterForRaceUseCase
|
||||
*/
|
||||
async execute(command: RegisterForRaceCommandDTO): Promise<void> {
|
||||
const { raceId, leagueId, driverId } = command;
|
||||
|
||||
this.logger.debug('RegisterForRaceUseCase: executing command', { raceId, leagueId, driverId });
|
||||
|
||||
const alreadyRegistered = await this.registrationRepository.isRegistered(raceId, driverId);
|
||||
if (alreadyRegistered) {
|
||||
this.logger.warn(`RegisterForRaceUseCase: driver ${driverId} already registered for race ${raceId}`);
|
||||
throw new BusinessRuleViolationError('Already registered for this race');
|
||||
}
|
||||
|
||||
|
||||
const membership = await this.membershipRepository.getMembership(leagueId, driverId);
|
||||
if (!membership || membership.status !== 'active') {
|
||||
this.logger.error(`RegisterForRaceUseCase: driver ${driverId} not an active member of league ${leagueId}`);
|
||||
throw new PermissionDeniedError('NOT_ACTIVE_MEMBER', 'Must be an active league member to register for races');
|
||||
}
|
||||
|
||||
|
||||
const registration = RaceRegistration.create({
|
||||
raceId,
|
||||
driverId,
|
||||
});
|
||||
|
||||
|
||||
await this.registrationRepository.register(registration);
|
||||
this.logger.info(`RegisterForRaceUseCase: driver ${driverId} successfully registered for race ${raceId}`);
|
||||
}
|
||||
}
|
||||
@@ -8,92 +8,191 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Car, CarClass, CarLicense } from '@gridpilot/racing/domain/entities/Car';
|
||||
import type { ICarRepository } from '@gridpilot/racing/domain/repositories/ICarRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryCarRepository implements ICarRepository {
|
||||
private cars: Map<string, Car>;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(seedData?: Car[]) {
|
||||
constructor(logger: ILogger, seedData?: Car[]) {
|
||||
this.logger = logger;
|
||||
this.cars = new Map();
|
||||
|
||||
this.logger.info('InMemoryCarRepository initialized');
|
||||
|
||||
if (seedData) {
|
||||
this.logger.debug(`Seeding ${seedData.length} cars.`);
|
||||
seedData.forEach(car => {
|
||||
this.cars.set(car.id, car);
|
||||
this.logger.debug(`Car ${car.id} seeded.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.cars.values())
|
||||
.filter(car => car.gameId === gameId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Attempting to find cars by game ID: ${gameId}.`);
|
||||
try {
|
||||
const cars = Array.from(this.cars.values())
|
||||
.filter(car => car.gameId === gameId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Successfully found ${cars.length} cars for game ID: ${gameId}.`);
|
||||
return cars;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding cars by game ID ${gameId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByClass(carClass: CarClass): Promise<Car[]> {
|
||||
return Array.from(this.cars.values())
|
||||
.filter(car => car.carClass === carClass)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Attempting to find cars by class: ${carClass}.`);
|
||||
try {
|
||||
const cars = Array.from(this.cars.values())
|
||||
.filter(car => car.carClass === carClass)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Successfully found ${cars.length} cars for class: ${carClass}.`);
|
||||
return cars;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding cars by class ${carClass}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByLicense(license: CarLicense): Promise<Car[]> {
|
||||
return Array.from(this.cars.values())
|
||||
.filter(car => car.license === license)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
this.logger.debug(`Attempting to find cars by license: ${license}.`);
|
||||
try {
|
||||
const cars = Array.from(this.cars.values())
|
||||
.filter(car => car.license === license)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Successfully found ${cars.length} cars for license: ${license}.`);
|
||||
return cars;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding cars by license ${license}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByManufacturer(manufacturer: string): Promise<Car[]> {
|
||||
const lowerManufacturer = manufacturer.toLowerCase();
|
||||
return Array.from(this.cars.values())
|
||||
.filter(car => car.manufacturer.toLowerCase() === lowerManufacturer)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Attempting to find cars by manufacturer: ${manufacturer}.`);
|
||||
try {
|
||||
const lowerManufacturer = manufacturer.toLowerCase();
|
||||
const cars = Array.from(this.cars.values())
|
||||
.filter(car => car.manufacturer.toLowerCase() === lowerManufacturer)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Successfully found ${cars.length} cars for manufacturer: ${manufacturer}.`);
|
||||
return cars;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding cars by manufacturer ${manufacturer}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchByName(query: string): Promise<Car[]> {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return Array.from(this.cars.values())
|
||||
.filter(car =>
|
||||
car.name.toLowerCase().includes(lowerQuery) ||
|
||||
car.shortName.toLowerCase().includes(lowerQuery) ||
|
||||
car.manufacturer.toLowerCase().includes(lowerQuery)
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Attempting to search cars by name query: ${query}.`);
|
||||
try {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const cars = Array.from(this.cars.values())
|
||||
.filter(car =>
|
||||
car.name.toLowerCase().includes(lowerQuery) ||
|
||||
car.shortName.toLowerCase().includes(lowerQuery) ||
|
||||
car.manufacturer.toLowerCase().includes(lowerQuery)
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Successfully found ${cars.length} cars matching search query: ${query}.`);
|
||||
return cars;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error searching cars by name query ${query}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(car: Car): Promise<Car> {
|
||||
if (await this.exists(car.id)) {
|
||||
throw new Error(`Car with ID ${car.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Attempting to create car: ${car.id}.`);
|
||||
try {
|
||||
if (await this.exists(car.id)) {
|
||||
this.logger.warn(`Car with ID ${car.id} already exists; creation aborted.`);
|
||||
throw new Error(`Car with ID ${car.id} already exists`);
|
||||
}
|
||||
|
||||
this.cars.set(car.id, car);
|
||||
return car;
|
||||
this.cars.set(car.id, car);
|
||||
this.logger.info(`Car ${car.id} created successfully.`);
|
||||
return car;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating car ${car.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(car: Car): Promise<Car> {
|
||||
if (!await this.exists(car.id)) {
|
||||
throw new Error(`Car with ID ${car.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Attempting to update car with ID: ${car.id}.`);
|
||||
try {
|
||||
if (!await this.exists(car.id)) {
|
||||
this.logger.warn(`Car with ID ${car.id} not found for update; update aborted.`);
|
||||
throw new Error(`Car with ID ${car.id} not found`);
|
||||
}
|
||||
|
||||
this.cars.set(car.id, car);
|
||||
return car;
|
||||
this.cars.set(car.id, car);
|
||||
this.logger.info(`Car ${car.id} updated successfully.`);
|
||||
return car;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating car ${car.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!await this.exists(id)) {
|
||||
throw new Error(`Car with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Attempting to delete car with ID: ${id}.`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Car with ID ${id} not found for deletion; deletion aborted.`);
|
||||
throw new Error(`Car with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.cars.delete(id);
|
||||
this.cars.delete(id);
|
||||
this.logger.info(`Car ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting car ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,73 +8,150 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryDriverRepository implements IDriverRepository {
|
||||
private drivers: Map<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();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(driver => {
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.debug(`Seeded driver: ${driver.id}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
const driver = Array.from(this.drivers.values()).find(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
return driver ?? null;
|
||||
this.logger.debug(`Finding driver by iRacing id: ${iracingId}`);
|
||||
try {
|
||||
const driver = Array.from(this.drivers.values()).find(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
if (driver) {
|
||||
this.logger.info(`Found driver with iRacing id: ${iracingId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Driver with iRacing id ${iracingId} not found.`);
|
||||
}
|
||||
return driver ?? null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver by iRacing id ${iracingId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(): Promise<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> {
|
||||
if (await this.exists(driver.id)) {
|
||||
throw new Error(`Driver with ID ${driver.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Creating driver: ${driver.id}`);
|
||||
try {
|
||||
if (await this.exists(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} already exists.`);
|
||||
throw new Error(`Driver with ID ${driver.id} already exists`);
|
||||
}
|
||||
|
||||
if (await this.existsByIRacingId(driver.iracingId)) {
|
||||
throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`);
|
||||
}
|
||||
if (await this.existsByIRacingId(driver.iracingId)) {
|
||||
this.logger.warn(`Driver with iRacing ID ${driver.iracingId} already exists.`);
|
||||
throw new Error(`Driver with iRacing ID ${driver.iracingId} already exists`);
|
||||
}
|
||||
|
||||
this.drivers.set(driver.id, driver);
|
||||
return driver;
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.info(`Driver ${driver.id} created successfully.`);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating driver ${driver.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(driver: Driver): Promise<Driver> {
|
||||
if (!await this.exists(driver.id)) {
|
||||
throw new Error(`Driver with ID ${driver.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Updating driver: ${driver.id}`);
|
||||
try {
|
||||
if (!await this.exists(driver.id)) {
|
||||
this.logger.warn(`Driver with ID ${driver.id} not found for update.`);
|
||||
throw new Error(`Driver with ID ${driver.id} not found`);
|
||||
}
|
||||
|
||||
this.drivers.set(driver.id, driver);
|
||||
return driver;
|
||||
this.drivers.set(driver.id, driver);
|
||||
this.logger.info(`Driver ${driver.id} updated successfully.`);
|
||||
return driver;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating driver ${driver.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!await this.exists(id)) {
|
||||
throw new Error(`Driver with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Deleting driver: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Driver with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Driver with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.drivers.delete(id);
|
||||
this.drivers.delete(id);
|
||||
this.logger.info(`Driver ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting driver ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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> {
|
||||
return Array.from(this.drivers.values()).some(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
this.logger.debug(`Checking existence of driver with iRacing id: ${iracingId}`);
|
||||
try {
|
||||
const exists = Array.from(this.drivers.values()).some(
|
||||
d => d.iracingId === iracingId
|
||||
);
|
||||
this.logger.debug(`Driver with iRacing id ${iracingId} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of driver with iRacing id ${iracingId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,16 +6,21 @@
|
||||
|
||||
import { Game } from '../../domain/entities/Game';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryGameRepository implements IGameRepository {
|
||||
private games: Map<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();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(game => {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded game: ${game.id}.`);
|
||||
});
|
||||
} else {
|
||||
// Default seed data for common sim racing games
|
||||
@@ -29,33 +34,64 @@ export class InMemoryGameRepository implements IGameRepository {
|
||||
];
|
||||
defaultGames.forEach(game => {
|
||||
this.games.set(game.id, game);
|
||||
this.logger.debug(`Seeded default game: ${game.id}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<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[]> {
|
||||
return Array.from(this.games.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug('Finding all games.');
|
||||
try {
|
||||
const games = Array.from(this.games.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${games.length} games.`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all games:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to add a game
|
||||
*/
|
||||
async create(game: Game): Promise<Game> {
|
||||
if (this.games.has(game.id)) {
|
||||
throw new Error(`Game with ID ${game.id} already exists`);
|
||||
this.logger.debug(`Creating game: ${game.id}`);
|
||||
try {
|
||||
if (this.games.has(game.id)) {
|
||||
this.logger.warn(`Game with ID ${game.id} already exists.`);
|
||||
throw new Error(`Game with ID ${game.id} already exists`);
|
||||
}
|
||||
this.games.set(game.id, game);
|
||||
this.logger.info(`Game ${game.id} created successfully.`);
|
||||
return game;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating game ${game.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.games.set(game.id, game);
|
||||
return game;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper to clear data
|
||||
*/
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all games.');
|
||||
this.games.clear();
|
||||
this.logger.info('All games cleared.');
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,16 @@ import type {
|
||||
JoinRequest,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||
private membershipsByLeague: Map<string, LeagueMembership[]>;
|
||||
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.joinRequestsByLeague = new Map();
|
||||
|
||||
@@ -24,6 +28,7 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
list.push(membership);
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
this.logger.debug(`Seeded membership for league ${membership.leagueId}, driver ${membership.driverId}.`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,69 +37,143 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
list.push(request);
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
this.logger.debug(`Seeded join request for league ${request.leagueId}, driver ${request.driverId}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) return null;
|
||||
return list.find((m) => m.driverId === driverId) ?? null;
|
||||
this.logger.debug(`Getting membership for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) {
|
||||
this.logger.warn(`No membership list found for league: ${leagueId}.`);
|
||||
return null;
|
||||
}
|
||||
const membership = list.find((m) => m.driverId === driverId) ?? null;
|
||||
if (membership) {
|
||||
this.logger.info(`Found membership for league: ${leagueId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership not found for league: ${leagueId}, driver: ${driverId}.`);
|
||||
}
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting membership for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueMembers(leagueId: string): Promise<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[]> {
|
||||
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> {
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex(
|
||||
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
|
||||
);
|
||||
this.logger.debug(`Saving membership for league: ${membership.leagueId}, driver: ${membership.driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(membership.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex(
|
||||
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = membership;
|
||||
} else {
|
||||
list.push(membership);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = membership;
|
||||
this.logger.info(`Updated existing membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
|
||||
} else {
|
||||
list.push(membership);
|
||||
this.logger.info(`Created new membership for league: ${membership.leagueId}, driver: ${membership.driverId}.`);
|
||||
}
|
||||
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving membership for league ${membership.leagueId}, driver ${membership.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.membershipsByLeague.set(membership.leagueId, list);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(leagueId: string, driverId: string): Promise<void> {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) return;
|
||||
this.logger.debug(`Removing membership for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByLeague.get(leagueId);
|
||||
if (!list) {
|
||||
this.logger.warn(`No membership list found for league: ${leagueId}. Cannot remove.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const next = list.filter((m) => m.driverId !== driverId);
|
||||
this.membershipsByLeague.set(leagueId, next);
|
||||
const next = list.filter((m) => m.driverId !== driverId);
|
||||
if (next.length < list.length) {
|
||||
this.membershipsByLeague.set(leagueId, next);
|
||||
this.logger.info(`Removed membership for league: ${leagueId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Membership not found for league: ${leagueId}, driver: ${driverId}. Cannot remove.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing membership for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async saveJoinRequest(request: JoinRequest): Promise<JoinRequest> {
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex((r) => r.id === request.id);
|
||||
this.logger.debug(`Saving join request for league: ${request.leagueId}, driver: ${request.driverId}, id: ${request.id}`);
|
||||
try {
|
||||
const list = this.joinRequestsByLeague.get(request.leagueId) ?? [];
|
||||
const existingIndex = list.findIndex((r) => r.id === request.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = request;
|
||||
} else {
|
||||
list.push(request);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = request;
|
||||
this.logger.info(`Updated existing join request: ${request.id}.`);
|
||||
} else {
|
||||
list.push(request);
|
||||
this.logger.info(`Created new join request: ${request.id}.`);
|
||||
}
|
||||
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving join request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.joinRequestsByLeague.set(request.leagueId, list);
|
||||
return request;
|
||||
}
|
||||
|
||||
async removeJoinRequest(requestId: string): Promise<void> {
|
||||
for (const [leagueId, requests] of this.joinRequestsByLeague.entries()) {
|
||||
const next = requests.filter((r) => r.id !== requestId);
|
||||
if (next.length !== requests.length) {
|
||||
this.joinRequestsByLeague.set(leagueId, next);
|
||||
break;
|
||||
this.logger.debug(`Removing join request with ID: ${requestId}`);
|
||||
try {
|
||||
let removed = false;
|
||||
for (const [leagueId, requests] of this.joinRequestsByLeague.entries()) {
|
||||
const next = requests.filter((r) => r.id !== requestId);
|
||||
if (next.length !== requests.length) {
|
||||
this.joinRequestsByLeague.set(leagueId, next);
|
||||
removed = true;
|
||||
this.logger.info(`Removed join request ${requestId} from league ${leagueId}.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
this.logger.warn(`Join request with ID ${requestId} not found for removal.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error removing join request ${requestId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,69 +8,142 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueRepository implements ILeagueRepository {
|
||||
private leagues: Map<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();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(league => {
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.debug(`Seeded league: ${league.id}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.leagues.values()).filter(
|
||||
league => league.ownerId === ownerId
|
||||
);
|
||||
this.logger.debug(`Finding leagues by owner id: ${ownerId}`);
|
||||
try {
|
||||
const leagues = Array.from(this.leagues.values()).filter(
|
||||
league => league.ownerId === ownerId
|
||||
);
|
||||
this.logger.info(`Found ${leagues.length} leagues for owner id: ${ownerId}.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding leagues by owner id ${ownerId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(league: League): Promise<League> {
|
||||
if (await this.exists(league.id)) {
|
||||
throw new Error(`League with ID ${league.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Creating league: ${league.id}`);
|
||||
try {
|
||||
if (await this.exists(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} already exists.`);
|
||||
throw new Error(`League with ID ${league.id} already exists`);
|
||||
}
|
||||
|
||||
this.leagues.set(league.id, league);
|
||||
return league;
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} created successfully.`);
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating league ${league.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(league: League): Promise<League> {
|
||||
if (!await this.exists(league.id)) {
|
||||
throw new Error(`League with ID ${league.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Updating league: ${league.id}`);
|
||||
try {
|
||||
if (!await this.exists(league.id)) {
|
||||
this.logger.warn(`League with ID ${league.id} not found for update.`);
|
||||
throw new Error(`League with ID ${league.id} not found`);
|
||||
}
|
||||
|
||||
this.leagues.set(league.id, league);
|
||||
return league;
|
||||
this.leagues.set(league.id, league);
|
||||
this.logger.info(`League ${league.id} updated successfully.`);
|
||||
return league;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating league ${league.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!await this.exists(id)) {
|
||||
throw new Error(`League with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Deleting league: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`League with ID ${id} not found for deletion.`);
|
||||
throw new Error(`League with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.leagues.delete(id);
|
||||
this.leagues.delete(id);
|
||||
this.logger.info(`League ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting league ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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[]> {
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
return Array.from(this.leagues.values()).filter(league =>
|
||||
league.name.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
this.logger.debug(`Searching leagues by name query: ${query}`);
|
||||
try {
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
const leagues = Array.from(this.leagues.values()).filter(league =>
|
||||
league.name.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
this.logger.info(`Found ${leagues.length} leagues matching search query: ${query}.`);
|
||||
return leagues;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error searching leagues by name query ${query}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,49 +6,116 @@
|
||||
|
||||
import type { LeagueWallet } from '../../domain/entities/LeagueWallet';
|
||||
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository {
|
||||
private wallets: Map<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> {
|
||||
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> {
|
||||
for (const wallet of this.wallets.values()) {
|
||||
if (wallet.leagueId === leagueId) {
|
||||
return wallet;
|
||||
this.logger.debug(`Finding league wallet by league id: ${leagueId}`);
|
||||
try {
|
||||
for (const wallet of this.wallets.values()) {
|
||||
if (wallet.leagueId === leagueId) {
|
||||
this.logger.info(`Found league wallet for league id: ${leagueId}.`);
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
this.logger.warn(`No league wallet found for league id: ${leagueId}.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding league wallet by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async create(wallet: LeagueWallet): Promise<LeagueWallet> {
|
||||
if (this.wallets.has(wallet.id)) {
|
||||
throw new Error('LeagueWallet with this ID already exists');
|
||||
this.logger.debug(`Creating league wallet: ${wallet.id}`);
|
||||
try {
|
||||
if (this.wallets.has(wallet.id)) {
|
||||
this.logger.warn(`LeagueWallet with ID ${wallet.id} already exists.`);
|
||||
throw new Error('LeagueWallet with this ID already exists');
|
||||
}
|
||||
this.wallets.set(wallet.id, wallet);
|
||||
this.logger.info(`LeagueWallet ${wallet.id} created successfully.`);
|
||||
return wallet;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating league wallet ${wallet.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.wallets.set(wallet.id, wallet);
|
||||
return wallet;
|
||||
}
|
||||
|
||||
async update(wallet: LeagueWallet): Promise<LeagueWallet> {
|
||||
if (!this.wallets.has(wallet.id)) {
|
||||
throw new Error('LeagueWallet not found');
|
||||
this.logger.debug(`Updating league wallet: ${wallet.id}`);
|
||||
try {
|
||||
if (!this.wallets.has(wallet.id)) {
|
||||
this.logger.warn(`LeagueWallet with ID ${wallet.id} not found for update.`);
|
||||
throw new Error('LeagueWallet not found');
|
||||
}
|
||||
this.wallets.set(wallet.id, wallet);
|
||||
this.logger.info(`LeagueWallet ${wallet.id} updated successfully.`);
|
||||
return wallet;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating league wallet ${wallet.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.wallets.set(wallet.id, wallet);
|
||||
return wallet;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.wallets.has(id);
|
||||
this.logger.debug(`Checking existence of league wallet with id: ${id}`);
|
||||
try {
|
||||
const exists = this.wallets.has(id);
|
||||
this.logger.debug(`LeagueWallet ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of league wallet with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all league wallets.');
|
||||
this.wallets.clear();
|
||||
this.logger.info('All league wallets cleared.');
|
||||
}
|
||||
}
|
||||
@@ -7,108 +7,253 @@
|
||||
import type { DriverLivery } from '../../domain/entities/DriverLivery';
|
||||
import type { LiveryTemplate } from '../../domain/entities/LiveryTemplate';
|
||||
import type { ILiveryRepository } from '../../domain/repositories/ILiveryRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryLiveryRepository implements ILiveryRepository {
|
||||
private driverLiveries: Map<string, DriverLivery> = 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
|
||||
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[]> {
|
||||
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> {
|
||||
for (const livery of this.driverLiveries.values()) {
|
||||
if (livery.driverId === driverId && livery.carId === carId) {
|
||||
return livery;
|
||||
this.logger.debug(`Finding driver livery by driver: ${driverId} and car: ${carId}`);
|
||||
try {
|
||||
for (const livery of this.driverLiveries.values()) {
|
||||
if (livery.driverId === driverId && livery.carId === carId) {
|
||||
this.logger.info(`Found driver livery for driver: ${driverId}, car: ${carId}.`);
|
||||
return livery;
|
||||
}
|
||||
}
|
||||
this.logger.warn(`Driver livery for driver ${driverId} and car ${carId} not found.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver livery by driver ${driverId}, car ${carId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async findDriverLiveriesByGameId(gameId: string): Promise<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[]> {
|
||||
return Array.from(this.driverLiveries.values()).filter(
|
||||
l => l.driverId === driverId && l.gameId === gameId
|
||||
);
|
||||
this.logger.debug(`Finding driver liveries by driver: ${driverId} and game: ${gameId}`);
|
||||
try {
|
||||
const liveries = Array.from(this.driverLiveries.values()).filter(
|
||||
l => l.driverId === driverId && l.gameId === gameId
|
||||
);
|
||||
this.logger.info(`Found ${liveries.length} driver liveries for driver: ${driverId}, game: ${gameId}.`);
|
||||
return liveries;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding driver liveries by driver ${driverId}, game ${gameId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createDriverLivery(livery: DriverLivery): Promise<DriverLivery> {
|
||||
if (this.driverLiveries.has(livery.id)) {
|
||||
throw new Error('DriverLivery with this ID already exists');
|
||||
this.logger.debug(`Creating driver livery: ${livery.id}`);
|
||||
try {
|
||||
if (this.driverLiveries.has(livery.id)) {
|
||||
this.logger.warn(`DriverLivery with ID ${livery.id} already exists.`);
|
||||
throw new Error('DriverLivery with this ID already exists');
|
||||
}
|
||||
this.driverLiveries.set(livery.id, livery);
|
||||
this.logger.info(`DriverLivery ${livery.id} created successfully.`);
|
||||
return livery;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating driver livery ${livery.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.driverLiveries.set(livery.id, livery);
|
||||
return livery;
|
||||
}
|
||||
|
||||
async updateDriverLivery(livery: DriverLivery): Promise<DriverLivery> {
|
||||
if (!this.driverLiveries.has(livery.id)) {
|
||||
throw new Error('DriverLivery not found');
|
||||
this.logger.debug(`Updating driver livery: ${livery.id}`);
|
||||
try {
|
||||
if (!this.driverLiveries.has(livery.id)) {
|
||||
this.logger.warn(`DriverLivery with ID ${livery.id} not found for update.`);
|
||||
throw new Error('DriverLivery not found');
|
||||
}
|
||||
this.driverLiveries.set(livery.id, livery);
|
||||
this.logger.info(`DriverLivery ${livery.id} updated successfully.`);
|
||||
return livery;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating driver livery ${livery.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.driverLiveries.set(livery.id, livery);
|
||||
return livery;
|
||||
}
|
||||
|
||||
async deleteDriverLivery(id: string): Promise<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
|
||||
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[]> {
|
||||
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> {
|
||||
for (const template of this.templates.values()) {
|
||||
if (template.seasonId === seasonId && template.carId === carId) {
|
||||
return template;
|
||||
this.logger.debug(`Finding livery template by season: ${seasonId} and car: ${carId}`);
|
||||
try {
|
||||
for (const template of this.templates.values()) {
|
||||
if (template.seasonId === seasonId && template.carId === carId) {
|
||||
this.logger.info(`Found livery template for season: ${seasonId}, car: ${carId}.`);
|
||||
return template;
|
||||
}
|
||||
}
|
||||
this.logger.warn(`Livery template for season ${seasonId} and car ${carId} not found.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding livery template by season ${seasonId}, car ${carId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async createTemplate(template: LiveryTemplate): Promise<LiveryTemplate> {
|
||||
if (this.templates.has(template.id)) {
|
||||
throw new Error('LiveryTemplate with this ID already exists');
|
||||
this.logger.debug(`Creating livery template: ${template.id}`);
|
||||
try {
|
||||
if (this.templates.has(template.id)) {
|
||||
this.logger.warn(`LiveryTemplate with ID ${template.id} already exists.`);
|
||||
throw new Error('LiveryTemplate with this ID already exists');
|
||||
}
|
||||
this.templates.set(template.id, template);
|
||||
this.logger.info(`LiveryTemplate ${template.id} created successfully.`);
|
||||
return template;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating livery template ${template.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.templates.set(template.id, template);
|
||||
return template;
|
||||
}
|
||||
|
||||
async updateTemplate(template: LiveryTemplate): Promise<LiveryTemplate> {
|
||||
if (!this.templates.has(template.id)) {
|
||||
throw new Error('LiveryTemplate not found');
|
||||
this.logger.debug(`Updating livery template: ${template.id}`);
|
||||
try {
|
||||
if (!this.templates.has(template.id)) {
|
||||
this.logger.warn(`LiveryTemplate with ID ${template.id} not found for update.`);
|
||||
throw new Error('LiveryTemplate not found');
|
||||
}
|
||||
this.templates.set(template.id, template);
|
||||
this.logger.info(`LiveryTemplate ${template.id} updated successfully.`);
|
||||
return template;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating livery template ${template.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.templates.set(template.id, template);
|
||||
return template;
|
||||
}
|
||||
|
||||
async deleteTemplate(id: string): Promise<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
|
||||
clearDriverLiveries(): void {
|
||||
this.logger.debug('Clearing all driver liveries.');
|
||||
this.driverLiveries.clear();
|
||||
this.logger.info('All driver liveries cleared.');
|
||||
}
|
||||
|
||||
clearTemplates(): void {
|
||||
this.logger.debug('Clearing all livery templates.');
|
||||
this.templates.clear();
|
||||
this.logger.info('All livery templates cleared.');
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all livery data.');
|
||||
this.driverLiveries.clear();
|
||||
this.templates.clear();
|
||||
this.logger.info('All livery data cleared.');
|
||||
}
|
||||
}
|
||||
@@ -6,65 +6,146 @@
|
||||
|
||||
import type { Penalty } from '../../domain/entities/Penalty';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryPenaltyRepository implements IPenaltyRepository {
|
||||
private penalties: Map<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 => {
|
||||
this.penalties.set(penalty.id, penalty);
|
||||
this.logger.debug(`Seeded penalty: ${penalty.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
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[]> {
|
||||
return Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.raceId === raceId
|
||||
);
|
||||
this.logger.debug(`Finding penalties by race id: ${raceId}`);
|
||||
try {
|
||||
const penalties = Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.raceId === raceId
|
||||
);
|
||||
this.logger.info(`Found ${penalties.length} penalties for race id: ${raceId}.`);
|
||||
return penalties;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding penalties by race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<Penalty[]> {
|
||||
return Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.driverId === driverId
|
||||
);
|
||||
this.logger.debug(`Finding penalties by driver id: ${driverId}`);
|
||||
try {
|
||||
const penalties = Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.driverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${penalties.length} penalties for driver id: ${driverId}.`);
|
||||
return penalties;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding penalties by driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByProtestId(protestId: string): Promise<Penalty[]> {
|
||||
return Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.protestId === protestId
|
||||
);
|
||||
this.logger.debug(`Finding penalties by protest id: ${protestId}`);
|
||||
try {
|
||||
const penalties = Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.protestId === protestId
|
||||
);
|
||||
this.logger.info(`Found ${penalties.length} penalties for protest id: ${protestId}.`);
|
||||
return penalties;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding penalties by protest id ${protestId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findPending(): Promise<Penalty[]> {
|
||||
return Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.isPending()
|
||||
);
|
||||
this.logger.debug('Finding pending penalties.');
|
||||
try {
|
||||
const penalties = Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.isPending()
|
||||
);
|
||||
this.logger.info(`Found ${penalties.length} pending penalties.`);
|
||||
return penalties;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding pending penalties:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findIssuedBy(stewardId: string): Promise<Penalty[]> {
|
||||
return Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.issuedBy === stewardId
|
||||
);
|
||||
this.logger.debug(`Finding penalties issued by steward: ${stewardId}`);
|
||||
try {
|
||||
const penalties = Array.from(this.penalties.values()).filter(
|
||||
penalty => penalty.issuedBy === stewardId
|
||||
);
|
||||
this.logger.info(`Found ${penalties.length} penalties issued by steward: ${stewardId}.`);
|
||||
return penalties;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding penalties issued by steward ${stewardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(penalty: Penalty): Promise<void> {
|
||||
if (this.penalties.has(penalty.id)) {
|
||||
throw new Error(`Penalty with ID ${penalty.id} already exists`);
|
||||
this.logger.debug(`Creating penalty: ${penalty.id}`);
|
||||
try {
|
||||
if (this.penalties.has(penalty.id)) {
|
||||
this.logger.warn(`Penalty with ID ${penalty.id} already exists.`);
|
||||
throw new Error(`Penalty with ID ${penalty.id} already exists`);
|
||||
}
|
||||
this.penalties.set(penalty.id, penalty);
|
||||
this.logger.info(`Penalty ${penalty.id} created successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating penalty ${penalty.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.penalties.set(penalty.id, penalty);
|
||||
}
|
||||
|
||||
async update(penalty: Penalty): Promise<void> {
|
||||
if (!this.penalties.has(penalty.id)) {
|
||||
throw new Error(`Penalty with ID ${penalty.id} not found`);
|
||||
this.logger.debug(`Updating penalty: ${penalty.id}`);
|
||||
try {
|
||||
if (!this.penalties.has(penalty.id)) {
|
||||
this.logger.warn(`Penalty with ID ${penalty.id} not found for update.`);
|
||||
throw new Error(`Penalty with ID ${penalty.id} not found`);
|
||||
}
|
||||
this.penalties.set(penalty.id, penalty);
|
||||
this.logger.info(`Penalty ${penalty.id} updated successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating penalty ${penalty.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.penalties.set(penalty.id, penalty);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,65 +6,146 @@
|
||||
|
||||
import type { Protest } from '../../domain/entities/Protest';
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryProtestRepository implements IProtestRepository {
|
||||
private protests: Map<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 => {
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.debug(`Seeded protest: ${protest.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
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[]> {
|
||||
return Array.from(this.protests.values()).filter(
|
||||
protest => protest.raceId === raceId
|
||||
);
|
||||
this.logger.debug(`Finding protests by race id: ${raceId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.raceId === raceId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests for race id: ${raceId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByProtestingDriverId(driverId: string): Promise<Protest[]> {
|
||||
return Array.from(this.protests.values()).filter(
|
||||
protest => protest.protestingDriverId === driverId
|
||||
);
|
||||
this.logger.debug(`Finding protests by protesting driver id: ${driverId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.protestingDriverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests by protesting driver id: ${driverId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by protesting driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByAccusedDriverId(driverId: string): Promise<Protest[]> {
|
||||
return Array.from(this.protests.values()).filter(
|
||||
protest => protest.accusedDriverId === driverId
|
||||
);
|
||||
this.logger.debug(`Finding protests by accused driver id: ${driverId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.accusedDriverId === driverId
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests by accused driver id: ${driverId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests by accused driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findPending(): Promise<Protest[]> {
|
||||
return Array.from(this.protests.values()).filter(
|
||||
protest => protest.isPending()
|
||||
);
|
||||
this.logger.debug('Finding pending protests.');
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.isPending()
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} pending protests.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding pending protests:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findUnderReviewBy(stewardId: string): Promise<Protest[]> {
|
||||
return Array.from(this.protests.values()).filter(
|
||||
protest => protest.reviewedBy === stewardId && protest.isUnderReview()
|
||||
);
|
||||
this.logger.debug(`Finding protests under review by steward: ${stewardId}`);
|
||||
try {
|
||||
const protests = Array.from(this.protests.values()).filter(
|
||||
protest => protest.reviewedBy === stewardId && protest.isUnderReview()
|
||||
);
|
||||
this.logger.info(`Found ${protests.length} protests under review by steward: ${stewardId}.`);
|
||||
return protests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding protests under review by steward ${stewardId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(protest: Protest): Promise<void> {
|
||||
if (this.protests.has(protest.id)) {
|
||||
throw new Error(`Protest with ID ${protest.id} already exists`);
|
||||
this.logger.debug(`Creating protest: ${protest.id}`);
|
||||
try {
|
||||
if (this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} already exists.`);
|
||||
throw new Error(`Protest with ID ${protest.id} already exists`);
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} created successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating protest ${protest.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
}
|
||||
|
||||
async update(protest: Protest): Promise<void> {
|
||||
if (!this.protests.has(protest.id)) {
|
||||
throw new Error(`Protest with ID ${protest.id} not found`);
|
||||
this.logger.debug(`Updating protest: ${protest.id}`);
|
||||
try {
|
||||
if (!this.protests.has(protest.id)) {
|
||||
this.logger.warn(`Protest with ID ${protest.id} not found for update.`);
|
||||
throw new Error(`Protest with ID ${protest.id} not found`);
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
this.logger.info(`Protest ${protest.id} updated successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating protest ${protest.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.protests.set(protest.id, protest);
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,70 +3,178 @@
|
||||
*/
|
||||
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
||||
import type { RaceEvent } from '../../domain/entities/RaceEvent';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRaceEventRepository implements IRaceEventRepository {
|
||||
private raceEvents: Map<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> {
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent => raceEvent.seasonId === seasonId
|
||||
);
|
||||
this.logger.debug(`Finding race events by season id: ${seasonId}`);
|
||||
try {
|
||||
const events = Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent => raceEvent.seasonId === seasonId
|
||||
);
|
||||
this.logger.info(`Found ${events.length} race events for season id: ${seasonId}.`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding race events by season id ${seasonId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<RaceEvent[]> {
|
||||
return Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent => raceEvent.leagueId === leagueId
|
||||
);
|
||||
this.logger.debug(`Finding race events by league id: ${leagueId}`);
|
||||
try {
|
||||
const events = Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent => raceEvent.leagueId === leagueId
|
||||
);
|
||||
this.logger.info(`Found ${events.length} race events for league id: ${leagueId}.`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding race events by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByStatus(status: string): Promise<RaceEvent[]> {
|
||||
return Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent => raceEvent.status === status
|
||||
);
|
||||
this.logger.debug(`Finding race events by status: ${status}`);
|
||||
try {
|
||||
const events = Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent => raceEvent.status === status
|
||||
);
|
||||
this.logger.info(`Found ${events.length} race events with status: ${status}.`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding race events by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAwaitingStewardingClose(): Promise<RaceEvent[]> {
|
||||
const now = new Date();
|
||||
return Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent =>
|
||||
raceEvent.status === 'awaiting_stewarding' &&
|
||||
raceEvent.stewardingClosesAt &&
|
||||
raceEvent.stewardingClosesAt <= now
|
||||
);
|
||||
this.logger.debug('Finding race events awaiting stewarding close.');
|
||||
try {
|
||||
const now = new Date();
|
||||
const events = Array.from(this.raceEvents.values()).filter(
|
||||
raceEvent =>
|
||||
raceEvent.status === 'awaiting_stewarding' &&
|
||||
raceEvent.stewardingClosesAt &&
|
||||
raceEvent.stewardingClosesAt <= now
|
||||
);
|
||||
this.logger.info(`Found ${events.length} race events awaiting stewarding close.`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding race events awaiting stewarding close:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(raceEvent: RaceEvent): Promise<RaceEvent> {
|
||||
this.raceEvents.set(raceEvent.id, raceEvent);
|
||||
return raceEvent;
|
||||
this.logger.debug(`Creating race event: ${raceEvent.id}`);
|
||||
try {
|
||||
this.raceEvents.set(raceEvent.id, raceEvent);
|
||||
this.logger.info(`Race event ${raceEvent.id} created successfully.`);
|
||||
return raceEvent;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating race event ${raceEvent.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(raceEvent: RaceEvent): Promise<RaceEvent> {
|
||||
this.raceEvents.set(raceEvent.id, raceEvent);
|
||||
return raceEvent;
|
||||
this.logger.debug(`Updating race event: ${raceEvent.id}`);
|
||||
try {
|
||||
if (!this.raceEvents.has(raceEvent.id)) {
|
||||
this.logger.warn(`Race event with id ${raceEvent.id} not found for update. Creating new.`);
|
||||
}
|
||||
this.raceEvents.set(raceEvent.id, raceEvent);
|
||||
this.logger.info(`Race event ${raceEvent.id} updated successfully.`);
|
||||
return raceEvent;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating race event ${raceEvent.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.raceEvents.has(id);
|
||||
this.logger.debug(`Checking existence of race event with id: ${id}`);
|
||||
try {
|
||||
const exists = this.raceEvents.has(id);
|
||||
this.logger.debug(`Race event ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of race event with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper methods
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all race events.');
|
||||
this.raceEvents.clear();
|
||||
this.logger.info('All race events cleared.');
|
||||
}
|
||||
|
||||
getAll(): RaceEvent[] {
|
||||
return Array.from(this.raceEvents.values());
|
||||
this.logger.debug('Getting all race events.');
|
||||
try {
|
||||
const events = Array.from(this.raceEvents.values());
|
||||
this.logger.info(`Retrieved ${events.length} race events.`);
|
||||
return events;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting all race events:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* Stores race registrations in Maps keyed by raceId and driverId.
|
||||
*/
|
||||
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
|
||||
@@ -13,12 +14,16 @@ type RaceRegistrationSeed = Pick<RaceRegistration, 'raceId' | 'driverId' | 'regi
|
||||
export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
|
||||
private registrationsByRace: 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.registrationsByDriver = new Map();
|
||||
|
||||
if (seedRegistrations) {
|
||||
this.logger.debug('Seeding with initial registrations', { count: seedRegistrations.length });
|
||||
seedRegistrations.forEach((registration) => {
|
||||
this.addToIndexes(registration.raceId, registration.driverId, registration.registeredAt);
|
||||
});
|
||||
@@ -26,92 +31,148 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo
|
||||
}
|
||||
|
||||
private addToIndexes(raceId: string, driverId: string, _registeredAt: Date): void {
|
||||
this.logger.debug('Attempting to add race registration to indexes', { raceId, driverId });
|
||||
let raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) {
|
||||
raceSet = new Set();
|
||||
this.registrationsByRace.set(raceId, raceSet);
|
||||
this.logger.debug('Created new race set as none existed', { raceId });
|
||||
}
|
||||
raceSet.add(driverId);
|
||||
this.logger.debug('Added driver to race set', { raceId, driverId });
|
||||
|
||||
let driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (!driverSet) {
|
||||
driverSet = new Set();
|
||||
this.registrationsByDriver.set(driverId, driverSet);
|
||||
this.logger.debug('Created new driver set as none existed', { driverId });
|
||||
}
|
||||
driverSet.add(raceId);
|
||||
this.logger.debug('Added race to driver set', { raceId, driverId });
|
||||
this.logger.info('Successfully added race registration to indexes', { raceId, driverId });
|
||||
}
|
||||
|
||||
private removeFromIndexes(raceId: string, driverId: string): void {
|
||||
this.logger.debug('Attempting to remove race registration from indexes', { raceId, driverId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (raceSet) {
|
||||
raceSet.delete(driverId);
|
||||
this.logger.debug('Removed driver from race set', { raceId, driverId });
|
||||
if (raceSet.size === 0) {
|
||||
this.registrationsByRace.delete(raceId);
|
||||
this.logger.debug('Deleted race set as it is now empty', { raceId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Race set not found during removal, potential inconsistency', { raceId });
|
||||
}
|
||||
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (driverSet) {
|
||||
driverSet.delete(raceId);
|
||||
this.logger.debug('Removed race from driver set', { raceId, driverId });
|
||||
if (driverSet.size === 0) {
|
||||
this.registrationsByDriver.delete(driverId);
|
||||
this.logger.debug('Deleted driver set as it is now empty', { driverId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Driver set not found during removal, potential inconsistency', { driverId });
|
||||
}
|
||||
this.logger.info('Successfully removed race registration from indexes', { raceId, driverId });
|
||||
}
|
||||
|
||||
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
|
||||
this.logger.info('Checking if driver is registered for race', { raceId, driverId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) return false;
|
||||
return raceSet.has(driverId);
|
||||
if (!raceSet) {
|
||||
this.logger.debug('Race set not found, driver not registered', { raceId, driverId });
|
||||
return false;
|
||||
}
|
||||
const isRegistered = raceSet.has(driverId);
|
||||
this.logger.debug('Registration status result', { raceId, driverId, isRegistered });
|
||||
return isRegistered;
|
||||
}
|
||||
|
||||
async getRegisteredDrivers(raceId: string): Promise<string[]> {
|
||||
this.logger.info('Attempting to fetch registered drivers for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) return [];
|
||||
return Array.from(raceSet.values());
|
||||
if (!raceSet) {
|
||||
this.logger.debug('No registered drivers found for race', { raceId });
|
||||
return [];
|
||||
}
|
||||
const drivers = Array.from(raceSet.values());
|
||||
this.logger.debug('Found registered drivers for race', { raceId, count: drivers.length });
|
||||
this.logger.info('Successfully fetched registered drivers for race', { raceId, count: drivers.length });
|
||||
return drivers;
|
||||
}
|
||||
|
||||
async getRegistrationCount(raceId: string): Promise<number> {
|
||||
this.logger.info('Attempting to get registration count for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
return raceSet ? raceSet.size : 0;
|
||||
const count = raceSet ? raceSet.size : 0;
|
||||
this.logger.debug('Registration count for race', { raceId, count });
|
||||
this.logger.info('Returning registration count for race', { raceId, count });
|
||||
return count;
|
||||
}
|
||||
|
||||
async register(registration: RaceRegistration): Promise<void> {
|
||||
this.logger.info('Attempting to register driver for race', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
const alreadyRegistered = await this.isRegistered(registration.raceId, registration.driverId);
|
||||
if (alreadyRegistered) {
|
||||
this.logger.warn('Driver already registered for race, registration aborted', { raceId: registration.raceId, driverId: registration.driverId });
|
||||
throw new Error('Already registered for this race');
|
||||
}
|
||||
this.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> {
|
||||
this.logger.info('Attempting to withdraw driver from race', { raceId, driverId });
|
||||
const alreadyRegistered = await this.isRegistered(raceId, driverId);
|
||||
if (!alreadyRegistered) {
|
||||
this.logger.warn('Driver not registered for race, withdrawal aborted', { raceId, driverId });
|
||||
throw new Error('Not registered for this race');
|
||||
}
|
||||
this.removeFromIndexes(raceId, driverId);
|
||||
this.logger.info('Driver successfully withdrew from race', { raceId, driverId });
|
||||
}
|
||||
|
||||
async getDriverRegistrations(driverId: string): Promise<string[]> {
|
||||
this.logger.info('Attempting to fetch registrations for driver', { driverId });
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (!driverSet) return [];
|
||||
return Array.from(driverSet.values());
|
||||
if (!driverSet) {
|
||||
this.logger.debug('No registrations found for driver', { driverId });
|
||||
return [];
|
||||
}
|
||||
const registrations = Array.from(driverSet.values());
|
||||
this.logger.debug('Found registrations for driver', { driverId, count: registrations.length });
|
||||
this.logger.info('Successfully fetched registrations for driver', { driverId, count: registrations.length });
|
||||
return registrations;
|
||||
}
|
||||
|
||||
async clearRaceRegistrations(raceId: string): Promise<void> {
|
||||
this.logger.info('Attempting to clear all registrations for race', { raceId });
|
||||
const raceSet = this.registrationsByRace.get(raceId);
|
||||
if (!raceSet) return;
|
||||
if (!raceSet) {
|
||||
this.logger.debug('No registrations to clear for race (race set not found)', { raceId });
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug('Found registrations to clear', { raceId, count: raceSet.size });
|
||||
for (const driverId of raceSet.values()) {
|
||||
const driverSet = this.registrationsByDriver.get(driverId);
|
||||
if (driverSet) {
|
||||
driverSet.delete(raceId);
|
||||
if (driverSet.size === 0) {
|
||||
this.registrationsByDriver.delete(driverId);
|
||||
this.logger.debug('Deleted driver set as it is now empty during race clear', { raceId, driverId });
|
||||
}
|
||||
} else {
|
||||
this.logger.warn('Driver set not found during race clear, potential inconsistency', { raceId, driverId });
|
||||
}
|
||||
this.logger.debug('Removed race from driver set during race clear', { raceId, driverId });
|
||||
}
|
||||
|
||||
this.registrationsByRace.delete(raceId);
|
||||
this.logger.info('Successfully cleared all registrations for race', { raceId });
|
||||
}
|
||||
}
|
||||
@@ -8,97 +8,194 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryRaceRepository implements IRaceRepository {
|
||||
private races: Map<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();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(race => {
|
||||
this.races.set(race.id, race);
|
||||
this.logger.debug(`Seeded race: ${race.id}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.races.values())
|
||||
.filter(race => race.leagueId === leagueId)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.debug(`Finding races by league id: ${leagueId}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race => race.leagueId === leagueId)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findUpcomingByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
const now = new Date();
|
||||
return Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'scheduled' &&
|
||||
race.scheduledAt > now
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.debug(`Finding upcoming races by league id: ${leagueId}`);
|
||||
try {
|
||||
const now = new Date();
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'scheduled' &&
|
||||
race.scheduledAt > now
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} upcoming races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding upcoming races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findCompletedByLeagueId(leagueId: string): Promise<Race[]> {
|
||||
return Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'completed'
|
||||
)
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
|
||||
this.logger.debug(`Finding completed races by league id: ${leagueId}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.leagueId === leagueId &&
|
||||
race.status === 'completed'
|
||||
)
|
||||
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} completed races for league id: ${leagueId}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding completed races by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByStatus(status: RaceStatus): Promise<Race[]> {
|
||||
return Array.from(this.races.values())
|
||||
.filter(race => race.status === status)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.debug(`Finding races by status: ${status}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race => race.status === status)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races with status: ${status}.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDateRange(startDate: Date, endDate: Date): Promise<Race[]> {
|
||||
return Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.scheduledAt >= startDate &&
|
||||
race.scheduledAt <= endDate
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.debug(`Finding races by date range: ${startDate.toISOString()} - ${endDate.toISOString()}`);
|
||||
try {
|
||||
const races = Array.from(this.races.values())
|
||||
.filter(race =>
|
||||
race.scheduledAt >= startDate &&
|
||||
race.scheduledAt <= endDate
|
||||
)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
this.logger.info(`Found ${races.length} races in date range.`);
|
||||
return races;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding races by date range:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(race: Race): Promise<Race> {
|
||||
if (await this.exists(race.id)) {
|
||||
throw new Error(`Race with ID ${race.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Creating race: ${race.id}`);
|
||||
try {
|
||||
if (await this.exists(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} already exists.`);
|
||||
throw new Error(`Race with ID ${race.id} already exists`);
|
||||
}
|
||||
|
||||
this.races.set(race.id, race);
|
||||
return race;
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} created successfully.`);
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating race ${race.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(race: Race): Promise<Race> {
|
||||
if (!await this.exists(race.id)) {
|
||||
throw new Error(`Race with ID ${race.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Updating race: ${race.id}`);
|
||||
try {
|
||||
if (!await this.exists(race.id)) {
|
||||
this.logger.warn(`Race with ID ${race.id} not found for update.`);
|
||||
throw new Error(`Race with ID ${race.id} not found`);
|
||||
}
|
||||
|
||||
this.races.set(race.id, race);
|
||||
return race;
|
||||
this.races.set(race.id, race);
|
||||
this.logger.info(`Race ${race.id} updated successfully.`);
|
||||
return race;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating race ${race.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!await this.exists(id)) {
|
||||
throw new Error(`Race with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Deleting race: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Race with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Race with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.races.delete(id);
|
||||
this.races.delete(id);
|
||||
this.logger.info(`Race ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting race ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,112 +9,221 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryResultRepository implements IResultRepository {
|
||||
private results: Map<string, Result>;
|
||||
private raceRepository: IRaceRepository | null;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(seedData?: Result[], raceRepository?: IRaceRepository | null) {
|
||||
constructor(logger: ILogger, seedData?: Result[], raceRepository?: IRaceRepository | null) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryResultRepository initialized.');
|
||||
this.results = new Map();
|
||||
this.raceRepository = raceRepository ?? null;
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(result => {
|
||||
this.results.set(result.id, result);
|
||||
this.logger.debug(`Seeded result: ${result.id}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.results.values())
|
||||
.filter(result => result.raceId === raceId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
this.logger.debug(`Finding results for race id: ${raceId}`);
|
||||
try {
|
||||
const results = Array.from(this.results.values())
|
||||
.filter(result => result.raceId === raceId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
this.logger.info(`Found ${results.length} results for race id: ${raceId}.`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding results for race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<Result[]> {
|
||||
return Array.from(this.results.values())
|
||||
.filter(result => result.driverId === driverId);
|
||||
this.logger.debug(`Finding results for driver id: ${driverId}`);
|
||||
try {
|
||||
const results = Array.from(this.results.values())
|
||||
.filter(result => result.driverId === driverId);
|
||||
this.logger.info(`Found ${results.length} results for driver id: ${driverId}.`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding results for driver id ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Result[]> {
|
||||
if (!this.raceRepository) {
|
||||
return [];
|
||||
this.logger.debug(`Finding results for driver id: ${driverId} and league id: ${leagueId}`);
|
||||
try {
|
||||
if (!this.raceRepository) {
|
||||
this.logger.warn('Race repository not provided to InMemoryResultRepository. Skipping league-filtered search.');
|
||||
return [];
|
||||
}
|
||||
|
||||
const leagueRaces = await this.raceRepository.findByLeagueId(leagueId);
|
||||
const leagueRaceIds = new Set(leagueRaces.map(race => race.id));
|
||||
this.logger.debug(`Found ${leagueRaces.length} races in league ${leagueId}.`);
|
||||
|
||||
const results = Array.from(this.results.values())
|
||||
.filter(result =>
|
||||
result.driverId === driverId &&
|
||||
leagueRaceIds.has(result.raceId)
|
||||
);
|
||||
this.logger.info(`Found ${results.length} results for driver ${driverId} in league ${leagueId}.`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding results for driver ${driverId} and league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const leagueRaces = await this.raceRepository.findByLeagueId(leagueId);
|
||||
const leagueRaceIds = new Set(leagueRaces.map(race => race.id));
|
||||
|
||||
return Array.from(this.results.values())
|
||||
.filter(result =>
|
||||
result.driverId === driverId &&
|
||||
leagueRaceIds.has(result.raceId)
|
||||
);
|
||||
}
|
||||
|
||||
async create(result: Result): Promise<Result> {
|
||||
if (await this.exists(result.id)) {
|
||||
throw new Error(`Result with ID ${result.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Creating result: ${result.id}`);
|
||||
try {
|
||||
if (await this.exists(result.id)) {
|
||||
this.logger.warn(`Result with ID ${result.id} already exists. Throwing error.`);
|
||||
throw new Error(`Result with ID ${result.id} already exists`);
|
||||
}
|
||||
|
||||
this.results.set(result.id, result);
|
||||
return result;
|
||||
this.results.set(result.id, result);
|
||||
this.logger.info(`Result ${result.id} created successfully.`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating result ${result.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createMany(results: Result[]): Promise<Result[]> {
|
||||
const created: Result[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (await this.exists(result.id)) {
|
||||
throw new Error(`Result with ID ${result.id} already exists`);
|
||||
this.logger.debug(`Creating ${results.length} results.`);
|
||||
try {
|
||||
const created: Result[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (await this.exists(result.id)) {
|
||||
this.logger.warn(`Result with ID ${result.id} already exists. Skipping creation.`);
|
||||
// In a real system, decide if this should throw or log and skip
|
||||
continue;
|
||||
}
|
||||
this.results.set(result.id, result);
|
||||
created.push(result);
|
||||
}
|
||||
this.results.set(result.id, result);
|
||||
created.push(result);
|
||||
this.logger.info(`Created ${created.length} results successfully.`);
|
||||
|
||||
return created;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating many results:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async update(result: Result): Promise<Result> {
|
||||
if (!await this.exists(result.id)) {
|
||||
throw new Error(`Result with ID ${result.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Updating result: ${result.id}`);
|
||||
try {
|
||||
if (!await this.exists(result.id)) {
|
||||
this.logger.warn(`Result with ID ${result.id} not found for update. Throwing error.`);
|
||||
throw new Error(`Result with ID ${result.id} not found`);
|
||||
}
|
||||
|
||||
this.results.set(result.id, result);
|
||||
return result;
|
||||
this.results.set(result.id, result);
|
||||
this.logger.info(`Result ${result.id} updated successfully.`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating result ${result.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!await this.exists(id)) {
|
||||
throw new Error(`Result with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Deleting result: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Result with ID ${id} not found for deletion. Throwing error.`);
|
||||
throw new Error(`Result with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.results.delete(id);
|
||||
this.results.delete(id);
|
||||
this.logger.info(`Result ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting result ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByRaceId(raceId: string): Promise<void> {
|
||||
const raceResults = await this.findByRaceId(raceId);
|
||||
raceResults.forEach(result => {
|
||||
this.results.delete(result.id);
|
||||
});
|
||||
this.logger.debug(`Deleting results for race id: ${raceId}`);
|
||||
try {
|
||||
const initialCount = this.results.size;
|
||||
const raceResults = Array.from(this.results.values()).filter(
|
||||
result => result.raceId === raceId
|
||||
);
|
||||
raceResults.forEach(result => {
|
||||
this.results.delete(result.id);
|
||||
});
|
||||
this.logger.info(`Deleted ${raceResults.length} results for race id: ${raceId}.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting results for race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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> {
|
||||
return Array.from(this.results.values()).some(
|
||||
result => result.raceId === raceId
|
||||
);
|
||||
}
|
||||
this.logger.debug(`Checking existence of results for race id: ${raceId}`);
|
||||
try {
|
||||
const exists = Array.from(this.results.values()).some(
|
||||
result => result.raceId === raceId
|
||||
);
|
||||
this.logger.debug(`Results for race ${raceId} exist: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of results for race id ${raceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to generate a new UUID
|
||||
|
||||
@@ -13,6 +13,22 @@ import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/r
|
||||
import { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding';
|
||||
import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType';
|
||||
import type { ParticipantRef } from '@gridpilot/racing/domain/types/ParticipantRef';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
class SilentLogger implements ILogger {
|
||||
debug(..._args: unknown[]): void {
|
||||
// console.debug(..._args);
|
||||
}
|
||||
info(..._args: unknown[]): void {
|
||||
// console.info(..._args);
|
||||
}
|
||||
warn(..._args: unknown[]): void {
|
||||
// console.warn(..._args);
|
||||
}
|
||||
error(..._args: unknown[]): void {
|
||||
// console.error(..._args);
|
||||
}
|
||||
}
|
||||
|
||||
export type LeagueScoringPresetPrimaryChampionshipType =
|
||||
| 'driver'
|
||||
@@ -248,70 +264,168 @@ export function getLeagueScoringPresetById(
|
||||
|
||||
export class InMemoryGameRepository implements IGameRepository {
|
||||
private games: Game[];
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(seedData?: Game[]) {
|
||||
constructor(logger: ILogger, seedData?: Game[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryGameRepository initialized.');
|
||||
this.games = seedData ? [...seedData] : [];
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<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[]> {
|
||||
return [...this.games];
|
||||
this.logger.debug('Finding all games.');
|
||||
try {
|
||||
const games = [...this.games];
|
||||
this.logger.info(`Found ${games.length} games.`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
this.logger.error('Error finding all games:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
seed(game: Game): void {
|
||||
this.games.push(game);
|
||||
this.logger.debug(`Seeding game: ${game.id}`);
|
||||
try {
|
||||
this.games.push(game);
|
||||
this.logger.info(`Game ${game.id} seeded successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding game ${game.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemorySeasonRepository implements ISeasonRepository {
|
||||
private seasons: Season[];
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(seedData?: Season[]) {
|
||||
constructor(logger: ILogger, seedData?: Season[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemorySeasonRepository initialized.');
|
||||
this.seasons = seedData ? [...seedData] : [];
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<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[]> {
|
||||
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> {
|
||||
// Backward-compatible alias for add()
|
||||
this.seasons.push(season);
|
||||
return season;
|
||||
this.logger.debug(`Creating season: ${season.id}`);
|
||||
try {
|
||||
// Backward-compatible alias for add()
|
||||
this.seasons.push(season);
|
||||
this.logger.info(`Season ${season.id} created successfully.`);
|
||||
return season;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating season ${season.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async add(season: Season): Promise<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> {
|
||||
const index = this.seasons.findIndex((s) => s.id === season.id);
|
||||
if (index === -1) {
|
||||
this.seasons.push(season);
|
||||
return;
|
||||
this.logger.debug(`Updating season: ${season.id}`);
|
||||
try {
|
||||
const index = this.seasons.findIndex((s) => s.id === season.id);
|
||||
if (index === -1) {
|
||||
this.logger.warn(`Season with id ${season.id} not found for update. Adding as new.`);
|
||||
this.seasons.push(season);
|
||||
return;
|
||||
}
|
||||
this.seasons[index] = season;
|
||||
this.logger.info(`Season ${season.id} updated successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating season ${season.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.seasons[index] = season;
|
||||
}
|
||||
|
||||
async listByLeague(leagueId: string): Promise<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[]> {
|
||||
return this.seasons.filter(
|
||||
(s) => s.leagueId === leagueId && s.status === 'active',
|
||||
);
|
||||
this.logger.debug(`Listing active seasons by league id: ${leagueId}`);
|
||||
try {
|
||||
const seasons = this.seasons.filter(
|
||||
(s) => s.leagueId === leagueId && s.status === 'active',
|
||||
);
|
||||
this.logger.info(`Found ${seasons.length} active seasons for league id: ${leagueId}.`);
|
||||
return seasons;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error listing active seasons by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
seed(season: Season): void {
|
||||
this.seasons.push(season);
|
||||
this.logger.debug(`Seeding season: ${season.id}`);
|
||||
try {
|
||||
this.seasons.push(season);
|
||||
this.logger.info(`Season ${season.id} seeded successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding season ${season.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,29 +433,59 @@ export class InMemoryLeagueScoringConfigRepository
|
||||
implements ILeagueScoringConfigRepository
|
||||
{
|
||||
private configs: LeagueScoringConfig[];
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(seedData?: LeagueScoringConfig[]) {
|
||||
constructor(logger: ILogger, seedData?: LeagueScoringConfig[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryLeagueScoringConfigRepository initialized.');
|
||||
this.configs = seedData ? [...seedData] : [];
|
||||
}
|
||||
|
||||
async findBySeasonId(seasonId: string): Promise<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> {
|
||||
const existingIndex = this.configs.findIndex(
|
||||
(c) => c.id === config.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
this.configs[existingIndex] = config;
|
||||
} else {
|
||||
this.configs.push(config);
|
||||
this.logger.debug(`Saving league scoring config: ${config.id} for seasonId: ${config.seasonId}`);
|
||||
try {
|
||||
const existingIndex = this.configs.findIndex(
|
||||
(c) => c.id === config.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
this.configs[existingIndex] = config;
|
||||
this.logger.info(`Updated existing league scoring config: ${config.id}.`);
|
||||
} else {
|
||||
this.configs.push(config);
|
||||
this.logger.info(`Created new league scoring config: ${config.id}.`);
|
||||
}
|
||||
return config;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving league scoring config ${config.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
seed(config: LeagueScoringConfig): void {
|
||||
this.configs.push(config);
|
||||
this.logger.debug(`Seeding league scoring config: ${config.id}`);
|
||||
try {
|
||||
this.configs.push(config);
|
||||
this.logger.info(`League scoring config ${config.id} seeded successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding league scoring config ${config.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,26 +493,63 @@ export class InMemoryChampionshipStandingRepository
|
||||
implements IChampionshipStandingRepository
|
||||
{
|
||||
private standings: ChampionshipStanding[] = [];
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger, seedData?: ChampionshipStanding[]) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryChampionshipStandingRepository initialized.');
|
||||
this.standings = seedData ? [...seedData] : [];
|
||||
}
|
||||
|
||||
async findBySeasonAndChampionship(
|
||||
seasonId: string,
|
||||
championshipId: string,
|
||||
): Promise<ChampionshipStanding[]> {
|
||||
return this.standings.filter(
|
||||
(s) => s.seasonId === seasonId && s.championshipId === championshipId,
|
||||
);
|
||||
this.logger.debug(`Finding championship standings for season: ${seasonId}, championship: ${championshipId}`);
|
||||
try {
|
||||
const standings = this.standings.filter(
|
||||
(s) => s.seasonId === seasonId && s.championshipId === championshipId,
|
||||
);
|
||||
this.logger.info(`Found ${standings.length} championship standings.`);
|
||||
return standings;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding championship standings for season ${seasonId}, championship ${championshipId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async saveAll(standings: ChampionshipStanding[]): Promise<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 {
|
||||
this.standings.push(standing);
|
||||
this.logger.debug(`Seeding championship standing: ${standing.id}`);
|
||||
try {
|
||||
this.standings.push(standing);
|
||||
this.logger.info(`Championship standing ${standing.id} seeded successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding championship standing ${standing.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getAll(): ChampionshipStanding[] {
|
||||
return [...this.standings];
|
||||
this.logger.debug('Getting all championship standings.');
|
||||
try {
|
||||
const standings = [...this.standings];
|
||||
this.logger.info(`Retrieved ${standings.length} championship standings.`);
|
||||
return standings;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting all championship standings:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +568,8 @@ export function createSprintMainDemoScoringSetup(params: {
|
||||
const seasonId = params.seasonId ?? 'season-sprint-main-demo';
|
||||
const championshipId = 'driver-champ';
|
||||
|
||||
const logger = new SilentLogger();
|
||||
|
||||
const game = Game.create({ id: 'iracing', name: 'iRacing' });
|
||||
|
||||
const season = Season.create({
|
||||
@@ -410,12 +593,12 @@ export function createSprintMainDemoScoringSetup(params: {
|
||||
seasonId: season.id,
|
||||
});
|
||||
|
||||
const gameRepo = new InMemoryGameRepository([game]);
|
||||
const seasonRepo = new InMemorySeasonRepository([season]);
|
||||
const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository([
|
||||
const gameRepo = new InMemoryGameRepository(logger, [game]);
|
||||
const seasonRepo = new InMemorySeasonRepository(logger, [season]);
|
||||
const scoringConfigRepo = new InMemoryLeagueScoringConfigRepository(logger, [
|
||||
leagueScoringConfig,
|
||||
]);
|
||||
const championshipStandingRepo = new InMemoryChampionshipStandingRepository();
|
||||
const championshipStandingRepo = new InMemoryChampionshipStandingRepository(logger);
|
||||
|
||||
return {
|
||||
gameRepo,
|
||||
|
||||
@@ -6,67 +6,165 @@
|
||||
|
||||
import type { SeasonSponsorship, SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRepository {
|
||||
private sponsorships: Map<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> {
|
||||
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[]> {
|
||||
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[]> {
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.sponsorships.values()).filter(
|
||||
s => s.seasonId === seasonId && s.tier === tier
|
||||
);
|
||||
this.logger.debug(`Finding season sponsorships by season id: ${seasonId} and tier: ${tier}`);
|
||||
try {
|
||||
const sponsorships = Array.from(this.sponsorships.values()).filter(
|
||||
s => s.seasonId === seasonId && s.tier === tier
|
||||
);
|
||||
this.logger.info(`Found ${sponsorships.length} season sponsorships for season id: ${seasonId}, tier: ${tier}.`);
|
||||
return sponsorships;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding season sponsorships by season id ${seasonId}, tier ${tier}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship> {
|
||||
if (this.sponsorships.has(sponsorship.id)) {
|
||||
throw new Error('SeasonSponsorship with this ID already exists');
|
||||
this.logger.debug(`Creating season sponsorship: ${sponsorship.id}`);
|
||||
try {
|
||||
if (this.sponsorships.has(sponsorship.id)) {
|
||||
this.logger.warn(`SeasonSponsorship with ID ${sponsorship.id} already exists.`);
|
||||
throw new Error('SeasonSponsorship with this ID already exists');
|
||||
}
|
||||
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||
this.logger.info(`SeasonSponsorship ${sponsorship.id} created successfully.`);
|
||||
return sponsorship;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating season sponsorship ${sponsorship.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||
return sponsorship;
|
||||
}
|
||||
|
||||
async update(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship> {
|
||||
if (!this.sponsorships.has(sponsorship.id)) {
|
||||
throw new Error('SeasonSponsorship not found');
|
||||
this.logger.debug(`Updating season sponsorship: ${sponsorship.id}`);
|
||||
try {
|
||||
if (!this.sponsorships.has(sponsorship.id)) {
|
||||
this.logger.warn(`SeasonSponsorship with ID ${sponsorship.id} not found for update.`);
|
||||
throw new Error('SeasonSponsorship not found');
|
||||
}
|
||||
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||
this.logger.info(`SeasonSponsorship ${sponsorship.id} updated successfully.`);
|
||||
return sponsorship;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating season sponsorship ${sponsorship.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||
return sponsorship;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.sponsorships.has(id);
|
||||
this.logger.debug(`Checking existence of season sponsorship with id: ${id}`);
|
||||
try {
|
||||
const exists = this.sponsorships.has(id);
|
||||
this.logger.debug(`SeasonSponsorship ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of season sponsorship with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(sponsorships: SeasonSponsorship[]): void {
|
||||
for (const sponsorship of sponsorships) {
|
||||
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||
this.logger.debug(`Seeding ${sponsorships.length} season sponsorships.`);
|
||||
try {
|
||||
for (const sponsorship of sponsorships) {
|
||||
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||
this.logger.debug(`Seeded season sponsorship: ${sponsorship.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${sponsorships.length} season sponsorships.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding season sponsorships:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all season sponsorships.');
|
||||
this.sponsorships.clear();
|
||||
this.logger.info('All season sponsorships cleared.');
|
||||
}
|
||||
}
|
||||
@@ -3,60 +3,158 @@
|
||||
*/
|
||||
import type { ISessionRepository } from '../../domain/repositories/ISessionRepository';
|
||||
import type { Session } from '../../domain/entities/Session';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySessionRepository implements ISessionRepository {
|
||||
private sessions: Map<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> {
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
session => session.raceEventId === raceEventId
|
||||
);
|
||||
this.logger.debug(`Finding sessions by race event id: ${raceEventId}`);
|
||||
try {
|
||||
const sessions = Array.from(this.sessions.values()).filter(
|
||||
session => session.raceEventId === raceEventId
|
||||
);
|
||||
this.logger.info(`Found ${sessions.length} sessions for race event id: ${raceEventId}.`);
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sessions by race event id ${raceEventId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<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
|
||||
// For now, return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
async findByStatus(status: string): Promise<Session[]> {
|
||||
return Array.from(this.sessions.values()).filter(
|
||||
session => session.status === status
|
||||
);
|
||||
this.logger.debug(`Finding sessions by status: ${status}`);
|
||||
try {
|
||||
const sessions = Array.from(this.sessions.values()).filter(
|
||||
session => session.status === status
|
||||
);
|
||||
this.logger.info(`Found ${sessions.length} sessions with status: ${status}.`);
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sessions by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(session: Session): Promise<Session> {
|
||||
this.sessions.set(session.id, session);
|
||||
return session;
|
||||
this.logger.debug(`Creating session: ${session.id}`);
|
||||
try {
|
||||
if (this.sessions.has(session.id)) {
|
||||
this.logger.warn(`Session with ID ${session.id} already exists.`);
|
||||
throw new Error(`Session with ID ${session.id} already exists`);
|
||||
}
|
||||
this.sessions.set(session.id, session);
|
||||
this.logger.info(`Session ${session.id} created successfully.`);
|
||||
return session;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating session ${session.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(session: Session): Promise<Session> {
|
||||
this.sessions.set(session.id, session);
|
||||
return session;
|
||||
this.logger.debug(`Updating session: ${session.id}`);
|
||||
try {
|
||||
if (!this.sessions.has(session.id)) {
|
||||
this.logger.warn(`Session with ID ${session.id} not found for update.`);
|
||||
throw new Error(`Session with ID ${session.id} not found`);
|
||||
}
|
||||
this.sessions.set(session.id, session);
|
||||
this.logger.info(`Session ${session.id} updated successfully.`);
|
||||
return session;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating session ${session.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.sessions.has(id);
|
||||
this.logger.debug(`Checking existence of session with id: ${id}`);
|
||||
try {
|
||||
const exists = this.sessions.has(id);
|
||||
this.logger.debug(`Session ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of session with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper methods
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sessions.');
|
||||
this.sessions.clear();
|
||||
this.logger.info('All sessions cleared.');
|
||||
}
|
||||
|
||||
getAll(): Session[] {
|
||||
return Array.from(this.sessions.values());
|
||||
this.logger.debug('Getting all sessions.');
|
||||
try {
|
||||
const sessions = Array.from(this.sessions.values());
|
||||
this.logger.info(`Retrieved ${sessions.length} sessions.`);
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting all sessions:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,62 +6,144 @@
|
||||
|
||||
import type { Sponsor } from '../../domain/entities/Sponsor';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorRepository implements ISponsorRepository {
|
||||
private sponsors: Map<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> {
|
||||
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[]> {
|
||||
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> {
|
||||
for (const sponsor of this.sponsors.values()) {
|
||||
if (sponsor.contactEmail === email) {
|
||||
return sponsor;
|
||||
this.logger.debug(`Finding sponsor by email: ${email}`);
|
||||
try {
|
||||
for (const sponsor of this.sponsors.values()) {
|
||||
if (sponsor.contactEmail === email) {
|
||||
this.logger.info(`Found sponsor with email: ${email}.`);
|
||||
return sponsor;
|
||||
}
|
||||
}
|
||||
this.logger.warn(`Sponsor with email ${email} not found.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsor by email ${email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async create(sponsor: Sponsor): Promise<Sponsor> {
|
||||
if (this.sponsors.has(sponsor.id)) {
|
||||
throw new Error('Sponsor with this ID already exists');
|
||||
this.logger.debug(`Creating sponsor: ${sponsor.id}`);
|
||||
try {
|
||||
if (this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} already exists.`);
|
||||
throw new Error('Sponsor with this ID already exists');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} created successfully.`);
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating sponsor ${sponsor.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
return sponsor;
|
||||
}
|
||||
|
||||
async update(sponsor: Sponsor): Promise<Sponsor> {
|
||||
if (!this.sponsors.has(sponsor.id)) {
|
||||
throw new Error('Sponsor not found');
|
||||
this.logger.debug(`Updating sponsor: ${sponsor.id}`);
|
||||
try {
|
||||
if (!this.sponsors.has(sponsor.id)) {
|
||||
this.logger.warn(`Sponsor with ID ${sponsor.id} not found for update.`);
|
||||
throw new Error('Sponsor not found');
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.info(`Sponsor ${sponsor.id} updated successfully.`);
|
||||
return sponsor;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating sponsor ${sponsor.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
return sponsor;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.sponsors.has(id);
|
||||
this.logger.debug(`Checking existence of sponsor with id: ${id}`);
|
||||
try {
|
||||
const exists = this.sponsors.has(id);
|
||||
this.logger.debug(`Sponsor ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsor with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(sponsors: Sponsor[]): void {
|
||||
for (const sponsor of sponsors) {
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.debug(`Seeding ${sponsors.length} sponsors.`);
|
||||
try {
|
||||
for (const sponsor of sponsors) {
|
||||
this.sponsors.set(sponsor.id, sponsor);
|
||||
this.logger.debug(`Seeded sponsor: ${sponsor.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${sponsors.length} sponsors.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsors:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsors.');
|
||||
this.sponsors.clear();
|
||||
this.logger.info('All sponsors cleared.');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
||||
import { SponsorshipPricing } from '../../domain/value-objects/SponsorshipPricing';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
interface StorageKey {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -13,48 +14,115 @@ interface StorageKey {
|
||||
|
||||
export class InMemorySponsorshipPricingRepository implements ISponsorshipPricingRepository {
|
||||
private pricings: Map<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 {
|
||||
return `${entityType}:${entityId}`;
|
||||
}
|
||||
|
||||
async findByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipPricing | null> {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
const entry = this.pricings.get(key);
|
||||
return entry?.pricing ?? null;
|
||||
this.logger.debug(`Finding sponsorship pricing for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
const entry = this.pricings.get(key);
|
||||
const pricing = entry?.pricing ?? null;
|
||||
if (pricing) {
|
||||
this.logger.info(`Found sponsorship pricing for entity: ${entityType}, ${entityId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsorship pricing for entity ${entityType}, ${entityId} not found.`);
|
||||
}
|
||||
return pricing;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship pricing for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async save(entityType: SponsorableEntityType, entityId: string, pricing: SponsorshipPricing): Promise<void> {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
this.pricings.set(key, { entityType, entityId, pricing });
|
||||
this.logger.debug(`Saving sponsorship pricing for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
if (this.pricings.has(key)) {
|
||||
this.logger.info(`Updating existing sponsorship pricing for entity: ${entityType}, ${entityId}.`);
|
||||
} else {
|
||||
this.logger.info(`Creating new sponsorship pricing for entity: ${entityType}, ${entityId}.`);
|
||||
}
|
||||
this.pricings.set(key, { entityType, entityId, pricing });
|
||||
this.logger.info(`Sponsorship pricing saved for entity: ${entityType}, ${entityId}.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving sponsorship pricing for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(entityType: SponsorableEntityType, entityId: string): Promise<void> {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
this.pricings.delete(key);
|
||||
this.logger.debug(`Deleting sponsorship pricing for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
if (this.pricings.delete(key)) {
|
||||
this.logger.info(`Sponsorship pricing deleted for entity: ${entityType}, ${entityId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Sponsorship pricing for entity ${entityType}, ${entityId} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting sponsorship pricing for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(entityType: SponsorableEntityType, entityId: string): Promise<boolean> {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
return this.pricings.has(key);
|
||||
this.logger.debug(`Checking existence of sponsorship pricing for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const key = this.makeKey(entityType, entityId);
|
||||
const exists = this.pricings.has(key);
|
||||
this.logger.debug(`Sponsorship pricing for entity ${entityType}, ${entityId} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsorship pricing for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAcceptingApplications(entityType: SponsorableEntityType): Promise<Array<{
|
||||
entityId: string;
|
||||
pricing: SponsorshipPricing;
|
||||
}>> {
|
||||
return Array.from(this.pricings.values())
|
||||
.filter(entry => entry.entityType === entityType && entry.pricing.acceptingApplications)
|
||||
.map(entry => ({ entityId: entry.entityId, pricing: entry.pricing }));
|
||||
this.logger.debug(`Finding entities accepting applications for type: ${entityType}`);
|
||||
try {
|
||||
const accepting = Array.from(this.pricings.values())
|
||||
.filter(entry => entry.entityType === entityType && entry.pricing.acceptingApplications)
|
||||
.map(entry => ({ entityId: entry.entityId, pricing: entry.pricing }));
|
||||
this.logger.info(`Found ${accepting.length} entities accepting applications for type: ${entityType}.`);
|
||||
return accepting;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding accepting applications for entity type ${entityType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(data: Array<{ entityType: SponsorableEntityType; entityId: string; pricing: SponsorshipPricing }>): void {
|
||||
for (const entry of data) {
|
||||
const key = this.makeKey(entry.entityType, entry.entityId);
|
||||
this.pricings.set(key, entry);
|
||||
this.logger.debug(`Seeding ${data.length} sponsorship pricing entries.`);
|
||||
try {
|
||||
for (const entry of data) {
|
||||
const key = this.makeKey(entry.entityType, entry.entityId);
|
||||
this.pricings.set(key, entry);
|
||||
this.logger.debug(`Seeded pricing for entity ${entry.entityType}, ${entry.entityId}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${data.length} sponsorship pricing entries.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsorship pricing data:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +130,8 @@ export class InMemorySponsorshipPricingRepository implements ISponsorshipPricing
|
||||
* Clear all data (for testing)
|
||||
*/
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all sponsorship pricing data.');
|
||||
this.pricings.clear();
|
||||
this.logger.info('All sponsorship pricing data cleared.');
|
||||
}
|
||||
}
|
||||
@@ -8,100 +8,225 @@ import {
|
||||
type SponsorableEntityType,
|
||||
type SponsorshipRequestStatus
|
||||
} from '../../domain/entities/SponsorshipRequest';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemorySponsorshipRequestRepository implements ISponsorshipRequestRepository {
|
||||
private requests: Map<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> {
|
||||
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[]> {
|
||||
return Array.from(this.requests.values()).filter(
|
||||
request => request.entityType === entityType && request.entityId === entityId
|
||||
);
|
||||
this.logger.debug(`Finding sponsorship requests by entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.entityType === entityType && request.entityId === entityId
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for entity: ${entityType}, ${entityId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<SponsorshipRequest[]> {
|
||||
return Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.debug(`Finding pending sponsorship requests by entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} pending sponsorship requests for entity: ${entityType}, ${entityId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding pending sponsorship requests by entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findBySponsorId(sponsorId: string): Promise<SponsorshipRequest[]> {
|
||||
return Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId
|
||||
);
|
||||
this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for sponsor id: ${sponsorId}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by sponsor id ${sponsorId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByStatus(status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
|
||||
return Array.from(this.requests.values()).filter(
|
||||
request => request.status === status
|
||||
);
|
||||
this.logger.debug(`Finding sponsorship requests by status: ${status}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.status === status
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests with status: ${status}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findBySponsorIdAndStatus(sponsorId: string, status: SponsorshipRequestStatus): Promise<SponsorshipRequest[]> {
|
||||
return Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId && request.status === status
|
||||
);
|
||||
this.logger.debug(`Finding sponsorship requests by sponsor id: ${sponsorId} and status: ${status}`);
|
||||
try {
|
||||
const requests = Array.from(this.requests.values()).filter(
|
||||
request => request.sponsorId === sponsorId && request.status === status
|
||||
);
|
||||
this.logger.info(`Found ${requests.length} sponsorship requests for sponsor id: ${sponsorId}, status: ${status}.`);
|
||||
return requests;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding sponsorship requests by sponsor id ${sponsorId}, status ${status}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async hasPendingRequest(sponsorId: string, entityType: SponsorableEntityType, entityId: string): Promise<boolean> {
|
||||
return Array.from(this.requests.values()).some(
|
||||
request =>
|
||||
request.sponsorId === sponsorId &&
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.debug(`Checking for pending request from sponsor: ${sponsorId} for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const has = Array.from(this.requests.values()).some(
|
||||
request =>
|
||||
request.sponsorId === sponsorId &&
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
);
|
||||
this.logger.debug(`Pending request exists: ${has}.`);
|
||||
return has;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking for pending request from sponsor ${sponsorId} for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async countPendingByEntity(entityType: SponsorableEntityType, entityId: string): Promise<number> {
|
||||
return Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
).length;
|
||||
this.logger.debug(`Counting pending requests for entity: ${entityType}, ${entityId}`);
|
||||
try {
|
||||
const count = Array.from(this.requests.values()).filter(
|
||||
request =>
|
||||
request.entityType === entityType &&
|
||||
request.entityId === entityId &&
|
||||
request.status === 'pending'
|
||||
).length;
|
||||
this.logger.info(`Counted ${count} pending requests for entity: ${entityType}, ${entityId}.`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error counting pending requests for entity ${entityType}, ${entityId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(request: SponsorshipRequest): Promise<SponsorshipRequest> {
|
||||
this.requests.set(request.id, request);
|
||||
return request;
|
||||
this.logger.debug(`Creating sponsorship request: ${request.id}`);
|
||||
try {
|
||||
if (this.requests.has(request.id)) {
|
||||
this.logger.warn(`SponsorshipRequest with ID ${request.id} already exists.`);
|
||||
throw new Error(`SponsorshipRequest with ID ${request.id} already exists`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`SponsorshipRequest ${request.id} created successfully.`);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating sponsorship request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(request: SponsorshipRequest): Promise<SponsorshipRequest> {
|
||||
if (!this.requests.has(request.id)) {
|
||||
throw new Error(`SponsorshipRequest ${request.id} not found`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
return request;
|
||||
this.logger.debug(`Updating sponsorship request: ${request.id}`);
|
||||
try {
|
||||
if (!this.requests.has(request.id)) {
|
||||
this.logger.warn(`SponsorshipRequest ${request.id} not found for update.`);
|
||||
throw new Error(`SponsorshipRequest ${request.id} not found`);
|
||||
}
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.info(`SponsorshipRequest ${request.id} updated successfully.`);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating sponsorship request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.requests.has(id);
|
||||
this.logger.debug(`Checking existence of sponsorship request with id: ${id}`);
|
||||
try {
|
||||
const exists = this.requests.has(id);
|
||||
this.logger.debug(`Sponsorship request ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of sponsorship request with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed initial data
|
||||
*/
|
||||
seed(requests: SponsorshipRequest[]): void {
|
||||
for (const request of requests) {
|
||||
this.requests.set(request.id, request);
|
||||
}
|
||||
this.logger.debug(`Seeding ${requests.length} sponsorship requests.`);
|
||||
try {
|
||||
for (const request of requests) {
|
||||
this.requests.set(request.id, request);
|
||||
this.logger.debug(`Seeded sponsorship request: ${request.id}.`);
|
||||
}
|
||||
this.logger.info(`Successfully seeded ${requests.length} sponsorship requests.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error seeding sponsorship requests:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data (for testing)
|
||||
*/
|
||||
clear(): void {
|
||||
this.requests.clear();
|
||||
this.logger.debug('Clearing all sponsorship requests.');
|
||||
this.requests.clear();
|
||||
this.logger.info('All sponsorship requests cleared.');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/
|
||||
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
|
||||
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
/**
|
||||
* Points systems presets
|
||||
@@ -31,13 +32,17 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
||||
private resultRepository: IResultRepository | null;
|
||||
private raceRepository: IRaceRepository | null;
|
||||
private leagueRepository: ILeagueRepository | null;
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(
|
||||
logger: ILogger,
|
||||
seedData?: Standing[],
|
||||
resultRepository?: IResultRepository | null,
|
||||
raceRepository?: IRaceRepository | null,
|
||||
leagueRepository?: ILeagueRepository | null
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.logger.info('InMemoryStandingRepository initialized.');
|
||||
this.standings = new Map();
|
||||
this.resultRepository = resultRepository ?? null;
|
||||
this.raceRepository = raceRepository ?? null;
|
||||
@@ -47,6 +52,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
||||
seedData.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.set(key, standing);
|
||||
this.logger.debug(`Seeded standing for league ${standing.leagueId}, driver ${standing.driverId}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -56,134 +62,235 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Standing[]> {
|
||||
return Array.from(this.standings.values())
|
||||
.filter(standing => standing.leagueId === leagueId)
|
||||
.sort((a, b) => {
|
||||
// Sort by position (lower is better)
|
||||
if (a.position !== b.position) {
|
||||
return a.position - b.position;
|
||||
}
|
||||
// If positions are equal, sort by points (higher is better)
|
||||
return b.points - a.points;
|
||||
});
|
||||
this.logger.debug(`Finding standings for league id: ${leagueId}`);
|
||||
try {
|
||||
const standings = Array.from(this.standings.values())
|
||||
.filter(standing => standing.leagueId === leagueId)
|
||||
.sort((a, b) => {
|
||||
// Sort by position (lower is better)
|
||||
if (a.position !== b.position) {
|
||||
return a.position - b.position;
|
||||
}
|
||||
// If positions are equal, sort by points (higher is better)
|
||||
return b.points - a.points;
|
||||
});
|
||||
this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`);
|
||||
return standings;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding standings for league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Standing | null> {
|
||||
const key = this.getKey(leagueId, driverId);
|
||||
return this.standings.get(key) ?? null;
|
||||
this.logger.debug(`Finding standing for driver: ${driverId}, league: ${leagueId}`);
|
||||
try {
|
||||
const key = this.getKey(leagueId, driverId);
|
||||
const standing = this.standings.get(key) ?? null;
|
||||
if (standing) {
|
||||
this.logger.info(`Found standing for driver: ${driverId}, league: ${leagueId}.`);
|
||||
} else {
|
||||
this.logger.warn(`Standing for driver ${driverId}, league ${leagueId} not found.`);
|
||||
}
|
||||
return standing;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(): Promise<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> {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.set(key, standing);
|
||||
return standing;
|
||||
this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`);
|
||||
try {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
if (this.standings.has(key)) {
|
||||
this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
|
||||
} else {
|
||||
this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
|
||||
}
|
||||
this.standings.set(key, standing);
|
||||
this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`);
|
||||
return standing;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async saveMany(standings: Standing[]): Promise<Standing[]> {
|
||||
standings.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.set(key, standing);
|
||||
});
|
||||
return standings;
|
||||
this.logger.debug(`Saving ${standings.length} standings.`);
|
||||
try {
|
||||
standings.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.set(key, standing);
|
||||
});
|
||||
this.logger.info(`${standings.length} standings saved successfully.`);
|
||||
return standings;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving many standings:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(leagueId: string, driverId: string): Promise<void> {
|
||||
const key = this.getKey(leagueId, driverId);
|
||||
this.standings.delete(key);
|
||||
this.logger.debug(`Deleting standing for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const key = this.getKey(leagueId, driverId);
|
||||
if (this.standings.delete(key)) {
|
||||
this.logger.info(`Standing for league ${leagueId}, driver ${driverId} deleted successfully.`);
|
||||
} else {
|
||||
this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByLeagueId(leagueId: string): Promise<void> {
|
||||
const toDelete = Array.from(this.standings.values())
|
||||
.filter(standing => standing.leagueId === leagueId);
|
||||
|
||||
toDelete.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.delete(key);
|
||||
});
|
||||
this.logger.debug(`Deleting all standings for league id: ${leagueId}`);
|
||||
try {
|
||||
const initialCount = Array.from(this.standings.values()).filter(s => s.leagueId === leagueId).length;
|
||||
const toDelete = Array.from(this.standings.values())
|
||||
.filter(standing => standing.leagueId === leagueId);
|
||||
|
||||
toDelete.forEach(standing => {
|
||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
||||
this.standings.delete(key);
|
||||
});
|
||||
this.logger.info(`Deleted ${toDelete.length} standings for league id: ${leagueId}.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting standings by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(leagueId: string, driverId: string): Promise<boolean> {
|
||||
const key = this.getKey(leagueId, driverId);
|
||||
return this.standings.has(key);
|
||||
this.logger.debug(`Checking existence of standing for league: ${leagueId}, driver: ${driverId}`);
|
||||
try {
|
||||
const key = this.getKey(leagueId, driverId);
|
||||
const exists = this.standings.has(key);
|
||||
this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async recalculate(leagueId: string): Promise<Standing[]> {
|
||||
if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) {
|
||||
throw new Error('Cannot recalculate standings: missing required repositories');
|
||||
}
|
||||
|
||||
// Get league to determine points system
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League with ID ${leagueId} not found`);
|
||||
}
|
||||
|
||||
// Get points system
|
||||
const resolvedPointsSystem =
|
||||
league.settings.customPoints ??
|
||||
POINTS_SYSTEMS[league.settings.pointsSystem] ??
|
||||
POINTS_SYSTEMS['f1-2024'];
|
||||
|
||||
if (!resolvedPointsSystem) {
|
||||
throw new Error('No points system configured for league');
|
||||
}
|
||||
|
||||
const pointsSystem: Record<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,
|
||||
});
|
||||
this.logger.debug(`Recalculating standings for league id: ${leagueId}`);
|
||||
try {
|
||||
if (!this.resultRepository || !this.raceRepository || !this.leagueRepository) {
|
||||
this.logger.error('Cannot recalculate standings: missing required repositories.');
|
||||
throw new Error('Cannot recalculate standings: missing required repositories');
|
||||
}
|
||||
|
||||
// Add points from this result
|
||||
standing = standing.addRaceResult(result.position, pointsSystem);
|
||||
standingsMap.set(result.driverId, standing);
|
||||
});
|
||||
// Get league to determine points system
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
if (!league) {
|
||||
this.logger.warn(`League with ID ${leagueId} not found during recalculation.`);
|
||||
throw new Error(`League with ID ${leagueId} not found`);
|
||||
}
|
||||
this.logger.debug(`League ${leagueId} found for recalculation.`);
|
||||
|
||||
// Sort by points and assign positions
|
||||
const sortedStandings = Array.from(standingsMap.values())
|
||||
.sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
// Get points system
|
||||
const resolvedPointsSystem =
|
||||
league.settings.customPoints ??
|
||||
POINTS_SYSTEMS[league.settings.pointsSystem] ??
|
||||
POINTS_SYSTEMS['f1-2024'];
|
||||
|
||||
if (!resolvedPointsSystem) {
|
||||
this.logger.error(`No points system configured for league ${leagueId}.`);
|
||||
throw new Error('No points system configured for league');
|
||||
}
|
||||
this.logger.debug(`Resolved points system for league ${leagueId}.`);
|
||||
|
||||
// Get all completed races for the league
|
||||
const races = await this.raceRepository.findCompletedByLeagueId(leagueId);
|
||||
this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`);
|
||||
|
||||
if (races.length === 0) {
|
||||
this.logger.warn(`No completed races found for league ${leagueId}. Standings will be empty.`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all results for these races
|
||||
const allResults = await Promise.all(
|
||||
races.map(async race => {
|
||||
this.logger.debug(`Fetching results for race ${race.id}.`);
|
||||
const results = await this.resultRepository!.findByRaceId(race.id);
|
||||
this.logger.debug(`Found ${results.length} results for race ${race.id}.`);
|
||||
return results;
|
||||
})
|
||||
);
|
||||
const results = allResults.flat();
|
||||
this.logger.debug(`Collected ${results.length} results from all completed races.`);
|
||||
|
||||
// Calculate standings per driver
|
||||
const standingsMap = new Map<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) {
|
||||
return b.wins - a.wins;
|
||||
}
|
||||
// Tie-breaker: most races completed
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
|
||||
// Add points from this result
|
||||
standing = standing.addRaceResult(result.position, pointsSystem);
|
||||
standingsMap.set(result.driverId, standing);
|
||||
this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`);
|
||||
});
|
||||
this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`);
|
||||
|
||||
// Sort by points and assign positions
|
||||
const sortedStandings = Array.from(standingsMap.values())
|
||||
.sort((a, b) => {
|
||||
if (b.points !== a.points) {
|
||||
return b.points - a.points;
|
||||
}
|
||||
// Tie-breaker: most wins
|
||||
if (b.wins !== a.wins) {
|
||||
return b.wins - a.wins;
|
||||
}
|
||||
// Tie-breaker: most races completed
|
||||
return b.racesCompleted - a.racesCompleted;
|
||||
});
|
||||
this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`);
|
||||
|
||||
// Assign positions
|
||||
const updatedStandings = sortedStandings.map((standing, index) => {
|
||||
const newStanding = standing.updatePosition(index + 1);
|
||||
this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`);
|
||||
return newStanding;
|
||||
});
|
||||
|
||||
// Assign positions
|
||||
const updatedStandings = sortedStandings.map((standing, index) =>
|
||||
standing.updatePosition(index + 1)
|
||||
);
|
||||
// Save all standings
|
||||
await this.saveMany(updatedStandings);
|
||||
this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`);
|
||||
|
||||
// Save all standings
|
||||
await this.saveMany(updatedStandings);
|
||||
|
||||
return updatedStandings;
|
||||
return updatedStandings;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error recalculating standings for league ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,12 +10,16 @@ import type {
|
||||
TeamJoinRequest,
|
||||
} from '@gridpilot/racing/domain/types/TeamMembership';
|
||||
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryTeamMembershipRepository implements ITeamMembershipRepository {
|
||||
private membershipsByTeam: Map<string, TeamMembership[]>;
|
||||
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.joinRequestsByTeam = new Map();
|
||||
|
||||
@@ -24,6 +28,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito
|
||||
const list = this.membershipsByTeam.get(membership.teamId) ?? [];
|
||||
list.push(membership);
|
||||
this.membershipsByTeam.set(membership.teamId, list);
|
||||
this.logger.debug(`Seeded membership for team ${membership.teamId}, driver ${membership.driverId}.`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +37,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito
|
||||
const list = this.joinRequestsByTeam.get(request.teamId) ?? [];
|
||||
list.push(request);
|
||||
this.joinRequestsByTeam.set(request.teamId, list);
|
||||
this.logger.debug(`Seeded join request for team ${request.teamId}, driver ${request.driverId}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -41,6 +47,7 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito
|
||||
if (!list) {
|
||||
list = [];
|
||||
this.membershipsByTeam.set(teamId, list);
|
||||
this.logger.debug(`Created new membership list for team: ${teamId}`);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -50,91 +57,177 @@ export class InMemoryTeamMembershipRepository implements ITeamMembershipReposito
|
||||
if (!list) {
|
||||
list = [];
|
||||
this.joinRequestsByTeam.set(teamId, list);
|
||||
this.logger.debug(`Created new join request list for team: ${teamId}`);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> {
|
||||
const list = this.membershipsByTeam.get(teamId);
|
||||
if (!list) return null;
|
||||
return list.find((m) => m.driverId === driverId) ?? null;
|
||||
async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> {
|
||||
this.logger.debug(`[getMembership] Entry - teamId: ${teamId}, driverId: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByTeam.get(teamId);
|
||||
if (!list) {
|
||||
this.logger.warn(`[getMembership] No membership list found for team: ${teamId}. Returning null.`);
|
||||
return null;
|
||||
}
|
||||
const membership = list.find((m) => m.driverId === driverId) ?? null;
|
||||
if (membership) {
|
||||
this.logger.info(`[getMembership] Success - found membership for team: ${teamId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.info(`[getMembership] Not found - membership for team: ${teamId}, driver: ${driverId}.`);
|
||||
}
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`[getMembership] Error getting membership for team ${teamId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveMembershipForDriver(driverId: string): Promise<TeamMembership | null> {
|
||||
for (const list of this.membershipsByTeam.values()) {
|
||||
const membership = list.find(
|
||||
(m) => m.driverId === driverId && m.status === 'active',
|
||||
);
|
||||
if (membership) {
|
||||
return membership;
|
||||
this.logger.debug(`[getActiveMembershipForDriver] Entry - driverId: ${driverId}`);
|
||||
try {
|
||||
for (const list of this.membershipsByTeam.values()) {
|
||||
const membership = list.find(
|
||||
(m) => m.driverId === driverId && m.status === 'active',
|
||||
);
|
||||
if (membership) {
|
||||
this.logger.info(`[getActiveMembershipForDriver] Success - found active membership for driver: ${driverId}, team: ${membership.teamId}.`);
|
||||
return membership;
|
||||
}
|
||||
}
|
||||
this.logger.info(`[getActiveMembershipForDriver] Not found - no active membership for driver: ${driverId}.`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`[getActiveMembershipForDriver] Error getting active membership for driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getTeamMembers(teamId: string): Promise<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> {
|
||||
const list = this.membershipsByTeam.get(teamId) ?? [];
|
||||
return list.filter((m) => m.status === 'active').length;
|
||||
this.logger.debug(`[countByTeamId] Entry - teamId: ${teamId}`);
|
||||
try {
|
||||
const list = this.membershipsByTeam.get(teamId) ?? [];
|
||||
const count = list.filter((m) => m.status === 'active').length;
|
||||
this.logger.info(`[countByTeamId] Success - counted ${count} active members for team: ${teamId}.`);
|
||||
return count;
|
||||
} catch (error) {
|
||||
this.logger.error(`[countByTeamId] Error counting members for team ${teamId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async saveMembership(membership: TeamMembership): Promise<TeamMembership> {
|
||||
const list = this.getMembershipList(membership.teamId);
|
||||
const existingIndex = list.findIndex(
|
||||
(m) => m.teamId === membership.teamId && m.driverId === membership.driverId,
|
||||
);
|
||||
this.logger.debug(`[saveMembership] Entry - teamId: ${membership.teamId}, driverId: ${membership.driverId}`);
|
||||
try {
|
||||
const list = this.getMembershipList(membership.teamId);
|
||||
const existingIndex = list.findIndex(
|
||||
(m) => m.teamId === membership.teamId && m.driverId === membership.driverId,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = membership;
|
||||
} else {
|
||||
list.push(membership);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = membership;
|
||||
this.logger.info(`[saveMembership] Success - updated existing membership for team: ${membership.teamId}, driver: ${membership.driverId}.`);
|
||||
} else {
|
||||
list.push(membership);
|
||||
this.logger.info(`[saveMembership] Success - created new membership for team: ${membership.teamId}, driver: ${membership.driverId}.`);
|
||||
}
|
||||
|
||||
this.membershipsByTeam.set(membership.teamId, list);
|
||||
return membership;
|
||||
} catch (error) {
|
||||
this.logger.error(`[saveMembership] Error saving membership for team ${membership.teamId}, driver ${membership.driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.membershipsByTeam.set(membership.teamId, list);
|
||||
return membership;
|
||||
}
|
||||
|
||||
async removeMembership(teamId: string, driverId: string): Promise<void> {
|
||||
const list = this.membershipsByTeam.get(teamId);
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
const index = list.findIndex((m) => m.driverId === driverId);
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1);
|
||||
this.membershipsByTeam.set(teamId, list);
|
||||
this.logger.debug(`[removeMembership] Entry - teamId: ${teamId}, driverId: ${driverId}`);
|
||||
try {
|
||||
const list = this.membershipsByTeam.get(teamId);
|
||||
if (!list) {
|
||||
this.logger.warn(`[removeMembership] No membership list found for team: ${teamId}. Cannot remove.`);
|
||||
return;
|
||||
}
|
||||
const index = list.findIndex((m) => m.driverId === driverId);
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1);
|
||||
this.membershipsByTeam.set(teamId, list);
|
||||
this.logger.info(`[removeMembership] Success - removed membership for team: ${teamId}, driver: ${driverId}.`);
|
||||
} else {
|
||||
this.logger.info(`[removeMembership] Not found - membership for team: ${teamId}, driver: ${driverId}. Cannot remove.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`[removeMembership] Error removing membership for team ${teamId}, driver ${driverId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getJoinRequests(teamId: string): Promise<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> {
|
||||
const list = this.getJoinRequestList(request.teamId);
|
||||
const existingIndex = list.findIndex((r) => r.id === request.id);
|
||||
this.logger.debug(`[saveJoinRequest] Entry - teamId: ${request.teamId}, driverId: ${request.driverId}, id: ${request.id}`);
|
||||
try {
|
||||
const list = this.getJoinRequestList(request.teamId);
|
||||
const existingIndex = list.findIndex((r) => r.id === request.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = request;
|
||||
} else {
|
||||
list.push(request);
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = request;
|
||||
this.logger.info(`[saveJoinRequest] Success - updated existing join request: ${request.id}.`);
|
||||
} else {
|
||||
list.push(request);
|
||||
this.logger.info(`[saveJoinRequest] Success - created new join request: ${request.id}.`);
|
||||
}
|
||||
|
||||
this.joinRequestsByTeam.set(request.teamId, list);
|
||||
return request;
|
||||
} catch (error) {
|
||||
this.logger.error(`[saveJoinRequest] Error saving join request ${request.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.joinRequestsByTeam.set(request.teamId, list);
|
||||
return request;
|
||||
}
|
||||
|
||||
async removeJoinRequest(requestId: string): Promise<void> {
|
||||
for (const [teamId, list] of this.joinRequestsByTeam.entries()) {
|
||||
const index = list.findIndex((r) => r.id === requestId);
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1);
|
||||
this.joinRequestsByTeam.set(teamId, list);
|
||||
return;
|
||||
this.logger.debug(`[removeJoinRequest] Entry - requestId: ${requestId}`);
|
||||
try {
|
||||
let removed = false;
|
||||
for (const [teamId, list] of this.joinRequestsByTeam.entries()) {
|
||||
const index = list.findIndex((r) => r.id === requestId);
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1);
|
||||
this.joinRequestsByTeam.set(teamId, list);
|
||||
removed = true;
|
||||
this.logger.info(`[removeJoinRequest] Success - removed join request ${requestId} from team ${teamId}.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
this.logger.warn(`[removeJoinRequest] Not found - join request with ID ${requestId} not found for removal.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`[removeJoinRequest] Error removing join request ${requestId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,61 +7,126 @@
|
||||
|
||||
import type { Team } from '@gridpilot/racing/domain/entities/Team';
|
||||
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryTeamRepository implements ITeamRepository {
|
||||
private teams: Map<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();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach((team) => {
|
||||
this.teams.set(team.id, team);
|
||||
this.logger.debug(`Seeded team: ${team.id}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.teams.values()).filter((team) =>
|
||||
team.leagues.includes(leagueId),
|
||||
);
|
||||
this.logger.debug(`Finding teams by league id: ${leagueId}`);
|
||||
try {
|
||||
const teams = Array.from(this.teams.values()).filter((team) =>
|
||||
team.leagues.includes(leagueId),
|
||||
);
|
||||
this.logger.info(`Found ${teams.length} teams for league id: ${leagueId}.`);
|
||||
return teams;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding teams by league id ${leagueId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(team: Team): Promise<Team> {
|
||||
if (await this.exists(team.id)) {
|
||||
throw new Error(`Team with ID ${team.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Creating team: ${team.id}`);
|
||||
try {
|
||||
if (await this.exists(team.id)) {
|
||||
this.logger.warn(`Team with ID ${team.id} already exists.`);
|
||||
throw new Error(`Team with ID ${team.id} already exists`);
|
||||
}
|
||||
|
||||
this.teams.set(team.id, team);
|
||||
return team;
|
||||
this.teams.set(team.id, team);
|
||||
this.logger.info(`Team ${team.id} created successfully.`);
|
||||
return team;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating team ${team.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(team: Team): Promise<Team> {
|
||||
if (!(await this.exists(team.id))) {
|
||||
throw new Error(`Team with ID ${team.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Updating team: ${team.id}`);
|
||||
try {
|
||||
if (!(await this.exists(team.id))) {
|
||||
this.logger.warn(`Team with ID ${team.id} not found for update.`);
|
||||
throw new Error(`Team with ID ${team.id} not found`);
|
||||
}
|
||||
|
||||
this.teams.set(team.id, team);
|
||||
return team;
|
||||
this.teams.set(team.id, team);
|
||||
this.logger.info(`Team ${team.id} updated successfully.`);
|
||||
return team;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating team ${team.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!(await this.exists(id))) {
|
||||
throw new Error(`Team with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Deleting team: ${id}`);
|
||||
try {
|
||||
if (!(await this.exists(id))) {
|
||||
this.logger.warn(`Team with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Team with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.teams.delete(id);
|
||||
this.teams.delete(id);
|
||||
this.logger.info(`Team ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting team ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,84 +8,173 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Track, TrackCategory } from '@gridpilot/racing/domain/entities/Track';
|
||||
import type { ITrackRepository } from '@gridpilot/racing/domain/repositories/ITrackRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryTrackRepository implements ITrackRepository {
|
||||
private tracks: Map<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();
|
||||
|
||||
if (seedData) {
|
||||
seedData.forEach(track => {
|
||||
this.tracks.set(track.id, track);
|
||||
this.logger.debug(`Seeded track: ${track.id}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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[]> {
|
||||
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[]> {
|
||||
return Array.from(this.tracks.values())
|
||||
.filter(track => track.gameId === gameId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Finding tracks by game id: ${gameId}`);
|
||||
try {
|
||||
const tracks = Array.from(this.tracks.values())
|
||||
.filter(track => track.gameId === gameId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${tracks.length} tracks for game id: ${gameId}.`);
|
||||
return tracks;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding tracks by game id ${gameId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByCategory(category: TrackCategory): Promise<Track[]> {
|
||||
return Array.from(this.tracks.values())
|
||||
.filter(track => track.category === category)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Finding tracks by category: ${category}`);
|
||||
try {
|
||||
const tracks = Array.from(this.tracks.values())
|
||||
.filter(track => track.category === category)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${tracks.length} tracks for category: ${category}.`);
|
||||
return tracks;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding tracks by category ${category}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findByCountry(country: string): Promise<Track[]> {
|
||||
return Array.from(this.tracks.values())
|
||||
.filter(track => track.country.toLowerCase() === country.toLowerCase())
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Finding tracks by country: ${country}`);
|
||||
try {
|
||||
const tracks = Array.from(this.tracks.values())
|
||||
.filter(track => track.country.toLowerCase() === country.toLowerCase())
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${tracks.length} tracks for country: ${country}.`);
|
||||
return tracks;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error finding tracks by country ${country}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchByName(query: string): Promise<Track[]> {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return Array.from(this.tracks.values())
|
||||
.filter(track =>
|
||||
track.name.toLowerCase().includes(lowerQuery) ||
|
||||
track.shortName.toLowerCase().includes(lowerQuery)
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.debug(`Searching tracks by name query: ${query}`);
|
||||
try {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const tracks = Array.from(this.tracks.values())
|
||||
.filter(track =>
|
||||
track.name.toLowerCase().includes(lowerQuery) ||
|
||||
track.shortName.toLowerCase().includes(lowerQuery)
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.logger.info(`Found ${tracks.length} tracks matching search query: ${query}.`);
|
||||
return tracks;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error searching tracks by name query ${query}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(track: Track): Promise<Track> {
|
||||
if (await this.exists(track.id)) {
|
||||
throw new Error(`Track with ID ${track.id} already exists`);
|
||||
}
|
||||
this.logger.debug(`Creating track: ${track.id}`);
|
||||
try {
|
||||
if (await this.exists(track.id)) {
|
||||
this.logger.warn(`Track with ID ${track.id} already exists.`);
|
||||
throw new Error(`Track with ID ${track.id} already exists`);
|
||||
}
|
||||
|
||||
this.tracks.set(track.id, track);
|
||||
return track;
|
||||
this.tracks.set(track.id, track);
|
||||
this.logger.info(`Track ${track.id} created successfully.`);
|
||||
return track;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating track ${track.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(track: Track): Promise<Track> {
|
||||
if (!await this.exists(track.id)) {
|
||||
throw new Error(`Track with ID ${track.id} not found`);
|
||||
}
|
||||
this.logger.debug(`Updating track: ${track.id}`);
|
||||
try {
|
||||
if (!await this.exists(track.id)) {
|
||||
this.logger.warn(`Track with ID ${track.id} not found for update.`);
|
||||
throw new Error(`Track with ID ${track.id} not found`);
|
||||
}
|
||||
|
||||
this.tracks.set(track.id, track);
|
||||
return track;
|
||||
this.tracks.set(track.id, track);
|
||||
this.logger.info(`Track ${track.id} updated successfully.`);
|
||||
return track;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating track ${track.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
if (!await this.exists(id)) {
|
||||
throw new Error(`Track with ID ${id} not found`);
|
||||
}
|
||||
this.logger.debug(`Deleting track: ${id}`);
|
||||
try {
|
||||
if (!await this.exists(id)) {
|
||||
this.logger.warn(`Track with ID ${id} not found for deletion.`);
|
||||
throw new Error(`Track with ID ${id} not found`);
|
||||
}
|
||||
|
||||
this.tracks.delete(id);
|
||||
this.tracks.delete(id);
|
||||
this.logger.info(`Track ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting track ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async exists(id: string): Promise<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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,48 +6,123 @@
|
||||
|
||||
import type { Transaction, TransactionType } from '../../domain/entities/Transaction';
|
||||
import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository';
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
|
||||
export class InMemoryTransactionRepository implements ITransactionRepository {
|
||||
private transactions: Map<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> {
|
||||
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[]> {
|
||||
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[]> {
|
||||
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> {
|
||||
if (this.transactions.has(transaction.id)) {
|
||||
throw new Error('Transaction with this ID already exists');
|
||||
this.logger.debug(`Creating transaction: ${transaction.id}`);
|
||||
try {
|
||||
if (this.transactions.has(transaction.id)) {
|
||||
this.logger.warn(`Transaction with ID ${transaction.id} already exists.`);
|
||||
throw new Error('Transaction with this ID already exists');
|
||||
}
|
||||
this.transactions.set(transaction.id, transaction);
|
||||
this.logger.info(`Transaction ${transaction.id} created successfully.`);
|
||||
return transaction;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating transaction ${transaction.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.transactions.set(transaction.id, transaction);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async update(transaction: Transaction): Promise<Transaction> {
|
||||
if (!this.transactions.has(transaction.id)) {
|
||||
throw new Error('Transaction not found');
|
||||
this.logger.debug(`Updating transaction: ${transaction.id}`);
|
||||
try {
|
||||
if (!this.transactions.has(transaction.id)) {
|
||||
this.logger.warn(`Transaction with ID ${transaction.id} not found for update.`);
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
this.transactions.set(transaction.id, transaction);
|
||||
this.logger.info(`Transaction ${transaction.id} updated successfully.`);
|
||||
return transaction;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating transaction ${transaction.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
this.transactions.set(transaction.id, transaction);
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<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> {
|
||||
return this.transactions.has(id);
|
||||
this.logger.debug(`Checking existence of transaction with id: ${id}`);
|
||||
try {
|
||||
const exists = this.transactions.has(id);
|
||||
this.logger.debug(`Transaction ${id} exists: ${exists}.`);
|
||||
return exists;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking existence of transaction with id ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper
|
||||
clear(): void {
|
||||
this.logger.debug('Clearing all transactions.');
|
||||
this.transactions.clear();
|
||||
this.logger.info('All transactions cleared.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user