resolve todos in website and api

This commit is contained in:
2025-12-20 10:45:56 +01:00
parent 656ec62426
commit 7bbad511e2
62 changed files with 2036 additions and 611 deletions

View File

@@ -0,0 +1,103 @@
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 { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { SponsorshipDetailOutput } from '../ports/output/SponsorSponsorshipsOutputPort';
export interface GetSeasonSponsorshipsParams {
seasonId: string;
}
export interface GetSeasonSponsorshipsOutputPort {
seasonId: string;
sponsorships: SponsorshipDetailOutput[];
}
export class GetSeasonSponsorshipsUseCase
implements AsyncUseCase<GetSeasonSponsorshipsParams, GetSeasonSponsorshipsOutputPort | null, 'REPOSITORY_ERROR'>
{
constructor(
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly raceRepository: IRaceRepository,
) {}
async execute(
params: GetSeasonSponsorshipsParams,
): Promise<Result<GetSeasonSponsorshipsOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
try {
const { seasonId } = params;
const season = await this.seasonRepository.findById(seasonId);
if (!season) {
return Result.ok(null);
}
const league = await this.leagueRepository.findById(season.leagueId);
if (!league) {
return Result.ok(null);
}
const sponsorships = await this.seasonSponsorshipRepository.findBySeasonId(seasonId);
// Pre-compute metrics shared across all sponsorships in this season
const memberships = await this.leagueMembershipRepository.getLeagueMembers(season.leagueId);
const driverCount = memberships.length;
const races = await this.raceRepository.findByLeagueId(season.leagueId);
const raceCount = races.length;
const completedRaces = races.filter(r => r.status === 'completed').length;
const impressions = completedRaces * driverCount * 100;
const sponsorshipDetails: SponsorshipDetailOutput[] = sponsorships.map(sponsorship => {
const platformFee = sponsorship.getPlatformFee();
const netAmount = sponsorship.getNetAmount();
return {
id: sponsorship.id,
leagueId: league.id,
leagueName: league.name,
seasonId: season.id,
seasonName: season.name,
...(season.startDate !== undefined ? { seasonStartDate: season.startDate } : {}),
...(season.endDate !== undefined ? { seasonEndDate: season.endDate } : {}),
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: raceCount,
completedRaces,
impressions,
},
createdAt: sponsorship.createdAt,
...(sponsorship.activatedAt !== undefined ? { activatedAt: sponsorship.activatedAt } : {}),
};
});
return Result.ok({
seasonId,
sponsorships: sponsorshipDetails,
});
} catch {
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch season sponsorships' });
}
}
}

View File

@@ -0,0 +1,70 @@
import { describe, it, expect } from 'vitest';
import {
applyScoringPresetToTimings,
type ScoringPresetTimings,
} from '@core/racing/domain/services/ScoringPresetTimingService';
describe('ScoringPresetTimingService', () => {
it('applies sprint/double style presets with two sessions and sprint minutes', () => {
const initial: ScoringPresetTimings = {
practiceMinutes: 5,
qualifyingMinutes: 10,
mainRaceMinutes: 20,
sessionCount: 1,
};
const result = applyScoringPresetToTimings('Sprint-Main-Double', initial);
expect(result.practiceMinutes).toBe(15);
expect(result.qualifyingMinutes).toBe(20);
expect(result.sprintRaceMinutes).toBe(20);
expect(result.mainRaceMinutes).toBe(35);
expect(result.sessionCount).toBe(2);
});
it('applies endurance/long style presets with single main session and no sprint', () => {
const initial: ScoringPresetTimings = {
practiceMinutes: 10,
qualifyingMinutes: 15,
sprintRaceMinutes: 10,
mainRaceMinutes: 30,
sessionCount: 2,
};
const result = applyScoringPresetToTimings('Endurance-Main', initial);
expect(result.practiceMinutes).toBe(30);
expect(result.qualifyingMinutes).toBe(30);
expect(result.mainRaceMinutes).toBe(90);
expect(result.sessionCount).toBe(1);
expect(result.sprintRaceMinutes).toBeUndefined();
});
it('applies default timing rules for non-matching presets and clears sprint minutes', () => {
const initial: ScoringPresetTimings = {
practiceMinutes: 10,
qualifyingMinutes: 15,
sprintRaceMinutes: 10,
mainRaceMinutes: 30,
sessionCount: 2,
};
const result = applyScoringPresetToTimings('club-default', initial);
expect(result.practiceMinutes).toBe(20);
expect(result.qualifyingMinutes).toBe(30);
expect(result.mainRaceMinutes).toBe(40);
expect(result.sessionCount).toBe(1);
expect(result.sprintRaceMinutes).toBeUndefined();
});
it('treats pattern id matching as case-insensitive', () => {
const initial: ScoringPresetTimings = {};
const lower = applyScoringPresetToTimings('endurance-main', initial);
const upper = applyScoringPresetToTimings('ENDURANCE-MAIN', initial);
expect(lower).toEqual(upper);
});
});

View File

@@ -0,0 +1,56 @@
export type ScoringPresetTimings = {
practiceMinutes?: number;
qualifyingMinutes?: number;
sprintRaceMinutes?: number;
mainRaceMinutes?: number;
sessionCount?: number;
roundsPlanned?: number;
raceDayOfWeek?: number;
raceTimeUtc?: string;
};
/**
* Apply high-level scoring preset semantics to league/session timings.
*
* This encapsulates the mapping between logical scoring presets (sprint, endurance, etc.)
* and their default timing configuration so that UI layers do not need to duplicate
* or interpret preset IDs directly.
*/
export function applyScoringPresetToTimings(
patternId: string,
currentTimings: ScoringPresetTimings,
): ScoringPresetTimings {
const lowerPresetId = patternId.toLowerCase();
let updatedTimings: ScoringPresetTimings = { ...currentTimings };
if (lowerPresetId.includes('sprint') || lowerPresetId.includes('double')) {
updatedTimings = {
...updatedTimings,
practiceMinutes: 15,
qualifyingMinutes: 20,
sprintRaceMinutes: 20,
mainRaceMinutes: 35,
sessionCount: 2,
};
} else if (lowerPresetId.includes('endurance') || lowerPresetId.includes('long')) {
updatedTimings = {
...updatedTimings,
practiceMinutes: 30,
qualifyingMinutes: 30,
mainRaceMinutes: 90,
sessionCount: 1,
};
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
} else {
updatedTimings = {
...updatedTimings,
practiceMinutes: 20,
qualifyingMinutes: 30,
mainRaceMinutes: 40,
sessionCount: 1,
};
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
}
return updatedTimings;
}