207 lines
7.0 KiB
TypeScript
207 lines
7.0 KiB
TypeScript
/**
|
|
* Application Use Case: GetSponsorDashboardUseCase
|
|
*
|
|
* 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';
|
|
import { Result } from '@core/shared/application/Result';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
|
import { Money } from '../../domain/value-objects/Money';
|
|
|
|
export interface GetSponsorDashboardInput {
|
|
sponsorId: string;
|
|
}
|
|
|
|
export interface SponsoredLeagueMetrics {
|
|
drivers: number;
|
|
races: number;
|
|
impressions: number;
|
|
}
|
|
|
|
export type SponsoredLeagueStatus = 'active' | 'upcoming' | 'completed';
|
|
|
|
export interface SponsoredLeagueSummary {
|
|
leagueId: string;
|
|
leagueName: string;
|
|
tier: 'main' | 'secondary';
|
|
metrics: SponsoredLeagueMetrics;
|
|
status: SponsoredLeagueStatus;
|
|
}
|
|
|
|
export interface SponsorDashboardMetrics {
|
|
impressions: number;
|
|
impressionsChange: number;
|
|
uniqueViewers: number;
|
|
viewersChange: number;
|
|
races: number;
|
|
drivers: number;
|
|
exposure: number;
|
|
exposureChange: number;
|
|
}
|
|
|
|
export interface SponsorInvestmentSummary {
|
|
activeSponsorships: number;
|
|
totalInvestment: Money;
|
|
costPerThousandViews: number;
|
|
}
|
|
|
|
export interface GetSponsorDashboardResult {
|
|
sponsorId: string;
|
|
sponsorName: string;
|
|
metrics: SponsorDashboardMetrics;
|
|
sponsoredLeagues: SponsoredLeagueSummary[];
|
|
investment: SponsorInvestmentSummary;
|
|
}
|
|
|
|
export type GetSponsorDashboardErrorCode = 'SPONSOR_NOT_FOUND' | 'REPOSITORY_ERROR';
|
|
|
|
export class GetSponsorDashboardUseCase {
|
|
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,
|
|
private readonly output: UseCaseOutputPort<GetSponsorDashboardResult>,
|
|
) {}
|
|
|
|
async execute(
|
|
params: GetSponsorDashboardInput,
|
|
): Promise<Result<void, ApplicationErrorCode<GetSponsorDashboardErrorCode, { message: string }>>> {
|
|
try {
|
|
const { sponsorId } = params;
|
|
|
|
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
|
if (!sponsor) {
|
|
return Result.err({
|
|
code: 'SPONSOR_NOT_FOUND',
|
|
details: {
|
|
message: 'Sponsor not found',
|
|
},
|
|
});
|
|
}
|
|
|
|
// 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 totalInvestmentMoney = Money.create(0, 'USD');
|
|
const sponsoredLeagues: SponsoredLeagueSummary[] = [];
|
|
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
|
|
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: SponsoredLeagueStatus = 'active';
|
|
if (season.endDate && season.endDate < now) {
|
|
status = 'completed';
|
|
} else if (season.startDate && season.startDate > now) {
|
|
status = 'upcoming';
|
|
}
|
|
|
|
// Add investment
|
|
totalInvestmentMoney = totalInvestmentMoney.add(
|
|
Money.create(sponsorship.pricing.amount, sponsorship.pricing.currency),
|
|
);
|
|
|
|
sponsoredLeagues.push({
|
|
leagueId: league.id.toString(),
|
|
leagueName: league.name.toString(),
|
|
tier: sponsorship.tier,
|
|
metrics: {
|
|
drivers: driverCount,
|
|
races: raceCount,
|
|
impressions: leagueImpressions,
|
|
},
|
|
status,
|
|
});
|
|
}
|
|
|
|
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
|
const costPerThousandViews = totalImpressions > 0
|
|
? totalInvestmentMoney.amount / (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;
|
|
|
|
const result: GetSponsorDashboardResult = {
|
|
sponsorId,
|
|
sponsorName: sponsor.name,
|
|
metrics: {
|
|
impressions: totalImpressions,
|
|
impressionsChange: 0,
|
|
uniqueViewers,
|
|
viewersChange: 0,
|
|
races: totalRaces,
|
|
drivers: totalDrivers,
|
|
exposure,
|
|
exposureChange: 0,
|
|
},
|
|
sponsoredLeagues,
|
|
investment: {
|
|
activeSponsorships,
|
|
totalInvestment: totalInvestmentMoney,
|
|
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
|
|
},
|
|
};
|
|
|
|
this.output.present(result);
|
|
|
|
return Result.ok(undefined);
|
|
} catch (err) {
|
|
const error = err as { message?: string } | undefined;
|
|
|
|
return Result.err({
|
|
code: 'REPOSITORY_ERROR',
|
|
details: {
|
|
message: error?.message ?? 'Failed to fetch sponsor dashboard',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|