268 lines
8.2 KiB
TypeScript
268 lines
8.2 KiB
TypeScript
/**
|
|
* Domain Entity: SeasonSponsorship
|
|
*
|
|
* Represents a sponsorship relationship between a Sponsor and a Season.
|
|
* Aggregate root for managing sponsorship slots and pricing.
|
|
*/
|
|
|
|
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
|
import type { IEntity } from '@gridpilot/shared/domain';
|
|
|
|
import type { Money } from '../value-objects/Money';
|
|
|
|
export type SponsorshipTier = 'main' | 'secondary';
|
|
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;
|
|
}
|
|
|
|
static create(props: Omit<SeasonSponsorshipProps, 'createdAt' | 'status'> & {
|
|
createdAt?: Date;
|
|
status?: SponsorshipStatus;
|
|
}): SeasonSponsorship {
|
|
this.validate(props);
|
|
|
|
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 } : {}),
|
|
});
|
|
}
|
|
|
|
private static validate(props: Omit<SeasonSponsorshipProps, 'createdAt' | 'status'>): void {
|
|
if (!props.id || props.id.trim().length === 0) {
|
|
throw new RacingDomainValidationError('SeasonSponsorship ID is required');
|
|
}
|
|
|
|
if (!props.seasonId || props.seasonId.trim().length === 0) {
|
|
throw new RacingDomainValidationError('SeasonSponsorship seasonId is required');
|
|
}
|
|
|
|
if (!props.sponsorId || props.sponsorId.trim().length === 0) {
|
|
throw new RacingDomainValidationError('SeasonSponsorship sponsorId is required');
|
|
}
|
|
|
|
if (!props.tier) {
|
|
throw new RacingDomainValidationError('SeasonSponsorship tier is required');
|
|
}
|
|
|
|
if (!props.pricing) {
|
|
throw new RacingDomainValidationError('SeasonSponsorship pricing is required');
|
|
}
|
|
|
|
if (props.pricing.amount <= 0) {
|
|
throw new RacingDomainValidationError('SeasonSponsorship pricing must be greater than zero');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Activate the sponsorship
|
|
*/
|
|
activate(): SeasonSponsorship {
|
|
if (this.status === 'active') {
|
|
throw new RacingDomainInvariantError('SeasonSponsorship is already active');
|
|
}
|
|
|
|
if (this.status === 'cancelled') {
|
|
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 =
|
|
this.description !== undefined
|
|
? { ...base, description: this.description }
|
|
: base;
|
|
|
|
return new SeasonSponsorship(next);
|
|
}
|
|
|
|
/**
|
|
* Cancel the sponsorship
|
|
*/
|
|
cancel(): SeasonSponsorship {
|
|
if (this.status === 'cancelled') {
|
|
throw new RacingDomainInvariantError('SeasonSponsorship is already cancelled');
|
|
}
|
|
|
|
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 next: SeasonSponsorshipProps =
|
|
this.description !== undefined
|
|
? { ...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);
|
|
}
|
|
|
|
/**
|
|
* Check if sponsorship is active
|
|
*/
|
|
isActive(): boolean {
|
|
return this.status === 'active';
|
|
}
|
|
|
|
/**
|
|
* Get the platform fee for this sponsorship
|
|
*/
|
|
getPlatformFee(): Money {
|
|
return this.pricing.calculatePlatformFee();
|
|
}
|
|
|
|
/**
|
|
* Get the net amount after platform fee
|
|
*/
|
|
getNetAmount(): Money {
|
|
return this.pricing.calculateNetAmount();
|
|
}
|
|
} |