fix issues in core
This commit is contained in:
93
core/racing/application/dto/LeagueConfigFormDTO.ts
Normal file
93
core/racing/application/dto/LeagueConfigFormDTO.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export interface LeagueConfigFormModel {
|
||||
basics?: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
gameId?: string;
|
||||
};
|
||||
structure?: {
|
||||
mode?: string;
|
||||
maxDrivers?: number;
|
||||
};
|
||||
championships?: {
|
||||
enableDriverChampionship?: boolean;
|
||||
enableTeamChampionship?: boolean;
|
||||
enableNationsChampionship?: boolean;
|
||||
enableTrophyChampionship?: boolean;
|
||||
};
|
||||
scoring?: {
|
||||
patternId?: string;
|
||||
customScoringEnabled?: boolean;
|
||||
};
|
||||
dropPolicy?: {
|
||||
strategy?: string;
|
||||
n?: number;
|
||||
};
|
||||
timings?: {
|
||||
qualifyingMinutes?: number;
|
||||
mainRaceMinutes?: number;
|
||||
sessionCount?: number;
|
||||
roundsPlanned?: number;
|
||||
seasonStartDate?: string;
|
||||
raceStartTime?: string;
|
||||
timezoneId?: string;
|
||||
recurrenceStrategy?: string;
|
||||
weekdays?: string[];
|
||||
intervalWeeks?: number;
|
||||
monthlyOrdinal?: number;
|
||||
monthlyWeekday?: string;
|
||||
};
|
||||
stewarding?: {
|
||||
decisionMode?: string;
|
||||
requiredVotes?: number;
|
||||
requireDefense?: boolean;
|
||||
defenseTimeLimit?: number;
|
||||
voteTimeLimit?: number;
|
||||
protestDeadlineHours?: number;
|
||||
stewardingClosesHours?: number;
|
||||
notifyAccusedOnProtest?: boolean;
|
||||
notifyOnVoteRequired?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LeagueStructureFormDTO {
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
}
|
||||
|
||||
export interface LeagueChampionshipsFormDTO {
|
||||
pointsSystem: string;
|
||||
customPoints?: Record<number, number>;
|
||||
}
|
||||
|
||||
export interface LeagueScoringFormDTO {
|
||||
pointsSystem: string;
|
||||
customPoints?: Record<number, number>;
|
||||
}
|
||||
|
||||
export interface LeagueDropPolicyFormDTO {
|
||||
dropWeeks?: number;
|
||||
bestResults?: number;
|
||||
}
|
||||
|
||||
export interface LeagueStructureMode {
|
||||
mode: 'simple' | 'advanced';
|
||||
}
|
||||
|
||||
export interface LeagueTimingsFormDTO {
|
||||
sessionDuration?: number;
|
||||
qualifyingFormat?: string;
|
||||
}
|
||||
|
||||
export interface LeagueStewardingFormDTO {
|
||||
decisionMode: string;
|
||||
requiredVotes?: number;
|
||||
requireDefense?: boolean;
|
||||
defenseTimeLimit?: number;
|
||||
voteTimeLimit?: number;
|
||||
protestDeadlineHours?: number;
|
||||
stewardingClosesHours?: number;
|
||||
notifyAccusedOnProtest?: boolean;
|
||||
notifyOnVoteRequired?: boolean;
|
||||
}
|
||||
30
core/racing/application/dto/LeagueDTO.ts
Normal file
30
core/racing/application/dto/LeagueDTO.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface LeagueDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
settings: {
|
||||
pointsSystem: string;
|
||||
sessionDuration?: number;
|
||||
qualifyingFormat?: string;
|
||||
customPoints?: Record<number, number>;
|
||||
maxDrivers?: number;
|
||||
stewarding?: {
|
||||
decisionMode: string;
|
||||
requiredVotes?: number;
|
||||
requireDefense?: boolean;
|
||||
defenseTimeLimit?: number;
|
||||
voteTimeLimit?: number;
|
||||
protestDeadlineHours?: number;
|
||||
stewardingClosesHours?: number;
|
||||
notifyAccusedOnProtest?: boolean;
|
||||
notifyOnVoteRequired?: boolean;
|
||||
};
|
||||
};
|
||||
createdAt: Date;
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
}
|
||||
11
core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts
Normal file
11
core/racing/application/dto/LeagueDriverSeasonStatsDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface LeagueDriverSeasonStatsDTO {
|
||||
driverId: string;
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
totalPoints: number;
|
||||
averagePoints: number;
|
||||
bestFinish: number;
|
||||
podiums: number;
|
||||
races: number;
|
||||
wins: number;
|
||||
}
|
||||
21
core/racing/application/dto/LeagueScheduleDTO.ts
Normal file
21
core/racing/application/dto/LeagueScheduleDTO.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface LeagueScheduleDTO {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
races: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledTime: Date;
|
||||
trackId: string;
|
||||
status: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LeagueSchedulePreviewDTO {
|
||||
leagueId: string;
|
||||
preview: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledTime: Date;
|
||||
trackId: string;
|
||||
}>;
|
||||
}
|
||||
9
core/racing/application/dto/RaceDTO.ts
Normal file
9
core/racing/application/dto/RaceDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface RaceDTO {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
name: string;
|
||||
scheduledTime: Date;
|
||||
trackId: string;
|
||||
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled';
|
||||
results?: string[];
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface ReopenRaceCommandDTO {
|
||||
raceId: string;
|
||||
}
|
||||
9
core/racing/application/dto/ResultDTO.ts
Normal file
9
core/racing/application/dto/ResultDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ResultDTO {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
time?: string;
|
||||
incidents?: number;
|
||||
}
|
||||
10
core/racing/application/dto/StandingDTO.ts
Normal file
10
core/racing/application/dto/StandingDTO.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface StandingDTO {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
races: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
}
|
||||
7
core/racing/application/dto/index.ts
Normal file
7
core/racing/application/dto/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './LeagueConfigFormDTO';
|
||||
export * from './LeagueDTO';
|
||||
export * from './LeagueDriverSeasonStatsDTO';
|
||||
export * from './LeagueScheduleDTO';
|
||||
export * from './RaceDTO';
|
||||
export * from './ResultDTO';
|
||||
export * from './StandingDTO';
|
||||
27
core/racing/application/ports/DriverRatingPort.ts
Normal file
27
core/racing/application/ports/DriverRatingPort.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface DriverRatingChange {
|
||||
driverId: string;
|
||||
oldRating: number;
|
||||
newRating: number;
|
||||
change: number;
|
||||
}
|
||||
|
||||
export interface RatingChange {
|
||||
driverId: string;
|
||||
oldRating: number;
|
||||
newRating: number;
|
||||
change: number;
|
||||
}
|
||||
|
||||
export interface DriverRatingPort {
|
||||
calculateRatingChange(
|
||||
driverId: string,
|
||||
raceId: string,
|
||||
finalPosition: number,
|
||||
incidents: number,
|
||||
baseRating: number,
|
||||
): Promise<RatingChange>;
|
||||
|
||||
getDriverRating(driverId: string): Promise<number>;
|
||||
|
||||
updateDriverRating(driverId: string, newRating: number): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface AllRacesPageOutputPort {
|
||||
races: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
scheduledTime: Date;
|
||||
trackId: string;
|
||||
status: string;
|
||||
participants: number;
|
||||
}>;
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface ChampionshipStandingsOutputPort {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
standings: Array<{
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
driverName: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
}>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ChampionshipStandingsRowOutputPort {
|
||||
driverId: string;
|
||||
position: number;
|
||||
points: number;
|
||||
driverName: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface DriverRegistrationStatusOutputPort {
|
||||
driverId: string;
|
||||
raceId: string;
|
||||
leagueId: string;
|
||||
registered: boolean;
|
||||
status: 'registered' | 'withdrawn' | 'pending' | 'not_registered';
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { Weekday } from '../../domain/types/Weekday';
|
||||
@@ -32,7 +32,7 @@ export interface SeasonSummaryDTO {
|
||||
seasonId: string;
|
||||
leagueId: string;
|
||||
name: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
status: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled';
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isPrimary: boolean;
|
||||
@@ -56,7 +56,7 @@ export interface SeasonDetailsDTO {
|
||||
leagueId: string;
|
||||
gameId: string;
|
||||
name: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
status: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled';
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
maxDrivers?: number;
|
||||
@@ -69,11 +69,11 @@ export interface SeasonDetailsDTO {
|
||||
customScoringEnabled: boolean;
|
||||
};
|
||||
dropPolicy?: {
|
||||
strategy: import('../../domain/value-objects/SeasonDropPolicy').SeasonDropStrategy;
|
||||
strategy: string;
|
||||
n?: number;
|
||||
};
|
||||
stewarding?: {
|
||||
decisionMode: import('../../domain/entities/League').StewardingDecisionMode;
|
||||
decisionMode: string;
|
||||
requiredVotes?: number;
|
||||
requireDefense: boolean;
|
||||
defenseTimeLimit: number;
|
||||
@@ -95,7 +95,7 @@ export interface ManageSeasonLifecycleCommand {
|
||||
|
||||
export interface ManageSeasonLifecycleResultDTO {
|
||||
seasonId: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
status: 'planned' | 'active' | 'completed' | 'archived' | 'cancelled';
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
@@ -140,7 +140,7 @@ export class SeasonApplicationService {
|
||||
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
leagueId: league.id.toString(),
|
||||
gameId: command.gameId,
|
||||
name: command.name,
|
||||
year: new Date().getFullYear(),
|
||||
@@ -163,7 +163,7 @@ export class SeasonApplicationService {
|
||||
throw new Error(`League not found: ${query.leagueId}`);
|
||||
}
|
||||
|
||||
const seasons = await this.seasonRepository.listByLeague(league.id);
|
||||
const seasons = await this.seasonRepository.listByLeague(league.id.toString());
|
||||
const items: SeasonSummaryDTO[] = seasons.map((s) => ({
|
||||
seasonId: s.id,
|
||||
leagueId: s.leagueId,
|
||||
@@ -184,7 +184,7 @@ export class SeasonApplicationService {
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(query.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
if (!season || season.leagueId !== league.id.toString()) {
|
||||
throw new Error(`Season ${query.seasonId} does not belong to league ${league.id}`);
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ export class SeasonApplicationService {
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(command.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
if (!season || season.leagueId !== league.id.toString()) {
|
||||
throw new Error(`Season ${command.seasonId} does not belong to league ${league.id}`);
|
||||
}
|
||||
|
||||
@@ -288,29 +288,38 @@ export class SeasonApplicationService {
|
||||
maxDrivers?: number;
|
||||
} {
|
||||
const schedule = this.buildScheduleFromTimings(config);
|
||||
const scoringConfig = new SeasonScoringConfig({
|
||||
scoringPresetId: config.scoring.patternId ?? 'custom',
|
||||
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
|
||||
});
|
||||
const dropPolicy = new SeasonDropPolicy({
|
||||
strategy: config.dropPolicy.strategy,
|
||||
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
});
|
||||
const stewardingConfig = new SeasonStewardingConfig({
|
||||
decisionMode: config.stewarding.decisionMode,
|
||||
...(config.stewarding.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: config.stewarding.requireDefense,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||
});
|
||||
|
||||
const scoringConfig = config.scoring
|
||||
? new SeasonScoringConfig({
|
||||
scoringPresetId: config.scoring.patternId ?? 'custom',
|
||||
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const structure = config.structure;
|
||||
const dropPolicy = config.dropPolicy
|
||||
? new SeasonDropPolicy({
|
||||
strategy: config.dropPolicy.strategy as any,
|
||||
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const stewardingConfig = config.stewarding
|
||||
? new SeasonStewardingConfig({
|
||||
decisionMode: config.stewarding.decisionMode as any,
|
||||
...(config.stewarding.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: config.stewarding.requireDefense ?? false,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit ?? 48,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit ?? 72,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours ?? 48,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours ?? 168,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest ?? true,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired ?? true,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const structure = config.structure ?? {};
|
||||
const maxDrivers =
|
||||
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||
? structure.maxDrivers
|
||||
@@ -318,28 +327,28 @@ export class SeasonApplicationService {
|
||||
|
||||
return {
|
||||
...(schedule !== undefined ? { schedule } : {}),
|
||||
scoringConfig,
|
||||
dropPolicy,
|
||||
stewardingConfig,
|
||||
...(scoringConfig !== undefined ? { scoringConfig } : {}),
|
||||
...(dropPolicy !== undefined ? { dropPolicy } : {}),
|
||||
...(stewardingConfig !== undefined ? { stewardingConfig } : {}),
|
||||
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private buildScheduleFromTimings(config: LeagueConfigFormModel): SeasonSchedule | undefined {
|
||||
const { timings } = config;
|
||||
if (!timings.seasonStartDate || !timings.raceStartTime) {
|
||||
if (!timings || !timings.seasonStartDate || !timings.raceStartTime) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startDate = new Date(timings.seasonStartDate);
|
||||
const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime);
|
||||
const timezoneId = timings.timezoneId ?? 'UTC';
|
||||
const timezone = new LeagueTimezone(timezoneId);
|
||||
const timezone = LeagueTimezone.create(timezoneId);
|
||||
|
||||
const plannedRounds =
|
||||
typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0
|
||||
? timings.roundsPlanned
|
||||
: timings.sessionCount;
|
||||
: timings.sessionCount ?? 0;
|
||||
|
||||
const recurrence = (() => {
|
||||
const weekdays: WeekdaySet =
|
||||
@@ -353,10 +362,10 @@ export class SeasonApplicationService {
|
||||
weekdays,
|
||||
);
|
||||
case 'monthlyNthWeekday': {
|
||||
const pattern = new MonthlyRecurrencePattern({
|
||||
ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||
weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||
});
|
||||
const pattern = MonthlyRecurrencePattern.create(
|
||||
(timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||
(timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||
);
|
||||
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||
}
|
||||
case 'weekly':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NotificationService } from '@/notifications/application/ports/NotificationService';
|
||||
import type { NotificationService } from '@core/notifications/application/ports/NotificationService';
|
||||
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* This creates an active sponsorship and notifies the sponsor.
|
||||
*/
|
||||
|
||||
import type { NotificationService } from '@/notifications/application/ports/NotificationService';
|
||||
import type { NotificationService } from '@core/notifications/application/ports/NotificationService';
|
||||
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
@@ -200,11 +200,16 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when offered amount is less than minimum', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyForSponsorshipUseCase(
|
||||
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
@@ -228,11 +233,16 @@ describe('ApplyForSponsorshipUseCase', () => {
|
||||
});
|
||||
|
||||
it('should create sponsorship request and return result on success', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyForSponsorshipUseCase(
|
||||
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
|
||||
mockSponsorshipPricingRepo as unknown as ISponsorshipPricingRepository,
|
||||
mockSponsorRepo as unknown as ISponsorRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
);
|
||||
|
||||
mockSponsorRepo.findById.mockResolvedValue({ id: 'sponsor1' });
|
||||
|
||||
@@ -52,6 +52,7 @@ export class ApplyForSponsorshipUseCase {
|
||||
| 'NO_SLOTS_AVAILABLE'
|
||||
| 'PENDING_REQUEST_EXISTS'
|
||||
| 'OFFERED_AMOUNT_TOO_LOW'
|
||||
| 'VALIDATION_ERROR'
|
||||
>
|
||||
>
|
||||
> {
|
||||
@@ -82,10 +83,17 @@ export class ApplyForSponsorshipUseCase {
|
||||
return Result.err({ code: 'ENTITY_NOT_ACCEPTING_APPLICATIONS' });
|
||||
}
|
||||
|
||||
// Validate tier type
|
||||
const tier = input.tier as 'main' | 'secondary';
|
||||
if (tier !== 'main' && tier !== 'secondary') {
|
||||
this.logger.warn('Invalid tier', { tier: input.tier });
|
||||
return Result.err({ code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
|
||||
// Check if the requested tier slot is available
|
||||
const slotAvailable = pricing.isSlotAvailable(input.tier);
|
||||
const slotAvailable = pricing.isSlotAvailable(tier);
|
||||
if (!slotAvailable) {
|
||||
this.logger.warn(`No ${input.tier} sponsorship slots are available for entity ${input.entityId}`);
|
||||
this.logger.warn(`No ${tier} sponsorship slots are available for entity ${input.entityId}`);
|
||||
return Result.err({ code: 'NO_SLOTS_AVAILABLE' });
|
||||
}
|
||||
|
||||
@@ -105,24 +113,24 @@ export class ApplyForSponsorshipUseCase {
|
||||
}
|
||||
|
||||
// Validate offered amount meets minimum price
|
||||
const minPrice = pricing.getPrice(input.tier);
|
||||
const minPrice = pricing.getPrice(tier);
|
||||
if (minPrice && input.offeredAmount < minPrice.amount) {
|
||||
this.logger.warn(
|
||||
`Offered amount ${input.offeredAmount} is less than minimum ${minPrice.amount} for entity ${input.entityId}, tier ${input.tier}`,
|
||||
`Offered amount ${input.offeredAmount} is less than minimum ${minPrice.amount} for entity ${input.entityId}, tier ${tier}`,
|
||||
);
|
||||
return Result.err({ code: 'OFFERED_AMOUNT_TOO_LOW' });
|
||||
}
|
||||
|
||||
// Create the sponsorship request
|
||||
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const offeredAmount = Money.create(input.offeredAmount, input.currency ?? 'USD');
|
||||
const offeredAmount = Money.create(input.offeredAmount, (input.currency as any) || 'USD');
|
||||
|
||||
const request = SponsorshipRequest.create({
|
||||
id: requestId,
|
||||
sponsorId: input.sponsorId,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
tier: input.tier,
|
||||
tier,
|
||||
offeredAmount,
|
||||
...(input.message !== undefined ? { message: input.message } : {}),
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { IProtestRepository } from '../../domain/repositories/IProtestRepos
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
describe('ApplyPenaltyUseCase', () => {
|
||||
let mockPenaltyRepo: {
|
||||
@@ -49,12 +48,17 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when race does not exist', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyPenaltyUseCase(
|
||||
mockPenaltyRepo as unknown as IPenaltyRepository,
|
||||
mockProtestRepo as unknown as IProtestRepository,
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue(null);
|
||||
@@ -73,12 +77,17 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when steward does not have authority', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyPenaltyUseCase(
|
||||
mockPenaltyRepo as unknown as IPenaltyRepository,
|
||||
mockProtestRepo as unknown as IProtestRepository,
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
@@ -100,12 +109,17 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when protest does not exist', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyPenaltyUseCase(
|
||||
mockPenaltyRepo as unknown as IPenaltyRepository,
|
||||
mockProtestRepo as unknown as IProtestRepository,
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
@@ -129,12 +143,17 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when protest is not upheld', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyPenaltyUseCase(
|
||||
mockPenaltyRepo as unknown as IPenaltyRepository,
|
||||
mockProtestRepo as unknown as IProtestRepository,
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
@@ -158,12 +177,17 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should return error when protest is not for this race', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyPenaltyUseCase(
|
||||
mockPenaltyRepo as unknown as IPenaltyRepository,
|
||||
mockProtestRepo as unknown as IProtestRepository,
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
@@ -187,12 +211,17 @@ describe('ApplyPenaltyUseCase', () => {
|
||||
});
|
||||
|
||||
it('should create penalty and return result on success', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
};
|
||||
|
||||
const useCase = new ApplyPenaltyUseCase(
|
||||
mockPenaltyRepo as unknown as IPenaltyRepository,
|
||||
mockProtestRepo as unknown as IProtestRepository,
|
||||
mockRaceRepo as unknown as IRaceRepository,
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
output as any,
|
||||
);
|
||||
|
||||
mockRaceRepo.findById.mockResolvedValue({ id: 'race1', leagueId: 'league1' });
|
||||
|
||||
@@ -68,10 +68,10 @@ export class ApplyPenaltyUseCase {
|
||||
// Validate steward has authority (owner or admin of the league)
|
||||
const memberships = await this.leagueMembershipRepository.getLeagueMembers(race.leagueId);
|
||||
const stewardMembership = memberships.find(
|
||||
m => m.driverId === command.stewardId && m.status === 'active'
|
||||
m => m.driverId.toString() === command.stewardId && m.status.toString() === 'active'
|
||||
);
|
||||
|
||||
if (!stewardMembership || (stewardMembership.role !== 'owner' && stewardMembership.role !== 'admin')) {
|
||||
if (!stewardMembership || (stewardMembership.role.toString() !== 'owner' && stewardMembership.role.toString() !== 'admin')) {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Steward ${command.stewardId} does not have authority for league ${race.leagueId}.`);
|
||||
return Result.err({ code: 'INSUFFICIENT_AUTHORITY' });
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export class ApplyPenaltyUseCase {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Protest with ID ${command.protestId} not found.`);
|
||||
return Result.err({ code: 'PROTEST_NOT_FOUND' });
|
||||
}
|
||||
if (protest.status !== 'upheld') {
|
||||
if (protest.status.toString() !== 'upheld') {
|
||||
this.logger.warn(`ApplyPenaltyUseCase: Protest ${protest.id} is not upheld. Status: ${protest.status}`);
|
||||
return Result.err({ code: 'PROTEST_NOT_UPHELD' });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
|
||||
|
||||
const useCase = new ApproveLeagueJoinRequestUseCase(
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
);
|
||||
|
||||
const leagueId = 'league-1';
|
||||
@@ -34,22 +33,24 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
|
||||
|
||||
mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue(joinRequests);
|
||||
|
||||
const result = await useCase.execute({ leagueId, requestId });
|
||||
const result = await useCase.execute({ leagueId, requestId }, output as unknown as UseCaseOutputPort<any>);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(mockLeagueMembershipRepo.removeJoinRequest).toHaveBeenCalledWith(requestId);
|
||||
expect(mockLeagueMembershipRepo.saveMembership).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
leagueId,
|
||||
driverId: 'driver-1',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: expect.any(Date),
|
||||
});
|
||||
expect(mockLeagueMembershipRepo.saveMembership).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
leagueId: expect.objectContaining({ toString: expect.any(Function) }),
|
||||
driverId: expect.objectContaining({ toString: expect.any(Function) }),
|
||||
role: expect.objectContaining({ toString: expect.any(Function) }),
|
||||
status: expect.objectContaining({ toString: expect.any(Function) }),
|
||||
joinedAt: expect.any(Date),
|
||||
})
|
||||
);
|
||||
expect(output.present).toHaveBeenCalledWith({ success: true, message: 'Join request approved.' });
|
||||
});
|
||||
|
||||
|
||||
it('should return error if request not found', async () => {
|
||||
const output = {
|
||||
present: vi.fn(),
|
||||
@@ -57,12 +58,11 @@ describe('ApproveLeagueJoinRequestUseCase', () => {
|
||||
|
||||
const useCase = new ApproveLeagueJoinRequestUseCase(
|
||||
mockLeagueMembershipRepo as unknown as ILeagueMembershipRepository,
|
||||
output as unknown as UseCaseOutputPort<any>,
|
||||
);
|
||||
|
||||
mockLeagueMembershipRepo.getJoinRequests.mockResolvedValue([]);
|
||||
|
||||
const result = await useCase.execute({ leagueId: 'league-1', requestId: 'req-1' });
|
||||
const result = await useCase.execute({ leagueId: 'league-1', requestId: 'req-1' }, output as unknown as UseCaseOutputPort<any>);
|
||||
|
||||
expect(result.isOk()).toBe(false);
|
||||
expect(result.error!.code).toBe('JOIN_REQUEST_NOT_FOUND');
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { JoinedAt } from '../../domain/value-objects/JoinedAt';
|
||||
import { LeagueId } from '../../domain/entities/LeagueId';
|
||||
import { DriverId } from '../../domain/entities/DriverId';
|
||||
import { MembershipRole } from '../../domain/entities/MembershipRole';
|
||||
import { MembershipStatus } from '../../domain/entities/MembershipStatus';
|
||||
|
||||
export interface ApproveLeagueJoinRequestInput {
|
||||
leagueId: string;
|
||||
@@ -33,10 +37,10 @@ export class ApproveLeagueJoinRequestUseCase {
|
||||
await this.leagueMembershipRepository.removeJoinRequest(input.requestId);
|
||||
await this.leagueMembershipRepository.saveMembership({
|
||||
id: randomUUID(),
|
||||
leagueId: input.leagueId,
|
||||
driverId: request.driverId,
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
leagueId: LeagueId.create(input.leagueId),
|
||||
driverId: DriverId.create(request.driverId.toString()),
|
||||
role: MembershipRole.create('member'),
|
||||
status: MembershipStatus.create('active'),
|
||||
joinedAt: JoinedAt.create(new Date()),
|
||||
});
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('CancelRaceUseCase', () => {
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED');
|
||||
expect(result.unwrapErr().details?.message).toContain('already cancelled');
|
||||
expect((result.unwrapErr() as any).details.message).toContain('already cancelled');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ describe('CancelRaceUseCase', () => {
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('NOT_AUTHORIZED');
|
||||
expect(result.unwrapErr().details?.message).toContain('completed race');
|
||||
expect((result.unwrapErr() as any).details.message).toContain('completed race');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,10 @@ export class CancelRaceUseCase {
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
this.logger.warn(`[CancelRaceUseCase] Race with ID ${raceId} not found.`);
|
||||
return Result.err({ code: 'RACE_NOT_FOUND' });
|
||||
return Result.err({
|
||||
code: 'RACE_NOT_FOUND',
|
||||
details: { message: 'Race not found' }
|
||||
});
|
||||
}
|
||||
|
||||
const cancelledRace = race.cancel();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CloseRaceEventStewardingUseCase, type CloseRaceEventStewardingResult }
|
||||
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { DomainEventPublisher } from '@/shared/domain/DomainEvent';
|
||||
import type { DomainEventPublisher } from '@core/shared/domain/DomainEvent';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { RaceEvent } from '../../domain/entities/RaceEvent';
|
||||
import { Session } from '../../domain/entities/Session';
|
||||
@@ -118,7 +118,7 @@ describe('CloseRaceEventStewardingUseCase', () => {
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toContain('DB error');
|
||||
expect((result.unwrapErr() as any).details.message).toContain('DB error');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { Logger } from '@core/shared/application';
|
||||
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { DomainEventPublisher } from '@/shared/domain/DomainEvent';
|
||||
import type { DomainEventPublisher } from '@core/shared/domain/DomainEvent';
|
||||
import { RaceEventStewardingClosedEvent } from '../../domain/events/RaceEventStewardingClosed';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -3,13 +3,11 @@ import {
|
||||
CompleteDriverOnboardingUseCase,
|
||||
type CompleteDriverOnboardingInput,
|
||||
type CompleteDriverOnboardingResult,
|
||||
type CompleteDriverOnboardingApplicationError,
|
||||
} from './CompleteDriverOnboardingUseCase';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { Result } from '@core/shared/application/Result';
|
||||
|
||||
describe('CompleteDriverOnboardingUseCase', () => {
|
||||
let useCase: CompleteDriverOnboardingUseCase;
|
||||
@@ -35,7 +33,6 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
useCase = new CompleteDriverOnboardingUseCase(
|
||||
driverRepository as unknown as IDriverRepository,
|
||||
logger,
|
||||
output,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -62,9 +59,7 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
expect(output.present).toHaveBeenCalledWith({ driver: createdDriver });
|
||||
expect(result.unwrap()).toEqual({ driver: createdDriver });
|
||||
expect(driverRepository.findById).toHaveBeenCalledWith('user-1');
|
||||
expect(driverRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -99,7 +94,6 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('DRIVER_ALREADY_EXISTS');
|
||||
expect(driverRepository.create).not.toHaveBeenCalled();
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error when repository create throws', async () => {
|
||||
@@ -120,7 +114,6 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
const error = result.unwrapErr() as { code: 'REPOSITORY_ERROR'; details?: { message: string } };
|
||||
expect(error.code).toBe('REPOSITORY_ERROR');
|
||||
expect(error.details?.message).toBe('DB error');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle bio being undefined', async () => {
|
||||
@@ -144,7 +137,7 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(result.unwrap()).toEqual({ driver: createdDriver });
|
||||
expect(driverRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'user-1',
|
||||
@@ -154,7 +147,5 @@ describe('CompleteDriverOnboardingUseCase', () => {
|
||||
bio: undefined,
|
||||
})
|
||||
);
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
expect(output.present).toHaveBeenCalledWith({ driver: createdDriver });
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCase, UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { UseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
export interface CompleteDriverOnboardingInput {
|
||||
|
||||
@@ -57,13 +57,19 @@ export class CompleteRaceUseCase {
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
return Result.err({ code: 'RACE_NOT_FOUND' });
|
||||
return Result.err<void, ApplicationErrorCode<CompleteRaceErrorCode, { message: string }>>({
|
||||
code: 'RACE_NOT_FOUND',
|
||||
details: { message: 'Race not found' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get registered drivers for this race
|
||||
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
||||
if (registeredDriverIds.length === 0) {
|
||||
return Result.err({ code: 'NO_REGISTERED_DRIVERS' });
|
||||
return Result.err<void, ApplicationErrorCode<CompleteRaceErrorCode, { message: string }>>({
|
||||
code: 'NO_REGISTERED_DRIVERS',
|
||||
details: { message: 'No registered drivers for this race' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get driver ratings using injected provider
|
||||
@@ -175,9 +181,10 @@ export class CompleteRaceUseCase {
|
||||
// Group results by driver
|
||||
const resultsByDriver = new Map<string, RaceResult[]>();
|
||||
for (const result of results) {
|
||||
const existing = resultsByDriver.get(result.driverId) || [];
|
||||
const driverIdStr = result.driverId.toString();
|
||||
const existing = resultsByDriver.get(driverIdStr) || [];
|
||||
existing.push(result);
|
||||
resultsByDriver.set(result.driverId, existing);
|
||||
resultsByDriver.set(driverIdStr, existing);
|
||||
}
|
||||
|
||||
// Update or create standings for each driver
|
||||
@@ -193,7 +200,7 @@ export class CompleteRaceUseCase {
|
||||
|
||||
// Add all results for this driver (should be just one for this race)
|
||||
for (const result of driverResults) {
|
||||
standing = standing.addRaceResult(result.position, {
|
||||
standing = standing.addRaceResult(result.position.toNumber(), {
|
||||
1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,16 +52,25 @@ export class CompleteRaceUseCaseWithRatings {
|
||||
|
||||
const race = await this.raceRepository.findById(raceId);
|
||||
if (!race) {
|
||||
return Result.err({ code: 'RACE_NOT_FOUND' });
|
||||
return Result.err({
|
||||
code: 'RACE_NOT_FOUND',
|
||||
details: { message: 'Race not found' }
|
||||
});
|
||||
}
|
||||
|
||||
if (race.status === 'completed') {
|
||||
return Result.err({ code: 'ALREADY_COMPLETED' });
|
||||
return Result.err({
|
||||
code: 'ALREADY_COMPLETED',
|
||||
details: { message: 'Race already completed' }
|
||||
});
|
||||
}
|
||||
|
||||
const registeredDriverIds = await this.raceRegistrationRepository.getRegisteredDrivers(raceId);
|
||||
if (registeredDriverIds.length === 0) {
|
||||
return Result.err({ code: 'NO_REGISTERED_DRIVERS' });
|
||||
return Result.err({
|
||||
code: 'NO_REGISTERED_DRIVERS',
|
||||
details: { message: 'No registered drivers' }
|
||||
});
|
||||
}
|
||||
|
||||
const driverRatings = this.driverRatingProvider.getRatings(registeredDriverIds);
|
||||
@@ -107,23 +116,24 @@ export class CompleteRaceUseCaseWithRatings {
|
||||
private async updateStandings(leagueId: string, results: RaceResult[]): Promise<void> {
|
||||
const resultsByDriver = new Map<string, RaceResult[]>();
|
||||
for (const result of results) {
|
||||
const existing = resultsByDriver.get(result.driverId) || [];
|
||||
const driverIdStr = result.driverId.toString();
|
||||
const existing = resultsByDriver.get(driverIdStr) || [];
|
||||
existing.push(result);
|
||||
resultsByDriver.set(result.driverId, existing);
|
||||
resultsByDriver.set(driverIdStr, existing);
|
||||
}
|
||||
|
||||
for (const [driverId, driverResults] of resultsByDriver) {
|
||||
let standing = await this.standingRepository.findByDriverIdAndLeagueId(driverId, leagueId);
|
||||
for (const [driverIdStr, driverResults] of resultsByDriver) {
|
||||
let standing = await this.standingRepository.findByDriverIdAndLeagueId(driverIdStr, leagueId);
|
||||
|
||||
if (!standing) {
|
||||
standing = Standing.create({
|
||||
leagueId,
|
||||
driverId,
|
||||
driverId: driverIdStr,
|
||||
});
|
||||
}
|
||||
|
||||
for (const result of driverResults) {
|
||||
standing = standing.addRaceResult(result.position, {
|
||||
standing = standing.addRaceResult(result.position.toNumber(), {
|
||||
1: 25,
|
||||
2: 18,
|
||||
3: 15,
|
||||
@@ -143,11 +153,11 @@ export class CompleteRaceUseCaseWithRatings {
|
||||
|
||||
private async updateDriverRatings(results: RaceResult[], totalDrivers: number): Promise<void> {
|
||||
const driverResults = results.map((result) => ({
|
||||
driverId: result.driverId,
|
||||
position: result.position,
|
||||
driverId: result.driverId.toString(),
|
||||
position: result.position.toNumber(),
|
||||
totalDrivers,
|
||||
incidents: result.incidents,
|
||||
startPosition: result.startPosition,
|
||||
incidents: result.incidents.toNumber(),
|
||||
startPosition: result.startPosition.toNumber(),
|
||||
}));
|
||||
|
||||
await this.ratingUpdateService.updateDriverRatingsAfterRace(driverResults);
|
||||
|
||||
@@ -89,10 +89,10 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as CreateLeagueWithSeasonAndScoringResult;
|
||||
expect(presented.league.id.toString()).toBeDefined();
|
||||
expect(presented.season.id).toBeDefined();
|
||||
expect(presented.scoringConfig.seasonId.toString()).toBe(presented.season.id);
|
||||
const presented = (output.present as Mock).mock.calls[0]?.[0] as unknown as CreateLeagueWithSeasonAndScoringResult;
|
||||
expect(presented?.league.id.toString()).toBeDefined();
|
||||
expect(presented?.season.id).toBeDefined();
|
||||
expect(presented?.scoringConfig.seasonId.toString()).toBe(presented?.season.id);
|
||||
expect(leagueRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(seasonRepository.create).toHaveBeenCalledTimes(1);
|
||||
expect(leagueScoringConfigRepository.save).toHaveBeenCalledTimes(1);
|
||||
@@ -114,8 +114,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toBe('League name is required');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('VALIDATION_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('League name is required');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -135,8 +138,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command as CreateLeagueWithSeasonAndScoringCommand);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toBe('League ownerId is required');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('VALIDATION_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('League ownerId is required');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -157,7 +163,7 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toBe('gameId is required');
|
||||
expect((result.unwrapErr() as any).details.message).toBe('gameId is required');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -175,8 +181,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command as CreateLeagueWithSeasonAndScoringCommand);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toBe('visibility is required');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('VALIDATION_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('visibility is required');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -197,8 +206,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toBe('maxDrivers must be greater than 0 when provided');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('VALIDATION_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('maxDrivers must be greater than 0 when provided');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -219,8 +231,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('VALIDATION_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toContain('Ranked leagues require at least 10 drivers');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('VALIDATION_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toContain('Ranked leagues require at least 10 drivers');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -243,8 +258,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('UNKNOWN_PRESET');
|
||||
expect(result.unwrapErr().details?.message).toBe('Unknown scoring preset: unknown-preset');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('UNKNOWN_PRESET');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('Unknown scoring preset: unknown-preset');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -272,8 +290,11 @@ describe('CreateLeagueWithSeasonAndScoringUseCase', () => {
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr().code).toBe('REPOSITORY_ERROR');
|
||||
expect(result.unwrapErr().details?.message).toBe('DB error');
|
||||
const err = result.unwrapErr();
|
||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
||||
if ('details' in err && err.details && typeof err.details === 'object' && 'message' in err.details) {
|
||||
expect(err.details.message).toBe('DB error');
|
||||
}
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -117,12 +117,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase {
|
||||
const scoringConfig = LeagueScoringConfig.create({
|
||||
seasonId,
|
||||
scoringPresetId: preset.id,
|
||||
championships: {
|
||||
driver: command.enableDriverChampionship,
|
||||
team: command.enableTeamChampionship,
|
||||
nations: command.enableNationsChampionship,
|
||||
trophy: command.enableTrophyChampionship,
|
||||
},
|
||||
championships: [], // Empty array - will be populated by preset
|
||||
});
|
||||
this.logger.debug(`Scoring configuration created from preset ${preset.id}.`);
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@ function createLeagueConfigFormModel(overrides?: Partial<LeagueConfigFormModel>)
|
||||
};
|
||||
}
|
||||
|
||||
type CreateSeasonErrorCode = ApplicationErrorCode<'LEAGUE_NOT_FOUND' | 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'>;
|
||||
type CreateSeasonErrorCode = ApplicationErrorCode<'LEAGUE_NOT_FOUND' | 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'> & {
|
||||
details?: { message: string };
|
||||
};
|
||||
|
||||
describe('CreateSeasonForLeagueUseCase', () => {
|
||||
const mockLeagueFindById = vi.fn();
|
||||
@@ -146,9 +148,9 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as CreateSeasonForLeagueResult;
|
||||
expect(presented.season).toBeInstanceOf(Season);
|
||||
expect(presented.league.id).toBe('league-1');
|
||||
const presented = (output.present as Mock).mock.calls[0]?.[0] as CreateSeasonForLeagueResult;
|
||||
expect(presented?.season).toBeInstanceOf(Season);
|
||||
expect(presented?.league.id).toBe('league-1');
|
||||
});
|
||||
|
||||
it('clones configuration from a source season when sourceSeasonId is provided', async () => {
|
||||
@@ -179,8 +181,8 @@ describe('CreateSeasonForLeagueUseCase', () => {
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0] as CreateSeasonForLeagueResult;
|
||||
expect(presented.season.maxDrivers).toBe(40);
|
||||
const presented = (output.present as Mock).mock.calls[0]?.[0] as CreateSeasonForLeagueResult;
|
||||
expect(presented?.season.maxDrivers).toBe(40);
|
||||
});
|
||||
|
||||
it('returns error when league not found and does not call output', async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
|
||||
import type { LeagueConfigFormModel } from '@core/racing/application/dto/LeagueConfigFormDTO';
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
|
||||
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
@@ -93,7 +93,7 @@ export class CreateSeasonForLeagueUseCase {
|
||||
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
leagueId: league.id.toString(),
|
||||
gameId: input.gameId,
|
||||
name: input.name,
|
||||
year: new Date().getFullYear(),
|
||||
@@ -129,28 +129,28 @@ export class CreateSeasonForLeagueUseCase {
|
||||
} {
|
||||
const schedule = this.buildScheduleFromTimings(config);
|
||||
const scoringConfig = new SeasonScoringConfig({
|
||||
scoringPresetId: config.scoring.patternId ?? 'custom',
|
||||
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
|
||||
scoringPresetId: config.scoring?.patternId ?? 'custom',
|
||||
customScoringEnabled: config.scoring?.customScoringEnabled ?? false,
|
||||
});
|
||||
const dropPolicy = new SeasonDropPolicy({
|
||||
strategy: config.dropPolicy.strategy,
|
||||
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
strategy: (config.dropPolicy?.strategy as any) ?? 'none',
|
||||
...(config.dropPolicy?.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
});
|
||||
const stewardingConfig = new SeasonStewardingConfig({
|
||||
decisionMode: config.stewarding.decisionMode,
|
||||
...(config.stewarding.requiredVotes !== undefined
|
||||
decisionMode: (config.stewarding?.decisionMode as any) ?? 'auto',
|
||||
...(config.stewarding?.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: config.stewarding.requireDefense,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||
requireDefense: config.stewarding?.requireDefense ?? false,
|
||||
defenseTimeLimit: config.stewarding?.defenseTimeLimit ?? 0,
|
||||
voteTimeLimit: config.stewarding?.voteTimeLimit ?? 0,
|
||||
protestDeadlineHours: config.stewarding?.protestDeadlineHours ?? 0,
|
||||
stewardingClosesHours: config.stewarding?.stewardingClosesHours ?? 0,
|
||||
notifyAccusedOnProtest: config.stewarding?.notifyAccusedOnProtest ?? false,
|
||||
notifyOnVoteRequired: config.stewarding?.notifyOnVoteRequired ?? false,
|
||||
});
|
||||
|
||||
const structure = config.structure;
|
||||
const structure = config.structure ?? {};
|
||||
const maxDrivers =
|
||||
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||
? structure.maxDrivers
|
||||
@@ -169,14 +169,14 @@ export class CreateSeasonForLeagueUseCase {
|
||||
config: LeagueConfigFormModel,
|
||||
): SeasonSchedule | undefined {
|
||||
const { timings } = config;
|
||||
if (!timings.seasonStartDate || !timings.raceStartTime) {
|
||||
if (!timings || !timings.seasonStartDate || !timings.raceStartTime) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startDate = new Date(timings.seasonStartDate);
|
||||
const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime);
|
||||
const timezoneId = timings.timezoneId ?? 'UTC';
|
||||
const timezone = new LeagueTimezone(timezoneId);
|
||||
const timezone = LeagueTimezone.create(timezoneId);
|
||||
|
||||
const plannedRounds =
|
||||
typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0
|
||||
@@ -197,10 +197,10 @@ export class CreateSeasonForLeagueUseCase {
|
||||
weekdays,
|
||||
);
|
||||
case 'monthlyNthWeekday': {
|
||||
const pattern = new MonthlyRecurrencePattern({
|
||||
ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||
weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||
});
|
||||
const pattern = MonthlyRecurrencePattern.create(
|
||||
(timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||
(timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||
);
|
||||
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||
}
|
||||
case 'weekly':
|
||||
@@ -214,7 +214,7 @@ export class CreateSeasonForLeagueUseCase {
|
||||
timeOfDay,
|
||||
timezone,
|
||||
recurrence,
|
||||
plannedRounds,
|
||||
plannedRounds: plannedRounds ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ describe('CreateSponsorUseCase', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0];
|
||||
expect(presented.sponsor.id).toBeDefined();
|
||||
expect(presented.sponsor.name).toBe('Test Sponsor');
|
||||
expect(presented.sponsor.contactEmail).toBe('test@example.com');
|
||||
expect(presented.sponsor.websiteUrl).toBe('https://example.com');
|
||||
expect(presented.sponsor.logoUrl).toBe('https://example.com/logo.png');
|
||||
expect(presented.sponsor.createdAt).toBeInstanceOf(Date);
|
||||
const presented = (output.present as Mock).mock.calls[0]?.[0];
|
||||
expect(presented?.sponsor.id).toBeDefined();
|
||||
expect(presented?.sponsor.name).toBe('Test Sponsor');
|
||||
expect(presented?.sponsor.contactEmail).toBe('test@example.com');
|
||||
expect(presented?.sponsor.websiteUrl).toBe('https://example.com');
|
||||
expect(presented?.sponsor.logoUrl).toBe('https://example.com/logo.png');
|
||||
expect(presented?.sponsor.createdAt).toBeInstanceOf(Date);
|
||||
expect(sponsorRepository.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('CreateSponsorUseCase', () => {
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
expect(output.present).toHaveBeenCalledTimes(1);
|
||||
const presented = output.present.mock.calls[0][0];
|
||||
const presented = (output.present as Mock).mock.calls[0]?.[0];
|
||||
expect(presented.sponsor.websiteUrl).toBeUndefined();
|
||||
expect(presented.sponsor.logoUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -61,27 +61,27 @@ export class CreateSponsorUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
private validate(command: CreateSponsorCommand): Result<void, ApplicationErrorCode<'VALIDATION_ERROR', { message: string }>> {
|
||||
this.logger.debug('Validating CreateSponsorCommand', { command });
|
||||
if (!command.name || command.name.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: Sponsor name is required', { command });
|
||||
private validate(input: CreateSponsorInput): Result<void, ApplicationErrorCode<'VALIDATION_ERROR', { message: string }>> {
|
||||
this.logger.debug('Validating CreateSponsorInput', { input });
|
||||
if (!input.name || input.name.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: Sponsor name is required', { input });
|
||||
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Sponsor name is required' } });
|
||||
}
|
||||
if (!command.contactEmail || command.contactEmail.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: Sponsor contact email is required', { command });
|
||||
if (!input.contactEmail || input.contactEmail.trim().length === 0) {
|
||||
this.logger.warn('Validation failed: Sponsor contact email is required', { input });
|
||||
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Sponsor contact email is required' } });
|
||||
}
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(command.contactEmail)) {
|
||||
this.logger.warn('Validation failed: Invalid sponsor contact email format', { command });
|
||||
if (!emailRegex.test(input.contactEmail)) {
|
||||
this.logger.warn('Validation failed: Invalid sponsor contact email format', { input });
|
||||
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Invalid sponsor contact email format' } });
|
||||
}
|
||||
if (command.websiteUrl && command.websiteUrl.trim().length > 0) {
|
||||
if (input.websiteUrl && input.websiteUrl.trim().length > 0) {
|
||||
try {
|
||||
new URL(command.websiteUrl);
|
||||
new URL(input.websiteUrl);
|
||||
} catch {
|
||||
this.logger.warn('Validation failed: Invalid sponsor website URL', { command });
|
||||
this.logger.warn('Validation failed: Invalid sponsor website URL', { input });
|
||||
return Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Invalid sponsor website URL' } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
DashboardOverviewUseCase,
|
||||
@@ -10,12 +10,11 @@ import { Race } from '@core/racing/domain/entities/Race';
|
||||
import { League } from '@core/racing/domain/entities/League';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/Result';
|
||||
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
|
||||
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import { Result as UseCaseResult } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
|
||||
describe('DashboardOverviewUseCase', () => {
|
||||
it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepo
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import { Race } from '../../domain/entities/Race';
|
||||
import { Result as RaceResult } from '../../domain/entities/Result';
|
||||
|
||||
@@ -49,12 +49,12 @@ export class GetAllTeamsUseCase {
|
||||
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: [...team.leagues],
|
||||
createdAt: team.createdAt,
|
||||
name: team.name.props,
|
||||
tag: team.tag.props,
|
||||
description: team.description.props,
|
||||
ownerId: team.ownerId.toString(),
|
||||
leagues: team.leagues.map(l => l.toString()),
|
||||
createdAt: team.createdAt.toDate(),
|
||||
memberCount,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -13,8 +13,9 @@ import type { Logger } from '@core/shared/application';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
|
||||
export type SponsorshipEntityType = 'season' | 'league' | 'team';
|
||||
export type SponsorshipEntityType = SponsorableEntityType;
|
||||
|
||||
export type GetEntitySponsorshipPricingInput = {
|
||||
entityType: SponsorshipEntityType;
|
||||
@@ -23,11 +24,10 @@ export type GetEntitySponsorshipPricingInput = {
|
||||
|
||||
export type SponsorshipPricingTier = {
|
||||
name: string;
|
||||
price: SponsorshipPricing['mainSlot'] extends SponsorshipSlotConfig
|
||||
? SponsorshipSlotConfig['price']
|
||||
: SponsorshipPricing['secondarySlots'] extends SponsorshipSlotConfig
|
||||
? SponsorshipSlotConfig['price']
|
||||
: never;
|
||||
price: {
|
||||
amount: number;
|
||||
currency: string;
|
||||
};
|
||||
benefits: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ export type DriverSeasonStats = {
|
||||
driverId: string;
|
||||
position: number;
|
||||
driverName: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
teamId: string | undefined;
|
||||
teamName: string | undefined;
|
||||
totalPoints: number;
|
||||
basePoints: number;
|
||||
penaltyPoints: number;
|
||||
@@ -102,8 +102,8 @@ export class GetLeagueDriverSeasonStatsUseCase {
|
||||
const driverRatings = new Map<string, { rating: number | null; ratingChange: number | null }>();
|
||||
for (const standing of standings) {
|
||||
const driverId = String(standing.driverId);
|
||||
const ratingInfo = this.driverRatingPort.getRating(driverId);
|
||||
driverRatings.set(driverId, ratingInfo);
|
||||
const rating = await this.driverRatingPort.getDriverRating(driverId);
|
||||
driverRatings.set(driverId, { rating, ratingChange: null });
|
||||
}
|
||||
|
||||
const driverResults = new Map<string, Array<{ position: number }>>();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { League } from '../../domain/entities/League';
|
||||
import type { Season } from '../../domain/entities/Season';
|
||||
import type { Season } from '../../domain/entities/season/Season';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from './GetSeasonDetailsUseCase';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
import type { Season } from '../../domain/entities/Season';
|
||||
import type { Season } from '../../domain/entities/season/Season';
|
||||
|
||||
export type GetSeasonDetailsInput = {
|
||||
leagueId: string;
|
||||
|
||||
@@ -12,8 +12,8 @@ import { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import { Sponsor } from '../../domain/entities/Sponsor';
|
||||
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import { SeasonSponsorship } from '../../domain/entities/season/SeasonSponsorship';
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import { Money } from '../../domain/value-objects/Money';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application';
|
||||
|
||||
@@ -142,8 +142,8 @@ export class GetSponsorDashboardUseCase {
|
||||
);
|
||||
|
||||
sponsoredLeagues.push({
|
||||
leagueId: league.id,
|
||||
leagueName: league.name,
|
||||
leagueId: league.id.toString(),
|
||||
leagueName: league.name.toString(),
|
||||
tier: sponsorship.tier,
|
||||
metrics: {
|
||||
drivers: driverCount,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import { Result as RaceResult } from '../../domain/entities/Result';
|
||||
import { Result as RaceResult } from '../../domain/entities/result/Result';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
@@ -56,7 +56,7 @@ export class RegisterForRaceUseCase {
|
||||
const alreadyRegistered = await this.registrationRepository.isRegistered(raceId, driverId);
|
||||
if (alreadyRegistered) {
|
||||
this.logger.warn(`RegisterForRaceUseCase: driver ${driverId} already registered for race ${raceId}`);
|
||||
return Result.err({
|
||||
return Result.err<void, ApplicationErrorCode<RegisterForRaceErrorCode, { message: string }>>({
|
||||
code: 'ALREADY_REGISTERED',
|
||||
details: { message: 'Already registered for this race' },
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export class RegisterForRaceUseCase {
|
||||
const membership = await this.membershipRepository.getMembership(leagueId, driverId);
|
||||
if (!membership || membership.status !== 'active') {
|
||||
this.logger.error(`RegisterForRaceUseCase: driver ${driverId} not an active member of league ${leagueId}`);
|
||||
return Result.err({
|
||||
return Result.err<void, ApplicationErrorCode<RegisterForRaceErrorCode, { message: string }>>({
|
||||
code: 'NOT_ACTIVE_MEMBER',
|
||||
details: { message: 'Must be an active league member to register for races' },
|
||||
});
|
||||
@@ -94,14 +94,13 @@ export class RegisterForRaceUseCase {
|
||||
? error.message
|
||||
: 'Failed to register for race';
|
||||
|
||||
this.logger.error('RegisterForRaceUseCase: unexpected error during registration', {
|
||||
this.logger.error('RegisterForRaceUseCase: unexpected error during registration', error instanceof Error ? error : undefined, {
|
||||
raceId,
|
||||
leagueId,
|
||||
driverId,
|
||||
error,
|
||||
});
|
||||
|
||||
return Result.err({
|
||||
return Result.err<void, ApplicationErrorCode<RegisterForRaceErrorCode, { message: string }>>({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message },
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import { Season } from '../../domain/entities/season/Season';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
|
||||
|
||||
Reference in New Issue
Block a user