Files
gridpilot.gg/core/racing/domain/entities/SponsorshipRequest.ts
2025-12-23 11:25:08 +01:00

239 lines
7.0 KiB
TypeScript

/**
* 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<string> {
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<SponsorshipRequestProps, 'createdAt' | 'status'> & {
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<SponsorshipRequestProps, 'createdAt' | 'status'>): 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();
}
}