124 lines
4.7 KiB
TypeScript
124 lines
4.7 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 type { AsyncUseCase } from '@gridpilot/shared/application';
|
|
import {
|
|
EntityNotFoundError,
|
|
BusinessRuleViolationError,
|
|
} from '../errors/RacingApplicationError';
|
|
import type { ILogger } from '../../../shared/src/logging/ILogger';
|
|
|
|
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
|
|
implements AsyncUseCase<ApplyForSponsorshipDTO, ApplyForSponsorshipResultDTO>
|
|
{
|
|
constructor(
|
|
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
|
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
|
private readonly sponsorRepo: ISponsorRepository,
|
|
private readonly logger: ILogger,
|
|
) {}
|
|
|
|
async execute(dto: ApplyForSponsorshipDTO): Promise<ApplyForSponsorshipResultDTO> {
|
|
this.logger.debug('Attempting to apply for sponsorship', { dto });
|
|
|
|
// Validate sponsor exists
|
|
const sponsor = await this.sponsorRepo.findById(dto.sponsorId);
|
|
if (!sponsor) {
|
|
this.logger.error('Sponsor not found', { sponsorId: dto.sponsorId });
|
|
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) {
|
|
this.logger.warn('Sponsorship pricing not set up for this entity', { entityType: dto.entityType, entityId: dto.entityId });
|
|
throw new BusinessRuleViolationError('This entity has not set up sponsorship pricing');
|
|
}
|
|
|
|
if (!pricing.acceptingApplications) {
|
|
this.logger.warn('Entity not accepting sponsorship applications', { entityType: dto.entityType, entityId: dto.entityId });
|
|
throw new BusinessRuleViolationError(
|
|
'This entity is not currently accepting sponsorship applications',
|
|
);
|
|
}
|
|
|
|
// Check if the requested tier slot is available
|
|
const slotAvailable = pricing.isSlotAvailable(dto.tier);
|
|
if (!slotAvailable) {
|
|
this.logger.warn(`No ${dto.tier} sponsorship slots are available for entity ${dto.entityId}`);
|
|
throw new BusinessRuleViolationError(
|
|
`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) {
|
|
this.logger.warn('Sponsor already has a pending request for this entity', { sponsorId: dto.sponsorId, entityType: dto.entityType, entityId: dto.entityId });
|
|
throw new BusinessRuleViolationError(
|
|
'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) {
|
|
this.logger.warn(`Offered amount ${dto.offeredAmount} is less than minimum ${minPrice.amount} for entity ${dto.entityId}, tier ${dto.tier}`);
|
|
throw new BusinessRuleViolationError(
|
|
`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,
|
|
...(dto.message !== undefined ? { message: dto.message } : {}),
|
|
});
|
|
|
|
await this.sponsorshipRequestRepo.create(request);
|
|
|
|
return {
|
|
requestId: request.id,
|
|
status: 'pending',
|
|
createdAt: request.createdAt,
|
|
};
|
|
}
|
|
} |