This commit is contained in:
2025-12-12 14:23:40 +01:00
parent 6a88fe93ab
commit 2cd3bfbb47
58 changed files with 2866 additions and 260 deletions

View File

@@ -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);
}