/** * Domain Entity: SponsorshipRequest * * Represents a sponsorship application from a Sponsor to any sponsorable entity * (driver, team, race, or league/season). The entity owner must approve/reject. */ import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError'; import type { IEntity } from '@core/shared/domain'; import type { Money } from '../value-objects/Money'; import type { SponsorshipTier } from './season/SeasonSponsorship'; export type SponsorableEntityType = 'driver' | 'team' | 'race' | 'season'; export type SponsorshipRequestStatus = 'pending' | 'accepted' | 'rejected' | 'withdrawn'; export interface SponsorshipRequestProps { id: string; sponsorId: string; entityType: SponsorableEntityType; entityId: string; tier: SponsorshipTier; offeredAmount: Money; message?: string; status: SponsorshipRequestStatus; createdAt: Date; respondedAt?: Date; respondedBy?: string; // driverId of the person who accepted/rejected rejectionReason?: string; } export class SponsorshipRequest implements IEntity { readonly id: string; readonly sponsorId: string; readonly entityType: SponsorableEntityType; readonly entityId: string; readonly tier: SponsorshipTier; readonly offeredAmount: Money; readonly message: string | undefined; readonly status: SponsorshipRequestStatus; readonly createdAt: Date; readonly respondedAt: Date | undefined; readonly respondedBy: string | undefined; readonly rejectionReason: string | undefined; private constructor(props: SponsorshipRequestProps) { this.id = props.id; this.sponsorId = props.sponsorId; this.entityType = props.entityType; this.entityId = props.entityId; this.tier = props.tier; this.offeredAmount = props.offeredAmount; this.message = props.message; this.status = props.status; this.createdAt = props.createdAt; this.respondedAt = props.respondedAt; this.respondedBy = props.respondedBy; this.rejectionReason = props.rejectionReason; } static create(props: Omit & { createdAt?: Date; status?: SponsorshipRequestStatus; }): SponsorshipRequest { this.validate(props); return new SponsorshipRequest({ ...props, createdAt: props.createdAt ?? new Date(), status: props.status ?? 'pending', }); } private static validate(props: Omit): void { if (!props.id || props.id.trim().length === 0) { throw new RacingDomainValidationError('SponsorshipRequest ID is required'); } if (!props.sponsorId || props.sponsorId.trim().length === 0) { throw new RacingDomainValidationError('SponsorshipRequest sponsorId is required'); } if (!props.entityType) { throw new RacingDomainValidationError('SponsorshipRequest entityType is required'); } if (!props.entityId || props.entityId.trim().length === 0) { throw new RacingDomainValidationError('SponsorshipRequest entityId is required'); } if (!props.tier) { throw new RacingDomainValidationError('SponsorshipRequest tier is required'); } if (!props.offeredAmount) { throw new RacingDomainValidationError('SponsorshipRequest offeredAmount is required'); } if (props.offeredAmount.amount <= 0) { throw new RacingDomainValidationError('SponsorshipRequest offeredAmount must be greater than zero'); } } /** * Accept the sponsorship request */ accept(respondedBy: string): SponsorshipRequest { if (this.status !== 'pending') { throw new RacingDomainInvariantError(`Cannot accept a ${this.status} sponsorship request`); } if (!respondedBy || respondedBy.trim().length === 0) { throw new RacingDomainValidationError('respondedBy is required when accepting'); } const base: SponsorshipRequestProps = { id: this.id, sponsorId: this.sponsorId, entityType: this.entityType, entityId: this.entityId, tier: this.tier, offeredAmount: this.offeredAmount, status: 'accepted', createdAt: this.createdAt, respondedAt: new Date(), respondedBy, }; const withMessage = this.message !== undefined ? { ...base, message: this.message } : base; const next: SponsorshipRequestProps = this.rejectionReason !== undefined ? { ...withMessage, rejectionReason: this.rejectionReason } : withMessage; return new SponsorshipRequest(next); } /** * Reject the sponsorship request */ reject(respondedBy: string, reason?: string): SponsorshipRequest { if (this.status !== 'pending') { throw new RacingDomainInvariantError(`Cannot reject a ${this.status} sponsorship request`); } if (!respondedBy || respondedBy.trim().length === 0) { throw new RacingDomainValidationError('respondedBy is required when rejecting'); } const base: SponsorshipRequestProps = { id: this.id, sponsorId: this.sponsorId, entityType: this.entityType, entityId: this.entityId, tier: this.tier, offeredAmount: this.offeredAmount, status: 'rejected', createdAt: this.createdAt, respondedAt: new Date(), respondedBy, }; const withMessage = this.message !== undefined ? { ...base, message: this.message } : base; const next: SponsorshipRequestProps = reason !== undefined ? { ...withMessage, rejectionReason: reason } : withMessage; return new SponsorshipRequest(next); } /** * Withdraw the sponsorship request (by the sponsor) */ withdraw(): SponsorshipRequest { if (this.status !== 'pending') { throw new RacingDomainInvariantError(`Cannot withdraw a ${this.status} sponsorship request`); } const base: SponsorshipRequestProps = { id: this.id, sponsorId: this.sponsorId, entityType: this.entityType, entityId: this.entityId, tier: this.tier, offeredAmount: this.offeredAmount, status: 'withdrawn', createdAt: this.createdAt, respondedAt: new Date(), }; const withRespondedBy = this.respondedBy !== undefined ? { ...base, respondedBy: this.respondedBy } : base; const withMessage = this.message !== undefined ? { ...withRespondedBy, message: this.message } : withRespondedBy; const next: SponsorshipRequestProps = this.rejectionReason !== undefined ? { ...withMessage, rejectionReason: this.rejectionReason } : withMessage; return new SponsorshipRequest(next); } /** * Check if request is pending */ isPending(): boolean { return this.status === 'pending'; } /** * Check if request was accepted */ isAccepted(): boolean { return this.status === 'accepted'; } /** * Get platform fee for this request */ getPlatformFee(): Money { return this.offeredAmount.calculatePlatformFee(); } /** * Get net amount after platform fee */ getNetAmount(): Money { return this.offeredAmount.calculateNetAmount(); } }