This commit is contained in:
2025-12-11 13:50:38 +01:00
parent e4c1be628d
commit c7e5de40d6
212 changed files with 2965 additions and 763 deletions

View File

@@ -1,5 +1,5 @@
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
import type { ParticipantRef } from '../value-objects/ParticipantRef';
import type { ChampionshipConfig } from '../types/ChampionshipConfig';
import type { ParticipantRef } from '../types/ParticipantRef';
import { ChampionshipStanding } from '../entities/ChampionshipStanding';
import type { ParticipantEventPoints } from './EventScoringService';
import { DropScoreApplier, type EventPointsEntry } from './DropScoreApplier';

View File

@@ -1,4 +1,5 @@
import type { DropScorePolicy } from '../value-objects/DropScorePolicy';
import type { DropScorePolicy } from '../types/DropScorePolicy';
import type { IDomainCalculationService } from '@gridpilot/shared/domain';
export interface EventPointsEntry {
eventId: string;
@@ -11,7 +12,16 @@ export interface DropScoreResult {
totalPoints: number;
}
export class DropScoreApplier {
export interface DropScoreInput {
policy: DropScorePolicy;
events: EventPointsEntry[];
}
export class DropScoreApplier implements IDomainCalculationService<DropScoreInput, DropScoreResult> {
calculate(input: DropScoreInput): DropScoreResult {
return this.apply(input.policy, input.events);
}
apply(policy: DropScorePolicy, events: EventPointsEntry[]): DropScoreResult {
if (policy.strategy === 'none' || events.length === 0) {
const totalPoints = events.reduce((sum, e) => sum + e.points, 0);

View File

@@ -1,12 +1,13 @@
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
import type { SessionType } from '../value-objects/SessionType';
import type { ParticipantRef } from '../value-objects/ParticipantRef';
import type { ChampionshipConfig } from '../types/ChampionshipConfig';
import type { SessionType } from '../types/SessionType';
import type { ParticipantRef } from '../types/ParticipantRef';
import type { Result } from '../entities/Result';
import type { Penalty } from '../entities/Penalty';
import type { BonusRule } from '../value-objects/BonusRule';
import type { ChampionshipType } from '../value-objects/ChampionshipType';
import type { BonusRule } from '../types/BonusRule';
import type { ChampionshipType } from '../types/ChampionshipType';
import type { PointsTable } from '../value-objects/PointsTable';
import type { IDomainCalculationService } from '@gridpilot/shared/domain';
export interface ParticipantEventPoints {
participant: ParticipantRef;
@@ -16,6 +17,14 @@ export interface ParticipantEventPoints {
totalPoints: number;
}
export interface EventScoringInput {
seasonId: string;
championship: ChampionshipConfig;
sessionType: SessionType;
results: Result[];
penalties: Penalty[];
}
function createDriverParticipant(driverId: string): ParticipantRef {
return {
type: 'driver' as ChampionshipType,
@@ -23,14 +32,14 @@ function createDriverParticipant(driverId: string): ParticipantRef {
};
}
export class EventScoringService {
scoreSession(params: {
seasonId: string;
championship: ChampionshipConfig;
sessionType: SessionType;
results: Result[];
penalties: Penalty[];
}): ParticipantEventPoints[] {
export class EventScoringService
implements IDomainCalculationService<EventScoringInput, ParticipantEventPoints[]>
{
calculate(input: EventScoringInput): ParticipantEventPoints[] {
return this.scoreSession(input);
}
scoreSession(params: EventScoringInput): ParticipantEventPoints[] {
const { championship, sessionType, results } = params;
const pointsTable = this.getPointsTableForSession(championship, sessionType);

View File

@@ -1,12 +1,7 @@
/**
* Domain Service Port: IImageService
* Backwards-compat alias for legacy imports.
*
* Thin abstraction used by racing application use cases to obtain image URLs
* for drivers, teams and leagues without depending directly on UI/media layers.
* New code should depend on IImageServicePort from
* packages/racing/application/ports/IImageServicePort.
*/
export interface IImageService {
getDriverAvatar(driverId: string): string;
getTeamLogo(teamId: string): string;
getLeagueCover(leagueId: string): string;
getLeagueLogo(leagueId: string): string;
}
export type { IImageServicePort as IImageService } from '../../application/ports/IImageServicePort';

View File

@@ -1,8 +1,9 @@
import { describe, it, expect } from 'vitest';
import { calculateRaceDates, getNextWeekday, type ScheduleConfig } from './ScheduleCalculator';
import type { Weekday } from '../value-objects/Weekday';
describe('ScheduleCalculator', () => {
/**
* Tests for ScheduleCalculator have been moved to:
* tests/unit/domain/services/ScheduleCalculator.test.ts
*
* This file is kept as a stub to avoid placing tests under domain/services.
*/
describe('calculateRaceDates', () => {
describe('with empty or invalid input', () => {
it('should return empty array when weekdays is empty', () => {

View File

@@ -1,4 +1,4 @@
import type { Weekday } from '../value-objects/Weekday';
import type { Weekday } from '../types/Weekday';
export type RecurrenceStrategy = 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';

View File

@@ -3,8 +3,9 @@ import { ScheduledRaceSlot } from '../value-objects/ScheduledRaceSlot';
import type { RecurrenceStrategy } from '../value-objects/RecurrenceStrategy';
import { RacingDomainValidationError } from '../errors/RacingDomainError';
import { RaceTimeOfDay } from '../value-objects/RaceTimeOfDay';
import type { Weekday } from '../value-objects/Weekday';
import { weekdayToIndex } from '../value-objects/Weekday';
import type { Weekday } from '../types/Weekday';
import { weekdayToIndex } from '../types/Weekday';
import type { IDomainCalculationService } from '@gridpilot/shared/domain';
function cloneDate(date: Date): Date {
return new Date(date.getTime());
@@ -173,4 +174,12 @@ export class SeasonScheduleGenerator {
return generateWeeklyOrEveryNWeeksSlots(schedule, maxRounds);
}
}
export class SeasonScheduleGeneratorService
implements IDomainCalculationService<SeasonSchedule, ScheduledRaceSlot[]>
{
calculate(schedule: SeasonSchedule): ScheduledRaceSlot[] {
return SeasonScheduleGenerator.generateSlots(schedule);
}
}

View File

@@ -1,10 +1,13 @@
import type { IDomainService } from '@gridpilot/shared/domain';
export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
/**
* Domain service for determining skill level based on rating.
* This encapsulates the business rule for skill tier classification.
*/
export class SkillLevelService {
export class SkillLevelService implements IDomainService {
readonly serviceName = 'SkillLevelService';
/**
* Map driver rating to skill level band.
* Business rule: iRating thresholds determine skill tiers.

View File

@@ -1,6 +1,8 @@
import type { IDomainCalculationService } from '@gridpilot/shared/domain';
/**
* Domain Service: StrengthOfFieldCalculator
*
*
* Calculates the Strength of Field (SOF) for a race based on participant ratings.
* SOF is the average rating of all participants in a race.
*/
@@ -21,7 +23,9 @@ export interface StrengthOfFieldCalculator {
/**
* Default implementation using simple average
*/
export class AverageStrengthOfFieldCalculator implements StrengthOfFieldCalculator {
export class AverageStrengthOfFieldCalculator
implements StrengthOfFieldCalculator, IDomainCalculationService<DriverRating[], number | null>
{
calculate(driverRatings: DriverRating[]): number | null {
if (driverRatings.length === 0) {
return null;