wip
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { ParticipantRef } from '@gridpilot/racing/domain/value-objects/ParticipantRef';
|
||||
import type { ParticipantRef } from '@gridpilot/racing/domain/types/ParticipantRef';
|
||||
|
||||
export interface ChampionshipStandingsRowDTO {
|
||||
participant: ParticipantRef;
|
||||
|
||||
@@ -53,9 +53,9 @@ export interface LeagueTimingsFormDTO {
|
||||
timezoneId?: string; // IANA ID, e.g. "Europe/Berlin", or "track" for track local time
|
||||
recurrenceStrategy?: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
|
||||
intervalWeeks?: number;
|
||||
weekdays?: import('../../domain/value-objects/Weekday').Weekday[];
|
||||
weekdays?: import('../../domain/types/Weekday').Weekday[];
|
||||
monthlyOrdinal?: 1 | 2 | 3 | 4;
|
||||
monthlyWeekday?: import('../../domain/value-objects/Weekday').Weekday;
|
||||
monthlyWeekday?: import('../../domain/types/Weekday').Weekday;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { LeagueTimingsFormDTO } from './LeagueConfigFormDTO';
|
||||
import type { Weekday } from '../../domain/value-objects/Weekday';
|
||||
import type { Weekday } from '../../domain/types/Weekday';
|
||||
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
|
||||
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
|
||||
import type { RecurrenceStrategy } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
import type { RecurrenceStrategy } from '../../domain/types/RecurrenceStrategy';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/types/RecurrenceStrategy';
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { BusinessRuleViolationError } from '../errors/RacingApplicationError';
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { Team, TeamJoinRequest, TeamMembership } from '../../domain/entities/Team';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type {
|
||||
TeamJoinRequest,
|
||||
TeamMembership,
|
||||
} from '../../domain/types/TeamMembership';
|
||||
|
||||
export interface JoinTeamCommandDTO {
|
||||
teamId: string;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
export abstract class RacingApplicationError extends Error {
|
||||
import type { IApplicationError, CommonApplicationErrorKind } from '@gridpilot/shared/errors';
|
||||
|
||||
export abstract class RacingApplicationError
|
||||
extends Error
|
||||
implements IApplicationError<CommonApplicationErrorKind | string, unknown>
|
||||
{
|
||||
readonly type = 'application' as const;
|
||||
readonly context = 'racing-application';
|
||||
abstract readonly kind: CommonApplicationErrorKind | string;
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
@@ -22,11 +29,16 @@ export interface EntityNotFoundDetails {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class EntityNotFoundError extends RacingApplicationError {
|
||||
export class EntityNotFoundError
|
||||
extends RacingApplicationError
|
||||
implements IApplicationError<'not_found', EntityNotFoundDetails>
|
||||
{
|
||||
readonly kind = 'not_found' as const;
|
||||
readonly details: EntityNotFoundDetails;
|
||||
|
||||
constructor(public readonly details: EntityNotFoundDetails) {
|
||||
constructor(details: EntityNotFoundDetails) {
|
||||
super(`${details.entity} not found for id: ${details.id}`);
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,15 +51,25 @@ export type PermissionDeniedReason =
|
||||
| 'TEAM_OWNER_CANNOT_LEAVE'
|
||||
| 'UNAUTHORIZED';
|
||||
|
||||
export class PermissionDeniedError extends RacingApplicationError {
|
||||
export class PermissionDeniedError
|
||||
extends RacingApplicationError
|
||||
implements IApplicationError<'forbidden', PermissionDeniedReason>
|
||||
{
|
||||
readonly kind = 'forbidden' as const;
|
||||
|
||||
constructor(public readonly reason: PermissionDeniedReason, message?: string) {
|
||||
super(message ?? `Permission denied: ${reason}`);
|
||||
}
|
||||
|
||||
get details(): PermissionDeniedReason {
|
||||
return this.reason;
|
||||
}
|
||||
}
|
||||
|
||||
export class BusinessRuleViolationError extends RacingApplicationError {
|
||||
export class BusinessRuleViolationError
|
||||
extends RacingApplicationError
|
||||
implements IApplicationError<'conflict', undefined>
|
||||
{
|
||||
readonly kind = 'conflict' as const;
|
||||
|
||||
constructor(message: string) {
|
||||
|
||||
@@ -54,13 +54,13 @@ export * from './ports/DriverRatingProvider';
|
||||
|
||||
export type { RaceRegistration } from '../domain/entities/RaceRegistration';
|
||||
|
||||
export type { Team } from '../domain/entities/Team';
|
||||
export type {
|
||||
Team,
|
||||
TeamMembership,
|
||||
TeamJoinRequest,
|
||||
TeamRole,
|
||||
TeamMembershipStatus,
|
||||
} from '../domain/entities/Team';
|
||||
} from '../domain/types/TeamMembership';
|
||||
|
||||
export type { DriverDTO } from './dto/DriverDTO';
|
||||
export type { LeagueDTO } from './dto/LeagueDTO';
|
||||
|
||||
12
packages/racing/application/ports/IImageServicePort.ts
Normal file
12
packages/racing/application/ports/IImageServicePort.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Application Port: IImageServicePort
|
||||
*
|
||||
* Abstraction used by racing application use cases to obtain image URLs
|
||||
* for drivers, teams and leagues without depending on UI/media layers.
|
||||
*/
|
||||
export interface IImageServicePort {
|
||||
getDriverAvatar(driverId: string): string;
|
||||
getTeamLogo(teamId: string): string;
|
||||
getLeagueCover(leagueId: string): string;
|
||||
getLeagueLogo(leagueId: string): string;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Team, TeamMembership } from '../../domain/entities/Team';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
|
||||
export interface DriverTeamViewModel {
|
||||
team: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Team, TeamMembership } from '../../domain/entities/Team';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
|
||||
export interface TeamDetailsViewModel {
|
||||
team: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TeamJoinRequest } from '../../domain/entities/Team';
|
||||
import type { TeamJoinRequest } from '../../domain/types/TeamMembership';
|
||||
|
||||
export interface TeamJoinRequestViewModel {
|
||||
requestId: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TeamMembership } from '../../domain/entities/Team';
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
|
||||
export interface TeamMemberViewModel {
|
||||
driverId: string;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export interface AcceptSponsorshipRequestDTO {
|
||||
requestId: string;
|
||||
@@ -23,7 +24,8 @@ export interface AcceptSponsorshipRequestResultDTO {
|
||||
netAmount: number;
|
||||
}
|
||||
|
||||
export class AcceptSponsorshipRequestUseCase {
|
||||
export class AcceptSponsorshipRequestUseCase
|
||||
implements AsyncUseCase<AcceptSponsorshipRequestDTO, AcceptSponsorshipRequestResultDTO> {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import { Money, type Currency } from '../../domain/value-objects/Money';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import {
|
||||
EntityNotFoundError,
|
||||
BusinessRuleViolationError,
|
||||
@@ -31,8 +32,10 @@ export interface ApplyForSponsorshipResultDTO {
|
||||
status: 'pending';
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class ApplyForSponsorshipUseCase {
|
||||
|
||||
export class ApplyForSponsorshipUseCase
|
||||
implements AsyncUseCase<ApplyForSponsorshipDTO, ApplyForSponsorshipResultDTO>
|
||||
{
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { IProtestRepository } from '../../domain/repositories/IProtestRepos
|
||||
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';
|
||||
|
||||
export interface ApplyPenaltyCommand {
|
||||
raceId: string;
|
||||
@@ -23,7 +24,8 @@ export interface ApplyPenaltyCommand {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class ApplyPenaltyUseCase {
|
||||
export class ApplyPenaltyUseCase
|
||||
implements AsyncUseCase<ApplyPenaltyCommand, { penaltyId: string }> {
|
||||
constructor(
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
|
||||
@@ -4,10 +4,12 @@ import type {
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
TeamJoinRequest,
|
||||
} from '../../domain/entities/Team';
|
||||
} from '../../domain/types/TeamMembership';
|
||||
import type { ApproveTeamJoinRequestCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export class ApproveTeamJoinRequestUseCase {
|
||||
export class ApproveTeamJoinRequestUseCase
|
||||
implements AsyncUseCase<ApproveTeamJoinRequestCommandDTO, void> {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
) {}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case: CancelRaceUseCase
|
||||
@@ -13,7 +14,8 @@ export interface CancelRaceCommandDTO {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class CancelRaceUseCase {
|
||||
export class CancelRaceUseCase
|
||||
implements AsyncUseCase<CancelRaceCommandDTO, void> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
) {}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
LeagueScoringPresetProvider,
|
||||
LeagueScoringPresetDTO,
|
||||
@@ -47,7 +48,8 @@ export interface CreateLeagueWithSeasonAndScoringResultDTO {
|
||||
scoringPresetName?: string;
|
||||
}
|
||||
|
||||
export class CreateLeagueWithSeasonAndScoringUseCase {
|
||||
export class CreateLeagueWithSeasonAndScoringUseCase
|
||||
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, CreateLeagueWithSeasonAndScoringResultDTO> {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import { Team } from '../../domain/entities/Team';
|
||||
import type {
|
||||
Team,
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
} from '../../domain/entities/Team';
|
||||
} from '../../domain/types/TeamMembership';
|
||||
import type {
|
||||
CreateTeamCommandDTO,
|
||||
CreateTeamResultDTO,
|
||||
|
||||
@@ -5,12 +5,15 @@ import type { ILeagueScoringConfigRepository } from '../../domain/repositories/I
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { IAllLeaguesWithCapacityAndScoringPresenter, LeagueEnrichedData } from '../presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all leagues with capacity and scoring information.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityAndScoringUseCase {
|
||||
export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
implements AsyncUseCase<void, void>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IAllLeaguesWithCapacityPresenter } from '../presenters/IAllLeaguesWithCapacityPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all leagues with capacity information.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllLeaguesWithCapacityUseCase {
|
||||
export class GetAllLeaguesWithCapacityUseCase
|
||||
implements AsyncUseCase<void, void>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
|
||||
@@ -6,8 +6,10 @@ import type {
|
||||
AllRacesListItemViewModel,
|
||||
AllRacesFilterOptionsViewModel,
|
||||
} from '../presenters/IAllRacesPagePresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export class GetAllRacesPageDataUseCase {
|
||||
export class GetAllRacesPageDataUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IAllTeamsPresenter } from '../presenters/IAllTeamsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllTeamsUseCase {
|
||||
export class GetAllTeamsUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IImageService } from '../../domain/services/IImageService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IDashboardOverviewPresenter,
|
||||
DashboardOverviewViewModel,
|
||||
@@ -33,7 +34,8 @@ export interface GetDashboardOverviewParams {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class GetDashboardOverviewUseCase {
|
||||
export class GetDashboardOverviewUseCase
|
||||
implements AsyncUseCase<GetDashboardOverviewParams, void> {
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
@@ -44,7 +46,7 @@ export class GetDashboardOverviewUseCase {
|
||||
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
|
||||
private readonly feedRepository: IFeedRepository,
|
||||
private readonly socialRepository: ISocialGraphRepository,
|
||||
private readonly imageService: IImageService,
|
||||
private readonly imageService: IImageServicePort,
|
||||
private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null,
|
||||
public readonly presenter: IDashboardOverviewPresenter,
|
||||
) {}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverTeamPresenter } from '../presenters/IDriverTeamPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a driver's team.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetDriverTeamUseCase {
|
||||
export class GetDriverTeamUseCase
|
||||
implements AsyncUseCase<string, boolean> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IRankingService } from '../../domain/services/IRankingService';
|
||||
import type { IDriverStatsService } from '../../domain/services/IDriverStatsService';
|
||||
import type { IImageService } from '../../domain/services/IImageService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { IDriversLeaderboardPresenter } from '../presenters/IDriversLeaderboardPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving driver leaderboard data.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetDriversLeaderboardUseCase {
|
||||
export class GetDriversLeaderboardUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly rankingService: IRankingService,
|
||||
private readonly driverStatsService: IDriverStatsService,
|
||||
private readonly imageService: IImageService,
|
||||
private readonly imageService: IImageServicePort,
|
||||
public readonly presenter: IDriversLeaderboardPresenter,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISe
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export interface GetEntitySponsorshipPricingDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -38,7 +39,8 @@ export interface GetEntitySponsorshipPricingResultDTO {
|
||||
secondarySlot?: SponsorshipSlotDTO;
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingUseCase {
|
||||
export class GetEntitySponsorshipPricingUseCase
|
||||
implements AsyncUseCase<GetEntitySponsorshipPricingDTO, void> {
|
||||
constructor(
|
||||
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IResultRepository } from '../../domain/repositories/IResultReposit
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueDriverSeasonStatsPresenter } from '../presenters/ILeagueDriverSeasonStatsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export interface DriverRatingPort {
|
||||
getRating(driverId: string): { rating: number | null; ratingChange: number | null };
|
||||
@@ -16,7 +17,8 @@ export interface GetLeagueDriverSeasonStatsUseCaseParams {
|
||||
* Use Case for retrieving league driver season statistics.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueDriverSeasonStatsUseCase {
|
||||
export class GetLeagueDriverSeasonStatsUseCase
|
||||
implements AsyncUseCase<GetLeagueDriverSeasonStatsUseCaseParams, void> {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly resultRepository: IResultRepository,
|
||||
|
||||
@@ -3,13 +3,16 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILeagueFullConfigPresenter, LeagueFullConfigData } from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import { EntityNotFoundError } from '../errors/RacingApplicationError';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a league's full configuration.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueFullConfigUseCase {
|
||||
export class GetLeagueFullConfigUseCase
|
||||
implements AsyncUseCase<{ leagueId: string }, void>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
|
||||
@@ -4,12 +4,14 @@ import type { ILeagueScoringConfigRepository } from '../../domain/repositories/I
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { ILeagueScoringConfigPresenter, LeagueScoringConfigData } from '../presenters/ILeagueScoringConfigPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a league's scoring configuration for its active season.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueScoringConfigUseCase {
|
||||
export class GetLeagueScoringConfigUseCase
|
||||
implements AsyncUseCase<{ leagueId: string }, void> {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { ILeagueStandingsPresenter } from '../presenters/ILeagueStandingsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
export interface GetLeagueStandingsUseCaseParams {
|
||||
leagueId: string;
|
||||
@@ -9,7 +10,8 @@ export interface GetLeagueStandingsUseCaseParams {
|
||||
* Use Case for retrieving league standings.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueStandingsUseCase {
|
||||
export class GetLeagueStandingsUseCase
|
||||
implements AsyncUseCase<GetLeagueStandingsUseCaseParams, void> {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
public readonly presenter: ILeagueStandingsPresenter,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
||||
import type { ILeagueStatsPresenter } from '../presenters/ILeagueStatsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import {
|
||||
AverageStrengthOfFieldCalculator,
|
||||
type StrengthOfFieldCalculator,
|
||||
@@ -20,7 +21,8 @@ export interface GetLeagueStatsUseCaseParams {
|
||||
/**
|
||||
* Use Case for retrieving league statistics including average SOF across completed races.
|
||||
*/
|
||||
export class GetLeagueStatsUseCase {
|
||||
export class GetLeagueStatsUseCase
|
||||
implements AsyncUseCase<GetLeagueStatsUseCaseParams, void> {
|
||||
private readonly sofCalculator: StrengthOfFieldCalculator;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IImageService } from '../../domain/services/IImageService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
|
||||
import type {
|
||||
IProfileOverviewPresenter,
|
||||
@@ -44,7 +44,7 @@ export class GetProfileOverviewUseCase {
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly socialRepository: ISocialGraphRepository,
|
||||
private readonly imageService: IImageService,
|
||||
private readonly imageService: IImageServicePort,
|
||||
private readonly getDriverStats: (driverId: string) => ProfileDriverStatsAdapter | null,
|
||||
private readonly getAllDriverRankings: () => DriverRankingEntry[],
|
||||
public readonly presenter: IProfileOverviewPresenter,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { IRaceRegistrationRepository } from '../../domain/repositories/IRac
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
|
||||
import type { IImageService } from '../../domain/services/IImageService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type {
|
||||
IRaceDetailPresenter,
|
||||
RaceDetailViewModel,
|
||||
@@ -39,7 +39,7 @@ export class GetRaceDetailUseCase {
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly driverRatingProvider: DriverRatingProvider,
|
||||
private readonly imageService: IImageService,
|
||||
private readonly imageService: IImageServicePort,
|
||||
public readonly presenter: IRaceDetailPresenter,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IImageService } from '../../domain/services/IImageService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ITeamJoinRequestsPresenter } from '../presenters/ITeamJoinRequestsPresenter';
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ export class GetTeamJoinRequestsUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageService,
|
||||
private readonly imageService: IImageServicePort,
|
||||
public readonly presenter: ITeamJoinRequestsPresenter,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IImageService } from '../../domain/services/IImageService';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ITeamMembersPresenter } from '../presenters/ITeamMembersPresenter';
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ export class GetTeamMembersUseCase {
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageService,
|
||||
private readonly imageService: IImageServicePort,
|
||||
public readonly presenter: ITeamMembersPresenter,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import { Result } from '../../domain/entities/Result';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import {
|
||||
BusinessRuleViolationError,
|
||||
EntityNotFoundError,
|
||||
@@ -26,8 +27,10 @@ export interface ImportRaceResultsParams {
|
||||
raceId: string;
|
||||
results: ImportRaceResultDTO[];
|
||||
}
|
||||
|
||||
export class ImportRaceResultsUseCase {
|
||||
|
||||
export class ImportRaceResultsUseCase
|
||||
implements AsyncUseCase<ImportRaceResultsParams, void>
|
||||
{
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type {
|
||||
ILeagueMembershipRepository,
|
||||
} from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type {
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import {
|
||||
LeagueMembership,
|
||||
MembershipRole,
|
||||
MembershipStatus,
|
||||
type MembershipRole,
|
||||
type MembershipStatus,
|
||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
||||
import type { JoinLeagueCommandDTO } from '../dto/JoinLeagueCommandDTO';
|
||||
import { BusinessRuleViolationError } from '../errors/RacingApplicationError';
|
||||
|
||||
export class JoinLeagueUseCase {
|
||||
|
||||
export class JoinLeagueUseCase implements AsyncUseCase<JoinLeagueCommandDTO, LeagueMembership> {
|
||||
constructor(private readonly membershipRepository: ILeagueMembershipRepository) {}
|
||||
|
||||
/**
|
||||
@@ -27,13 +28,12 @@ export class JoinLeagueUseCase {
|
||||
throw new BusinessRuleViolationError('Already a member or have a pending request');
|
||||
}
|
||||
|
||||
const membership: LeagueMembership = {
|
||||
const membership = LeagueMembership.create({
|
||||
leagueId,
|
||||
driverId,
|
||||
role: 'member' as MembershipRole,
|
||||
status: 'active' as MembershipStatus,
|
||||
joinedAt: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
return this.membershipRepository.saveMembership(membership);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import type {
|
||||
TeamMembership,
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
} from '../../domain/entities/Team';
|
||||
} from '../../domain/types/TeamMembership';
|
||||
import type { JoinTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import {
|
||||
BusinessRuleViolationError,
|
||||
EntityNotFoundError,
|
||||
} from '../errors/RacingApplicationError';
|
||||
|
||||
export class JoinTeamUseCase {
|
||||
|
||||
export class JoinTeamUseCase implements AsyncUseCase<JoinTeamCommandDTO, void> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IR
|
||||
import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository';
|
||||
import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository';
|
||||
|
||||
import type { ChampionshipConfig } from '@gridpilot/racing/domain/value-objects/ChampionshipConfig';
|
||||
import type { SessionType } from '@gridpilot/racing/domain/value-objects/SessionType';
|
||||
import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig';
|
||||
import type { SessionType } from '@gridpilot/racing/domain/types/SessionType';
|
||||
import type { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding';
|
||||
import { EventScoringService } from '@gridpilot/racing/domain/services/EventScoringService';
|
||||
import { ChampionshipAggregator } from '@gridpilot/racing/domain/services/ChampionshipAggregator';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
|
||||
import type { RegisterForRaceCommandDTO } from '../dto/RegisterForRaceCommandDTO';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import {
|
||||
BusinessRuleViolationError,
|
||||
PermissionDeniedError,
|
||||
} from '../errors/RacingApplicationError';
|
||||
|
||||
export class RegisterForRaceUseCase {
|
||||
|
||||
export class RegisterForRaceUseCase
|
||||
implements AsyncUseCase<RegisterForRaceCommandDTO, void>
|
||||
{
|
||||
constructor(
|
||||
private readonly registrationRepository: IRaceRegistrationRepository,
|
||||
private readonly membershipRepository: ILeagueMembershipRepository,
|
||||
@@ -32,11 +35,10 @@ export class RegisterForRaceUseCase {
|
||||
throw new PermissionDeniedError('NOT_ACTIVE_MEMBER', 'Must be an active league member to register for races');
|
||||
}
|
||||
|
||||
const registration: RaceRegistration = {
|
||||
const registration = RaceRegistration.create({
|
||||
raceId,
|
||||
driverId,
|
||||
registeredAt: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
await this.registrationRepository.register(registration);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { isLeagueStewardOrHigherRole } from '../../domain/value-objects/LeagueRoles';
|
||||
import { isLeagueStewardOrHigherRole } from '../../domain/types/LeagueRoles';
|
||||
|
||||
export interface RequestProtestDefenseCommand {
|
||||
protestId: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import { Team } from '../../domain/entities/Team';
|
||||
import type { UpdateTeamCommandDTO } from '../dto/TeamCommandAndQueryDTO';
|
||||
|
||||
export class UpdateTeamUseCase {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* Domain Entity: Car
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
*
|
||||
*
|
||||
* Represents a racing car/vehicle in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export type CarClass = 'formula' | 'gt' | 'prototype' | 'touring' | 'sports' | 'oval' | 'dirt';
|
||||
export type CarLicense = 'R' | 'D' | 'C' | 'B' | 'A' | 'Pro';
|
||||
|
||||
export class Car {
|
||||
export class Car implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly shortName: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ParticipantRef } from '../value-objects/ParticipantRef';
|
||||
import type { ParticipantRef } from '../types/ParticipantRef';
|
||||
|
||||
export class ChampionshipStanding {
|
||||
readonly seasonId: string;
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* Represents a driver profile in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export class Driver {
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Driver implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly iracingId: string;
|
||||
readonly name: string;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Domain Entity: DriverLivery
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
*
|
||||
* Represents a driver's custom livery for a specific car.
|
||||
* Includes user-placed decals and league-specific overrides.
|
||||
@@ -31,7 +32,7 @@ export interface DriverLiveryProps {
|
||||
validatedAt?: Date;
|
||||
}
|
||||
|
||||
export class DriverLivery {
|
||||
export class DriverLivery implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly driverId: string;
|
||||
readonly gameId: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export class Game {
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Game implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Represents a league in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
/**
|
||||
* Stewarding decision mode for protests
|
||||
@@ -78,8 +79,8 @@ export interface LeagueSocialLinks {
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
}
|
||||
|
||||
export class League {
|
||||
|
||||
export class League implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
|
||||
@@ -1,19 +1,75 @@
|
||||
/**
|
||||
* Domain Entity: LeagueMembership and JoinRequest
|
||||
*
|
||||
* Extracted from racing-application memberships module so that
|
||||
* membership-related types live in the racing-domain package.
|
||||
* Represents a driver's membership in a league and join requests.
|
||||
*/
|
||||
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export type MembershipRole = 'owner' | 'admin' | 'steward' | 'member';
|
||||
export type MembershipStatus = 'active' | 'pending' | 'none';
|
||||
|
||||
export interface LeagueMembership {
|
||||
export interface LeagueMembershipProps {
|
||||
id?: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
role: MembershipRole;
|
||||
status: MembershipStatus;
|
||||
joinedAt: Date;
|
||||
status?: MembershipStatus;
|
||||
joinedAt?: Date;
|
||||
}
|
||||
|
||||
export class LeagueMembership implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly driverId: string;
|
||||
readonly role: MembershipRole;
|
||||
readonly status: MembershipStatus;
|
||||
readonly joinedAt: Date;
|
||||
|
||||
private constructor(props: Required<LeagueMembershipProps>) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
this.driverId = props.driverId;
|
||||
this.role = props.role;
|
||||
this.status = props.status;
|
||||
this.joinedAt = props.joinedAt;
|
||||
}
|
||||
|
||||
static create(props: LeagueMembershipProps): LeagueMembership {
|
||||
this.validate(props);
|
||||
|
||||
const id =
|
||||
props.id && props.id.trim().length > 0
|
||||
? props.id
|
||||
: `${props.leagueId}:${props.driverId}`;
|
||||
|
||||
const status = props.status ?? 'pending';
|
||||
const joinedAt = props.joinedAt ?? new Date();
|
||||
|
||||
return new LeagueMembership({
|
||||
id,
|
||||
leagueId: props.leagueId,
|
||||
driverId: props.driverId,
|
||||
role: props.role,
|
||||
status,
|
||||
joinedAt,
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: LeagueMembershipProps): void {
|
||||
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('League ID is required');
|
||||
}
|
||||
|
||||
if (!props.driverId || props.driverId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Driver ID is required');
|
||||
}
|
||||
|
||||
if (!props.role) {
|
||||
throw new RacingDomainValidationError('Membership role is required');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface JoinRequest {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChampionshipConfig } from '../value-objects/ChampionshipConfig';
|
||||
import type { ChampionshipConfig } from '../types/ChampionshipConfig';
|
||||
|
||||
export interface LeagueScoringConfig {
|
||||
id: string;
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Represents a league's financial wallet.
|
||||
* Aggregate root for managing league finances and transactions.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
import type { Transaction } from './Transaction';
|
||||
@@ -18,7 +19,7 @@ export interface LeagueWalletProps {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class LeagueWallet {
|
||||
export class LeagueWallet implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly balance: Money;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Domain Entity: LiveryTemplate
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
*
|
||||
* Represents an admin-defined livery template for a specific car.
|
||||
* Contains base image and sponsor decal placements.
|
||||
@@ -21,7 +22,7 @@ export interface LiveryTemplateProps {
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export class LiveryTemplate {
|
||||
export class LiveryTemplate implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly seasonId: string;
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Represents a penalty applied to a driver for an incident during a race.
|
||||
* Penalties can be applied as a result of an upheld protest or directly by stewards.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export type PenaltyType =
|
||||
| 'time_penalty' // Add time to race result (e.g., +5 seconds)
|
||||
@@ -45,7 +46,7 @@ export interface PenaltyProps {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class Penalty {
|
||||
export class Penalty implements IEntity<string> {
|
||||
private constructor(private readonly props: PenaltyProps) {}
|
||||
|
||||
static create(props: PenaltyProps): Penalty {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*
|
||||
* Represents a prize awarded to a driver for a specific position in a season.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
@@ -23,7 +24,7 @@ export interface PrizeProps {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class Prize {
|
||||
export class Prize implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly seasonId: string;
|
||||
readonly position: number;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Domain Entity: Protest
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
*
|
||||
* Represents a protest filed by a driver against another driver for an incident during a race.
|
||||
*
|
||||
@@ -66,7 +67,7 @@ export interface ProtestProps {
|
||||
defenseRequestedBy?: string;
|
||||
}
|
||||
|
||||
export class Protest {
|
||||
export class Protest implements IEntity<string> {
|
||||
private constructor(private readonly props: ProtestProps) {}
|
||||
|
||||
static create(props: ProtestProps): Protest {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
* Represents a race/session in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export type SessionType = 'practice' | 'qualifying' | 'race';
|
||||
export type RaceStatus = 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||
|
||||
export class Race {
|
||||
|
||||
export class Race implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly scheduledAt: Date;
|
||||
|
||||
@@ -1,12 +1,57 @@
|
||||
/**
|
||||
* Domain Entity: RaceRegistration
|
||||
*
|
||||
* Extracted from racing-application registrations module so that
|
||||
* registration-related types live in the racing-domain package.
|
||||
* Represents a registration of a driver for a specific race.
|
||||
*/
|
||||
|
||||
export interface RaceRegistration {
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export interface RaceRegistrationProps {
|
||||
id?: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
registeredAt: Date;
|
||||
registeredAt?: Date;
|
||||
}
|
||||
|
||||
export class RaceRegistration implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly raceId: string;
|
||||
readonly driverId: string;
|
||||
readonly registeredAt: Date;
|
||||
|
||||
private constructor(props: Required<RaceRegistrationProps>) {
|
||||
this.id = props.id;
|
||||
this.raceId = props.raceId;
|
||||
this.driverId = props.driverId;
|
||||
this.registeredAt = props.registeredAt;
|
||||
}
|
||||
|
||||
static create(props: RaceRegistrationProps): RaceRegistration {
|
||||
this.validate(props);
|
||||
|
||||
const id =
|
||||
props.id && props.id.trim().length > 0
|
||||
? props.id
|
||||
: `${props.raceId}:${props.driverId}`;
|
||||
|
||||
const registeredAt = props.registeredAt ?? new Date();
|
||||
|
||||
return new RaceRegistration({
|
||||
id,
|
||||
raceId: props.raceId,
|
||||
driverId: props.driverId,
|
||||
registeredAt,
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: RaceRegistrationProps): void {
|
||||
if (!props.raceId || props.raceId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Race ID is required');
|
||||
}
|
||||
|
||||
if (!props.driverId || props.driverId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Driver ID is required');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@
|
||||
* Represents a race result in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Result {
|
||||
export class Result implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly raceId: string;
|
||||
readonly driverId: string;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export type SeasonStatus = 'planned' | 'active' | 'completed';
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export class Season {
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Season implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly gameId: string;
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Represents a sponsorship relationship between a Sponsor and a Season.
|
||||
* Aggregate root for managing sponsorship slots and pricing.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
@@ -24,7 +25,7 @@ export interface SeasonSponsorshipProps {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class SeasonSponsorship {
|
||||
export class SeasonSponsorship implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly seasonId: string;
|
||||
readonly sponsorId: string;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Domain Entity: Sponsor
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
*
|
||||
* Represents a sponsor that can sponsor leagues/seasons.
|
||||
* Aggregate root for sponsor information.
|
||||
@@ -17,7 +18,7 @@ export interface SponsorProps {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class Sponsor {
|
||||
export class Sponsor implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly contactEmail: string;
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Represents a sponsorship application from a Sponsor to any sponsorable entity
|
||||
* (driver, team, race, or league/season). The entity owner must approve/reject.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
import type { SponsorshipTier } from './SeasonSponsorship';
|
||||
@@ -28,7 +29,7 @@ export interface SponsorshipRequestProps {
|
||||
rejectionReason?: string;
|
||||
}
|
||||
|
||||
export class SponsorshipRequest {
|
||||
export class SponsorshipRequest implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly sponsorId: string;
|
||||
readonly entityType: SponsorableEntityType;
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
/**
|
||||
* Domain Entity: Standing
|
||||
*
|
||||
*
|
||||
* Represents a championship standing in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export class Standing {
|
||||
export class Standing implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly driverId: string;
|
||||
readonly points: number;
|
||||
readonly wins: number;
|
||||
readonly position: number;
|
||||
readonly racesCompleted: number;
|
||||
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
points: number;
|
||||
@@ -23,6 +26,7 @@ export class Standing {
|
||||
position: number;
|
||||
racesCompleted: number;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
this.driverId = props.driverId;
|
||||
this.points = props.points;
|
||||
@@ -35,6 +39,7 @@ export class Standing {
|
||||
* Factory method to create a new Standing entity
|
||||
*/
|
||||
static create(props: {
|
||||
id?: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
points?: number;
|
||||
@@ -44,7 +49,12 @@ export class Standing {
|
||||
}): Standing {
|
||||
this.validate(props);
|
||||
|
||||
const id = props.id && props.id.trim().length > 0
|
||||
? props.id
|
||||
: `${props.leagueId}:${props.driverId}`;
|
||||
|
||||
return new Standing({
|
||||
id,
|
||||
leagueId: props.leagueId,
|
||||
driverId: props.driverId,
|
||||
points: props.points ?? 0,
|
||||
@@ -58,15 +68,16 @@ export class Standing {
|
||||
* Domain validation logic
|
||||
*/
|
||||
private static validate(props: {
|
||||
id?: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
}): void {
|
||||
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
||||
throw new RacingDomainError('League ID is required');
|
||||
throw new RacingDomainValidationError('League ID is required');
|
||||
}
|
||||
|
||||
|
||||
if (!props.driverId || props.driverId.trim().length === 0) {
|
||||
throw new RacingDomainError('Driver ID is required');
|
||||
throw new RacingDomainValidationError('Driver ID is required');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +89,7 @@ export class Standing {
|
||||
const isWin = position === 1;
|
||||
|
||||
return new Standing({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
driverId: this.driverId,
|
||||
points: this.points + racePoints,
|
||||
|
||||
@@ -1,35 +1,128 @@
|
||||
/**
|
||||
* Domain Entities: Team, TeamMembership, TeamJoinRequest
|
||||
* Domain Entity: Team
|
||||
*
|
||||
* Extracted from racing-application teams module so that
|
||||
* team-related types live in the racing-domain package.
|
||||
* Represents a racing team in the GridPilot platform.
|
||||
* Implements the shared IEntity<string> contract and encapsulates
|
||||
* basic invariants around identity and core properties.
|
||||
*/
|
||||
|
||||
export type TeamRole = 'owner' | 'manager' | 'driver';
|
||||
export type TeamMembershipStatus = 'active' | 'pending' | 'none';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt: Date;
|
||||
}
|
||||
export class Team implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly tag: string;
|
||||
readonly description: string;
|
||||
readonly ownerId: string;
|
||||
readonly leagues: string[];
|
||||
readonly createdAt: Date;
|
||||
|
||||
export interface TeamMembership {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
role: TeamRole;
|
||||
status: TeamMembershipStatus;
|
||||
joinedAt: Date;
|
||||
}
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt: Date;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
this.tag = props.tag;
|
||||
this.description = props.description;
|
||||
this.ownerId = props.ownerId;
|
||||
this.leagues = props.leagues;
|
||||
this.createdAt = props.createdAt;
|
||||
}
|
||||
|
||||
export interface TeamJoinRequest {
|
||||
id: string;
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string;
|
||||
/**
|
||||
* Factory method to create a new Team entity.
|
||||
*/
|
||||
static create(props: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
createdAt?: Date;
|
||||
}): Team {
|
||||
this.validate(props);
|
||||
|
||||
return new Team({
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
tag: props.tag,
|
||||
description: props.description,
|
||||
ownerId: props.ownerId,
|
||||
leagues: [...props.leagues],
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy with updated properties.
|
||||
*/
|
||||
update(props: Partial<{
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
}>): Team {
|
||||
const next: Team = new Team({
|
||||
id: this.id,
|
||||
name: props.name ?? this.name,
|
||||
tag: props.tag ?? this.tag,
|
||||
description: props.description ?? this.description,
|
||||
ownerId: props.ownerId ?? this.ownerId,
|
||||
leagues: props.leagues ? [...props.leagues] : [...this.leagues],
|
||||
createdAt: this.createdAt,
|
||||
});
|
||||
|
||||
// Re-validate updated aggregate
|
||||
Team.validate({
|
||||
id: next.id,
|
||||
name: next.name,
|
||||
tag: next.tag,
|
||||
description: next.description,
|
||||
ownerId: next.ownerId,
|
||||
leagues: next.leagues,
|
||||
});
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain validation logic for core invariants.
|
||||
*/
|
||||
private static validate(props: {
|
||||
id: string;
|
||||
name: string;
|
||||
tag: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
leagues: string[];
|
||||
}): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Team ID is required');
|
||||
}
|
||||
|
||||
if (!props.name || props.name.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Team name is required');
|
||||
}
|
||||
|
||||
if (!props.tag || props.tag.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Team tag is required');
|
||||
}
|
||||
|
||||
if (!props.ownerId || props.ownerId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Team owner ID is required');
|
||||
}
|
||||
|
||||
if (!Array.isArray(props.leagues)) {
|
||||
throw new RacingDomainValidationError('Team leagues must be an array');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,14 @@
|
||||
* Represents a racing track/circuit in the GridPilot platform.
|
||||
* Immutable entity with factory methods and domain validation.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export type TrackCategory = 'oval' | 'road' | 'street' | 'dirt';
|
||||
export type TrackDifficulty = 'beginner' | 'intermediate' | 'advanced' | 'expert';
|
||||
|
||||
export class Track {
|
||||
export class Track implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly shortName: string;
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
*
|
||||
* Represents a financial transaction in the league wallet system.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
export type TransactionType =
|
||||
| 'sponsorship_payment'
|
||||
@@ -30,8 +31,8 @@ export interface TransactionProps {
|
||||
description?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class Transaction {
|
||||
|
||||
export class Transaction implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly walletId: string;
|
||||
readonly type: TransactionType;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export abstract class RacingDomainError extends Error {
|
||||
import type { IDomainError, CommonDomainErrorKind } from '@gridpilot/shared/errors';
|
||||
|
||||
export abstract class RacingDomainError extends Error implements IDomainError<CommonDomainErrorKind> {
|
||||
readonly type = 'domain' as const;
|
||||
readonly context = 'racing-domain';
|
||||
abstract readonly kind: CommonDomainErrorKind;
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
@@ -7,7 +11,10 @@ export abstract class RacingDomainError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class RacingDomainValidationError extends RacingDomainError {
|
||||
export class RacingDomainValidationError
|
||||
extends RacingDomainError
|
||||
implements IDomainError<'validation'>
|
||||
{
|
||||
readonly kind = 'validation' as const;
|
||||
|
||||
constructor(message: string) {
|
||||
@@ -15,7 +22,10 @@ export class RacingDomainValidationError extends RacingDomainError {
|
||||
}
|
||||
}
|
||||
|
||||
export class RacingDomainInvariantError extends RacingDomainError {
|
||||
export class RacingDomainInvariantError
|
||||
extends RacingDomainError
|
||||
implements IDomainError<'invariant'>
|
||||
{
|
||||
readonly kind = 'invariant' as const;
|
||||
|
||||
constructor(message: string) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import type {
|
||||
TeamMembership,
|
||||
TeamJoinRequest,
|
||||
} from '../entities/Team';
|
||||
} from '../types/TeamMembership';
|
||||
|
||||
export interface ITeamMembershipRepository {
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Weekday } from '../value-objects/Weekday';
|
||||
import type { Weekday } from '../types/Weekday';
|
||||
|
||||
export type RecurrenceStrategy = 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
21
packages/racing/domain/types/ChampionshipConfig.ts
Normal file
21
packages/racing/domain/types/ChampionshipConfig.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ChampionshipType } from '../types/ChampionshipType';
|
||||
import type { SessionType } from '../types/SessionType';
|
||||
import type { PointsTable } from '../value-objects/PointsTable';
|
||||
import type { BonusRule } from '../types/BonusRule';
|
||||
import type { DropScorePolicy } from '../types/DropScorePolicy';
|
||||
|
||||
/**
|
||||
* Domain Type: ChampionshipConfig
|
||||
*
|
||||
* Pure configuration shape for a championship's scoring model.
|
||||
* This is not a value object and intentionally lives under domain/types.
|
||||
*/
|
||||
export interface ChampionshipConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ChampionshipType;
|
||||
sessionTypes: SessionType[];
|
||||
pointsTableBySessionType: Record<SessionType, PointsTable>;
|
||||
bonusRulesBySessionType?: Record<SessionType, BonusRule[]>;
|
||||
dropScorePolicy: DropScorePolicy;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Domain Value Object: LeagueRoles
|
||||
*
|
||||
* Domain Types/Utilities: LeagueRoles
|
||||
*
|
||||
* Utility functions for working with league membership roles.
|
||||
*/
|
||||
|
||||
25
packages/racing/domain/types/TeamMembership.ts
Normal file
25
packages/racing/domain/types/TeamMembership.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Domain Types: TeamRole, TeamMembershipStatus, TeamMembership, TeamJoinRequest
|
||||
*
|
||||
* These are pure domain data shapes (no behavior) used across repositories
|
||||
* and application DTOs. They are not entities or value objects.
|
||||
*/
|
||||
|
||||
export type TeamRole = 'owner' | 'manager' | 'driver';
|
||||
export type TeamMembershipStatus = 'active' | 'pending' | 'none';
|
||||
|
||||
export interface TeamMembership {
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
role: TeamRole;
|
||||
status: TeamMembershipStatus;
|
||||
joinedAt: Date;
|
||||
}
|
||||
|
||||
export interface TeamJoinRequest {
|
||||
id: string;
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ChampionshipType } from './ChampionshipType';
|
||||
import type { SessionType } from './SessionType';
|
||||
import { PointsTable } from './PointsTable';
|
||||
import type { BonusRule } from './BonusRule';
|
||||
import type { DropScorePolicy } from './DropScorePolicy';
|
||||
|
||||
export interface ChampionshipConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ChampionshipType;
|
||||
sessionTypes: SessionType[];
|
||||
pointsTableBySessionType: Record<SessionType, PointsTable>;
|
||||
bonusRulesBySessionType?: Record<SessionType, BonusRule[]>;
|
||||
dropScorePolicy: DropScorePolicy;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Domain Value Object: GameConstraints
|
||||
*
|
||||
*
|
||||
* Represents game-specific constraints for leagues.
|
||||
* Different sim racing games have different maximum grid sizes.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface GameConstraintsData {
|
||||
readonly maxDrivers: number;
|
||||
readonly maxTeams: number;
|
||||
@@ -14,6 +16,11 @@ export interface GameConstraintsData {
|
||||
readonly supportsMultiClass: boolean;
|
||||
}
|
||||
|
||||
export interface GameConstraintsProps {
|
||||
gameId: string;
|
||||
constraints: GameConstraintsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Game-specific constraints for popular sim racing games
|
||||
*/
|
||||
@@ -69,7 +76,7 @@ const GAME_CONSTRAINTS: Record<string, GameConstraintsData> = {
|
||||
},
|
||||
};
|
||||
|
||||
export class GameConstraints {
|
||||
export class GameConstraints implements IValueObject<GameConstraintsProps> {
|
||||
readonly gameId: string;
|
||||
readonly constraints: GameConstraintsData;
|
||||
|
||||
@@ -78,6 +85,17 @@ export class GameConstraints {
|
||||
this.constraints = constraints;
|
||||
}
|
||||
|
||||
get props(): GameConstraintsProps {
|
||||
return {
|
||||
gameId: this.gameId,
|
||||
constraints: this.constraints,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<GameConstraintsProps>): boolean {
|
||||
return this.props.gameId === other.props.gameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get constraints for a specific game
|
||||
*/
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*
|
||||
* Represents a valid league description with validation rules.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface LeagueDescriptionValidationResult {
|
||||
valid: boolean;
|
||||
@@ -17,7 +18,11 @@ export const LEAGUE_DESCRIPTION_CONSTRAINTS = {
|
||||
recommendedMinLength: 50,
|
||||
} as const;
|
||||
|
||||
export class LeagueDescription {
|
||||
export interface LeagueDescriptionProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class LeagueDescription implements IValueObject<LeagueDescriptionProps> {
|
||||
readonly value: string;
|
||||
|
||||
private constructor(value: string) {
|
||||
@@ -70,6 +75,10 @@ export class LeagueDescription {
|
||||
return new LeagueDescription(value.trim());
|
||||
}
|
||||
|
||||
get props(): LeagueDescriptionProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to create a LeagueDescription, returning null if invalid
|
||||
*/
|
||||
@@ -84,8 +93,8 @@ export class LeagueDescription {
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: LeagueDescription): boolean {
|
||||
return this.value === other.value;
|
||||
|
||||
equals(other: IValueObject<LeagueDescriptionProps>): boolean {
|
||||
return this.props.value === other.props.value;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
*
|
||||
* Represents a valid league name with validation rules.
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface LeagueNameValidationResult {
|
||||
valid: boolean;
|
||||
@@ -22,7 +23,11 @@ export const LEAGUE_NAME_CONSTRAINTS = {
|
||||
],
|
||||
} as const;
|
||||
|
||||
export class LeagueName {
|
||||
export interface LeagueNameProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class LeagueName implements IValueObject<LeagueNameProps> {
|
||||
readonly value: string;
|
||||
|
||||
private constructor(value: string) {
|
||||
@@ -83,6 +88,10 @@ export class LeagueName {
|
||||
return new LeagueName(value.trim());
|
||||
}
|
||||
|
||||
get props(): LeagueNameProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to create a LeagueName, returning null if invalid
|
||||
*/
|
||||
@@ -97,8 +106,8 @@ export class LeagueName {
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: LeagueName): boolean {
|
||||
return this.value === other.value;
|
||||
|
||||
equals(other: IValueObject<LeagueNameProps>): boolean {
|
||||
return this.props.value === other.props.value;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export class LeagueTimezone {
|
||||
export interface LeagueTimezoneProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class LeagueTimezone implements IValueObject<LeagueTimezoneProps> {
|
||||
private readonly id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
@@ -13,4 +18,12 @@ export class LeagueTimezone {
|
||||
getId(): string {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get props(): LeagueTimezoneProps {
|
||||
return { id: this.id };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<LeagueTimezoneProps>): boolean {
|
||||
return this.props.id === other.props.id;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
/**
|
||||
* Domain Value Object: LeagueVisibility
|
||||
*
|
||||
*
|
||||
* Represents the visibility and ranking status of a league.
|
||||
*
|
||||
*
|
||||
* - 'ranked' (public): Competitive leagues visible to everyone, affects driver ratings.
|
||||
* Requires minimum 10 players to ensure competitive integrity.
|
||||
* - 'unranked' (private): Casual leagues for friends/private groups, no rating impact.
|
||||
* Can have any number of players.
|
||||
*/
|
||||
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export type LeagueVisibilityType = 'ranked' | 'unranked';
|
||||
|
||||
@@ -33,7 +36,11 @@ const VISIBILITY_CONSTRAINTS: Record<LeagueVisibilityType, LeagueVisibilityConst
|
||||
},
|
||||
};
|
||||
|
||||
export class LeagueVisibility {
|
||||
export interface LeagueVisibilityProps {
|
||||
type: LeagueVisibilityType;
|
||||
}
|
||||
|
||||
export class LeagueVisibility implements IValueObject<LeagueVisibilityProps> {
|
||||
readonly type: LeagueVisibilityType;
|
||||
readonly constraints: LeagueVisibilityConstraints;
|
||||
|
||||
@@ -112,6 +119,10 @@ export class LeagueVisibility {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
get props(): LeagueVisibilityProps {
|
||||
return { type: this.type };
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility with existing 'public'/'private' terminology
|
||||
*/
|
||||
@@ -119,8 +130,8 @@ export class LeagueVisibility {
|
||||
return this.type === 'ranked' ? 'public' : 'private';
|
||||
}
|
||||
|
||||
equals(other: LeagueVisibility): boolean {
|
||||
return this.type === other.type;
|
||||
equals(other: IValueObject<LeagueVisibilityProps>): boolean {
|
||||
return this.props.type === other.props.type;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* Value Object: LiveryDecal
|
||||
* Represents a decal/logo placed on a livery
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export type DecalType = 'sponsor' | 'user';
|
||||
|
||||
@@ -16,8 +19,8 @@ export interface LiveryDecalProps {
|
||||
zIndex: number;
|
||||
type: DecalType;
|
||||
}
|
||||
|
||||
export class LiveryDecal {
|
||||
|
||||
export class LiveryDecal implements IValueObject<LiveryDecalProps> {
|
||||
readonly id: string;
|
||||
readonly imageUrl: string;
|
||||
readonly x: number;
|
||||
@@ -138,6 +141,20 @@ export class LiveryDecal {
|
||||
return `rotate(${this.rotation}deg)`;
|
||||
}
|
||||
|
||||
get props(): LiveryDecalProps {
|
||||
return {
|
||||
id: this.id,
|
||||
imageUrl: this.imageUrl,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
rotation: this.rotation,
|
||||
zIndex: this.zIndex,
|
||||
type: this.type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this decal overlaps with another
|
||||
*/
|
||||
@@ -146,7 +163,7 @@ export class LiveryDecal {
|
||||
const thisBottom = this.y + this.height;
|
||||
const otherRight = other.x + other.width;
|
||||
const otherBottom = other.y + other.height;
|
||||
|
||||
|
||||
return !(
|
||||
thisRight <= other.x ||
|
||||
this.x >= otherRight ||
|
||||
@@ -154,4 +171,20 @@ export class LiveryDecal {
|
||||
this.y >= otherBottom
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: IValueObject<LiveryDecalProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.id === b.id &&
|
||||
a.imageUrl === b.imageUrl &&
|
||||
a.x === b.x &&
|
||||
a.y === b.y &&
|
||||
a.width === b.width &&
|
||||
a.height === b.height &&
|
||||
a.rotation === b.rotation &&
|
||||
a.zIndex === b.zIndex &&
|
||||
a.type === b.type
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
* Value Object: MembershipFee
|
||||
* Represents membership fee configuration for league drivers
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
import type { Money } from './Money';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export type MembershipFeeType = 'season' | 'monthly' | 'per_race';
|
||||
|
||||
@@ -14,7 +15,7 @@ export interface MembershipFeeProps {
|
||||
amount: Money;
|
||||
}
|
||||
|
||||
export class MembershipFee {
|
||||
export class MembershipFee implements IValueObject<MembershipFeeProps> {
|
||||
readonly type: MembershipFeeType;
|
||||
readonly amount: Money;
|
||||
|
||||
@@ -53,6 +54,13 @@ export class MembershipFee {
|
||||
return this.amount.calculateNetAmount();
|
||||
}
|
||||
|
||||
get props(): MembershipFeeProps {
|
||||
return {
|
||||
type: this.type,
|
||||
amount: this.amount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a recurring fee
|
||||
*/
|
||||
@@ -60,6 +68,12 @@ export class MembershipFee {
|
||||
return this.type === 'monthly';
|
||||
}
|
||||
|
||||
equals(other: IValueObject<MembershipFeeProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return a.type === b.type && a.amount.equals(b.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for fee type
|
||||
*/
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
* Value Object: Money
|
||||
* Represents a monetary amount with currency and platform fee calculation
|
||||
*/
|
||||
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export type Currency = 'USD' | 'EUR' | 'GBP';
|
||||
|
||||
export class Money {
|
||||
export interface MoneyProps {
|
||||
amount: number;
|
||||
currency: Currency;
|
||||
}
|
||||
|
||||
export class Money implements IValueObject<MoneyProps> {
|
||||
private static readonly PLATFORM_FEE_PERCENTAGE = 0.10;
|
||||
|
||||
readonly amount: number;
|
||||
@@ -78,11 +84,20 @@ export class Money {
|
||||
return this.amount > other.amount;
|
||||
}
|
||||
|
||||
get props(): MoneyProps {
|
||||
return {
|
||||
amount: this.amount,
|
||||
currency: this.currency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this money equals another
|
||||
*/
|
||||
equals(other: Money): boolean {
|
||||
return this.amount === other.amount && this.currency === other.currency;
|
||||
equals(other: IValueObject<MoneyProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return a.amount === b.amount && a.currency === b.currency;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { Weekday } from './Weekday';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export class MonthlyRecurrencePattern {
|
||||
export interface MonthlyRecurrencePatternProps {
|
||||
ordinal: 1 | 2 | 3 | 4;
|
||||
weekday: Weekday;
|
||||
}
|
||||
|
||||
export class MonthlyRecurrencePattern implements IValueObject<MonthlyRecurrencePatternProps> {
|
||||
readonly ordinal: 1 | 2 | 3 | 4;
|
||||
readonly weekday: Weekday;
|
||||
|
||||
@@ -8,4 +14,17 @@ export class MonthlyRecurrencePattern {
|
||||
this.ordinal = ordinal;
|
||||
this.weekday = weekday;
|
||||
}
|
||||
|
||||
get props(): MonthlyRecurrencePatternProps {
|
||||
return {
|
||||
ordinal: this.ordinal,
|
||||
weekday: this.weekday,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<MonthlyRecurrencePatternProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return a.ordinal === b.ordinal && a.weekday === b.weekday;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
export class PointsTable {
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface PointsTableProps {
|
||||
pointsByPosition: Map<number, number>;
|
||||
}
|
||||
|
||||
export class PointsTable implements IValueObject<PointsTableProps> {
|
||||
private readonly pointsByPosition: Map<number, number>;
|
||||
|
||||
constructor(pointsByPosition: Record<number, number> | Map<number, number>) {
|
||||
@@ -18,4 +24,23 @@ export class PointsTable {
|
||||
const value = this.pointsByPosition.get(position);
|
||||
return typeof value === 'number' ? value : 0;
|
||||
}
|
||||
|
||||
get props(): PointsTableProps {
|
||||
return {
|
||||
pointsByPosition: new Map(this.pointsByPosition),
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<PointsTableProps>): boolean {
|
||||
const a = this.props.pointsByPosition;
|
||||
const b = other.props.pointsByPosition;
|
||||
|
||||
if (a.size !== b.size) return false;
|
||||
for (const [key, value] of a.entries()) {
|
||||
if (b.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export class RaceTimeOfDay {
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface RaceTimeOfDayProps {
|
||||
hour: number;
|
||||
minute: number;
|
||||
}
|
||||
|
||||
export class RaceTimeOfDay implements IValueObject<RaceTimeOfDayProps> {
|
||||
readonly hour: number;
|
||||
readonly minute: number;
|
||||
|
||||
@@ -21,16 +27,29 @@ export class RaceTimeOfDay {
|
||||
if (!match) {
|
||||
throw new RacingDomainValidationError(`RaceTimeOfDay string must be in HH:MM 24h format, got "${value}"`);
|
||||
}
|
||||
|
||||
|
||||
const hour = Number(match[1]);
|
||||
const minute = Number(match[2]);
|
||||
|
||||
|
||||
return new RaceTimeOfDay(hour, minute);
|
||||
}
|
||||
|
||||
get props(): RaceTimeOfDayProps {
|
||||
return {
|
||||
hour: this.hour,
|
||||
minute: this.minute,
|
||||
};
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const hh = this.hour.toString().padStart(2, '0');
|
||||
const mm = this.minute.toString().padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<RaceTimeOfDayProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return a.hour === b.hour && a.minute === b.minute;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { WeekdaySet } from './WeekdaySet';
|
||||
import { MonthlyRecurrencePattern } from './MonthlyRecurrencePattern';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export type RecurrenceStrategyKind = 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
|
||||
|
||||
export type WeeklyRecurrence = {
|
||||
kind: 'weekly';
|
||||
weekdays: WeekdaySet;
|
||||
};
|
||||
|
||||
export type EveryNWeeksRecurrence = {
|
||||
kind: 'everyNWeeks';
|
||||
intervalWeeks: number;
|
||||
weekdays: WeekdaySet;
|
||||
};
|
||||
|
||||
export type MonthlyNthWeekdayRecurrence = {
|
||||
kind: 'monthlyNthWeekday';
|
||||
monthlyPattern: MonthlyRecurrencePattern;
|
||||
};
|
||||
|
||||
export type RecurrenceStrategy =
|
||||
| WeeklyRecurrence
|
||||
| EveryNWeeksRecurrence
|
||||
| MonthlyNthWeekdayRecurrence;
|
||||
|
||||
export class RecurrenceStrategyFactory {
|
||||
static weekly(weekdays: WeekdaySet): RecurrenceStrategy {
|
||||
return {
|
||||
kind: 'weekly',
|
||||
weekdays,
|
||||
};
|
||||
}
|
||||
|
||||
static everyNWeeks(intervalWeeks: number, weekdays: WeekdaySet): RecurrenceStrategy {
|
||||
if (!Number.isInteger(intervalWeeks) || intervalWeeks < 1 || intervalWeeks > 12) {
|
||||
throw new RacingDomainValidationError(
|
||||
'everyNWeeks intervalWeeks must be an integer between 1 and 12',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'everyNWeeks',
|
||||
intervalWeeks,
|
||||
weekdays,
|
||||
};
|
||||
}
|
||||
|
||||
static monthlyNthWeekday(monthlyPattern: MonthlyRecurrencePattern): RecurrenceStrategy {
|
||||
return {
|
||||
kind: 'monthlyNthWeekday',
|
||||
monthlyPattern,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { LeagueTimezone } from './LeagueTimezone';
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export class ScheduledRaceSlot {
|
||||
export interface ScheduledRaceSlotProps {
|
||||
roundNumber: number;
|
||||
scheduledAt: Date;
|
||||
timezone: LeagueTimezone;
|
||||
}
|
||||
|
||||
export class ScheduledRaceSlot implements IValueObject<ScheduledRaceSlotProps> {
|
||||
readonly roundNumber: number;
|
||||
readonly scheduledAt: Date;
|
||||
readonly timezone: LeagueTimezone;
|
||||
@@ -19,4 +26,22 @@ export class ScheduledRaceSlot {
|
||||
this.scheduledAt = params.scheduledAt;
|
||||
this.timezone = params.timezone;
|
||||
}
|
||||
|
||||
get props(): ScheduledRaceSlotProps {
|
||||
return {
|
||||
roundNumber: this.roundNumber,
|
||||
scheduledAt: this.scheduledAt,
|
||||
timezone: this.timezone,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<ScheduledRaceSlotProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.roundNumber === b.roundNumber &&
|
||||
a.scheduledAt.getTime() === b.scheduledAt.getTime() &&
|
||||
a.timezone.equals(b.timezone)
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user