wip
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
export type BonusRuleType = 'fastestLap' | 'polePosition' | 'mostPositionsGained';
|
||||
|
||||
export interface BonusRule {
|
||||
id: string;
|
||||
type: BonusRuleType;
|
||||
points: number;
|
||||
requiresFinishInTopN?: number;
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
export type ChampionshipType = 'driver' | 'team' | 'nations' | 'trophy';
|
||||
@@ -1,13 +0,0 @@
|
||||
export type DropScoreStrategy = 'none' | 'bestNResults' | 'dropWorstN';
|
||||
|
||||
export interface DropScorePolicy {
|
||||
strategy: DropScoreStrategy;
|
||||
/**
|
||||
* For 'bestNResults': number of best-scoring events to count.
|
||||
*/
|
||||
count?: number;
|
||||
/**
|
||||
* For 'dropWorstN': number of worst-scoring events to drop.
|
||||
*/
|
||||
dropCount?: number;
|
||||
}
|
||||
@@ -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,73 +0,0 @@
|
||||
/**
|
||||
* Domain Value Object: LeagueRoles
|
||||
*
|
||||
* Utility functions for working with league membership roles.
|
||||
*/
|
||||
|
||||
import type { MembershipRole } from '../entities/LeagueMembership';
|
||||
|
||||
/**
|
||||
* Role hierarchy (higher number = more authority)
|
||||
*/
|
||||
const ROLE_HIERARCHY: Record<MembershipRole, number> = {
|
||||
member: 0,
|
||||
steward: 1,
|
||||
admin: 2,
|
||||
owner: 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a role is at least steward level
|
||||
*/
|
||||
export function isLeagueStewardOrHigherRole(role: MembershipRole): boolean {
|
||||
return ROLE_HIERARCHY[role] >= ROLE_HIERARCHY.steward;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a role is at least admin level
|
||||
*/
|
||||
export function isLeagueAdminOrHigherRole(role: MembershipRole): boolean {
|
||||
return ROLE_HIERARCHY[role] >= ROLE_HIERARCHY.admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a role is owner
|
||||
*/
|
||||
export function isLeagueOwnerRole(role: MembershipRole): boolean {
|
||||
return role === 'owner';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two roles
|
||||
* Returns positive if role1 > role2, negative if role1 < role2, 0 if equal
|
||||
*/
|
||||
export function compareRoles(role1: MembershipRole, role2: MembershipRole): number {
|
||||
return ROLE_HIERARCHY[role1] - ROLE_HIERARCHY[role2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role display name
|
||||
*/
|
||||
export function getRoleDisplayName(role: MembershipRole): string {
|
||||
const names: Record<MembershipRole, string> = {
|
||||
member: 'Member',
|
||||
steward: 'Steward',
|
||||
admin: 'Admin',
|
||||
owner: 'Owner',
|
||||
};
|
||||
return names[role];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all roles in order of hierarchy
|
||||
*/
|
||||
export function getAllRolesOrdered(): MembershipRole[] {
|
||||
return ['member', 'steward', 'admin', 'owner'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get roles that can be assigned (excludes owner as it's transferred, not assigned)
|
||||
*/
|
||||
export function getAssignableRoles(): MembershipRole[] {
|
||||
return ['member', 'steward', 'admin'];
|
||||
}
|
||||
@@ -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,6 +0,0 @@
|
||||
import type { ChampionshipType } from './ChampionshipType';
|
||||
|
||||
export interface ParticipantRef {
|
||||
type: ChampionshipType;
|
||||
id: string;
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,17 @@ import { RaceTimeOfDay } from './RaceTimeOfDay';
|
||||
import { LeagueTimezone } from './LeagueTimezone';
|
||||
import type { RecurrenceStrategy } from './RecurrenceStrategy';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export class SeasonSchedule {
|
||||
export interface SeasonScheduleProps {
|
||||
startDate: Date;
|
||||
timeOfDay: RaceTimeOfDay;
|
||||
timezone: LeagueTimezone;
|
||||
recurrence: RecurrenceStrategy;
|
||||
plannedRounds: number;
|
||||
}
|
||||
|
||||
export class SeasonSchedule implements IValueObject<SeasonScheduleProps> {
|
||||
readonly startDate: Date;
|
||||
readonly timeOfDay: RaceTimeOfDay;
|
||||
readonly timezone: LeagueTimezone;
|
||||
@@ -34,4 +43,26 @@ export class SeasonSchedule {
|
||||
this.recurrence = params.recurrence;
|
||||
this.plannedRounds = params.plannedRounds;
|
||||
}
|
||||
|
||||
get props(): SeasonScheduleProps {
|
||||
return {
|
||||
startDate: this.startDate,
|
||||
timeOfDay: this.timeOfDay,
|
||||
timezone: this.timezone,
|
||||
recurrence: this.recurrence,
|
||||
plannedRounds: this.plannedRounds,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonScheduleProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.startDate.getTime() === b.startDate.getTime() &&
|
||||
a.timeOfDay.equals(b.timeOfDay) &&
|
||||
a.timezone.equals(b.timezone) &&
|
||||
a.recurrence.kind === b.recurrence.kind &&
|
||||
a.plannedRounds === b.plannedRounds
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export type SessionType =
|
||||
| 'practice'
|
||||
| 'qualifying'
|
||||
| 'q1'
|
||||
| 'q2'
|
||||
| 'q3'
|
||||
| 'sprint'
|
||||
| 'main'
|
||||
| 'timeTrial';
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Value Object: SponsorshipPricing
|
||||
*
|
||||
*
|
||||
* Represents the sponsorship slot configuration and pricing for any sponsorable entity.
|
||||
* Used by drivers, teams, races, and leagues to define their sponsorship offerings.
|
||||
*/
|
||||
|
||||
import { Money } from './Money';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface SponsorshipSlotConfig {
|
||||
tier: 'main' | 'secondary';
|
||||
@@ -22,7 +23,7 @@ export interface SponsorshipPricingProps {
|
||||
customRequirements?: string;
|
||||
}
|
||||
|
||||
export class SponsorshipPricing {
|
||||
export class SponsorshipPricing implements IValueObject<SponsorshipPricingProps> {
|
||||
readonly mainSlot?: SponsorshipSlotConfig;
|
||||
readonly secondarySlots?: SponsorshipSlotConfig;
|
||||
readonly acceptingApplications: boolean;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
export type Weekday = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun';
|
||||
|
||||
import { RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
|
||||
export const ALL_WEEKDAYS: Weekday[] = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
|
||||
export function weekdayToIndex(day: Weekday): number {
|
||||
switch (day) {
|
||||
case 'Mon':
|
||||
return 1;
|
||||
case 'Tue':
|
||||
return 2;
|
||||
case 'Wed':
|
||||
return 3;
|
||||
case 'Thu':
|
||||
return 4;
|
||||
case 'Fri':
|
||||
return 5;
|
||||
case 'Sat':
|
||||
return 6;
|
||||
case 'Sun':
|
||||
return 7;
|
||||
default:
|
||||
// This should be unreachable because Weekday is a closed union.
|
||||
throw new RacingDomainInvariantError(`Unknown weekday: ${day}`);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
import type { Weekday } from './Weekday';
|
||||
import { weekdayToIndex } from './Weekday';
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
|
||||
export class WeekdaySet {
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface WeekdaySetProps {
|
||||
days: Weekday[];
|
||||
}
|
||||
|
||||
export class WeekdaySet implements IValueObject<WeekdaySetProps> {
|
||||
private readonly days: Weekday[];
|
||||
|
||||
constructor(days: Weekday[]) {
|
||||
if (!Array.isArray(days) || days.length === 0) {
|
||||
throw new RacingDomainValidationError('WeekdaySet requires at least one weekday');
|
||||
}
|
||||
|
||||
|
||||
const unique = Array.from(new Set(days));
|
||||
this.days = unique.sort((a, b) => weekdayToIndex(a) - weekdayToIndex(b));
|
||||
}
|
||||
|
||||
get props(): WeekdaySetProps {
|
||||
return { days: [...this.days] };
|
||||
}
|
||||
|
||||
getAll(): Weekday[] {
|
||||
return [...this.days];
|
||||
}
|
||||
@@ -21,4 +30,11 @@ export class WeekdaySet {
|
||||
includes(day: Weekday): boolean {
|
||||
return this.days.includes(day);
|
||||
}
|
||||
|
||||
equals(other: IValueObject<WeekdaySetProps>): boolean {
|
||||
const a = this.props.days;
|
||||
const b = other.props.days;
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((day, index) => day === b[index]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user