wip
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
|
||||
@@ -29,6 +30,7 @@ export class AcceptSponsorshipRequestUseCase
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
) {}
|
||||
|
||||
async execute(dto: AcceptSponsorshipRequestDTO): Promise<AcceptSponsorshipRequestResultDTO> {
|
||||
@@ -50,9 +52,15 @@ export class AcceptSponsorshipRequestUseCase
|
||||
let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
if (request.entityType === 'season') {
|
||||
const season = await this.seasonRepository.findById(request.entityId);
|
||||
if (!season) {
|
||||
throw new Error('Season not found for sponsorship request');
|
||||
}
|
||||
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: sponsorshipId,
|
||||
seasonId: request.entityId,
|
||||
seasonId: season.id,
|
||||
leagueId: season.leagueId,
|
||||
sponsorId: request.sponsorId,
|
||||
tier: request.tier,
|
||||
pricing: request.offeredAmount,
|
||||
|
||||
460
packages/racing/application/use-cases/SeasonUseCases.ts
Normal file
460
packages/racing/application/use-cases/SeasonUseCases.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
|
||||
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
|
||||
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
|
||||
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
|
||||
import type { Weekday } from '../../domain/types/Weekday';
|
||||
import { normalizeVisibility } from '../dto/LeagueConfigFormDTO';
|
||||
import { LeagueVisibility } from '../../domain/value-objects/LeagueVisibility';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* DTOs and helpers shared across Season-focused use cases.
|
||||
*/
|
||||
|
||||
export interface CreateSeasonForLeagueCommand {
|
||||
leagueId: string;
|
||||
name: string;
|
||||
gameId: string;
|
||||
sourceSeasonId?: string;
|
||||
/**
|
||||
* Optional high-level wizard config used to derive schedule/scoring/drop/stewarding.
|
||||
* When omitted, the Season will be created with minimal metadata only.
|
||||
*/
|
||||
config?: LeagueConfigFormModel;
|
||||
}
|
||||
|
||||
export interface CreateSeasonForLeagueResultDTO {
|
||||
seasonId: string;
|
||||
}
|
||||
|
||||
export interface SeasonSummaryDTO {
|
||||
seasonId: string;
|
||||
leagueId: string;
|
||||
name: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface ListSeasonsForLeagueQuery {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export interface ListSeasonsForLeagueResultDTO {
|
||||
items: SeasonSummaryDTO[];
|
||||
}
|
||||
|
||||
export interface GetSeasonDetailsQuery {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
}
|
||||
|
||||
export interface SeasonDetailsDTO {
|
||||
seasonId: string;
|
||||
leagueId: string;
|
||||
gameId: string;
|
||||
name: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
maxDrivers?: number;
|
||||
schedule?: {
|
||||
startDate: Date;
|
||||
plannedRounds: number;
|
||||
};
|
||||
scoring?: {
|
||||
scoringPresetId: string;
|
||||
customScoringEnabled: boolean;
|
||||
};
|
||||
dropPolicy?: {
|
||||
strategy: import('../../domain/value-objects/SeasonDropPolicy').SeasonDropStrategy;
|
||||
n?: number;
|
||||
};
|
||||
stewarding?: {
|
||||
decisionMode: import('../../domain/entities/League').StewardingDecisionMode;
|
||||
requiredVotes?: number;
|
||||
requireDefense: boolean;
|
||||
defenseTimeLimit: number;
|
||||
voteTimeLimit: number;
|
||||
protestDeadlineHours: number;
|
||||
stewardingClosesHours: number;
|
||||
notifyAccusedOnProtest: boolean;
|
||||
notifyOnVoteRequired: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type SeasonLifecycleTransition =
|
||||
| 'activate'
|
||||
| 'complete'
|
||||
| 'archive'
|
||||
| 'cancel';
|
||||
|
||||
export interface ManageSeasonLifecycleCommand {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
transition: SeasonLifecycleTransition;
|
||||
}
|
||||
|
||||
export interface ManageSeasonLifecycleResultDTO {
|
||||
seasonId: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateSeasonForLeagueUseCase
|
||||
*
|
||||
* Creates a new Season for an existing League, optionally cloning or deriving
|
||||
* configuration from a source Season or a league config form.
|
||||
*/
|
||||
export class CreateSeasonForLeagueUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: CreateSeasonForLeagueCommand,
|
||||
): Promise<CreateSeasonForLeagueResultDTO> {
|
||||
const league = await this.leagueRepository.findById(command.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${command.leagueId}`);
|
||||
}
|
||||
|
||||
let baseSeasonProps: {
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
} = {};
|
||||
|
||||
if (command.sourceSeasonId) {
|
||||
const source = await this.seasonRepository.findById(command.sourceSeasonId);
|
||||
if (!source) {
|
||||
throw new Error(`Source Season not found: ${command.sourceSeasonId}`);
|
||||
}
|
||||
baseSeasonProps = {
|
||||
...(source.schedule !== undefined ? { schedule: source.schedule } : {}),
|
||||
...(source.scoringConfig !== undefined
|
||||
? { scoringConfig: source.scoringConfig }
|
||||
: {}),
|
||||
...(source.dropPolicy !== undefined ? { dropPolicy: source.dropPolicy } : {}),
|
||||
...(source.stewardingConfig !== undefined
|
||||
? { stewardingConfig: source.stewardingConfig }
|
||||
: {}),
|
||||
...(source.maxDrivers !== undefined ? { maxDrivers: source.maxDrivers } : {}),
|
||||
};
|
||||
} else if (command.config) {
|
||||
baseSeasonProps = this.deriveSeasonPropsFromConfig(command.config);
|
||||
}
|
||||
|
||||
const seasonId = uuidv4();
|
||||
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
gameId: command.gameId,
|
||||
name: command.name,
|
||||
year: new Date().getFullYear(),
|
||||
status: 'planned',
|
||||
...(baseSeasonProps?.schedule
|
||||
? { schedule: baseSeasonProps.schedule }
|
||||
: {}),
|
||||
...(baseSeasonProps?.scoringConfig
|
||||
? { scoringConfig: baseSeasonProps.scoringConfig }
|
||||
: {}),
|
||||
...(baseSeasonProps?.dropPolicy
|
||||
? { dropPolicy: baseSeasonProps.dropPolicy }
|
||||
: {}),
|
||||
...(baseSeasonProps?.stewardingConfig
|
||||
? { stewardingConfig: baseSeasonProps.stewardingConfig }
|
||||
: {}),
|
||||
...(baseSeasonProps?.maxDrivers !== undefined
|
||||
? { maxDrivers: baseSeasonProps.maxDrivers }
|
||||
: {}),
|
||||
});
|
||||
|
||||
await this.seasonRepository.add(season);
|
||||
|
||||
return { seasonId };
|
||||
}
|
||||
|
||||
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
} {
|
||||
const schedule = this.buildScheduleFromTimings(config);
|
||||
const scoringConfig = new SeasonScoringConfig({
|
||||
scoringPresetId: config.scoring.patternId ?? 'custom',
|
||||
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
|
||||
});
|
||||
const dropPolicy = new SeasonDropPolicy({
|
||||
strategy: config.dropPolicy.strategy,
|
||||
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
});
|
||||
const stewardingConfig = new SeasonStewardingConfig({
|
||||
decisionMode: config.stewarding.decisionMode,
|
||||
...(config.stewarding.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: config.stewarding.requireDefense,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||
});
|
||||
|
||||
const structure = config.structure;
|
||||
const maxDrivers =
|
||||
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||
? structure.maxDrivers
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...(schedule !== undefined ? { schedule } : {}),
|
||||
scoringConfig,
|
||||
dropPolicy,
|
||||
stewardingConfig,
|
||||
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private buildScheduleFromTimings(
|
||||
config: LeagueConfigFormModel,
|
||||
): SeasonSchedule | undefined {
|
||||
const { timings } = config;
|
||||
if (!timings.seasonStartDate || !timings.raceStartTime) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startDate = new Date(timings.seasonStartDate);
|
||||
const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime);
|
||||
const timezoneId = timings.timezoneId ?? 'UTC';
|
||||
const timezone = new LeagueTimezone(timezoneId);
|
||||
|
||||
const plannedRounds =
|
||||
typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0
|
||||
? timings.roundsPlanned
|
||||
: timings.sessionCount;
|
||||
|
||||
const recurrence = (() => {
|
||||
const weekdays: WeekdaySet =
|
||||
timings.weekdays && timings.weekdays.length > 0
|
||||
? WeekdaySet.fromArray(
|
||||
timings.weekdays as unknown as Weekday[],
|
||||
)
|
||||
: WeekdaySet.fromArray(['Mon']);
|
||||
switch (timings.recurrenceStrategy) {
|
||||
case 'everyNWeeks':
|
||||
return RecurrenceStrategyFactory.everyNWeeks(
|
||||
timings.intervalWeeks ?? 2,
|
||||
weekdays,
|
||||
);
|
||||
case 'monthlyNthWeekday': {
|
||||
const pattern = new MonthlyRecurrencePattern({
|
||||
ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||
weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||
});
|
||||
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||
}
|
||||
case 'weekly':
|
||||
default:
|
||||
return RecurrenceStrategyFactory.weekly(weekdays);
|
||||
}
|
||||
})();
|
||||
|
||||
return new SeasonSchedule({
|
||||
startDate,
|
||||
timeOfDay,
|
||||
timezone,
|
||||
recurrence,
|
||||
plannedRounds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ListSeasonsForLeagueUseCase
|
||||
*/
|
||||
export class ListSeasonsForLeagueUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
query: ListSeasonsForLeagueQuery,
|
||||
): Promise<ListSeasonsForLeagueResultDTO> {
|
||||
const league = await this.leagueRepository.findById(query.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${query.leagueId}`);
|
||||
}
|
||||
|
||||
const seasons = await this.seasonRepository.listByLeague(league.id);
|
||||
const items: SeasonSummaryDTO[] = seasons.map((s) => ({
|
||||
seasonId: s.id,
|
||||
leagueId: s.leagueId,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
...(s.startDate !== undefined ? { startDate: s.startDate } : {}),
|
||||
...(s.endDate !== undefined ? { endDate: s.endDate } : {}),
|
||||
// League currently does not track primarySeasonId, so mark false for now.
|
||||
isPrimary: false,
|
||||
}));
|
||||
|
||||
return { items };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GetSeasonDetailsUseCase
|
||||
*/
|
||||
export class GetSeasonDetailsUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
) {}
|
||||
|
||||
async execute(query: GetSeasonDetailsQuery): Promise<SeasonDetailsDTO> {
|
||||
const league = await this.leagueRepository.findById(query.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${query.leagueId}`);
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(query.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
throw new Error(
|
||||
`Season ${query.seasonId} does not belong to league ${league.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
seasonId: season.id,
|
||||
leagueId: season.leagueId,
|
||||
gameId: season.gameId,
|
||||
name: season.name,
|
||||
status: season.status,
|
||||
...(season.startDate !== undefined ? { startDate: season.startDate } : {}),
|
||||
...(season.endDate !== undefined ? { endDate: season.endDate } : {}),
|
||||
...(season.maxDrivers !== undefined ? { maxDrivers: season.maxDrivers } : {}),
|
||||
...(season.schedule
|
||||
? {
|
||||
schedule: {
|
||||
startDate: season.schedule.startDate,
|
||||
plannedRounds: season.schedule.plannedRounds,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(season.scoringConfig
|
||||
? {
|
||||
scoring: {
|
||||
scoringPresetId: season.scoringConfig.scoringPresetId,
|
||||
customScoringEnabled:
|
||||
season.scoringConfig.customScoringEnabled ?? false,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(season.dropPolicy
|
||||
? {
|
||||
dropPolicy: {
|
||||
strategy: season.dropPolicy.strategy,
|
||||
...(season.dropPolicy.n !== undefined
|
||||
? { n: season.dropPolicy.n }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(season.stewardingConfig
|
||||
? {
|
||||
stewarding: {
|
||||
decisionMode: season.stewardingConfig.decisionMode,
|
||||
...(season.stewardingConfig.requiredVotes !== undefined
|
||||
? { requiredVotes: season.stewardingConfig.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: season.stewardingConfig.requireDefense,
|
||||
defenseTimeLimit: season.stewardingConfig.defenseTimeLimit,
|
||||
voteTimeLimit: season.stewardingConfig.voteTimeLimit,
|
||||
protestDeadlineHours:
|
||||
season.stewardingConfig.protestDeadlineHours,
|
||||
stewardingClosesHours:
|
||||
season.stewardingConfig.stewardingClosesHours,
|
||||
notifyAccusedOnProtest:
|
||||
season.stewardingConfig.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired:
|
||||
season.stewardingConfig.notifyOnVoteRequired,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ManageSeasonLifecycleUseCase
|
||||
*/
|
||||
export class ManageSeasonLifecycleUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
command: ManageSeasonLifecycleCommand,
|
||||
): Promise<ManageSeasonLifecycleResultDTO> {
|
||||
const league = await this.leagueRepository.findById(command.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${command.leagueId}`);
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(command.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
throw new Error(
|
||||
`Season ${command.seasonId} does not belong to league ${league.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
let updated: Season;
|
||||
switch (command.transition) {
|
||||
case 'activate':
|
||||
updated = season.activate();
|
||||
break;
|
||||
case 'complete':
|
||||
updated = season.complete();
|
||||
break;
|
||||
case 'archive':
|
||||
updated = season.archive();
|
||||
break;
|
||||
case 'cancel':
|
||||
updated = season.cancel();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported Season lifecycle transition`);
|
||||
}
|
||||
|
||||
await this.seasonRepository.update(updated);
|
||||
|
||||
return {
|
||||
seasonId: updated.id,
|
||||
status: updated.status,
|
||||
...(updated.startDate !== undefined ? { startDate: updated.startDate } : {}),
|
||||
...(updated.endDate !== undefined ? { endDate: updated.endDate } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ export interface DriverLiveryProps {
|
||||
userDecals: LiveryDecal[];
|
||||
leagueOverrides: DecalOverride[];
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
validatedAt?: Date;
|
||||
updatedAt: Date | undefined;
|
||||
validatedAt: Date | undefined;
|
||||
}
|
||||
|
||||
export class DriverLivery implements IEntity<string> {
|
||||
@@ -39,8 +39,8 @@ export class DriverLivery implements IEntity<string> {
|
||||
readonly userDecals: LiveryDecal[];
|
||||
readonly leagueOverrides: DecalOverride[];
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt?: Date;
|
||||
readonly validatedAt?: Date;
|
||||
readonly updatedAt: Date | undefined;
|
||||
readonly validatedAt: Date | undefined;
|
||||
|
||||
private constructor(props: DriverLiveryProps) {
|
||||
this.id = props.id;
|
||||
@@ -50,7 +50,7 @@ export class DriverLivery implements IEntity<string> {
|
||||
this.uploadedImageUrl = props.uploadedImageUrl;
|
||||
this.userDecals = props.userDecals;
|
||||
this.leagueOverrides = props.leagueOverrides;
|
||||
this.createdAt = props.createdAt;
|
||||
this.createdAt = props.createdAt ?? new Date();
|
||||
this.updatedAt = props.updatedAt;
|
||||
this.validatedAt = props.validatedAt;
|
||||
}
|
||||
@@ -101,9 +101,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: [...this.userDecals, decal],
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,9 +125,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: updatedDecals,
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,16 +145,23 @@ export class DriverLivery implements IEntity<string> {
|
||||
const index = this.userDecals.findIndex(d => d.id === decalId);
|
||||
|
||||
if (index === -1) {
|
||||
throw new RacingDomainError('Decal not found in livery');
|
||||
throw new RacingDomainValidationError('Decal not found in livery');
|
||||
}
|
||||
|
||||
const updatedDecals = [...this.userDecals];
|
||||
updatedDecals[index] = updatedDecal;
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: updatedDecals,
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,9 +184,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: this.userDecals,
|
||||
leagueOverrides: updatedOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,9 +206,16 @@ export class DriverLivery implements IEntity<string> {
|
||||
);
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: this.userDecals,
|
||||
leagueOverrides: updatedOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: new Date(),
|
||||
validatedAt: this.validatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,7 +233,15 @@ export class DriverLivery implements IEntity<string> {
|
||||
*/
|
||||
markAsValidated(): DriverLivery {
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
id: this.id,
|
||||
driverId: this.driverId,
|
||||
gameId: this.gameId,
|
||||
carId: this.carId,
|
||||
uploadedImageUrl: this.uploadedImageUrl,
|
||||
userDecals: this.userDecals,
|
||||
leagueOverrides: this.leagueOverrides,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
validatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface LiveryTemplateProps {
|
||||
baseImageUrl: string;
|
||||
adminDecals: LiveryDecal[];
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
updatedAt: Date | undefined;
|
||||
}
|
||||
|
||||
export class LiveryTemplate implements IEntity<string> {
|
||||
@@ -28,7 +28,7 @@ export class LiveryTemplate implements IEntity<string> {
|
||||
readonly baseImageUrl: string;
|
||||
readonly adminDecals: LiveryDecal[];
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt?: Date;
|
||||
readonly updatedAt: Date | undefined;
|
||||
|
||||
private constructor(props: LiveryTemplateProps) {
|
||||
this.id = props.id;
|
||||
@@ -37,7 +37,7 @@ export class LiveryTemplate implements IEntity<string> {
|
||||
this.carId = props.carId;
|
||||
this.baseImageUrl = props.baseImageUrl;
|
||||
this.adminDecals = props.adminDecals;
|
||||
this.createdAt = props.createdAt;
|
||||
this.createdAt = props.createdAt ?? new Date();
|
||||
this.updatedAt = props.updatedAt;
|
||||
}
|
||||
|
||||
@@ -113,9 +113,9 @@ export class LiveryTemplate implements IEntity<string> {
|
||||
*/
|
||||
updateDecal(decalId: string, updatedDecal: LiveryDecal): LiveryTemplate {
|
||||
const index = this.adminDecals.findIndex(d => d.id === decalId);
|
||||
|
||||
|
||||
if (index === -1) {
|
||||
throw new RacingDomainError('Decal not found in template');
|
||||
throw new RacingDomainValidationError('Decal not found in template');
|
||||
}
|
||||
|
||||
const updatedDecals = [...this.adminDecals];
|
||||
|
||||
@@ -19,9 +19,9 @@ export interface PrizeProps {
|
||||
driverId?: string;
|
||||
status: PrizeStatus;
|
||||
createdAt: Date;
|
||||
awardedAt?: Date;
|
||||
paidAt?: Date;
|
||||
description?: string;
|
||||
awardedAt: Date | undefined;
|
||||
paidAt: Date | undefined;
|
||||
description: string | undefined;
|
||||
}
|
||||
|
||||
export class Prize implements IEntity<string> {
|
||||
@@ -29,12 +29,12 @@ export class Prize implements IEntity<string> {
|
||||
readonly seasonId: string;
|
||||
readonly position: number;
|
||||
readonly amount: Money;
|
||||
readonly driverId?: string;
|
||||
readonly driverId: string | undefined;
|
||||
readonly status: PrizeStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly awardedAt?: Date;
|
||||
readonly paidAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly awardedAt: Date | undefined;
|
||||
readonly paidAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: PrizeProps) {
|
||||
this.id = props.id;
|
||||
@@ -43,7 +43,7 @@ export class Prize implements IEntity<string> {
|
||||
this.amount = props.amount;
|
||||
this.driverId = props.driverId;
|
||||
this.status = props.status;
|
||||
this.createdAt = props.createdAt;
|
||||
this.createdAt = props.createdAt ?? new Date();
|
||||
this.awardedAt = props.awardedAt;
|
||||
this.paidAt = props.paidAt;
|
||||
this.description = props.description;
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
export type SeasonStatus = 'planned' | 'active' | 'completed';
|
||||
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
export type SeasonStatus =
|
||||
| 'planned'
|
||||
| 'active'
|
||||
| 'completed'
|
||||
| 'archived'
|
||||
| 'cancelled';
|
||||
|
||||
import {
|
||||
RacingDomainInvariantError,
|
||||
RacingDomainValidationError,
|
||||
} from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
|
||||
import type { SeasonSchedule } from '../value-objects/SeasonSchedule';
|
||||
import type { SeasonScoringConfig } from '../value-objects/SeasonScoringConfig';
|
||||
import type { SeasonDropPolicy } from '../value-objects/SeasonDropPolicy';
|
||||
import type { SeasonStewardingConfig } from '../value-objects/SeasonStewardingConfig';
|
||||
|
||||
export class Season implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
@@ -13,6 +25,11 @@ export class Season implements IEntity<string> {
|
||||
readonly status: SeasonStatus;
|
||||
readonly startDate: Date | undefined;
|
||||
readonly endDate: Date | undefined;
|
||||
readonly schedule: SeasonSchedule | undefined;
|
||||
readonly scoringConfig: SeasonScoringConfig | undefined;
|
||||
readonly dropPolicy: SeasonDropPolicy | undefined;
|
||||
readonly stewardingConfig: SeasonStewardingConfig | undefined;
|
||||
readonly maxDrivers: number | undefined;
|
||||
|
||||
private constructor(props: {
|
||||
id: string;
|
||||
@@ -24,6 +41,11 @@ export class Season implements IEntity<string> {
|
||||
status: SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
}) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
@@ -34,6 +56,11 @@ export class Season implements IEntity<string> {
|
||||
this.status = props.status;
|
||||
this.startDate = props.startDate;
|
||||
this.endDate = props.endDate;
|
||||
this.schedule = props.schedule;
|
||||
this.scoringConfig = props.scoringConfig;
|
||||
this.dropPolicy = props.dropPolicy;
|
||||
this.stewardingConfig = props.stewardingConfig;
|
||||
this.maxDrivers = props.maxDrivers;
|
||||
}
|
||||
|
||||
static create(props: {
|
||||
@@ -46,6 +73,11 @@ export class Season implements IEntity<string> {
|
||||
status?: SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
}): Season {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Season ID is required');
|
||||
@@ -75,6 +107,15 @@ export class Season implements IEntity<string> {
|
||||
status,
|
||||
...(props.startDate !== undefined ? { startDate: props.startDate } : {}),
|
||||
...(props.endDate !== undefined ? { endDate: props.endDate } : {}),
|
||||
...(props.schedule !== undefined ? { schedule: props.schedule } : {}),
|
||||
...(props.scoringConfig !== undefined
|
||||
? { scoringConfig: props.scoringConfig }
|
||||
: {}),
|
||||
...(props.dropPolicy !== undefined ? { dropPolicy: props.dropPolicy } : {}),
|
||||
...(props.stewardingConfig !== undefined
|
||||
? { stewardingConfig: props.stewardingConfig }
|
||||
: {}),
|
||||
...(props.maxDrivers !== undefined ? { maxDrivers: props.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,4 +139,281 @@ export class Season implements IEntity<string> {
|
||||
isCompleted(): boolean {
|
||||
return this.status === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is planned (not yet active)
|
||||
*/
|
||||
isPlanned(): boolean {
|
||||
return this.status === 'planned';
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the season from planned state.
|
||||
*/
|
||||
activate(): Season {
|
||||
if (this.status !== 'planned') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only planned seasons can be activated',
|
||||
);
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'active',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {
|
||||
startDate: new Date(),
|
||||
}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the season as completed.
|
||||
*/
|
||||
complete(): Season {
|
||||
if (this.status !== 'active') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only active seasons can be completed',
|
||||
);
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'completed',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {
|
||||
endDate: new Date(),
|
||||
}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a completed season.
|
||||
*/
|
||||
archive(): Season {
|
||||
if (!this.isCompleted()) {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Only completed seasons can be archived',
|
||||
);
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'archived',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a planned or active season.
|
||||
*/
|
||||
cancel(): Season {
|
||||
if (this.status === 'completed' || this.status === 'archived') {
|
||||
throw new RacingDomainInvariantError(
|
||||
'Cannot cancel a completed or archived season',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.status === 'cancelled') {
|
||||
return this;
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: 'cancelled',
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {
|
||||
endDate: new Date(),
|
||||
}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update schedule while keeping other properties intact.
|
||||
*/
|
||||
withSchedule(schedule: SeasonSchedule): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
schedule,
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scoring configuration for the season.
|
||||
*/
|
||||
withScoringConfig(scoringConfig: SeasonScoringConfig): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
scoringConfig,
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update drop policy for the season.
|
||||
*/
|
||||
withDropPolicy(dropPolicy: SeasonDropPolicy): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
dropPolicy,
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stewarding configuration for the season.
|
||||
*/
|
||||
withStewardingConfig(stewardingConfig: SeasonStewardingConfig): Season {
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
stewardingConfig,
|
||||
...(this.maxDrivers !== undefined ? { maxDrivers: this.maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update max driver capacity for the season.
|
||||
*/
|
||||
withMaxDrivers(maxDrivers: number | undefined): Season {
|
||||
if (maxDrivers !== undefined && maxDrivers <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'Season maxDrivers must be greater than 0 when provided',
|
||||
);
|
||||
}
|
||||
|
||||
return Season.create({
|
||||
id: this.id,
|
||||
leagueId: this.leagueId,
|
||||
gameId: this.gameId,
|
||||
name: this.name,
|
||||
...(this.year !== undefined ? { year: this.year } : {}),
|
||||
...(this.order !== undefined ? { order: this.order } : {}),
|
||||
status: this.status,
|
||||
...(this.startDate !== undefined ? { startDate: this.startDate } : {}),
|
||||
...(this.endDate !== undefined ? { endDate: this.endDate } : {}),
|
||||
...(this.schedule !== undefined ? { schedule: this.schedule } : {}),
|
||||
...(this.scoringConfig !== undefined
|
||||
? { scoringConfig: this.scoringConfig }
|
||||
: {}),
|
||||
...(this.dropPolicy !== undefined ? { dropPolicy: this.dropPolicy } : {}),
|
||||
...(this.stewardingConfig !== undefined
|
||||
? { stewardingConfig: this.stewardingConfig }
|
||||
: {}),
|
||||
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,40 +11,53 @@ import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
export type SponsorshipTier = 'main' | 'secondary';
|
||||
export type SponsorshipStatus = 'pending' | 'active' | 'cancelled';
|
||||
export type SponsorshipStatus = 'pending' | 'active' | 'ended' | 'cancelled';
|
||||
|
||||
export interface SeasonSponsorshipProps {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
/**
|
||||
* Optional denormalized leagueId for fast league-level aggregations.
|
||||
* Must always match the owning Season's leagueId when present.
|
||||
*/
|
||||
leagueId?: string;
|
||||
sponsorId: string;
|
||||
tier: SponsorshipTier;
|
||||
pricing: Money;
|
||||
status: SponsorshipStatus;
|
||||
createdAt: Date;
|
||||
activatedAt?: Date;
|
||||
endedAt?: Date;
|
||||
cancelledAt?: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class SeasonSponsorship implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly seasonId: string;
|
||||
readonly leagueId: string | undefined;
|
||||
readonly sponsorId: string;
|
||||
readonly tier: SponsorshipTier;
|
||||
readonly pricing: Money;
|
||||
readonly status: SponsorshipStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly activatedAt: Date | undefined;
|
||||
readonly endedAt: Date | undefined;
|
||||
readonly cancelledAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: SeasonSponsorshipProps) {
|
||||
this.id = props.id;
|
||||
this.seasonId = props.seasonId;
|
||||
this.leagueId = props.leagueId;
|
||||
this.sponsorId = props.sponsorId;
|
||||
this.tier = props.tier;
|
||||
this.pricing = props.pricing;
|
||||
this.status = props.status;
|
||||
this.createdAt = props.createdAt;
|
||||
this.activatedAt = props.activatedAt;
|
||||
this.endedAt = props.endedAt;
|
||||
this.cancelledAt = props.cancelledAt;
|
||||
this.description = props.description;
|
||||
}
|
||||
|
||||
@@ -57,12 +70,15 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
return new SeasonSponsorship({
|
||||
id: props.id,
|
||||
seasonId: props.seasonId,
|
||||
...(props.leagueId !== undefined ? { leagueId: props.leagueId } : {}),
|
||||
sponsorId: props.sponsorId,
|
||||
tier: props.tier,
|
||||
pricing: props.pricing,
|
||||
status: props.status ?? 'pending',
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
...(props.activatedAt !== undefined ? { activatedAt: props.activatedAt } : {}),
|
||||
...(props.endedAt !== undefined ? { endedAt: props.endedAt } : {}),
|
||||
...(props.cancelledAt !== undefined ? { cancelledAt: props.cancelledAt } : {}),
|
||||
...(props.description !== undefined ? { description: props.description } : {}),
|
||||
});
|
||||
}
|
||||
@@ -105,15 +121,56 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
throw new RacingDomainInvariantError('Cannot activate a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
if (this.status === 'ended') {
|
||||
throw new RacingDomainInvariantError('Cannot activate an ended SeasonSponsorship');
|
||||
}
|
||||
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'active',
|
||||
createdAt: this.createdAt,
|
||||
activatedAt: new Date(),
|
||||
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
||||
...(this.cancelledAt !== undefined ? { cancelledAt: this.cancelledAt } : {}),
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the sponsorship as ended (completed term)
|
||||
*/
|
||||
end(): SeasonSponsorship {
|
||||
if (this.status === 'cancelled') {
|
||||
throw new RacingDomainInvariantError('Cannot end a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
if (this.status === 'ended') {
|
||||
throw new RacingDomainInvariantError('SeasonSponsorship is already ended');
|
||||
}
|
||||
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'ended',
|
||||
createdAt: this.createdAt,
|
||||
...(this.activatedAt !== undefined ? { activatedAt: this.activatedAt } : {}),
|
||||
endedAt: new Date(),
|
||||
...(this.cancelledAt !== undefined ? { cancelledAt: this.cancelledAt } : {}),
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
@@ -135,22 +192,55 @@ export class SeasonSponsorship implements IEntity<string> {
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing: this.pricing,
|
||||
status: 'cancelled',
|
||||
createdAt: this.createdAt,
|
||||
...(this.activatedAt !== undefined ? { activatedAt: this.activatedAt } : {}),
|
||||
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
||||
cancelledAt: new Date(),
|
||||
};
|
||||
|
||||
const withActivated =
|
||||
this.activatedAt !== undefined
|
||||
? { ...base, activatedAt: this.activatedAt }
|
||||
: base;
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...withActivated, description: this.description }
|
||||
: withActivated;
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pricing/terms when allowed
|
||||
*/
|
||||
withPricing(pricing: Money): SeasonSponsorship {
|
||||
if (pricing.amount <= 0) {
|
||||
throw new RacingDomainValidationError('SeasonSponsorship pricing must be greater than zero');
|
||||
}
|
||||
|
||||
if (this.status === 'cancelled' || this.status === 'ended') {
|
||||
throw new RacingDomainInvariantError('Cannot update pricing for ended or cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
const base: SeasonSponsorshipProps = {
|
||||
id: this.id,
|
||||
seasonId: this.seasonId,
|
||||
...(this.leagueId !== undefined ? { leagueId: this.leagueId } : {}),
|
||||
sponsorId: this.sponsorId,
|
||||
tier: this.tier,
|
||||
pricing,
|
||||
status: this.status,
|
||||
createdAt: this.createdAt,
|
||||
...(this.activatedAt !== undefined ? { activatedAt: this.activatedAt } : {}),
|
||||
...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),
|
||||
...(this.cancelledAt !== undefined ? { cancelledAt: this.cancelledAt } : {}),
|
||||
};
|
||||
|
||||
const next: SeasonSponsorshipProps =
|
||||
this.description !== undefined
|
||||
? { ...base, description: this.description }
|
||||
: base;
|
||||
|
||||
return new SeasonSponsorship(next);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ export interface TransactionProps {
|
||||
netAmount: Money;
|
||||
status: TransactionStatus;
|
||||
createdAt: Date;
|
||||
completedAt?: Date;
|
||||
description?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
completedAt: Date | undefined;
|
||||
description: string | undefined;
|
||||
metadata: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
export class Transaction implements IEntity<string> {
|
||||
@@ -41,9 +41,9 @@ export class Transaction implements IEntity<string> {
|
||||
readonly netAmount: Money;
|
||||
readonly status: TransactionStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly completedAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly metadata?: Record<string, unknown>;
|
||||
readonly completedAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
readonly metadata: Record<string, unknown> | undefined;
|
||||
|
||||
private constructor(props: TransactionProps) {
|
||||
this.id = props.id;
|
||||
|
||||
@@ -2,6 +2,34 @@ import type { Season } from '../entities/Season';
|
||||
|
||||
export interface ISeasonRepository {
|
||||
findById(id: string): Promise<Season | null>;
|
||||
/**
|
||||
* Backward-compatible alias retained for existing callers.
|
||||
* Prefer listByLeague for new usage.
|
||||
*/
|
||||
findByLeagueId(leagueId: string): Promise<Season[]>;
|
||||
/**
|
||||
* Backward-compatible alias retained for existing callers.
|
||||
* Prefer add for new usage.
|
||||
*/
|
||||
create(season: Season): Promise<Season>;
|
||||
|
||||
/**
|
||||
* Add a new Season aggregate.
|
||||
*/
|
||||
add(season: Season): Promise<void>;
|
||||
|
||||
/**
|
||||
* Persist changes to an existing Season aggregate.
|
||||
*/
|
||||
update(season: Season): Promise<void>;
|
||||
|
||||
/**
|
||||
* List all Seasons for a given League.
|
||||
*/
|
||||
listByLeague(leagueId: string): Promise<Season[]>;
|
||||
|
||||
/**
|
||||
* List Seasons for a League that are currently active.
|
||||
*/
|
||||
listActiveByLeague(leagueId: string): Promise<Season[]>;
|
||||
}
|
||||
@@ -9,6 +9,12 @@ import type { SeasonSponsorship, SponsorshipTier } from '../entities/SeasonSpons
|
||||
export interface ISeasonSponsorshipRepository {
|
||||
findById(id: string): Promise<SeasonSponsorship | null>;
|
||||
findBySeasonId(seasonId: string): Promise<SeasonSponsorship[]>;
|
||||
/**
|
||||
* Convenience lookup for aggregating sponsorships at league level.
|
||||
* Implementations should rely on the denormalized leagueId where present,
|
||||
* falling back to joining through Seasons if needed.
|
||||
*/
|
||||
findByLeagueId(leagueId: string): Promise<SeasonSponsorship[]>;
|
||||
findBySponsorId(sponsorId: string): Promise<SeasonSponsorship[]>;
|
||||
findBySeasonAndTier(seasonId: string, tier: SponsorshipTier): Promise<SeasonSponsorship[]>;
|
||||
create(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship>;
|
||||
|
||||
@@ -9,10 +9,20 @@ export interface MonthlyRecurrencePatternProps {
|
||||
export class MonthlyRecurrencePattern implements IValueObject<MonthlyRecurrencePatternProps> {
|
||||
readonly ordinal: 1 | 2 | 3 | 4;
|
||||
readonly weekday: Weekday;
|
||||
|
||||
constructor(ordinal: 1 | 2 | 3 | 4, weekday: Weekday) {
|
||||
this.ordinal = ordinal;
|
||||
this.weekday = weekday;
|
||||
|
||||
constructor(ordinal: 1 | 2 | 3 | 4, weekday: Weekday);
|
||||
constructor(props: MonthlyRecurrencePatternProps);
|
||||
constructor(
|
||||
ordinalOrProps: 1 | 2 | 3 | 4 | MonthlyRecurrencePatternProps,
|
||||
weekday?: Weekday,
|
||||
) {
|
||||
if (typeof ordinalOrProps === 'object') {
|
||||
this.ordinal = ordinalOrProps.ordinal;
|
||||
this.weekday = ordinalOrProps.weekday;
|
||||
} else {
|
||||
this.ordinal = ordinalOrProps;
|
||||
this.weekday = weekday as Weekday;
|
||||
}
|
||||
}
|
||||
|
||||
get props(): MonthlyRecurrencePatternProps {
|
||||
|
||||
59
packages/racing/domain/value-objects/SeasonDropPolicy.ts
Normal file
59
packages/racing/domain/value-objects/SeasonDropPolicy.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export type SeasonDropStrategy = 'none' | 'bestNResults' | 'dropWorstN';
|
||||
|
||||
export interface SeasonDropPolicyProps {
|
||||
strategy: SeasonDropStrategy;
|
||||
/**
|
||||
* Number of results to consider for strategies that require a count.
|
||||
* - bestNResults: keep best N
|
||||
* - dropWorstN: drop worst N
|
||||
*/
|
||||
n?: number;
|
||||
}
|
||||
|
||||
export class SeasonDropPolicy implements IValueObject<SeasonDropPolicyProps> {
|
||||
readonly strategy: SeasonDropStrategy;
|
||||
readonly n?: number;
|
||||
|
||||
constructor(props: SeasonDropPolicyProps) {
|
||||
if (!props.strategy) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonDropPolicy.strategy is required',
|
||||
);
|
||||
}
|
||||
|
||||
if (props.strategy === 'bestNResults' || props.strategy === 'dropWorstN') {
|
||||
if (props.n === undefined || !Number.isInteger(props.n) || props.n <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonDropPolicy.n must be a positive integer when using bestNResults or dropWorstN',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.strategy === 'none' && props.n !== undefined) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonDropPolicy.n must be undefined when strategy is none',
|
||||
);
|
||||
}
|
||||
|
||||
this.strategy = props.strategy;
|
||||
if (props.n !== undefined) {
|
||||
this.n = props.n;
|
||||
}
|
||||
}
|
||||
|
||||
get props(): SeasonDropPolicyProps {
|
||||
return {
|
||||
strategy: this.strategy,
|
||||
...(this.n !== undefined ? { n: this.n } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonDropPolicyProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return a.strategy === b.strategy && a.n === b.n;
|
||||
}
|
||||
}
|
||||
66
packages/racing/domain/value-objects/SeasonScoringConfig.ts
Normal file
66
packages/racing/domain/value-objects/SeasonScoringConfig.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
/**
|
||||
* Value Object: SeasonScoringConfig
|
||||
*
|
||||
* Represents the scoring configuration owned by a Season.
|
||||
* It is intentionally lightweight and primarily captures which
|
||||
* preset (or custom mode) is applied for this Season.
|
||||
*
|
||||
* Detailed championship scoring rules are still modeled via
|
||||
* `LeagueScoringConfig` and related types.
|
||||
*/
|
||||
export interface SeasonScoringConfigProps {
|
||||
/**
|
||||
* Identifier of the scoring preset applied to this Season.
|
||||
* Examples:
|
||||
* - 'sprint-main-driver'
|
||||
* - 'club-default'
|
||||
* - 'endurance-main-double'
|
||||
* - 'custom'
|
||||
*/
|
||||
scoringPresetId: string;
|
||||
|
||||
/**
|
||||
* Whether the Season uses custom scoring rather than a pure preset.
|
||||
* When true, `scoringPresetId` acts as a label rather than a strict preset key.
|
||||
*/
|
||||
customScoringEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class SeasonScoringConfig
|
||||
implements IValueObject<SeasonScoringConfigProps>
|
||||
{
|
||||
readonly scoringPresetId: string;
|
||||
readonly customScoringEnabled: boolean;
|
||||
|
||||
constructor(params: SeasonScoringConfigProps) {
|
||||
if (!params.scoringPresetId || params.scoringPresetId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonScoringConfig.scoringPresetId must be a non-empty string',
|
||||
);
|
||||
}
|
||||
|
||||
this.scoringPresetId = params.scoringPresetId.trim();
|
||||
this.customScoringEnabled = Boolean(params.customScoringEnabled);
|
||||
}
|
||||
|
||||
get props(): SeasonScoringConfigProps {
|
||||
return {
|
||||
scoringPresetId: this.scoringPresetId,
|
||||
...(this.customScoringEnabled
|
||||
? { customScoringEnabled: this.customScoringEnabled }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonScoringConfigProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.scoringPresetId === b.scoringPresetId &&
|
||||
Boolean(a.customScoringEnabled) === Boolean(b.customScoringEnabled)
|
||||
);
|
||||
}
|
||||
}
|
||||
131
packages/racing/domain/value-objects/SeasonStewardingConfig.ts
Normal file
131
packages/racing/domain/value-objects/SeasonStewardingConfig.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { RacingDomainValidationError } from '../errors/RacingDomainError';
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
import type { StewardingDecisionMode } from '../entities/League';
|
||||
|
||||
export interface SeasonStewardingConfigProps {
|
||||
decisionMode: StewardingDecisionMode;
|
||||
requiredVotes?: number;
|
||||
requireDefense: boolean;
|
||||
defenseTimeLimit: number;
|
||||
voteTimeLimit: number;
|
||||
protestDeadlineHours: number;
|
||||
stewardingClosesHours: number;
|
||||
notifyAccusedOnProtest: boolean;
|
||||
notifyOnVoteRequired: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object: SeasonStewardingConfig
|
||||
*
|
||||
* Encapsulates stewarding configuration owned by a Season.
|
||||
* Shape intentionally mirrors LeagueStewardingFormDTO used by the wizard.
|
||||
*/
|
||||
export class SeasonStewardingConfig
|
||||
implements IValueObject<SeasonStewardingConfigProps>
|
||||
{
|
||||
readonly decisionMode: StewardingDecisionMode;
|
||||
readonly requiredVotes?: number;
|
||||
readonly requireDefense: boolean;
|
||||
readonly defenseTimeLimit: number;
|
||||
readonly voteTimeLimit: number;
|
||||
readonly protestDeadlineHours: number;
|
||||
readonly stewardingClosesHours: number;
|
||||
readonly notifyAccusedOnProtest: boolean;
|
||||
readonly notifyOnVoteRequired: boolean;
|
||||
|
||||
constructor(props: SeasonStewardingConfigProps) {
|
||||
if (!props.decisionMode) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.decisionMode is required',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(props.decisionMode === 'steward_vote' ||
|
||||
props.decisionMode === 'member_vote' ||
|
||||
props.decisionMode === 'steward_veto' ||
|
||||
props.decisionMode === 'member_veto') &&
|
||||
(props.requiredVotes === undefined ||
|
||||
!Number.isInteger(props.requiredVotes) ||
|
||||
props.requiredVotes <= 0)
|
||||
) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.requiredVotes must be a positive integer for voting/veto modes',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(props.defenseTimeLimit) || props.defenseTimeLimit < 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.defenseTimeLimit must be a non-negative integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(props.voteTimeLimit) || props.voteTimeLimit <= 0) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.voteTimeLimit must be a positive integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Number.isInteger(props.protestDeadlineHours) ||
|
||||
props.protestDeadlineHours <= 0
|
||||
) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.protestDeadlineHours must be a positive integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Number.isInteger(props.stewardingClosesHours) ||
|
||||
props.stewardingClosesHours <= 0
|
||||
) {
|
||||
throw new RacingDomainValidationError(
|
||||
'SeasonStewardingConfig.stewardingClosesHours must be a positive integer (hours)',
|
||||
);
|
||||
}
|
||||
|
||||
this.decisionMode = props.decisionMode;
|
||||
if (props.requiredVotes !== undefined) {
|
||||
this.requiredVotes = props.requiredVotes;
|
||||
}
|
||||
this.requireDefense = props.requireDefense;
|
||||
this.defenseTimeLimit = props.defenseTimeLimit;
|
||||
this.voteTimeLimit = props.voteTimeLimit;
|
||||
this.protestDeadlineHours = props.protestDeadlineHours;
|
||||
this.stewardingClosesHours = props.stewardingClosesHours;
|
||||
this.notifyAccusedOnProtest = props.notifyAccusedOnProtest;
|
||||
this.notifyOnVoteRequired = props.notifyOnVoteRequired;
|
||||
}
|
||||
|
||||
get props(): SeasonStewardingConfigProps {
|
||||
return {
|
||||
decisionMode: this.decisionMode,
|
||||
...(this.requiredVotes !== undefined
|
||||
? { requiredVotes: this.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: this.requireDefense,
|
||||
defenseTimeLimit: this.defenseTimeLimit,
|
||||
voteTimeLimit: this.voteTimeLimit,
|
||||
protestDeadlineHours: this.protestDeadlineHours,
|
||||
stewardingClosesHours: this.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: this.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: this.notifyOnVoteRequired,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: IValueObject<SeasonStewardingConfigProps>): boolean {
|
||||
const a = this.props;
|
||||
const b = other.props;
|
||||
return (
|
||||
a.decisionMode === b.decisionMode &&
|
||||
a.requiredVotes === b.requiredVotes &&
|
||||
a.requireDefense === b.requireDefense &&
|
||||
a.defenseTimeLimit === b.defenseTimeLimit &&
|
||||
a.voteTimeLimit === b.voteTimeLimit &&
|
||||
a.protestDeadlineHours === b.protestDeadlineHours &&
|
||||
a.stewardingClosesHours === b.stewardingClosesHours &&
|
||||
a.notifyAccusedOnProtest === b.notifyAccusedOnProtest &&
|
||||
a.notifyOnVoteRequired === b.notifyOnVoteRequired
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ export interface WeekdaySetProps {
|
||||
export class WeekdaySet implements IValueObject<WeekdaySetProps> {
|
||||
private readonly days: Weekday[];
|
||||
|
||||
static fromArray(days: Weekday[]): WeekdaySet {
|
||||
return new WeekdaySet(days);
|
||||
}
|
||||
|
||||
constructor(days: Weekday[]) {
|
||||
if (!Array.isArray(days) || days.length === 0) {
|
||||
throw new RacingDomainValidationError('WeekdaySet requires at least one weekday');
|
||||
|
||||
@@ -282,10 +282,34 @@ export class InMemorySeasonRepository implements ISeasonRepository {
|
||||
}
|
||||
|
||||
async create(season: Season): Promise<Season> {
|
||||
// Backward-compatible alias for add()
|
||||
this.seasons.push(season);
|
||||
return season;
|
||||
}
|
||||
|
||||
async add(season: Season): Promise<void> {
|
||||
this.seasons.push(season);
|
||||
}
|
||||
|
||||
async update(season: Season): Promise<void> {
|
||||
const index = this.seasons.findIndex((s) => s.id === season.id);
|
||||
if (index === -1) {
|
||||
this.seasons.push(season);
|
||||
return;
|
||||
}
|
||||
this.seasons[index] = season;
|
||||
}
|
||||
|
||||
async listByLeague(leagueId: string): Promise<Season[]> {
|
||||
return this.seasons.filter((s) => s.leagueId === leagueId);
|
||||
}
|
||||
|
||||
async listActiveByLeague(leagueId: string): Promise<Season[]> {
|
||||
return this.seasons.filter(
|
||||
(s) => s.leagueId === leagueId && s.status === 'active',
|
||||
);
|
||||
}
|
||||
|
||||
seed(season: Season): void {
|
||||
this.seasons.push(season);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRe
|
||||
return Array.from(this.sponsorships.values()).filter(s => s.seasonId === seasonId);
|
||||
}
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<SeasonSponsorship[]> {
|
||||
return Array.from(this.sponsorships.values()).filter(s => s.leagueId === leagueId);
|
||||
}
|
||||
|
||||
async findBySponsorId(sponsorId: string): Promise<SeasonSponsorship[]> {
|
||||
return Array.from(this.sponsorships.values()).filter(s => s.sponsorId === sponsorId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user