fix issues in adapters
This commit is contained in:
@@ -163,12 +163,6 @@ export function loadAutomationConfig(): AutomationEnvironmentConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to validate automation mode string.
|
|
||||||
*/
|
|
||||||
function isValidAutomationMode(value: string | undefined): value is AutomationMode {
|
|
||||||
return value === 'production' || value === 'development' || value === 'test';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard to validate legacy automation mode string.
|
* Type guard to validate legacy automation mode string.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
|
import { SignupWithEmailUseCase } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
|
||||||
import { CreateAchievementUseCase } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase';
|
import { CreateAchievementUseCase } from '@core/identity/application/use-cases/achievement/CreateAchievementUseCase';
|
||||||
import type { Logger } from '@core/shared/application/Logger';
|
import type { Logger } from '@core/shared/application';
|
||||||
import {
|
import {
|
||||||
DRIVER_ACHIEVEMENTS,
|
DRIVER_ACHIEVEMENTS,
|
||||||
STEWARD_ACHIEVEMENTS,
|
STEWARD_ACHIEVEMENTS,
|
||||||
|
|||||||
@@ -218,12 +218,12 @@ export const leagueScoringPresets: LeagueScoringPreset[] = [
|
|||||||
dropScorePolicy,
|
dropScorePolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return LeagueScoringConfig.create({
|
||||||
id: `lsc-${seasonId}-endurance-main-double`,
|
id: `lsc-${seasonId}-endurance-main-double`,
|
||||||
seasonId,
|
seasonId,
|
||||||
scoringPresetId: 'endurance-main-double',
|
scoringPresetId: 'endurance-main-double',
|
||||||
championships: [championship],
|
championships: [championship],
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ import { getLeagueScoringPresetById } from './LeagueScoringPresets';
|
|||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
class SilentLogger implements Logger {
|
class SilentLogger implements Logger {
|
||||||
debug(...args: unknown[]): void {
|
debug(..._args: unknown[]): void {
|
||||||
// console.debug(...args);
|
// console.debug(...args);
|
||||||
}
|
}
|
||||||
info(...args: unknown[]): void {
|
info(..._args: unknown[]): void {
|
||||||
// console.info(...args);
|
// console.info(...args);
|
||||||
}
|
}
|
||||||
warn(...args: unknown[]): void {
|
warn(..._args: unknown[]): void {
|
||||||
// console.warn(...args);
|
// console.warn(...args);
|
||||||
}
|
}
|
||||||
error(...args: unknown[]): void {
|
error(..._args: unknown[]): void {
|
||||||
// console.error(...args);
|
// console.error(...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class InMemoryAchievementRepository implements IAchievementRepository {
|
|||||||
this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`);
|
this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`);
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error finding user achievement for user ${userId}, achievement ${achievementId}:`, error);
|
this.logger.error(`Error finding user achievement for user ${userId}, achievement ${achievementId}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class InMemoryUserRatingRepository implements IUserRatingRepository {
|
|||||||
this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`);
|
this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`);
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error finding user ratings for user ids ${userIds.join(', ')}:`, error);
|
this.logger.error(`Error finding user ratings for user ids ${userIds.join(', ')}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class InMemoryFaceValidationAdapter implements FaceValidationPort {
|
|||||||
this.logger.info('InMemoryFaceValidationAdapter initialized.');
|
this.logger.info('InMemoryFaceValidationAdapter initialized.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateFacePhoto(imageData: string | Buffer): Promise<FaceValidationResult> {
|
async validateFacePhoto(_imageData: string | Buffer): Promise<FaceValidationResult> {
|
||||||
this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).');
|
this.logger.debug('[InMemoryFaceValidationAdapter] Validating face photo (mock).');
|
||||||
// Simulate a successful validation for any input for demo purposes
|
// Simulate a successful validation for any input for demo purposes
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
|||||||
@@ -5,18 +5,18 @@
|
|||||||
* Currently a stub - to be implemented when Discord integration is needed.
|
* Currently a stub - to be implemented when Discord integration is needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Notification } from '../../domain/entities/Notification';
|
import type { Notification } from '@core/notifications/domain/entities/Notification';
|
||||||
import type {
|
import type {
|
||||||
INotificationGateway,
|
NotificationGateway,
|
||||||
NotificationDeliveryResult
|
NotificationDeliveryResult
|
||||||
} from '../../application/ports/INotificationGateway';
|
} from '@core/notifications/application/ports/NotificationGateway';
|
||||||
import type { NotificationChannel } from '../../domain/types/NotificationTypes';
|
import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
|
||||||
|
|
||||||
export interface DiscordAdapterConfig {
|
export interface DiscordAdapterConfig {
|
||||||
webhookUrl?: string;
|
webhookUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DiscordNotificationAdapter implements INotificationGateway {
|
export class DiscordNotificationAdapter implements NotificationGateway {
|
||||||
private readonly channel: NotificationChannel = 'discord';
|
private readonly channel: NotificationChannel = 'discord';
|
||||||
private webhookUrl: string | undefined;
|
private webhookUrl: string | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
* Currently a stub - to be implemented when email integration is needed.
|
* Currently a stub - to be implemented when email integration is needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Notification } from '../../domain/entities/Notification';
|
import type { Notification } from '@core/notifications/domain/entities/Notification';
|
||||||
import type {
|
import type {
|
||||||
INotificationGateway,
|
NotificationGateway,
|
||||||
NotificationDeliveryResult
|
NotificationDeliveryResult
|
||||||
} from '../../application/ports/INotificationGateway';
|
} from '@core/notifications/application/ports/NotificationGateway';
|
||||||
import type { NotificationChannel } from '../../domain/types/NotificationTypes';
|
import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
|
||||||
|
|
||||||
export interface EmailAdapterConfig {
|
export interface EmailAdapterConfig {
|
||||||
smtpHost?: string;
|
smtpHost?: string;
|
||||||
@@ -20,7 +20,7 @@ export interface EmailAdapterConfig {
|
|||||||
fromAddress?: string;
|
fromAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmailNotificationAdapter implements INotificationGateway {
|
export class EmailNotificationAdapter implements NotificationGateway {
|
||||||
private readonly channel: NotificationChannel = 'email';
|
private readonly channel: NotificationChannel = 'email';
|
||||||
private config: EmailAdapterConfig;
|
private config: EmailAdapterConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* Infrastructure Adapter: InAppNotificationAdapter
|
* Infrastructure Adapter: InAppNotificationAdapter (Stub)
|
||||||
*
|
*
|
||||||
* Handles in-app notifications (stored in database, shown in UI).
|
* Handles in-app notifications.
|
||||||
* This is the primary/default notification channel.
|
* Currently a stub - to be implemented when in-app notification system is needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Notification } from '../../domain/entities/Notification';
|
import type { Notification } from '@core/notifications/domain/entities/Notification';
|
||||||
import type {
|
import type {
|
||||||
INotificationGateway,
|
NotificationGateway,
|
||||||
NotificationDeliveryResult
|
NotificationDeliveryResult
|
||||||
} from '../../application/ports/INotificationGateway';
|
} from '@core/notifications/application/ports/NotificationGateway';
|
||||||
import type { NotificationChannel } from '../../domain/types/NotificationTypes';
|
import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
|
||||||
|
|
||||||
export class InAppNotificationAdapter implements INotificationGateway {
|
export class InAppNotificationAdapter implements NotificationGateway {
|
||||||
private readonly channel: NotificationChannel = 'in_app';
|
private readonly channel: NotificationChannel = 'in_app';
|
||||||
|
|
||||||
/**
|
constructor() {
|
||||||
* For in_app, sending is essentially a no-op since the notification
|
// In-app notifications don't need external configuration
|
||||||
* is already persisted by the use case. This just confirms delivery.
|
}
|
||||||
*/
|
|
||||||
async send(notification: Notification): Promise<NotificationDeliveryResult> {
|
async send(notification: Notification): Promise<NotificationDeliveryResult> {
|
||||||
// In-app notifications are stored directly in the repository
|
// In-app notifications are stored in the database, so this is a stub
|
||||||
// This adapter just confirms the "delivery" was successful
|
// that simulates successful delivery
|
||||||
|
console.log(`[InApp Stub] Notification stored in database:`, {
|
||||||
|
id: notification.id,
|
||||||
|
recipientId: notification.recipientId,
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
channel: this.channel,
|
channel: this.channel,
|
||||||
@@ -35,7 +42,8 @@ export class InAppNotificationAdapter implements INotificationGateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isConfigured(): boolean {
|
isConfigured(): boolean {
|
||||||
return true; // Always configured
|
// In-app notifications are always configured
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannel(): NotificationChannel {
|
getChannel(): NotificationChannel {
|
||||||
|
|||||||
@@ -4,31 +4,31 @@
|
|||||||
* Manages notification gateways and routes notifications to appropriate channels.
|
* Manages notification gateways and routes notifications to appropriate channels.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Notification } from '../../domain/entities/Notification';
|
import type { Notification } from '@core/notifications/domain/entities/Notification';
|
||||||
import type { NotificationChannel } from '../../domain/types/NotificationTypes';
|
import type { NotificationChannel } from '@core/notifications/domain/types/NotificationTypes';
|
||||||
import type {
|
import type {
|
||||||
INotificationGateway,
|
NotificationGateway,
|
||||||
INotificationGatewayRegistry,
|
NotificationGatewayRegistry as INotificationGatewayRegistry,
|
||||||
NotificationDeliveryResult
|
NotificationDeliveryResult
|
||||||
} from '../../application/ports/INotificationGateway';
|
} from '@core/notifications/application/ports/NotificationGateway';
|
||||||
|
|
||||||
export class NotificationGatewayRegistry implements INotificationGatewayRegistry {
|
export class NotificationGatewayRegistry implements INotificationGatewayRegistry {
|
||||||
private gateways: Map<NotificationChannel, INotificationGateway> = new Map();
|
private gateways: Map<NotificationChannel, NotificationGateway> = new Map();
|
||||||
|
|
||||||
constructor(initialGateways: INotificationGateway[] = []) {
|
constructor(initialGateways: NotificationGateway[] = []) {
|
||||||
initialGateways.forEach(gateway => this.register(gateway));
|
initialGateways.forEach(gateway => this.register(gateway));
|
||||||
}
|
}
|
||||||
|
|
||||||
register(gateway: INotificationGateway): void {
|
register(gateway: NotificationGateway): void {
|
||||||
const channel = gateway.getChannel();
|
const channel = gateway.getChannel();
|
||||||
this.gateways.set(channel, gateway);
|
this.gateways.set(channel, gateway);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGateway(channel: NotificationChannel): INotificationGateway | null {
|
getGateway(channel: NotificationChannel): NotificationGateway | null {
|
||||||
return this.gateways.get(channel) || null;
|
return this.gateways.get(channel) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllGateways(): INotificationGateway[] {
|
getAllGateways(): NotificationGateway[] {
|
||||||
return Array.from(this.gateways.values());
|
return Array.from(this.gateways.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Provides an in-memory storage implementation for notifications.
|
* Provides an in-memory storage implementation for notifications.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Notification } from '../../domain/entities/Notification';
|
import { Notification } from '@core/notifications/domain/entities/Notification';
|
||||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
import type { INotificationRepository } from '@core/notifications/domain/repositories/INotificationRepository';
|
||||||
import type { NotificationType } from '../../domain/types/NotificationTypes';
|
import type { NotificationType } from '@core/notifications/domain/types/NotificationTypes';
|
||||||
import type { Logger } from '@core/shared/application';
|
import type { Logger } from '@core/shared/application';
|
||||||
|
|
||||||
export class InMemoryNotificationRepository implements INotificationRepository {
|
export class InMemoryNotificationRepository implements INotificationRepository {
|
||||||
@@ -75,7 +75,7 @@ export class InMemoryNotificationRepository implements INotificationRepository {
|
|||||||
this.logger.info(`Found ${notifications.length} notifications for recipient ID: ${recipientId}, type: ${type}.`);
|
this.logger.info(`Found ${notifications.length} notifications for recipient ID: ${recipientId}, type: ${type}.`);
|
||||||
return notifications;
|
return notifications;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error finding notifications for recipient ID ${recipientId}, type ${type}:`, error);
|
this.logger.error(`Error finding notifications for recipient ID ${recipientId}, type ${type}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,6 @@ export class InMemoryNotificationRepository implements INotificationRepository {
|
|||||||
async deleteAllByRecipientId(recipientId: string): Promise<void> {
|
async deleteAllByRecipientId(recipientId: string): Promise<void> {
|
||||||
this.logger.debug(`Deleting all notifications for recipient ID: ${recipientId}`);
|
this.logger.debug(`Deleting all notifications for recipient ID: ${recipientId}`);
|
||||||
try {
|
try {
|
||||||
const initialCount = this.notifications.size;
|
|
||||||
const toDelete = Array.from(this.notifications.values())
|
const toDelete = Array.from(this.notifications.values())
|
||||||
.filter(n => n.recipientId === recipientId)
|
.filter(n => n.recipientId === recipientId)
|
||||||
.map(n => n.id);
|
.map(n => n.id);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||||
import { LeagueMembership, JoinRequest } from '@core/racing/domain/entities/LeagueMembership';
|
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||||
|
import { JoinRequest } from '@core/racing/domain/entities/JoinRequest';
|
||||||
import { Logger } from '@core/shared/application';
|
import { Logger } from '@core/shared/application';
|
||||||
|
|
||||||
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
|
||||||
@@ -25,33 +26,33 @@ export class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepo
|
|||||||
async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
async findActiveByLeagueIdAndDriverId(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
|
||||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding active membership for league ${leagueId}, driver ${driverId}.`);
|
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding active membership for league ${leagueId}, driver ${driverId}.`);
|
||||||
const membership = await this.getMembership(leagueId, driverId);
|
const membership = await this.getMembership(leagueId, driverId);
|
||||||
return Promise.resolve(membership && membership.status === 'active' ? membership : null);
|
return Promise.resolve(membership && membership.status.toString() === 'active' ? membership : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByLeagueId(leagueId: string): Promise<LeagueMembership[]> {
|
async findAllByLeagueId(leagueId: string): Promise<LeagueMembership[]> {
|
||||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for league ${leagueId}.`);
|
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for league ${leagueId}.`);
|
||||||
const filteredMemberships = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId);
|
const filteredMemberships = Array.from(this.memberships.values()).filter(mem => mem.leagueId.toString() === leagueId);
|
||||||
this.logger.info(`Found ${filteredMemberships.length} memberships for league ${leagueId}.`);
|
this.logger.info(`Found ${filteredMemberships.length} memberships for league ${leagueId}.`);
|
||||||
return Promise.resolve(filteredMemberships);
|
return Promise.resolve(filteredMemberships);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAllByDriverId(driverId: string): Promise<LeagueMembership[]> {
|
async findAllByDriverId(driverId: string): Promise<LeagueMembership[]> {
|
||||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for driver ${driverId}.`);
|
this.logger.debug(`[InMemoryLeagueMembershipRepository] Finding all memberships for driver ${driverId}.`);
|
||||||
const memberships = Array.from(this.memberships.values()).filter(mem => mem.driverId === driverId);
|
const memberships = Array.from(this.memberships.values()).filter(mem => mem.driverId.toString() === driverId);
|
||||||
this.logger.info(`Found ${memberships.length} memberships for driver ${driverId}.`);
|
this.logger.info(`Found ${memberships.length} memberships for driver ${driverId}.`);
|
||||||
return Promise.resolve(memberships);
|
return Promise.resolve(memberships);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
|
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
|
||||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting active members for league ${leagueId}.`);
|
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting active members for league ${leagueId}.`);
|
||||||
const members = Array.from(this.memberships.values()).filter(mem => mem.leagueId === leagueId && mem.status === 'active');
|
const members = Array.from(this.memberships.values()).filter(mem => mem.leagueId.toString() === leagueId && mem.status.toString() === 'active');
|
||||||
this.logger.info(`Found ${members.length} active members for league ${leagueId}.`);
|
this.logger.info(`Found ${members.length} active members for league ${leagueId}.`);
|
||||||
return Promise.resolve(members);
|
return Promise.resolve(members);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
|
async getJoinRequests(leagueId: string): Promise<JoinRequest[]> {
|
||||||
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting join requests for league ${leagueId}.`);
|
this.logger.debug(`[InMemoryLeagueMembershipRepository] Getting join requests for league ${leagueId}.`);
|
||||||
const requests = Array.from(this.joinRequests.values()).filter(req => req.leagueId === leagueId);
|
const requests = Array.from(this.joinRequests.values()).filter(req => req.leagueId.toString() === leagueId);
|
||||||
this.logger.info(`Found ${requests.length} join requests for league ${leagueId}.`);
|
this.logger.info(`Found ${requests.length} join requests for league ${leagueId}.`);
|
||||||
return Promise.resolve(requests);
|
return Promise.resolve(requests);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ describe('InMemoryLeagueStandingsRepository', () => {
|
|||||||
repository = new InMemoryLeagueStandingsRepository(mockLogger);
|
repository = new InMemoryLeagueStandingsRepository(mockLogger);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createTestStanding = (id: string, leagueId: string, driverId: string, position: number, points: number): RawStanding => ({
|
const createTestStanding = (_id: string, _leagueId: string, driverId: string, position: number, points: number): RawStanding => ({
|
||||||
id,
|
|
||||||
leagueId,
|
|
||||||
driverId,
|
driverId,
|
||||||
position,
|
position,
|
||||||
points,
|
points,
|
||||||
wins: 0,
|
wins: 0,
|
||||||
racesCompleted: 0,
|
races: 0,
|
||||||
|
poles: 0,
|
||||||
|
podiums: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Provides an in-memory storage implementation for penalties.
|
* Provides an in-memory storage implementation for penalties.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Penalty } from '@core/racing/domain/entities/Penalty';
|
import type { Penalty } from '@core/racing/domain/entities/penalty/Penalty';
|
||||||
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
|
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
|
||||||
import type { Logger } from '@core/shared/application';
|
import type { Logger } from '@core/shared/application';
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ export class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepo
|
|||||||
return Promise.resolve(driverIds);
|
return Promise.resolve(driverIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByRaceId(raceId: string): Promise<RaceRegistration[]> {
|
||||||
|
this.logger.debug(`[InMemoryRaceRegistrationRepository] Finding all registrations for race ${raceId}.`);
|
||||||
|
const registrations = Array.from(this.registrations.values()).filter(
|
||||||
|
reg => reg.raceId.toString() === raceId
|
||||||
|
);
|
||||||
|
this.logger.info(`Found ${registrations.length} registrations for race ${raceId}.`);
|
||||||
|
return Promise.resolve(registrations);
|
||||||
|
}
|
||||||
|
|
||||||
async getRegistrationCount(raceId: string): Promise<number> {
|
async getRegistrationCount(raceId: string): Promise<number> {
|
||||||
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`);
|
this.logger.debug(`[InMemoryRaceRegistrationRepository] Getting registration count for race ${raceId}.`);
|
||||||
const count = Array.from(this.registrations.values()).filter(reg => reg.raceId.toString() === raceId).length;
|
const count = Array.from(this.registrations.values()).filter(reg => reg.raceId.toString() === raceId).length;
|
||||||
|
|||||||
@@ -110,8 +110,15 @@ describe('InMemorySeasonRepository', () => {
|
|||||||
await repository.create(season);
|
await repository.create(season);
|
||||||
|
|
||||||
const updatedSeason = Season.create({
|
const updatedSeason = Season.create({
|
||||||
...season,
|
id: season.id,
|
||||||
|
leagueId: season.leagueId.toString(),
|
||||||
|
gameId: season.gameId.toString(),
|
||||||
name: 'Updated Season',
|
name: 'Updated Season',
|
||||||
|
status: season.status.toString() as 'planned' | 'active' | 'completed',
|
||||||
|
year: 2025,
|
||||||
|
order: 1,
|
||||||
|
startDate: new Date('2025-01-01'),
|
||||||
|
endDate: new Date('2025-12-31'),
|
||||||
});
|
});
|
||||||
await repository.update(updatedSeason);
|
await repository.update(updatedSeason);
|
||||||
const found = await repository.findById('1');
|
const found = await repository.findById('1');
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe('InMemorySponsorshipRequestRepository', () => {
|
|||||||
|
|
||||||
it('should seed initial requests', () => {
|
it('should seed initial requests', () => {
|
||||||
const request = createTestRequest('req-1');
|
const request = createTestRequest('req-1');
|
||||||
const repoWithSeed = new InMemorySponsorshipRequestRepository(mockLogger, [request]);
|
new InMemorySponsorshipRequestRepository(mockLogger, [request]);
|
||||||
expect(mockLogger.debug).toHaveBeenCalledWith('Seeded sponsorship request: req-1.');
|
expect(mockLogger.debug).toHaveBeenCalledWith('Seeded sponsorship request: req-1.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,11 +46,9 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
const standings = Array.from(this.standings.values())
|
const standings = Array.from(this.standings.values())
|
||||||
.filter(standing => standing.leagueId.toString() === leagueId)
|
.filter(standing => standing.leagueId.toString() === leagueId)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Sort by position (lower is better)
|
|
||||||
if (a.position.toNumber() !== b.position.toNumber()) {
|
if (a.position.toNumber() !== b.position.toNumber()) {
|
||||||
return a.position.toNumber() - b.position.toNumber();
|
return a.position.toNumber() - b.position.toNumber();
|
||||||
}
|
}
|
||||||
// If positions are equal, sort by points (higher is better)
|
|
||||||
return b.points.toNumber() - a.points.toNumber();
|
return b.points.toNumber() - a.points.toNumber();
|
||||||
});
|
});
|
||||||
this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`);
|
this.logger.info(`Found ${standings.length} standings for league id: ${leagueId}.`);
|
||||||
@@ -73,7 +71,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
}
|
}
|
||||||
return standing;
|
return standing;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error);
|
this.logger.error(`Error finding standing for driver ${driverId}, league ${leagueId}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +91,9 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
async save(standing: Standing): Promise<Standing> {
|
async save(standing: Standing): Promise<Standing> {
|
||||||
this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`);
|
this.logger.debug(`Saving standing for league: ${standing.leagueId}, driver: ${standing.driverId}`);
|
||||||
try {
|
try {
|
||||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
|
||||||
if (this.standings.has(key)) {
|
if (this.standings.has(key)) {
|
||||||
this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
|
this.logger.debug(`Updating existing standing for league: ${standing.leagueId}, driver ${standing.driverId}.`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
|
this.logger.debug(`Creating new standing for league: ${standing.leagueId}, driver: ${standing.driverId}.`);
|
||||||
}
|
}
|
||||||
@@ -103,7 +101,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`);
|
this.logger.info(`Standing for league ${standing.leagueId}, driver ${standing.driverId} saved successfully.`);
|
||||||
return standing;
|
return standing;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error);
|
this.logger.error(`Error saving standing for league ${standing.leagueId}, driver ${standing.driverId}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +110,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
this.logger.debug(`Saving ${standings.length} standings.`);
|
this.logger.debug(`Saving ${standings.length} standings.`);
|
||||||
try {
|
try {
|
||||||
standings.forEach(standing => {
|
standings.forEach(standing => {
|
||||||
const key = this.getKey(standing.leagueId, standing.driverId);
|
const key = this.getKey(standing.leagueId.toString(), standing.driverId.toString());
|
||||||
this.standings.set(key, standing);
|
this.standings.set(key, standing);
|
||||||
});
|
});
|
||||||
this.logger.info(`${standings.length} standings saved successfully.`);
|
this.logger.info(`${standings.length} standings saved successfully.`);
|
||||||
@@ -133,7 +131,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`);
|
this.logger.warn(`Standing for league ${leagueId}, driver ${driverId} not found for deletion.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error);
|
this.logger.error(`Error deleting standing for league ${leagueId}, driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +161,7 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`);
|
this.logger.debug(`Standing for league ${leagueId}, driver ${driverId} exists: ${exists}.`);
|
||||||
return exists;
|
return exists;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error);
|
this.logger.error(`Error checking existence of standing for league ${leagueId}, driver ${driverId}:`, error instanceof Error ? error : new Error(String(error)));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +174,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
throw new Error('Cannot recalculate standings: missing required repositories');
|
throw new Error('Cannot recalculate standings: missing required repositories');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get league to determine points system
|
|
||||||
const league = await this.leagueRepository.findById(leagueId);
|
const league = await this.leagueRepository.findById(leagueId);
|
||||||
if (!league) {
|
if (!league) {
|
||||||
this.logger.warn(`League with ID ${leagueId} not found during recalculation.`);
|
this.logger.warn(`League with ID ${leagueId} not found during recalculation.`);
|
||||||
@@ -184,7 +181,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
}
|
}
|
||||||
this.logger.debug(`League ${leagueId} found for recalculation.`);
|
this.logger.debug(`League ${leagueId} found for recalculation.`);
|
||||||
|
|
||||||
// Get points system
|
|
||||||
const resolvedPointsSystem =
|
const resolvedPointsSystem =
|
||||||
league.settings.customPoints ??
|
league.settings.customPoints ??
|
||||||
this.pointsSystems[league.settings.pointsSystem] ??
|
this.pointsSystems[league.settings.pointsSystem] ??
|
||||||
@@ -196,7 +192,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
}
|
}
|
||||||
this.logger.debug(`Resolved points system for league ${leagueId}.`);
|
this.logger.debug(`Resolved points system for league ${leagueId}.`);
|
||||||
|
|
||||||
// Get all completed races for the league
|
|
||||||
const races = await this.raceRepository.findCompletedByLeagueId(leagueId);
|
const races = await this.raceRepository.findCompletedByLeagueId(leagueId);
|
||||||
this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`);
|
this.logger.debug(`Found ${races.length} completed races for league ${leagueId}.`);
|
||||||
|
|
||||||
@@ -205,7 +200,6 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all results for these races
|
|
||||||
const allResults = await Promise.all(
|
const allResults = await Promise.all(
|
||||||
races.map(async race => {
|
races.map(async race => {
|
||||||
this.logger.debug(`Fetching results for race ${race.id}.`);
|
this.logger.debug(`Fetching results for race ${race.id}.`);
|
||||||
@@ -217,50 +211,44 @@ export class InMemoryStandingRepository implements IStandingRepository {
|
|||||||
const results = allResults.flat();
|
const results = allResults.flat();
|
||||||
this.logger.debug(`Collected ${results.length} results from all completed races.`);
|
this.logger.debug(`Collected ${results.length} results from all completed races.`);
|
||||||
|
|
||||||
// Calculate standings per driver
|
|
||||||
const standingsMap = new Map<string, Standing>();
|
const standingsMap = new Map<string, Standing>();
|
||||||
|
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
let standing = standingsMap.get(result.driverId);
|
const driverIdStr = result.driverId.toString();
|
||||||
|
let standing = standingsMap.get(driverIdStr);
|
||||||
|
|
||||||
if (!standing) {
|
if (!standing) {
|
||||||
standing = Standing.create({
|
standing = Standing.create({
|
||||||
leagueId,
|
leagueId,
|
||||||
driverId: result.driverId,
|
driverId: driverIdStr,
|
||||||
});
|
});
|
||||||
this.logger.debug(`Created new standing for driver ${result.driverId} in league ${leagueId}.`);
|
this.logger.debug(`Created new standing for driver ${driverIdStr} in league ${leagueId}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add points from this result
|
standing = standing.addRaceResult(result.position.toNumber(), resolvedPointsSystem);
|
||||||
standing = standing.addRaceResult(result.position, resolvedPointsSystem);
|
standingsMap.set(driverIdStr, standing);
|
||||||
standingsMap.set(result.driverId, standing);
|
this.logger.debug(`Driver ${driverIdStr} in league ${leagueId} accumulated ${standing.points} points.`);
|
||||||
this.logger.debug(`Driver ${result.driverId} in league ${leagueId} accumulated ${standing.points} points.`);
|
|
||||||
});
|
});
|
||||||
this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`);
|
this.logger.debug(`Calculated initial standings for ${standingsMap.size} drivers.`);
|
||||||
|
|
||||||
// Sort by points and assign positions
|
|
||||||
const sortedStandings = Array.from(standingsMap.values())
|
const sortedStandings = Array.from(standingsMap.values())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (b.points !== a.points) {
|
if (b.points.toNumber() !== a.points.toNumber()) {
|
||||||
return b.points - a.points;
|
return b.points.toNumber() - a.points.toNumber();
|
||||||
}
|
}
|
||||||
// Tie-breaker: most wins
|
|
||||||
if (b.wins !== a.wins) {
|
if (b.wins !== a.wins) {
|
||||||
return b.wins - a.wins;
|
return b.wins - a.wins;
|
||||||
}
|
}
|
||||||
// Tie-breaker: most races completed
|
|
||||||
return b.racesCompleted - a.racesCompleted;
|
return b.racesCompleted - a.racesCompleted;
|
||||||
});
|
});
|
||||||
this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`);
|
this.logger.debug(`Sorted standings for ${sortedStandings.length} drivers.`);
|
||||||
|
|
||||||
// Assign positions
|
|
||||||
const updatedStandings = sortedStandings.map((standing, index) => {
|
const updatedStandings = sortedStandings.map((standing, index) => {
|
||||||
const newStanding = standing.updatePosition(index + 1);
|
const newStanding = standing.updatePosition(index + 1);
|
||||||
this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`);
|
this.logger.debug(`Assigned position ${newStanding.position} to driver ${newStanding.driverId}.`);
|
||||||
return newStanding;
|
return newStanding;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save all standings
|
|
||||||
await this.saveMany(updatedStandings);
|
await this.saveMany(updatedStandings);
|
||||||
this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`);
|
this.logger.info(`Successfully recalculated and saved standings for league ${leagueId}.`);
|
||||||
|
|
||||||
|
|||||||
1
core/league/application/index.ts
Normal file
1
core/league/application/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './ports/ILeagueStandingsRepository';
|
||||||
13
core/league/application/ports/ILeagueStandingsRepository.ts
Normal file
13
core/league/application/ports/ILeagueStandingsRepository.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface RawStanding {
|
||||||
|
driverId: string;
|
||||||
|
position: number;
|
||||||
|
points: number;
|
||||||
|
races: number;
|
||||||
|
wins: number;
|
||||||
|
poles: number;
|
||||||
|
podiums: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILeagueStandingsRepository {
|
||||||
|
getLeagueStandings(leagueId: string): Promise<RawStanding[]>;
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ export * from './use-cases/AcceptSponsorshipRequestUseCase';
|
|||||||
export * from './use-cases/RejectSponsorshipRequestUseCase';
|
export * from './use-cases/RejectSponsorshipRequestUseCase';
|
||||||
export * from './use-cases/GetPendingSponsorshipRequestsUseCase';
|
export * from './use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||||
export * from './use-cases/GetEntitySponsorshipPricingUseCase';
|
export * from './use-cases/GetEntitySponsorshipPricingUseCase';
|
||||||
|
export * from './ports/LeagueScoringPresetProvider';
|
||||||
|
|
||||||
// Re-export domain types for legacy callers (type-only)
|
// Re-export domain types for legacy callers (type-only)
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
17
core/racing/application/ports/LeagueScoringPresetProvider.ts
Normal file
17
core/racing/application/ports/LeagueScoringPresetProvider.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||||
|
|
||||||
|
export interface LeagueScoringPresetDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
primaryChampionshipType: 'driver' | 'team' | 'nations' | 'trophy';
|
||||||
|
sessionSummary: string;
|
||||||
|
bonusSummary: string;
|
||||||
|
dropPolicySummary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueScoringPresetProvider {
|
||||||
|
listPresets(): LeagueScoringPresetDTO[];
|
||||||
|
getPresetById(id: string): LeagueScoringPresetDTO | undefined;
|
||||||
|
createScoringConfigFromPreset(presetId: string, seasonId: string): LeagueScoringConfig;
|
||||||
|
}
|
||||||
@@ -2,4 +2,6 @@ export * from './UseCase';
|
|||||||
export * from './AsyncUseCase';
|
export * from './AsyncUseCase';
|
||||||
export * from './Service';
|
export * from './Service';
|
||||||
export * from './Logger';
|
export * from './Logger';
|
||||||
export * from './UseCaseOutputPort';
|
export * from './UseCaseOutputPort';
|
||||||
|
export * from './ErrorReporter';
|
||||||
|
export * from './Result';
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './DomainError';
|
export * from './DomainError';
|
||||||
export * from './ApplicationError';
|
export * from './ApplicationError';
|
||||||
|
export * from './ApplicationErrorCode';
|
||||||
Reference in New Issue
Block a user