refactor racing use cases
This commit is contained in:
@@ -10,45 +10,58 @@ import type { ISeasonRepository } from '../../domain/repositories/ISeasonReposit
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { SponsorDashboardOutputPort } from '../ports/output/SponsorDashboardOutputPort';
|
||||
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 GetSponsorDashboardQueryParams {
|
||||
export interface GetSponsorDashboardInput {
|
||||
sponsorId: string;
|
||||
}
|
||||
|
||||
export interface SponsoredLeagueDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
tier: 'main' | 'secondary';
|
||||
export interface SponsoredLeagueMetrics {
|
||||
drivers: number;
|
||||
races: number;
|
||||
impressions: number;
|
||||
status: 'active' | 'upcoming' | 'completed';
|
||||
}
|
||||
|
||||
export interface SponsorDashboardDTO {
|
||||
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: {
|
||||
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;
|
||||
};
|
||||
metrics: SponsorDashboardMetrics;
|
||||
sponsoredLeagues: SponsoredLeagueSummary[];
|
||||
investment: SponsorInvestmentSummary;
|
||||
}
|
||||
|
||||
export type GetSponsorDashboardErrorCode = 'SPONSOR_NOT_FOUND' | 'REPOSITORY_ERROR';
|
||||
|
||||
export class GetSponsorDashboardUseCase {
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
@@ -57,17 +70,23 @@ export class GetSponsorDashboardUseCase {
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly output: UseCaseOutputPort<GetSponsorDashboardResult>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
params: GetSponsorDashboardQueryParams,
|
||||
): Promise<Result<SponsorDashboardOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
params: GetSponsorDashboardInput,
|
||||
): Promise<Result<void, ApplicationErrorCode<GetSponsorDashboardErrorCode, { message: string }>>> {
|
||||
try {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const sponsor = await this.sponsorRepository.findById(sponsorId);
|
||||
if (!sponsor) {
|
||||
return Result.ok(null);
|
||||
return Result.err({
|
||||
code: 'SPONSOR_NOT_FOUND',
|
||||
details: {
|
||||
message: 'Sponsor not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Get all sponsorships for this sponsor
|
||||
@@ -77,8 +96,8 @@ export class GetSponsorDashboardUseCase {
|
||||
let totalImpressions = 0;
|
||||
let totalDrivers = 0;
|
||||
let totalRaces = 0;
|
||||
let totalInvestment = 0;
|
||||
const sponsoredLeagues: SponsoredLeagueDTO[] = [];
|
||||
let totalInvestmentMoney = Money.create(0, 'USD');
|
||||
const sponsoredLeagues: SponsoredLeagueSummary[] = [];
|
||||
const seenLeagues = new Set<string>();
|
||||
|
||||
for (const sponsorship of sponsorships) {
|
||||
@@ -104,14 +123,13 @@ export class GetSponsorDashboardUseCase {
|
||||
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';
|
||||
let status: SponsoredLeagueStatus = 'active';
|
||||
if (season.endDate && season.endDate < now) {
|
||||
status = 'completed';
|
||||
} else if (season.startDate && season.startDate > now) {
|
||||
@@ -119,22 +137,26 @@ export class GetSponsorDashboardUseCase {
|
||||
}
|
||||
|
||||
// Add investment
|
||||
totalInvestment += sponsorship.pricing.amount;
|
||||
totalInvestmentMoney = totalInvestmentMoney.add(
|
||||
Money.create(sponsorship.pricing.amount, sponsorship.pricing.currency),
|
||||
);
|
||||
|
||||
sponsoredLeagues.push({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
leagueId: league.id,
|
||||
leagueName: league.name,
|
||||
tier: sponsorship.tier,
|
||||
drivers: driverCount,
|
||||
races: raceCount,
|
||||
impressions: leagueImpressions,
|
||||
metrics: {
|
||||
drivers: driverCount,
|
||||
races: raceCount,
|
||||
impressions: leagueImpressions,
|
||||
},
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
const activeSponsorships = sponsorships.filter(s => s.status === 'active').length;
|
||||
const costPerThousandViews = totalImpressions > 0
|
||||
? (totalInvestment / (totalImpressions / 1000))
|
||||
? totalInvestmentMoney.amount / (totalImpressions / 1000)
|
||||
: 0;
|
||||
|
||||
// Calculate unique viewers (simplified: assume 70% of impressions are unique)
|
||||
@@ -146,7 +168,7 @@ export class GetSponsorDashboardUseCase {
|
||||
? Math.min(100, (mainSponsorships * 30) + (sponsorships.length * 10))
|
||||
: 0;
|
||||
|
||||
const outputPort: SponsorDashboardOutputPort = {
|
||||
const result: GetSponsorDashboardResult = {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics: {
|
||||
@@ -162,14 +184,23 @@ export class GetSponsorDashboardUseCase {
|
||||
sponsoredLeagues,
|
||||
investment: {
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
totalInvestment: totalInvestmentMoney,
|
||||
costPerThousandViews: Math.round(costPerThousandViews * 100) / 100,
|
||||
},
|
||||
};
|
||||
|
||||
return Result.ok(outputPort);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch sponsor dashboard' });
|
||||
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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user