103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
/**
|
|
* Use Case: ApplyForSponsorshipUseCase
|
|
*
|
|
* Allows a sponsor to apply for a sponsorship slot on any entity
|
|
* (driver, team, race, or season/league).
|
|
*/
|
|
|
|
import { SponsorshipRequest, type SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
|
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
|
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
|
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
|
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
|
import { Money, type Currency } from '../../domain/value-objects/Money';
|
|
import {
|
|
EntityNotFoundError,
|
|
BusinessRuleViolationError,
|
|
} from '../errors/RacingApplicationError';
|
|
|
|
export interface ApplyForSponsorshipDTO {
|
|
sponsorId: string;
|
|
entityType: SponsorableEntityType;
|
|
entityId: string;
|
|
tier: SponsorshipTier;
|
|
offeredAmount: number; // in cents
|
|
currency?: Currency;
|
|
message?: string;
|
|
}
|
|
|
|
export interface ApplyForSponsorshipResultDTO {
|
|
requestId: string;
|
|
status: 'pending';
|
|
createdAt: Date;
|
|
}
|
|
|
|
export class ApplyForSponsorshipUseCase {
|
|
constructor(
|
|
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
|
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
|
private readonly sponsorRepo: ISponsorRepository,
|
|
) {}
|
|
|
|
async execute(dto: ApplyForSponsorshipDTO): Promise<ApplyForSponsorshipResultDTO> {
|
|
// Validate sponsor exists
|
|
const sponsor = await this.sponsorRepo.findById(dto.sponsorId);
|
|
if (!sponsor) {
|
|
throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId });
|
|
}
|
|
|
|
// Check if entity accepts sponsorship applications
|
|
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
|
if (!pricing) {
|
|
throw new BusinessRuleViolationError('This entity has not set up sponsorship pricing');
|
|
}
|
|
|
|
if (!pricing.acceptingApplications) {
|
|
throw new RacingApplicationError('This entity is not currently accepting sponsorship applications');
|
|
}
|
|
|
|
// Check if the requested tier slot is available
|
|
const slotAvailable = pricing.isSlotAvailable(dto.tier);
|
|
if (!slotAvailable) {
|
|
throw new RacingApplicationError(`No ${dto.tier} sponsorship slots are available`);
|
|
}
|
|
|
|
// Check if sponsor already has a pending request for this entity
|
|
const hasPending = await this.sponsorshipRequestRepo.hasPendingRequest(
|
|
dto.sponsorId,
|
|
dto.entityType,
|
|
dto.entityId
|
|
);
|
|
if (hasPending) {
|
|
throw new RacingApplicationError('You already have a pending sponsorship request for this entity');
|
|
}
|
|
|
|
// Validate offered amount meets minimum price
|
|
const minPrice = pricing.getPrice(dto.tier);
|
|
if (minPrice && dto.offeredAmount < minPrice.amount) {
|
|
throw new RacingApplicationError(`Offered amount must be at least ${minPrice.format()}`);
|
|
}
|
|
|
|
// Create the sponsorship request
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const offeredAmount = Money.create(dto.offeredAmount, dto.currency ?? 'USD');
|
|
|
|
const request = SponsorshipRequest.create({
|
|
id: requestId,
|
|
sponsorId: dto.sponsorId,
|
|
entityType: dto.entityType,
|
|
entityId: dto.entityId,
|
|
tier: dto.tier,
|
|
offeredAmount,
|
|
message: dto.message,
|
|
});
|
|
|
|
await this.sponsorshipRequestRepo.create(request);
|
|
|
|
return {
|
|
requestId: request.id,
|
|
status: 'pending',
|
|
createdAt: request.createdAt,
|
|
};
|
|
}
|
|
} |