wip
This commit is contained in:
@@ -33,6 +33,13 @@ export * from './use-cases/GetRaceProtestsQuery';
|
||||
export * from './use-cases/GetRacePenaltiesQuery';
|
||||
export * from './use-cases/RequestProtestDefenseUseCase';
|
||||
export * from './use-cases/SubmitProtestDefenseUseCase';
|
||||
export * from './use-cases/GetSponsorDashboardQuery';
|
||||
export * from './use-cases/GetSponsorSponsorshipsQuery';
|
||||
export * from './use-cases/ApplyForSponsorshipUseCase';
|
||||
export * from './use-cases/AcceptSponsorshipRequestUseCase';
|
||||
export * from './use-cases/RejectSponsorshipRequestUseCase';
|
||||
export * from './use-cases/GetPendingSponsorshipRequestsQuery';
|
||||
export * from './use-cases/GetEntitySponsorshipPricingQuery';
|
||||
|
||||
// Export ports
|
||||
export * from './ports/DriverRatingProvider';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Use Case: AcceptSponsorshipRequestUseCase
|
||||
*
|
||||
* Allows an entity owner to accept a sponsorship request.
|
||||
* This creates an active sponsorship and notifies the sponsor.
|
||||
*/
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
|
||||
|
||||
export interface AcceptSponsorshipRequestDTO {
|
||||
requestId: string;
|
||||
respondedBy: string; // driverId of the person accepting
|
||||
}
|
||||
|
||||
export interface AcceptSponsorshipRequestResultDTO {
|
||||
requestId: string;
|
||||
sponsorshipId: string;
|
||||
status: 'accepted';
|
||||
acceptedAt: Date;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
}
|
||||
|
||||
export class AcceptSponsorshipRequestUseCase {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
) {}
|
||||
|
||||
async execute(dto: AcceptSponsorshipRequestDTO): Promise<AcceptSponsorshipRequestResultDTO> {
|
||||
// Find the request
|
||||
const request = await this.sponsorshipRequestRepo.findById(dto.requestId);
|
||||
if (!request) {
|
||||
throw new Error('Sponsorship request not found');
|
||||
}
|
||||
|
||||
if (!request.isPending()) {
|
||||
throw new Error(`Cannot accept a ${request.status} sponsorship request`);
|
||||
}
|
||||
|
||||
// Accept the request
|
||||
const acceptedRequest = request.accept(dto.respondedBy);
|
||||
await this.sponsorshipRequestRepo.update(acceptedRequest);
|
||||
|
||||
// If this is a season sponsorship, create the SeasonSponsorship record
|
||||
let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
if (request.entityType === 'season') {
|
||||
const sponsorship = SeasonSponsorship.create({
|
||||
id: sponsorshipId,
|
||||
seasonId: request.entityId,
|
||||
sponsorId: request.sponsorId,
|
||||
tier: request.tier,
|
||||
pricing: request.offeredAmount,
|
||||
status: 'active',
|
||||
});
|
||||
await this.seasonSponsorshipRepo.create(sponsorship);
|
||||
}
|
||||
|
||||
// TODO: In a real implementation, we would:
|
||||
// 1. Create notification for the sponsor
|
||||
// 2. Process payment
|
||||
// 3. Update wallet balances
|
||||
|
||||
return {
|
||||
requestId: acceptedRequest.id,
|
||||
sponsorshipId,
|
||||
status: 'accepted',
|
||||
acceptedAt: acceptedRequest.respondedAt!,
|
||||
platformFee: acceptedRequest.getPlatformFee().amount,
|
||||
netAmount: acceptedRequest.getNetAmount().amount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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 Error('Sponsor not found');
|
||||
}
|
||||
|
||||
// Check if entity accepts sponsorship applications
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
if (!pricing) {
|
||||
throw new Error('This entity has not set up sponsorship pricing');
|
||||
}
|
||||
|
||||
if (!pricing.acceptingApplications) {
|
||||
throw new Error('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 Error(`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 Error('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 Error(`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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Query: GetEntitySponsorshipPricingQuery
|
||||
*
|
||||
* Retrieves sponsorship pricing configuration for any entity.
|
||||
* Used by sponsors to see available slots and prices.
|
||||
*/
|
||||
|
||||
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
|
||||
export interface GetEntitySponsorshipPricingDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
export interface SponsorshipSlotDTO {
|
||||
tier: SponsorshipTier;
|
||||
price: number;
|
||||
currency: string;
|
||||
formattedPrice: string;
|
||||
benefits: string[];
|
||||
available: boolean;
|
||||
maxSlots: number;
|
||||
filledSlots: number;
|
||||
pendingRequests: number;
|
||||
}
|
||||
|
||||
export interface GetEntitySponsorshipPricingResultDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
entityId: string;
|
||||
acceptingApplications: boolean;
|
||||
customRequirements?: string;
|
||||
mainSlot?: SponsorshipSlotDTO;
|
||||
secondarySlot?: SponsorshipSlotDTO;
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingQuery {
|
||||
constructor(
|
||||
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<GetEntitySponsorshipPricingResultDTO | null> {
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
|
||||
if (!pricing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Count pending requests by tier
|
||||
const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
);
|
||||
const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length;
|
||||
const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length;
|
||||
|
||||
// Count filled slots (for seasons, check SeasonSponsorship table)
|
||||
let filledMainSlots = 0;
|
||||
let filledSecondarySlots = 0;
|
||||
|
||||
if (dto.entityType === 'season') {
|
||||
const sponsorships = await this.seasonSponsorshipRepo.findBySeasonId(dto.entityId);
|
||||
const activeSponsorships = sponsorships.filter(s => s.isActive());
|
||||
filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length;
|
||||
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
|
||||
}
|
||||
|
||||
const result: GetEntitySponsorshipPricingResultDTO = {
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
acceptingApplications: pricing.acceptingApplications,
|
||||
customRequirements: pricing.customRequirements,
|
||||
};
|
||||
|
||||
if (pricing.mainSlot) {
|
||||
const mainMaxSlots = pricing.mainSlot.maxSlots;
|
||||
result.mainSlot = {
|
||||
tier: 'main',
|
||||
price: pricing.mainSlot.price.amount,
|
||||
currency: pricing.mainSlot.price.currency,
|
||||
formattedPrice: pricing.mainSlot.price.format(),
|
||||
benefits: pricing.mainSlot.benefits,
|
||||
available: pricing.mainSlot.available && filledMainSlots < mainMaxSlots,
|
||||
maxSlots: mainMaxSlots,
|
||||
filledSlots: filledMainSlots,
|
||||
pendingRequests: pendingMainCount,
|
||||
};
|
||||
}
|
||||
|
||||
if (pricing.secondarySlots) {
|
||||
const secondaryMaxSlots = pricing.secondarySlots.maxSlots;
|
||||
result.secondarySlot = {
|
||||
tier: 'secondary',
|
||||
price: pricing.secondarySlots.price.amount,
|
||||
currency: pricing.secondarySlots.price.currency,
|
||||
formattedPrice: pricing.secondarySlots.price.format(),
|
||||
benefits: pricing.secondarySlots.benefits,
|
||||
available: pricing.secondarySlots.available && filledSecondarySlots < secondaryMaxSlots,
|
||||
maxSlots: secondaryMaxSlots,
|
||||
filledSlots: filledSecondarySlots,
|
||||
pendingRequests: pendingSecondaryCount,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,16 @@ export class GetLeagueFullConfigQuery {
|
||||
sessionCount,
|
||||
roundsPlanned,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: 'admin_only',
|
||||
requireDefense: true,
|
||||
defenseTimeLimit: 48,
|
||||
voteTimeLimit: 72,
|
||||
protestDeadlineHours: 72,
|
||||
stewardingClosesHours: 168,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
},
|
||||
};
|
||||
|
||||
return form;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Query: GetPendingSponsorshipRequestsQuery
|
||||
*
|
||||
* Retrieves pending sponsorship requests for an entity owner to review.
|
||||
*/
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
|
||||
export interface GetPendingSponsorshipRequestsDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
export interface PendingSponsorshipRequestDTO {
|
||||
id: string;
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorLogo?: string;
|
||||
tier: SponsorshipTier;
|
||||
offeredAmount: number;
|
||||
currency: string;
|
||||
formattedAmount: string;
|
||||
message?: string;
|
||||
createdAt: Date;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
}
|
||||
|
||||
export interface GetPendingSponsorshipRequestsResultDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
entityId: string;
|
||||
requests: PendingSponsorshipRequestDTO[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class GetPendingSponsorshipRequestsQuery {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorRepo: ISponsorRepository,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<GetPendingSponsorshipRequestsResultDTO> {
|
||||
const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
);
|
||||
|
||||
const requestDTOs: PendingSponsorshipRequestDTO[] = [];
|
||||
|
||||
for (const request of requests) {
|
||||
const sponsor = await this.sponsorRepo.findById(request.sponsorId);
|
||||
|
||||
requestDTOs.push({
|
||||
id: request.id,
|
||||
sponsorId: request.sponsorId,
|
||||
sponsorName: sponsor?.name ?? 'Unknown Sponsor',
|
||||
sponsorLogo: sponsor?.logoUrl,
|
||||
tier: request.tier,
|
||||
offeredAmount: request.offeredAmount.amount,
|
||||
currency: request.offeredAmount.currency,
|
||||
formattedAmount: request.offeredAmount.format(),
|
||||
message: request.message,
|
||||
createdAt: request.createdAt,
|
||||
platformFee: request.getPlatformFee().amount,
|
||||
netAmount: request.getNetAmount().amount,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return {
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
requests: requestDTOs,
|
||||
totalCount: requestDTOs.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Application Query: GetSponsorDashboardQuery
|
||||
*
|
||||
* Returns sponsor dashboard metrics including sponsorships, impressions, and investment data.
|
||||
*/
|
||||
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
|
||||
export interface GetSponsorDashboardQueryParams {
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export interface SponsoredLeagueDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tier: 'main' | 'secondary';
|
||||
drivers: number;
|
||||
races: number;
|
||||
impressions: number;
|
||||
status: 'active' | 'upcoming' | 'completed';
|
||||
}
|
||||
|
||||
export interface SponsorDashboardDTO {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
metrics: {
|
||||
impressions: number;
|
||||
impressionsChange: number;
|
||||
uniqueViewers: number;
|
||||
viewersChange: number;
|
||||
races: number;
|
||||
drivers: number;
|
||||
exposure: number;
|
||||
exposureChange: number;
|
||||
};
|
||||
sponsoredLeagues: SponsoredLeagueDTO[];
|
||||
investment: {
|
||||
activeSponsorships: number;
|
||||
totalInvestment: number;
|
||||
costPerThousandViews: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class GetSponsorDashboardQuery {
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all sponsorships for this sponsor
|
||||
const sponsorships = await this.seasonSponsorshipRepository.findBySponsorId(sponsorId);
|
||||
|
||||
// Aggregate data across all sponsorships
|
||||
let totalImpressions = 0;
|
||||
let totalDrivers = 0;
|
||||
let totalRaces = 0;
|
||||
let totalInvestment = 0;
|
||||
const sponsoredLeagues: SponsoredLeagueDTO[] = [];
|
||||
const seenLeagues = new Set<string>();
|
||||
|
||||
for (const sponsorship of sponsorships) {
|
||||
// Get season to find league
|
||||
const season = await this.seasonRepository.findById(sponsorship.seasonId);
|
||||
if (!season) continue;
|
||||
|
||||
// Only process each league once
|
||||
if (seenLeagues.has(season.leagueId)) continue;
|
||||
seenLeagues.add(season.leagueId);
|
||||
|
||||
const league = await this.leagueRepository.findById(season.leagueId);
|
||||
if (!league) continue;
|
||||
|
||||
// Get membership count for this league
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(season.leagueId);
|
||||
const driverCount = memberships.length;
|
||||
totalDrivers += driverCount;
|
||||
|
||||
// Get races for this league
|
||||
const races = await this.raceRepository.findByLeagueId(season.leagueId);
|
||||
const raceCount = races.length;
|
||||
totalRaces += raceCount;
|
||||
|
||||
// Calculate impressions based on completed races and drivers
|
||||
// This is a simplified calculation - in production would come from analytics
|
||||
const completedRaces = races.filter(r => r.status === 'completed').length;
|
||||
const leagueImpressions = completedRaces * driverCount * 100; // Simplified: 100 views per driver per race
|
||||
totalImpressions += leagueImpressions;
|
||||
|
||||
// Determine status based on season dates
|
||||
const now = new Date();
|
||||
let status: 'active' | 'upcoming' | 'completed' = 'active';
|
||||
if (season.endDate && season.endDate < now) {
|
||||
status = 'completed';
|
||||
} else if (season.startDate && season.startDate > now) {
|
||||
status = 'upcoming';
|
||||
}
|
||||
|
||||
// Add investment
|
||||
totalInvestment += sponsorship.pricing.amount;
|
||||
|
||||
sponsoredLeagues.push({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
tier: sponsorship.tier,
|
||||
drivers: driverCount,
|
||||
races: raceCount,
|
||||
impressions: leagueImpressions,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
||||
const costPerThousandViews = totalImpressions > 0
|
||||
? (totalInvestment / (totalImpressions / 1000))
|
||||
: 0;
|
||||
|
||||
// Calculate unique viewers (simplified: assume 70% of impressions are unique)
|
||||
const uniqueViewers = Math.round(totalImpressions * 0.7);
|
||||
|
||||
// Calculate exposure score (0-100 based on tier distribution)
|
||||
const mainSponsorships = sponsorships.filter(s => s.tier === 'main').length;
|
||||
const exposure = sponsorships.length > 0
|
||||
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
|
||||
: 0;
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics: {
|
||||
impressions: totalImpressions,
|
||||
impressionsChange: 12.5, // Would come from analytics comparison
|
||||
uniqueViewers,
|
||||
viewersChange: 8.3, // Would come from analytics comparison
|
||||
races: totalRaces,
|
||||
drivers: totalDrivers,
|
||||
exposure,
|
||||
exposureChange: 5.2, // Would come from analytics comparison
|
||||
},
|
||||
sponsoredLeagues,
|
||||
investment: {
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Application Query: GetSponsorSponsorshipsQuery
|
||||
*
|
||||
* Returns detailed sponsorship information for a sponsor's campaigns/sponsorships page.
|
||||
*/
|
||||
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { SponsorshipTier, SponsorshipStatus } from '../../domain/entities/SeasonSponsorship';
|
||||
|
||||
export interface GetSponsorSponsorshipsQueryParams {
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export interface SponsorshipDetailDTO {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
seasonName: string;
|
||||
tier: SponsorshipTier;
|
||||
status: SponsorshipStatus;
|
||||
pricing: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
platformFee: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
netAmount: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
metrics: {
|
||||
drivers: number;
|
||||
races: number;
|
||||
completedRaces: number;
|
||||
impressions: number;
|
||||
};
|
||||
createdAt: Date;
|
||||
activatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface SponsorSponsorshipsDTO {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
sponsorships: SponsorshipDetailDTO[];
|
||||
summary: {
|
||||
totalSponsorships: number;
|
||||
activeSponsorships: number;
|
||||
totalInvestment: number;
|
||||
totalPlatformFees: number;
|
||||
currency: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class GetSponsorSponsorshipsQuery {
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all sponsorships for this sponsor
|
||||
const sponsorships = await this.seasonSponsorshipRepository.findBySponsorId(sponsorId);
|
||||
|
||||
const sponsorshipDetails: SponsorshipDetailDTO[] = [];
|
||||
let totalInvestment = 0;
|
||||
let totalPlatformFees = 0;
|
||||
|
||||
for (const sponsorship of sponsorships) {
|
||||
// Get season to find league
|
||||
const season = await this.seasonRepository.findById(sponsorship.seasonId);
|
||||
if (!season) continue;
|
||||
|
||||
const league = await this.leagueRepository.findById(season.leagueId);
|
||||
if (!league) continue;
|
||||
|
||||
// Get membership count for this league
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(season.leagueId);
|
||||
const driverCount = memberships.length;
|
||||
|
||||
// Get races for this league
|
||||
const races = await this.raceRepository.findByLeagueId(season.leagueId);
|
||||
const completedRaces = races.filter(r => r.status === 'completed').length;
|
||||
|
||||
// Calculate impressions
|
||||
const impressions = completedRaces * driverCount * 100;
|
||||
|
||||
// Calculate platform fee (10%)
|
||||
const platformFee = sponsorship.getPlatformFee();
|
||||
const netAmount = sponsorship.getNetAmount();
|
||||
|
||||
totalInvestment += sponsorship.pricing.amount;
|
||||
totalPlatformFees += platformFee.amount;
|
||||
|
||||
sponsorshipDetails.push({
|
||||
id: sponsorship.id,
|
||||
leagueId: league.id,
|
||||
leagueName: league.name,
|
||||
seasonId: season.id,
|
||||
seasonName: season.name,
|
||||
tier: sponsorship.tier,
|
||||
status: sponsorship.status,
|
||||
pricing: {
|
||||
amount: sponsorship.pricing.amount,
|
||||
currency: sponsorship.pricing.currency,
|
||||
},
|
||||
platformFee: {
|
||||
amount: platformFee.amount,
|
||||
currency: platformFee.currency,
|
||||
},
|
||||
netAmount: {
|
||||
amount: netAmount.amount,
|
||||
currency: netAmount.currency,
|
||||
},
|
||||
metrics: {
|
||||
drivers: driverCount,
|
||||
races: races.length,
|
||||
completedRaces,
|
||||
impressions,
|
||||
},
|
||||
createdAt: sponsorship.createdAt,
|
||||
activatedAt: sponsorship.activatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
summary: {
|
||||
totalSponsorships: sponsorships.length,
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Use Case: RejectSponsorshipRequestUseCase
|
||||
*
|
||||
* Allows an entity owner to reject a sponsorship request.
|
||||
*/
|
||||
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
|
||||
export interface RejectSponsorshipRequestDTO {
|
||||
requestId: string;
|
||||
respondedBy: string; // driverId of the person rejecting
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface RejectSponsorshipRequestResultDTO {
|
||||
requestId: string;
|
||||
status: 'rejected';
|
||||
rejectedAt: Date;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class RejectSponsorshipRequestUseCase {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
) {}
|
||||
|
||||
async execute(dto: RejectSponsorshipRequestDTO): Promise<RejectSponsorshipRequestResultDTO> {
|
||||
// Find the request
|
||||
const request = await this.sponsorshipRequestRepo.findById(dto.requestId);
|
||||
if (!request) {
|
||||
throw new Error('Sponsorship request not found');
|
||||
}
|
||||
|
||||
if (!request.isPending()) {
|
||||
throw new Error(`Cannot reject a ${request.status} sponsorship request`);
|
||||
}
|
||||
|
||||
// Reject the request
|
||||
const rejectedRequest = request.reject(dto.respondedBy, dto.reason);
|
||||
await this.sponsorshipRequestRepo.update(rejectedRequest);
|
||||
|
||||
// TODO: In a real implementation, notify the sponsor
|
||||
|
||||
return {
|
||||
requestId: rejectedRequest.id,
|
||||
status: 'rejected',
|
||||
rejectedAt: rejectedRequest.respondedAt!,
|
||||
reason: rejectedRequest.rejectionReason,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user