This commit is contained in:
2025-12-16 10:50:15 +01:00
parent 775d41e055
commit 8ed6ba1fd1
144 changed files with 5763 additions and 1985 deletions

View File

@@ -18,6 +18,9 @@ export * from './use-cases/GetLeagueStandingsUseCase';
export * from './use-cases/GetLeagueDriverSeasonStatsUseCase';
export * from './use-cases/GetAllLeaguesWithCapacityUseCase';
export * from './use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
export * from './use-cases/GetAllRacesUseCase';
export * from './use-cases/GetTotalRacesUseCase';
export * from './use-cases/ImportRaceResultsApiUseCase';
export * from './use-cases/ListLeagueScoringPresetsUseCase';
export * from './use-cases/GetLeagueScoringConfigUseCase';
export * from './use-cases/RecalculateChampionshipStandingsUseCase';

View File

@@ -0,0 +1,20 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface SponsorDto {
id: string;
name: string;
contactEmail: string;
websiteUrl: string | undefined;
logoUrl: string | undefined;
createdAt: Date;
}
export interface CreateSponsorViewModel {
sponsor: SponsorDto;
}
export interface CreateSponsorResultDTO {
sponsor: SponsorDto;
}
export interface ICreateSponsorPresenter extends Presenter<CreateSponsorResultDTO, CreateSponsorViewModel> {}

View File

@@ -1,5 +1,4 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
import type { GetEntitySponsorshipPricingResultDTO } from '../use-cases/GetEntitySponsorshipPricingUseCase';
export interface IEntitySponsorshipPricingPresenter {
present(data: GetEntitySponsorshipPricingResultDTO | null): void;
}
export interface IEntitySponsorshipPricingPresenter extends Presenter<GetEntitySponsorshipPricingResultDTO | null, GetEntitySponsorshipPricingResultDTO | null> {}

View File

@@ -0,0 +1,20 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface RaceViewModel {
id: string;
name: string;
date: string;
leagueName?: string;
}
export interface AllRacesPageViewModel {
races: RaceViewModel[];
totalCount: number;
}
export interface GetAllRacesResultDTO {
races: RaceViewModel[];
totalCount: number;
}
export interface IGetAllRacesPresenter extends Presenter<GetAllRacesResultDTO, AllRacesPageViewModel> {}

View File

@@ -0,0 +1,20 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface SponsorDto {
id: string;
name: string;
contactEmail: string;
websiteUrl: string | undefined;
logoUrl: string | undefined;
createdAt: Date;
}
export interface GetSponsorsViewModel {
sponsors: SponsorDto[];
}
export interface GetSponsorsResultDTO {
sponsors: SponsorDto[];
}
export interface IGetSponsorsPresenter extends Presenter<GetSponsorsResultDTO, GetSponsorsViewModel> {}

View File

@@ -0,0 +1,18 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface SponsorshipPricingItemDto {
id: string;
level: string;
price: number;
currency: string;
}
export interface GetSponsorshipPricingResultDTO {
pricing: SponsorshipPricingItemDto[];
}
export interface GetSponsorshipPricingViewModel {
pricing: SponsorshipPricingItemDto[];
}
export interface IGetSponsorshipPricingPresenter extends Presenter<GetSponsorshipPricingResultDTO, GetSponsorshipPricingViewModel> {}

View File

@@ -0,0 +1,11 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface GetTotalRacesViewModel {
totalRaces: number;
}
export interface GetTotalRacesResultDTO {
totalRaces: number;
}
export interface IGetTotalRacesPresenter extends Presenter<GetTotalRacesResultDTO, GetTotalRacesViewModel> {}

View File

@@ -0,0 +1,19 @@
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
export interface ImportRaceResultsSummaryViewModel {
success: boolean;
raceId: string;
driversProcessed: number;
resultsRecorded: number;
errors?: string[];
}
export interface ImportRaceResultsApiResultDTO {
success: boolean;
raceId: string;
driversProcessed: number;
resultsRecorded: number;
errors?: string[];
}
export interface IImportRaceResultsApiPresenter extends Presenter<ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel> {}

View File

@@ -0,0 +1,61 @@
/**
* Application Use Case: CreateSponsorUseCase
*
* Creates a new sponsor.
*/
import { Sponsor, type SponsorProps } from '../../domain/entities/Sponsor';
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import type {
ICreateSponsorPresenter,
CreateSponsorResultDTO,
CreateSponsorViewModel,
} from '../presenters/ICreateSponsorPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface CreateSponsorInput {
name: string;
contactEmail: string;
websiteUrl?: string;
logoUrl?: string;
}
export class CreateSponsorUseCase
implements UseCase<CreateSponsorInput, CreateSponsorResultDTO, CreateSponsorViewModel, ICreateSponsorPresenter>
{
constructor(
private readonly sponsorRepository: ISponsorRepository,
) {}
async execute(
input: CreateSponsorInput,
presenter: ICreateSponsorPresenter,
): Promise<void> {
presenter.reset();
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const sponsor = Sponsor.create({
id,
name: input.name,
contactEmail: input.contactEmail,
...(input.websiteUrl !== undefined ? { websiteUrl: input.websiteUrl } : {}),
...(input.logoUrl !== undefined ? { logoUrl: input.logoUrl } : {}),
} as any);
await this.sponsorRepository.create(sponsor);
const dto: CreateSponsorResultDTO = {
sponsor: {
id: sponsor.id,
name: sponsor.name,
contactEmail: sponsor.contactEmail,
websiteUrl: sponsor.websiteUrl,
logoUrl: sponsor.logoUrl,
createdAt: sponsor.createdAt,
},
};
presenter.present(dto);
}
}

View File

@@ -0,0 +1,34 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IGetAllRacesPresenter, GetAllRacesResultDTO, AllRacesPageViewModel } from '../presenters/IGetAllRacesPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetAllRacesUseCaseParams {}
export class GetAllRacesUseCase implements UseCase<GetAllRacesUseCaseParams, GetAllRacesResultDTO, AllRacesPageViewModel, IGetAllRacesPresenter> {
constructor(
private readonly raceRepository: IRaceRepository,
private readonly leagueRepository: ILeagueRepository,
) {}
async execute(params: GetAllRacesUseCaseParams, presenter: IGetAllRacesPresenter): Promise<void> {
const races = await this.raceRepository.findAll();
const leagues = await this.leagueRepository.findAll();
const leagueMap = new Map(leagues.map(league => [league.id, league.name]));
const raceViewModels = races.map(race => ({
id: race.id,
name: `Race ${race.id}`, // Placeholder, adjust based on domain
date: race.scheduledAt.toISOString(),
leagueName: leagueMap.get(race.leagueId) || 'Unknown League',
}));
const dto: GetAllRacesResultDTO = {
races: raceViewModels,
totalCount: races.length,
};
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -11,8 +11,7 @@ import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISe
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { Logger } from '../../../shared/src/logging/Logger';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetEntitySponsorshipPricingDTO {
entityType: SponsorableEntityType;
@@ -41,35 +40,28 @@ export interface GetEntitySponsorshipPricingResultDTO {
}
export class GetEntitySponsorshipPricingUseCase
implements AsyncUseCase<GetEntitySponsorshipPricingDTO, void> {
implements UseCase<GetEntitySponsorshipPricingDTO, GetEntitySponsorshipPricingResultDTO | null, GetEntitySponsorshipPricingResultDTO | null, IEntitySponsorshipPricingPresenter>
{
constructor(
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly presenter: IEntitySponsorshipPricingPresenter,
private readonly logger: Logger,
) {}
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> {
this.logger.debug(
`Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
{ dto },
);
async execute(
dto: GetEntitySponsorshipPricingDTO,
presenter: IEntitySponsorshipPricingPresenter,
): Promise<void> {
presenter.reset();
try {
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
if (!pricing) {
this.logger.warn(
`No pricing found for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Presenting null.`,
{ dto },
);
this.presenter.present(null);
presenter.present(null);
return;
}
this.logger.debug(`Found pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, { pricing });
// Count pending requests by tier
const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity(
dto.entityType,
@@ -78,10 +70,6 @@ export class GetEntitySponsorshipPricingUseCase
const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length;
const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length;
this.logger.debug(
`Pending requests counts: main=${pendingMainCount}, secondary=${pendingSecondaryCount}`,
);
// Count filled slots (for seasons, check SeasonSponsorship table)
let filledMainSlots = 0;
let filledSecondarySlots = 0;
@@ -91,9 +79,6 @@ export class GetEntitySponsorshipPricingUseCase
const activeSponsorships = sponsorships.filter(s => s.isActive());
filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length;
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
this.logger.debug(
`Filled slots for season: main=${filledMainSlots}, secondary=${filledSecondarySlots}`,
);
}
const result: GetEntitySponsorshipPricingResultDTO = {
@@ -118,7 +103,6 @@ export class GetEntitySponsorshipPricingUseCase
filledSlots: filledMainSlots,
pendingRequests: pendingMainCount,
};
this.logger.debug(`Main slot pricing information processed`, { mainSlot: result.mainSlot });
}
if (pricing.secondarySlots) {
@@ -135,26 +119,10 @@ export class GetEntitySponsorshipPricingUseCase
filledSlots: filledSecondarySlots,
pendingRequests: pendingSecondaryCount,
};
this.logger.debug(`Secondary slot pricing information processed`, {
secondarySlot: result.secondarySlot,
});
}
this.logger.info(
`Successfully retrieved and processed entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
{ result },
);
this.presenter.present(result);
presenter.present(result);
} catch (error: unknown) {
let errorMessage = 'An unknown error occurred';
if (error instanceof Error) {
errorMessage = error.message;
}
this.logger.error(
`Failed to get entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Error: ${errorMessage}`,
{ error, dto },
);
// Re-throw the error or present an error state if the presenter supports it
throw error;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Application Use Case: GetSponsorsUseCase
*
* Retrieves all sponsors.
*/
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
import type {
IGetSponsorsPresenter,
GetSponsorsResultDTO,
GetSponsorsViewModel,
} from '../presenters/IGetSponsorsPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export class GetSponsorsUseCase
implements UseCase<void, GetSponsorsResultDTO, GetSponsorsViewModel, IGetSponsorsPresenter>
{
constructor(
private readonly sponsorRepository: ISponsorRepository,
) {}
async execute(
_input: void,
presenter: IGetSponsorsPresenter,
): Promise<void> {
presenter.reset();
const sponsors = await this.sponsorRepository.findAll();
const dto: GetSponsorsResultDTO = {
sponsors: sponsors.map(sponsor => ({
id: sponsor.id,
name: sponsor.name,
contactEmail: sponsor.contactEmail,
websiteUrl: sponsor.websiteUrl,
logoUrl: sponsor.logoUrl,
createdAt: sponsor.createdAt,
})),
};
presenter.present(dto);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Application Use Case: GetSponsorshipPricingUseCase
*
* Retrieves general sponsorship pricing tiers.
*/
import type {
IGetSponsorshipPricingPresenter,
GetSponsorshipPricingResultDTO,
GetSponsorshipPricingViewModel,
} from '../presenters/IGetSponsorshipPricingPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export class GetSponsorshipPricingUseCase
implements UseCase<void, GetSponsorshipPricingResultDTO, GetSponsorshipPricingViewModel, IGetSponsorshipPricingPresenter>
{
constructor() {}
async execute(
_input: void,
presenter: IGetSponsorshipPricingPresenter,
): Promise<void> {
presenter.reset();
const dto: GetSponsorshipPricingResultDTO = {
pricing: [
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
],
};
presenter.present(dto);
}
}

View File

@@ -0,0 +1,20 @@
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IGetTotalRacesPresenter, GetTotalRacesResultDTO, GetTotalRacesViewModel } from '../presenters/IGetTotalRacesPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface GetTotalRacesUseCaseParams {}
export interface GetTotalRacesResultDTO {
totalRaces: number;
}
export class GetTotalRacesUseCase implements UseCase<GetTotalRacesUseCaseParams, GetTotalRacesResultDTO, GetTotalRacesViewModel, IGetTotalRacesPresenter> {
constructor(private readonly raceRepository: IRaceRepository) {}
async execute(params: GetTotalRacesUseCaseParams, presenter: IGetTotalRacesPresenter): Promise<void> {
const races = await this.raceRepository.findAll();
const dto: GetTotalRacesResultDTO = { totalRaces: races.length };
presenter.reset();
presenter.present(dto);
}
}

View File

@@ -0,0 +1,25 @@
import type { IImportRaceResultsApiPresenter, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel } from '../presenters/IImportRaceResultsApiPresenter';
import type { UseCase } from '@gridpilot/shared/application/UseCase';
export interface ImportRaceResultsApiParams {
raceId: string;
resultsFileContent: string;
}
export class ImportRaceResultsApiUseCase implements UseCase<ImportRaceResultsApiParams, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel, IImportRaceResultsApiPresenter> {
constructor() {} // No repositories for mock
async execute(params: ImportRaceResultsApiParams, presenter: IImportRaceResultsApiPresenter): Promise<void> {
// Mock implementation
const dto: ImportRaceResultsApiResultDTO = {
success: true,
raceId: params.raceId,
driversProcessed: 10,
resultsRecorded: 10,
errors: [],
};
presenter.reset();
presenter.present(dto);
}
}