more seeds
This commit is contained in:
@@ -256,6 +256,177 @@ export const leagueScoringPresets: LeagueScoringPreset[] = [
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'sprint-main-team',
|
||||||
|
name: 'Sprint + Main (Teams)',
|
||||||
|
description: 'Teams championship using the Sprint + Main weekend format.',
|
||||||
|
primaryChampionshipType: 'team',
|
||||||
|
dropPolicySummary: 'Best 6 results of 8 count towards the championship.',
|
||||||
|
sessionSummary: 'Sprint + Main',
|
||||||
|
bonusSummary: 'Fastest lap +1 point in main race if finishing P10 or better.',
|
||||||
|
defaultTimings: {
|
||||||
|
practiceMinutes: 20,
|
||||||
|
qualifyingMinutes: 30,
|
||||||
|
sprintRaceMinutes: 20,
|
||||||
|
mainRaceMinutes: 40,
|
||||||
|
sessionCount: 2,
|
||||||
|
},
|
||||||
|
createConfig: ({ seasonId }) => {
|
||||||
|
const fastestLapBonus: BonusRule = {
|
||||||
|
id: 'fastest-lap-main-team',
|
||||||
|
type: 'fastestLap',
|
||||||
|
points: 1,
|
||||||
|
requiresFinishInTopN: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionTypes: SessionType[] = ['sprint', 'main'];
|
||||||
|
|
||||||
|
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||||
|
sprint: sprintPointsSprintMain,
|
||||||
|
main: mainPointsSprintMain,
|
||||||
|
practice: new PointsTable({}),
|
||||||
|
qualifying: new PointsTable({}),
|
||||||
|
q1: new PointsTable({}),
|
||||||
|
q2: new PointsTable({}),
|
||||||
|
q3: new PointsTable({}),
|
||||||
|
timeTrial: new PointsTable({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const bonusRulesBySessionType: Record<SessionType, BonusRule[]> = {
|
||||||
|
sprint: [],
|
||||||
|
main: [fastestLapBonus],
|
||||||
|
practice: [],
|
||||||
|
qualifying: [],
|
||||||
|
q1: [],
|
||||||
|
q2: [],
|
||||||
|
q3: [],
|
||||||
|
timeTrial: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropScorePolicy: DropScorePolicy = {
|
||||||
|
strategy: 'bestNResults',
|
||||||
|
count: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
const championship: ChampionshipConfig = {
|
||||||
|
id: 'team-champ-sprint-main',
|
||||||
|
name: 'Team Championship',
|
||||||
|
type: 'team' as ChampionshipType,
|
||||||
|
sessionTypes,
|
||||||
|
pointsTableBySessionType,
|
||||||
|
bonusRulesBySessionType,
|
||||||
|
dropScorePolicy,
|
||||||
|
};
|
||||||
|
|
||||||
|
return LeagueScoringConfig.create({
|
||||||
|
id: `lsc-${seasonId}-sprint-main-team`,
|
||||||
|
seasonId,
|
||||||
|
scoringPresetId: 'sprint-main-team',
|
||||||
|
championships: [championship],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'club-default-nations',
|
||||||
|
name: 'Club ladder (Nations)',
|
||||||
|
description: 'Nation vs nation ladder with a single main race and no bonuses.',
|
||||||
|
primaryChampionshipType: 'nations',
|
||||||
|
dropPolicySummary: 'All race results count, no drop scores.',
|
||||||
|
sessionSummary: 'Main race only',
|
||||||
|
bonusSummary: 'No bonus points.',
|
||||||
|
defaultTimings: {
|
||||||
|
practiceMinutes: 20,
|
||||||
|
qualifyingMinutes: 20,
|
||||||
|
sprintRaceMinutes: 0,
|
||||||
|
mainRaceMinutes: 40,
|
||||||
|
sessionCount: 1,
|
||||||
|
},
|
||||||
|
createConfig: ({ seasonId }) => {
|
||||||
|
const sessionTypes: SessionType[] = ['main'];
|
||||||
|
|
||||||
|
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||||
|
sprint: new PointsTable({}),
|
||||||
|
main: clubMainPoints,
|
||||||
|
practice: new PointsTable({}),
|
||||||
|
qualifying: new PointsTable({}),
|
||||||
|
q1: new PointsTable({}),
|
||||||
|
q2: new PointsTable({}),
|
||||||
|
q3: new PointsTable({}),
|
||||||
|
timeTrial: new PointsTable({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropScorePolicy: DropScorePolicy = {
|
||||||
|
strategy: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
const championship: ChampionshipConfig = {
|
||||||
|
id: 'nations-champ-club-default',
|
||||||
|
name: 'Nations Championship',
|
||||||
|
type: 'nations' as ChampionshipType,
|
||||||
|
sessionTypes,
|
||||||
|
pointsTableBySessionType,
|
||||||
|
dropScorePolicy,
|
||||||
|
};
|
||||||
|
|
||||||
|
return LeagueScoringConfig.create({
|
||||||
|
id: `lsc-${seasonId}-club-default-nations`,
|
||||||
|
seasonId,
|
||||||
|
scoringPresetId: 'club-default-nations',
|
||||||
|
championships: [championship],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'endurance-main-trophy',
|
||||||
|
name: 'Endurance Trophy Event',
|
||||||
|
description: 'Trophy-style endurance event with a single long main race.',
|
||||||
|
primaryChampionshipType: 'trophy',
|
||||||
|
dropPolicySummary: 'Best 4 results of 6 count towards the championship.',
|
||||||
|
sessionSummary: 'Main race only',
|
||||||
|
bonusSummary: 'No bonus points.',
|
||||||
|
defaultTimings: {
|
||||||
|
practiceMinutes: 30,
|
||||||
|
qualifyingMinutes: 20,
|
||||||
|
sprintRaceMinutes: 0,
|
||||||
|
mainRaceMinutes: 120,
|
||||||
|
sessionCount: 1,
|
||||||
|
},
|
||||||
|
createConfig: ({ seasonId }) => {
|
||||||
|
const sessionTypes: SessionType[] = ['main'];
|
||||||
|
|
||||||
|
const pointsTableBySessionType: Record<SessionType, PointsTable> = {
|
||||||
|
sprint: new PointsTable({}),
|
||||||
|
main: enduranceMainPoints,
|
||||||
|
practice: new PointsTable({}),
|
||||||
|
qualifying: new PointsTable({}),
|
||||||
|
q1: new PointsTable({}),
|
||||||
|
q2: new PointsTable({}),
|
||||||
|
q3: new PointsTable({}),
|
||||||
|
timeTrial: new PointsTable({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropScorePolicy: DropScorePolicy = {
|
||||||
|
strategy: 'bestNResults',
|
||||||
|
count: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const championship: ChampionshipConfig = {
|
||||||
|
id: 'trophy-champ-endurance-main',
|
||||||
|
name: 'Trophy Championship',
|
||||||
|
type: 'trophy' as ChampionshipType,
|
||||||
|
sessionTypes,
|
||||||
|
pointsTableBySessionType,
|
||||||
|
dropScorePolicy,
|
||||||
|
};
|
||||||
|
|
||||||
|
return LeagueScoringConfig.create({
|
||||||
|
id: `lsc-${seasonId}-endurance-main-trophy`,
|
||||||
|
seasonId,
|
||||||
|
scoringPresetId: 'endurance-main-trophy',
|
||||||
|
championships: [championship],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function listLeagueScoringPresets(): LeagueScoringPreset[] {
|
export function listLeagueScoringPresets(): LeagueScoringPreset[] {
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepo
|
|||||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||||
import type { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
|
import type { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
|
||||||
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
||||||
|
import type { ILeagueScoringConfigRepository } from '@core/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||||
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
|
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||||
import type { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository';
|
import type { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||||
import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
|
import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
|
||||||
import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository';
|
import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository';
|
||||||
|
import type { Season } from '@core/racing/domain/entities/season/Season';
|
||||||
|
import { getLeagueScoringPresetById } from './LeagueScoringPresets';
|
||||||
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
|
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
|
||||||
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
|
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
|
||||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||||
@@ -24,6 +27,7 @@ export type RacingSeedDependencies = {
|
|||||||
driverRepository: IDriverRepository;
|
driverRepository: IDriverRepository;
|
||||||
leagueRepository: ILeagueRepository;
|
leagueRepository: ILeagueRepository;
|
||||||
seasonRepository: ISeasonRepository;
|
seasonRepository: ISeasonRepository;
|
||||||
|
leagueScoringConfigRepository: ILeagueScoringConfigRepository;
|
||||||
seasonSponsorshipRepository: ISeasonSponsorshipRepository;
|
seasonSponsorshipRepository: ISeasonSponsorshipRepository;
|
||||||
sponsorshipRequestRepository: ISponsorshipRequestRepository;
|
sponsorshipRequestRepository: ISponsorshipRequestRepository;
|
||||||
leagueWalletRepository: ILeagueWalletRepository;
|
leagueWalletRepository: ILeagueWalletRepository;
|
||||||
@@ -51,7 +55,8 @@ export class SeedRacingData {
|
|||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
||||||
if (existingDrivers.length > 0) {
|
if (existingDrivers.length > 0) {
|
||||||
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist)');
|
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs');
|
||||||
|
await this.ensureScoringConfigsForExistingData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +95,26 @@ export class SeedRacingData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeSeasons = seed.seasons.filter((season) => season.status === 'active');
|
||||||
|
for (const season of activeSeasons) {
|
||||||
|
const presetId = this.selectScoringPresetIdForSeason(season);
|
||||||
|
const preset = getLeagueScoringPresetById(presetId);
|
||||||
|
|
||||||
|
if (!preset) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[Bootstrap] Scoring preset not found (presetId=${presetId}, seasonId=${season.id}, leagueId=${season.leagueId})`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoringConfig = preset.createConfig({ seasonId: season.id });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.seedDeps.leagueScoringConfigRepository.save(scoringConfig);
|
||||||
|
} catch {
|
||||||
|
// ignore duplicates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const sponsorship of seed.seasonSponsorships) {
|
for (const sponsorship of seed.seasonSponsorships) {
|
||||||
try {
|
try {
|
||||||
@@ -239,4 +264,64 @@ export class SeedRacingData {
|
|||||||
`[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
|
`[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureScoringConfigsForExistingData(): Promise<void> {
|
||||||
|
const leagues = await this.seedDeps.leagueRepository.findAll();
|
||||||
|
|
||||||
|
for (const league of leagues) {
|
||||||
|
const seasons = await this.seedDeps.seasonRepository.findByLeagueId(league.id.toString());
|
||||||
|
const activeSeasons = seasons.filter((season) => season.status === 'active');
|
||||||
|
|
||||||
|
for (const season of activeSeasons) {
|
||||||
|
const existing = await this.seedDeps.leagueScoringConfigRepository.findBySeasonId(season.id);
|
||||||
|
if (existing) continue;
|
||||||
|
|
||||||
|
const presetId = this.selectScoringPresetIdForSeason(season);
|
||||||
|
const preset = getLeagueScoringPresetById(presetId);
|
||||||
|
|
||||||
|
if (!preset) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[Bootstrap] Scoring preset not found (presetId=${presetId}, seasonId=${season.id}, leagueId=${season.leagueId})`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoringConfig = preset.createConfig({ seasonId: season.id });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.seedDeps.leagueScoringConfigRepository.save(scoringConfig);
|
||||||
|
} catch {
|
||||||
|
// ignore duplicates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectScoringPresetIdForSeason(season: Season): string {
|
||||||
|
if (season.leagueId === 'league-5' && season.status === 'active') {
|
||||||
|
return 'sprint-main-driver';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (season.leagueId === 'league-3') {
|
||||||
|
return season.id.endsWith('-b') ? 'sprint-main-team' : 'club-default-nations';
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = /^league-(\d+)$/.exec(season.leagueId);
|
||||||
|
const leagueNumber = match ? Number(match[1]) : undefined;
|
||||||
|
|
||||||
|
if (leagueNumber !== undefined) {
|
||||||
|
switch (leagueNumber % 4) {
|
||||||
|
case 0:
|
||||||
|
return 'sprint-main-team';
|
||||||
|
case 1:
|
||||||
|
return 'endurance-main-trophy';
|
||||||
|
case 2:
|
||||||
|
return 'sprint-main-driver';
|
||||||
|
case 3:
|
||||||
|
return 'club-default-nations';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'club-default';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,12 @@ export class RacingLeagueFactory {
|
|||||||
const owner = faker.helpers.arrayElement(this.drivers);
|
const owner = faker.helpers.arrayElement(this.drivers);
|
||||||
const config = leagueConfigs[idx % leagueConfigs.length]!;
|
const config = leagueConfigs[idx % leagueConfigs.length]!;
|
||||||
|
|
||||||
|
const createdAt =
|
||||||
|
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
|
||||||
|
idx % 6 === 0
|
||||||
|
? faker.date.recent({ days: 6, refDate: this.baseDate })
|
||||||
|
: faker.date.past({ years: 2, refDate: this.baseDate });
|
||||||
|
|
||||||
const leagueData: {
|
const leagueData: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -52,7 +58,7 @@ export class RacingLeagueFactory {
|
|||||||
description: faker.lorem.sentences(2),
|
description: faker.lorem.sentences(2),
|
||||||
ownerId: owner.id.toString(),
|
ownerId: owner.id.toString(),
|
||||||
settings: config,
|
settings: config,
|
||||||
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
createdAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add social links with varying completeness
|
// Add social links with varying completeness
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const racingSeedDefaults: Readonly<
|
|||||||
Required<RacingSeedOptions>
|
Required<RacingSeedOptions>
|
||||||
> = {
|
> = {
|
||||||
driverCount: 100,
|
driverCount: 100,
|
||||||
baseDate: new Date('2025-01-15T12:00:00.000Z'),
|
baseDate: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
class RacingSeedFactory {
|
class RacingSeedFactory {
|
||||||
|
|||||||
@@ -33,9 +33,13 @@ describe('InMemoryGameRepository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('findAll', () => {
|
describe('findAll', () => {
|
||||||
it('should return an empty array', async () => {
|
it('should return default seeded games', async () => {
|
||||||
const result = await repository.findAll();
|
const result = await repository.findAll();
|
||||||
expect(result).toEqual([]);
|
|
||||||
|
const ids = result.map((g) => g.id.toString());
|
||||||
|
expect(ids).toEqual(expect.arrayContaining(['iracing', 'acc', 'f1-24', 'f1-23']));
|
||||||
|
expect(result).toHaveLength(4);
|
||||||
|
|
||||||
expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryGameRepository] Finding all games.');
|
expect(mockLogger.debug).toHaveBeenCalledWith('[InMemoryGameRepository] Finding all games.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,20 @@ export class InMemoryGameRepository implements IGameRepository {
|
|||||||
|
|
||||||
constructor(private readonly logger: Logger) {
|
constructor(private readonly logger: Logger) {
|
||||||
this.logger.info('InMemoryGameRepository initialized.');
|
this.logger.info('InMemoryGameRepository initialized.');
|
||||||
|
this.seedDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
private seedDefaults(): void {
|
||||||
|
const defaults = [
|
||||||
|
Game.create({ id: 'iracing', name: 'iRacing' }),
|
||||||
|
Game.create({ id: 'acc', name: 'Assetto Corsa Competizione' }),
|
||||||
|
Game.create({ id: 'f1-24', name: 'F1 24' }),
|
||||||
|
Game.create({ id: 'f1-23', name: 'F1 23' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const game of defaults) {
|
||||||
|
this.games.set(game.id.toString(), game);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<Game | null> {
|
async findById(id: string): Promise<Game | null> {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe('InMemoryLeagueScoringPresetProvider', () => {
|
|||||||
it('should return all available presets', () => {
|
it('should return all available presets', () => {
|
||||||
const presets = provider.listPresets();
|
const presets = provider.listPresets();
|
||||||
|
|
||||||
expect(presets).toHaveLength(3);
|
expect(presets.length).toBeGreaterThanOrEqual(3);
|
||||||
expect(presets).toEqual(
|
expect(presets).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const BootstrapProviders: Provider[] = [
|
|||||||
driverRepository: RacingSeedDependencies['driverRepository'],
|
driverRepository: RacingSeedDependencies['driverRepository'],
|
||||||
leagueRepository: RacingSeedDependencies['leagueRepository'],
|
leagueRepository: RacingSeedDependencies['leagueRepository'],
|
||||||
seasonRepository: RacingSeedDependencies['seasonRepository'],
|
seasonRepository: RacingSeedDependencies['seasonRepository'],
|
||||||
|
leagueScoringConfigRepository: RacingSeedDependencies['leagueScoringConfigRepository'],
|
||||||
seasonSponsorshipRepository: RacingSeedDependencies['seasonSponsorshipRepository'],
|
seasonSponsorshipRepository: RacingSeedDependencies['seasonSponsorshipRepository'],
|
||||||
sponsorshipRequestRepository: RacingSeedDependencies['sponsorshipRequestRepository'],
|
sponsorshipRequestRepository: RacingSeedDependencies['sponsorshipRequestRepository'],
|
||||||
leagueWalletRepository: RacingSeedDependencies['leagueWalletRepository'],
|
leagueWalletRepository: RacingSeedDependencies['leagueWalletRepository'],
|
||||||
@@ -67,6 +68,7 @@ export const BootstrapProviders: Provider[] = [
|
|||||||
driverRepository,
|
driverRepository,
|
||||||
leagueRepository,
|
leagueRepository,
|
||||||
seasonRepository,
|
seasonRepository,
|
||||||
|
leagueScoringConfigRepository,
|
||||||
seasonSponsorshipRepository,
|
seasonSponsorshipRepository,
|
||||||
sponsorshipRequestRepository,
|
sponsorshipRequestRepository,
|
||||||
leagueWalletRepository,
|
leagueWalletRepository,
|
||||||
@@ -88,6 +90,7 @@ export const BootstrapProviders: Provider[] = [
|
|||||||
'IDriverRepository',
|
'IDriverRepository',
|
||||||
'ILeagueRepository',
|
'ILeagueRepository',
|
||||||
'ISeasonRepository',
|
'ISeasonRepository',
|
||||||
|
'ILeagueScoringConfigRepository',
|
||||||
'ISeasonSponsorshipRepository',
|
'ISeasonSponsorshipRepository',
|
||||||
'ISponsorshipRequestRepository',
|
'ISponsorshipRequestRepository',
|
||||||
'ILeagueWalletRepository',
|
'ILeagueWalletRepository',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
|||||||
import { Public } from '../auth/Public';
|
import { Public } from '../auth/Public';
|
||||||
import { LeagueService } from './LeagueService';
|
import { LeagueService } from './LeagueService';
|
||||||
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
||||||
|
import { AllLeaguesWithCapacityAndScoringDTO } from './dtos/AllLeaguesWithCapacityAndScoringDTO';
|
||||||
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
|
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
|
||||||
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
|
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
|
||||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||||
@@ -49,6 +50,14 @@ export class LeagueController {
|
|||||||
return this.leagueService.getAllLeaguesWithCapacity();
|
return this.leagueService.getAllLeaguesWithCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('all-with-capacity-and-scoring')
|
||||||
|
@ApiOperation({ summary: 'Get all leagues with capacity and scoring information' })
|
||||||
|
@ApiResponse({ status: 200, description: 'List of leagues with capacity and scoring', type: AllLeaguesWithCapacityAndScoringDTO })
|
||||||
|
async getAllLeaguesWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringDTO> {
|
||||||
|
return this.leagueService.getAllLeaguesWithCapacityAndScoring();
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Get('total-leagues')
|
@Get('total-leagues')
|
||||||
@ApiOperation({ summary: 'Get the total number of leagues' })
|
@ApiOperation({ summary: 'Get the total number of leagues' })
|
||||||
|
|||||||
@@ -15,16 +15,15 @@ import type { Logger } from '@core/shared/application/Logger';
|
|||||||
// Import concrete in-memory implementations
|
// Import concrete in-memory implementations
|
||||||
import type { ILeagueWalletRepository } from "@core/racing/domain/repositories/ILeagueWalletRepository";
|
import type { ILeagueWalletRepository } from "@core/racing/domain/repositories/ILeagueWalletRepository";
|
||||||
import type { ITransactionRepository } from "@core/racing/domain/repositories/ITransactionRepository";
|
import type { ITransactionRepository } from "@core/racing/domain/repositories/ITransactionRepository";
|
||||||
import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
|
import { getLeagueScoringPresetById, listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
|
||||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||||
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
|
||||||
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
|
||||||
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||||
|
|
||||||
// Import use cases
|
// Import use cases
|
||||||
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
||||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||||
|
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||||
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
||||||
@@ -50,6 +49,7 @@ import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-ca
|
|||||||
|
|
||||||
// Import presenters
|
// Import presenters
|
||||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||||
|
import { AllLeaguesWithCapacityAndScoringPresenter } from './presenters/AllLeaguesWithCapacityAndScoringPresenter';
|
||||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||||
@@ -89,6 +89,7 @@ export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
|||||||
export const LOGGER_TOKEN = 'Logger';
|
export const LOGGER_TOKEN = 'Logger';
|
||||||
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
||||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
|
||||||
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE = 'GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||||
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
|
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
|
||||||
export const GET_LEAGUE_FULL_CONFIG_USE_CASE = 'GetLeagueFullConfigUseCase';
|
export const GET_LEAGUE_FULL_CONFIG_USE_CASE = 'GetLeagueFullConfigUseCase';
|
||||||
export const GET_LEAGUE_SCORING_CONFIG_USE_CASE = 'GetLeagueScoringConfigUseCase';
|
export const GET_LEAGUE_SCORING_CONFIG_USE_CASE = 'GetLeagueScoringConfigUseCase';
|
||||||
@@ -114,6 +115,7 @@ export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUse
|
|||||||
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
|
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
|
||||||
|
|
||||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
|
||||||
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityAndScoringOutputPort_TOKEN';
|
||||||
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
|
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
|
||||||
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
|
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
|
||||||
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
|
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
|
||||||
@@ -144,22 +146,13 @@ export const LeagueProviders: Provider[] = [
|
|||||||
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
||||||
inject: [LOGGER_TOKEN],
|
inject: [LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: GAME_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryGameRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: LOGGER_TOKEN,
|
provide: LOGGER_TOKEN,
|
||||||
useClass: ConsoleLogger,
|
useClass: ConsoleLogger,
|
||||||
},
|
},
|
||||||
// Presenters
|
// Presenters
|
||||||
AllLeaguesWithCapacityPresenter,
|
AllLeaguesWithCapacityPresenter,
|
||||||
|
AllLeaguesWithCapacityAndScoringPresenter,
|
||||||
ApproveLeagueJoinRequestPresenter,
|
ApproveLeagueJoinRequestPresenter,
|
||||||
CreateLeaguePresenter,
|
CreateLeaguePresenter,
|
||||||
GetLeagueAdminPermissionsPresenter,
|
GetLeagueAdminPermissionsPresenter,
|
||||||
@@ -189,6 +182,10 @@ export const LeagueProviders: Provider[] = [
|
|||||||
provide: GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
|
provide: GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
|
||||||
useExisting: AllLeaguesWithCapacityPresenter,
|
useExisting: AllLeaguesWithCapacityPresenter,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
|
||||||
|
useExisting: AllLeaguesWithCapacityAndScoringPresenter,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
|
provide: GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
|
||||||
useExisting: LeagueStandingsPresenter,
|
useExisting: LeagueStandingsPresenter,
|
||||||
@@ -284,6 +281,34 @@ export const LeagueProviders: Provider[] = [
|
|||||||
new GetAllLeaguesWithCapacityUseCase(leagueRepo, membershipRepo),
|
new GetAllLeaguesWithCapacityUseCase(leagueRepo, membershipRepo),
|
||||||
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
|
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE,
|
||||||
|
useFactory: (
|
||||||
|
leagueRepo: ILeagueRepository,
|
||||||
|
membershipRepo: ILeagueMembershipRepository,
|
||||||
|
seasonRepo: ISeasonRepository,
|
||||||
|
scoringRepo: import('@core/racing/domain/repositories/ILeagueScoringConfigRepository').ILeagueScoringConfigRepository,
|
||||||
|
gameRepo: import('@core/racing/domain/repositories/IGameRepository').IGameRepository,
|
||||||
|
output: AllLeaguesWithCapacityAndScoringPresenter,
|
||||||
|
) =>
|
||||||
|
new GetAllLeaguesWithCapacityAndScoringUseCase(
|
||||||
|
leagueRepo,
|
||||||
|
membershipRepo,
|
||||||
|
seasonRepo,
|
||||||
|
scoringRepo,
|
||||||
|
gameRepo,
|
||||||
|
{ getPresetById: getLeagueScoringPresetById },
|
||||||
|
output,
|
||||||
|
),
|
||||||
|
inject: [
|
||||||
|
LEAGUE_REPOSITORY_TOKEN,
|
||||||
|
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||||
|
SEASON_REPOSITORY_TOKEN,
|
||||||
|
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||||
|
GAME_REPOSITORY_TOKEN,
|
||||||
|
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: GET_LEAGUE_STANDINGS_USE_CASE,
|
provide: GET_LEAGUE_STANDINGS_USE_CASE,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ describe('LeagueService', () => {
|
|||||||
const err = async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } });
|
const err = async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } });
|
||||||
|
|
||||||
const getAllLeaguesWithCapacityUseCase: any = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
|
const getAllLeaguesWithCapacityUseCase: any = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
|
||||||
|
const getAllLeaguesWithCapacityAndScoringUseCase: any = { execute: vi.fn(ok) };
|
||||||
const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
|
const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
|
||||||
const getLeagueStatsUseCase = { execute: vi.fn(ok) };
|
const getLeagueStatsUseCase = { execute: vi.fn(ok) };
|
||||||
const getLeagueFullConfigUseCase: any = { execute: vi.fn(ok) };
|
const getLeagueFullConfigUseCase: any = { execute: vi.fn(ok) };
|
||||||
@@ -35,6 +36,10 @@ describe('LeagueService', () => {
|
|||||||
const getSeasonSponsorshipsUseCase = { execute: vi.fn(ok) };
|
const getSeasonSponsorshipsUseCase = { execute: vi.fn(ok) };
|
||||||
|
|
||||||
const allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) };
|
const allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) };
|
||||||
|
const allLeaguesWithCapacityAndScoringPresenter = {
|
||||||
|
present: vi.fn(),
|
||||||
|
getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })),
|
||||||
|
};
|
||||||
const leagueStandingsPresenter = { getResponseModel: vi.fn(() => ({ standings: [] })) };
|
const leagueStandingsPresenter = { getResponseModel: vi.fn(() => ({ standings: [] })) };
|
||||||
const leagueProtestsPresenter = { getResponseModel: vi.fn(() => ({ protests: [] })) };
|
const leagueProtestsPresenter = { getResponseModel: vi.fn(() => ({ protests: [] })) };
|
||||||
const seasonSponsorshipsPresenter = { getViewModel: vi.fn(() => ({ sponsorships: [] })) };
|
const seasonSponsorshipsPresenter = { getViewModel: vi.fn(() => ({ sponsorships: [] })) };
|
||||||
@@ -62,6 +67,7 @@ describe('LeagueService', () => {
|
|||||||
|
|
||||||
const service = new LeagueService(
|
const service = new LeagueService(
|
||||||
getAllLeaguesWithCapacityUseCase as any,
|
getAllLeaguesWithCapacityUseCase as any,
|
||||||
|
getAllLeaguesWithCapacityAndScoringUseCase as any,
|
||||||
getLeagueStandingsUseCase as any,
|
getLeagueStandingsUseCase as any,
|
||||||
getLeagueStatsUseCase as any,
|
getLeagueStatsUseCase as any,
|
||||||
getLeagueFullConfigUseCase as any,
|
getLeagueFullConfigUseCase as any,
|
||||||
@@ -87,6 +93,7 @@ describe('LeagueService', () => {
|
|||||||
getSeasonSponsorshipsUseCase as any,
|
getSeasonSponsorshipsUseCase as any,
|
||||||
logger as any,
|
logger as any,
|
||||||
allLeaguesWithCapacityPresenter as any,
|
allLeaguesWithCapacityPresenter as any,
|
||||||
|
allLeaguesWithCapacityAndScoringPresenter as any,
|
||||||
leagueStandingsPresenter as any,
|
leagueStandingsPresenter as any,
|
||||||
leagueProtestsPresenter as any,
|
leagueProtestsPresenter as any,
|
||||||
seasonSponsorshipsPresenter as any,
|
seasonSponsorshipsPresenter as any,
|
||||||
@@ -149,6 +156,7 @@ describe('LeagueService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] });
|
await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] });
|
||||||
|
await expect(service.getAllLeaguesWithCapacityAndScoring()).resolves.toEqual({ leagues: [], totalCount: 0 });
|
||||||
|
|
||||||
// Error branch: getAllLeaguesWithCapacity throws on result.isErr()
|
// Error branch: getAllLeaguesWithCapacity throws on result.isErr()
|
||||||
getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } }));
|
getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } }));
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { WithdrawFromLeagueWalletOutputDTO } from './dtos/WithdrawFromLeagueWall
|
|||||||
|
|
||||||
// Core imports for view models
|
// Core imports for view models
|
||||||
import type { AllLeaguesWithCapacityDTO as AllLeaguesWithCapacityViewModel } from './dtos/AllLeaguesWithCapacityDTO';
|
import type { AllLeaguesWithCapacityDTO as AllLeaguesWithCapacityViewModel } from './dtos/AllLeaguesWithCapacityDTO';
|
||||||
|
import type { AllLeaguesWithCapacityAndScoringDTO as AllLeaguesWithCapacityAndScoringViewModel } from './dtos/AllLeaguesWithCapacityAndScoringDTO';
|
||||||
import type { CreateLeagueViewModel } from './dtos/CreateLeagueDTO';
|
import type { CreateLeagueViewModel } from './dtos/CreateLeagueDTO';
|
||||||
import type { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
|
import type { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
|
||||||
import { TotalLeaguesDTO } from './dtos/TotalLeaguesDTO';
|
import { TotalLeaguesDTO } from './dtos/TotalLeaguesDTO';
|
||||||
@@ -46,6 +47,7 @@ import type { Logger } from '@core/shared/application';
|
|||||||
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
||||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||||
|
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||||
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
||||||
@@ -70,6 +72,7 @@ import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-ca
|
|||||||
|
|
||||||
// API Presenters
|
// API Presenters
|
||||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||||
|
import { AllLeaguesWithCapacityAndScoringPresenter } from './presenters/AllLeaguesWithCapacityAndScoringPresenter';
|
||||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||||
@@ -98,6 +101,7 @@ import {
|
|||||||
LOGGER_TOKEN,
|
LOGGER_TOKEN,
|
||||||
GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE,
|
GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE,
|
||||||
GET_LEAGUE_STANDINGS_USE_CASE,
|
GET_LEAGUE_STANDINGS_USE_CASE,
|
||||||
|
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE,
|
||||||
GET_LEAGUE_STATS_USE_CASE,
|
GET_LEAGUE_STATS_USE_CASE,
|
||||||
GET_LEAGUE_FULL_CONFIG_USE_CASE,
|
GET_LEAGUE_FULL_CONFIG_USE_CASE,
|
||||||
GET_LEAGUE_SCORING_CONFIG_USE_CASE,
|
GET_LEAGUE_SCORING_CONFIG_USE_CASE,
|
||||||
@@ -121,6 +125,7 @@ import {
|
|||||||
WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE,
|
WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE,
|
||||||
GET_SEASON_SPONSORSHIPS_USE_CASE,
|
GET_SEASON_SPONSORSHIPS_USE_CASE,
|
||||||
GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
|
GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
|
||||||
|
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
|
||||||
GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
|
GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
|
||||||
GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
|
GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
|
||||||
GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
|
GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
|
||||||
@@ -149,6 +154,7 @@ import {
|
|||||||
export class LeagueService {
|
export class LeagueService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE) private readonly getAllLeaguesWithCapacityUseCase: GetAllLeaguesWithCapacityUseCase,
|
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE) private readonly getAllLeaguesWithCapacityUseCase: GetAllLeaguesWithCapacityUseCase,
|
||||||
|
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE) private readonly getAllLeaguesWithCapacityAndScoringUseCase: GetAllLeaguesWithCapacityAndScoringUseCase,
|
||||||
@Inject(GET_LEAGUE_STANDINGS_USE_CASE) private readonly getLeagueStandingsUseCase: GetLeagueStandingsUseCase,
|
@Inject(GET_LEAGUE_STANDINGS_USE_CASE) private readonly getLeagueStandingsUseCase: GetLeagueStandingsUseCase,
|
||||||
@Inject(GET_LEAGUE_STATS_USE_CASE) private readonly getLeagueStatsUseCase: GetLeagueStatsUseCase,
|
@Inject(GET_LEAGUE_STATS_USE_CASE) private readonly getLeagueStatsUseCase: GetLeagueStatsUseCase,
|
||||||
@Inject(GET_LEAGUE_FULL_CONFIG_USE_CASE) private readonly getLeagueFullConfigUseCase: GetLeagueFullConfigUseCase,
|
@Inject(GET_LEAGUE_FULL_CONFIG_USE_CASE) private readonly getLeagueFullConfigUseCase: GetLeagueFullConfigUseCase,
|
||||||
@@ -175,6 +181,7 @@ export class LeagueService {
|
|||||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||||
// Injected presenters
|
// Injected presenters
|
||||||
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityPresenter: AllLeaguesWithCapacityPresenter,
|
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityPresenter: AllLeaguesWithCapacityPresenter,
|
||||||
|
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityAndScoringPresenter: AllLeaguesWithCapacityAndScoringPresenter,
|
||||||
@Inject(GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN) private readonly leagueStandingsPresenter: LeagueStandingsPresenter,
|
@Inject(GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN) private readonly leagueStandingsPresenter: LeagueStandingsPresenter,
|
||||||
@Inject(GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN) private readonly leagueProtestsPresenter: GetLeagueProtestsPresenter,
|
@Inject(GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN) private readonly leagueProtestsPresenter: GetLeagueProtestsPresenter,
|
||||||
@Inject(GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN) private readonly seasonSponsorshipsPresenter: GetSeasonSponsorshipsPresenter,
|
@Inject(GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN) private readonly seasonSponsorshipsPresenter: GetSeasonSponsorshipsPresenter,
|
||||||
@@ -218,6 +225,22 @@ export class LeagueService {
|
|||||||
return this.allLeaguesWithCapacityPresenter.getViewModel();
|
return this.allLeaguesWithCapacityPresenter.getViewModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllLeaguesWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringViewModel> {
|
||||||
|
this.logger.debug('[LeagueService] Fetching all leagues with capacity and scoring.');
|
||||||
|
|
||||||
|
const result = await this.getAllLeaguesWithCapacityAndScoringUseCase.execute({});
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
const err = result.unwrapErr();
|
||||||
|
this.logger.error('[LeagueService] Failed to fetch leagues with capacity and scoring', new Error(err.code), {
|
||||||
|
details: err.details,
|
||||||
|
});
|
||||||
|
throw new Error(err.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allLeaguesWithCapacityAndScoringPresenter.getViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
async getTotalLeagues(): Promise<TotalLeaguesDTO> {
|
async getTotalLeagues(): Promise<TotalLeaguesDTO> {
|
||||||
this.logger.debug('[LeagueService] Fetching total leagues count.');
|
this.logger.debug('[LeagueService] Fetching total leagues count.');
|
||||||
await this.getTotalLeaguesUseCase.execute({});
|
await this.getTotalLeaguesUseCase.execute({});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const LOGGER_TOKEN = 'Logger';
|
|||||||
|
|
||||||
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
||||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
|
||||||
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE = 'GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||||
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
|
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
|
||||||
export const GET_LEAGUE_FULL_CONFIG_USE_CASE = 'GetLeagueFullConfigUseCase';
|
export const GET_LEAGUE_FULL_CONFIG_USE_CASE = 'GetLeagueFullConfigUseCase';
|
||||||
export const GET_LEAGUE_SCORING_CONFIG_USE_CASE = 'GetLeagueScoringConfigUseCase';
|
export const GET_LEAGUE_SCORING_CONFIG_USE_CASE = 'GetLeagueScoringConfigUseCase';
|
||||||
@@ -39,6 +40,7 @@ export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUse
|
|||||||
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
|
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
|
||||||
|
|
||||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
|
||||||
|
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityAndScoringOutputPort_TOKEN';
|
||||||
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
|
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
|
||||||
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
|
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
|
||||||
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
|
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
|
||||||
|
|||||||
@@ -1,14 +1,124 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNumber, IsArray, ValidateNested } from 'class-validator';
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { LeagueSummaryDTO } from './LeagueSummaryDTO';
|
import { IsArray, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
|
export class LeagueCapacityAndScoringSocialLinksDTO {
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
discordUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
youtubeUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
websiteUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LeagueCapacityAndScoringSettingsDTO {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNumber()
|
||||||
|
maxDrivers!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
sessionDuration?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
qualifyingFormat?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LeagueCapacityAndScoringSummaryScoringDTO {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
gameId!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
gameName!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
primaryChampionshipType!: 'driver' | 'team' | 'nations' | 'trophy';
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
scoringPresetId!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
scoringPresetName!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
dropPolicySummary!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
scoringPatternSummary!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LeagueWithCapacityAndScoringDTO {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ownerId!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
createdAt!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: LeagueCapacityAndScoringSettingsDTO })
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => LeagueCapacityAndScoringSettingsDTO)
|
||||||
|
settings!: LeagueCapacityAndScoringSettingsDTO;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNumber()
|
||||||
|
usedSlots!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSocialLinksDTO })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => LeagueCapacityAndScoringSocialLinksDTO)
|
||||||
|
socialLinks?: LeagueCapacityAndScoringSocialLinksDTO;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSummaryScoringDTO })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => LeagueCapacityAndScoringSummaryScoringDTO)
|
||||||
|
scoring?: LeagueCapacityAndScoringSummaryScoringDTO;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
timingSummary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class AllLeaguesWithCapacityAndScoringDTO {
|
export class AllLeaguesWithCapacityAndScoringDTO {
|
||||||
@ApiProperty({ type: [LeagueSummaryDTO] })
|
@ApiProperty({ type: [LeagueWithCapacityAndScoringDTO] })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => LeagueSummaryDTO)
|
@Type(() => LeagueWithCapacityAndScoringDTO)
|
||||||
leagues!: LeagueSummaryDTO[];
|
leagues!: LeagueWithCapacityAndScoringDTO[];
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||||
|
import type { GetAllLeaguesWithCapacityAndScoringResult } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||||
|
import type {
|
||||||
|
AllLeaguesWithCapacityAndScoringDTO,
|
||||||
|
LeagueWithCapacityAndScoringDTO,
|
||||||
|
} from '../dtos/AllLeaguesWithCapacityAndScoringDTO';
|
||||||
|
|
||||||
|
export class AllLeaguesWithCapacityAndScoringPresenter
|
||||||
|
implements UseCaseOutputPort<GetAllLeaguesWithCapacityAndScoringResult>
|
||||||
|
{
|
||||||
|
private result: AllLeaguesWithCapacityAndScoringDTO | null = null;
|
||||||
|
|
||||||
|
present(result: GetAllLeaguesWithCapacityAndScoringResult): void {
|
||||||
|
const leagues: LeagueWithCapacityAndScoringDTO[] = result.leagues.map((summary) => {
|
||||||
|
const timingSummary = summary.preset
|
||||||
|
? formatTimingSummary(summary.preset.defaultTimings.mainRaceMinutes)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: summary.league.id.toString(),
|
||||||
|
name: summary.league.name.toString(),
|
||||||
|
description: summary.league.description?.toString() || '',
|
||||||
|
ownerId: summary.league.ownerId.toString(),
|
||||||
|
createdAt: summary.league.createdAt.toDate().toISOString(),
|
||||||
|
settings: {
|
||||||
|
maxDrivers: summary.maxDrivers,
|
||||||
|
...(summary.league.settings.sessionDuration !== undefined
|
||||||
|
? { sessionDuration: summary.league.settings.sessionDuration }
|
||||||
|
: {}),
|
||||||
|
...(summary.league.settings.qualifyingFormat !== undefined
|
||||||
|
? { qualifyingFormat: summary.league.settings.qualifyingFormat.toString() }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
usedSlots: summary.currentDrivers,
|
||||||
|
...mapSocialLinks(summary.league.socialLinks),
|
||||||
|
...(summary.scoringConfig && summary.game && summary.preset
|
||||||
|
? {
|
||||||
|
scoring: {
|
||||||
|
gameId: summary.game.id.toString(),
|
||||||
|
gameName: summary.game.name.toString(),
|
||||||
|
primaryChampionshipType: summary.preset.primaryChampionshipType,
|
||||||
|
scoringPresetId: summary.scoringConfig.scoringPresetId?.toString() ?? 'custom',
|
||||||
|
scoringPresetName: summary.preset.name,
|
||||||
|
dropPolicySummary: summary.preset.dropPolicySummary,
|
||||||
|
scoringPatternSummary: summary.preset.sessionSummary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(timingSummary ? { timingSummary } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.result = {
|
||||||
|
leagues,
|
||||||
|
totalCount: leagues.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewModel(): AllLeaguesWithCapacityAndScoringDTO {
|
||||||
|
if (!this.result) throw new Error('Presenter not presented');
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimingSummary(mainRaceMinutes: number): string {
|
||||||
|
if (!Number.isFinite(mainRaceMinutes) || mainRaceMinutes <= 0) return '';
|
||||||
|
|
||||||
|
if (mainRaceMinutes >= 60 && mainRaceMinutes % 60 === 0) {
|
||||||
|
return `${mainRaceMinutes / 60}h Race`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${mainRaceMinutes}m Race`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocialLinksInput = {
|
||||||
|
discordUrl?: string | undefined;
|
||||||
|
youtubeUrl?: string | undefined;
|
||||||
|
websiteUrl?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SocialLinksOutput = {
|
||||||
|
discordUrl?: string;
|
||||||
|
youtubeUrl?: string;
|
||||||
|
websiteUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapSocialLinks(socialLinks: SocialLinksInput | undefined): { socialLinks: SocialLinksOutput } | {} {
|
||||||
|
if (!socialLinks) return {};
|
||||||
|
|
||||||
|
const mapped: SocialLinksOutput = {
|
||||||
|
...(socialLinks.discordUrl ? { discordUrl: socialLinks.discordUrl } : {}),
|
||||||
|
...(socialLinks.youtubeUrl ? { youtubeUrl: socialLinks.youtubeUrl } : {}),
|
||||||
|
...(socialLinks.websiteUrl ? { websiteUrl: socialLinks.websiteUrl } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.keys(mapped).length > 0 ? { socialLinks: mapped } : {};
|
||||||
|
}
|
||||||
@@ -21,6 +21,76 @@ type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
|
|||||||
type UploadMediaInput = UploadMediaInputDTO;
|
type UploadMediaInput = UploadMediaInputDTO;
|
||||||
type UpdateAvatarInput = UpdateAvatarInputDTO;
|
type UpdateAvatarInput = UpdateAvatarInputDTO;
|
||||||
|
|
||||||
|
function hashToHue(input: string): number {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < input.length; i += 1) {
|
||||||
|
hash = (hash * 31 + input.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return Math.abs(hash) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(input: string): string {
|
||||||
|
return input
|
||||||
|
.replaceAll('&', '\u0026amp;')
|
||||||
|
.replaceAll('<', '\u0026lt;')
|
||||||
|
.replaceAll('>', '\u0026gt;')
|
||||||
|
.replaceAll('"', '\u0026quot;')
|
||||||
|
.replaceAll("'", '\u0026apos;');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveLeagueLabel(leagueId: string): string {
|
||||||
|
const digits = leagueId.match(/\d+/)?.[0];
|
||||||
|
if (digits) return digits.slice(-2);
|
||||||
|
return leagueId.replaceAll(/[^a-zA-Z]/g, '').slice(0, 2).toUpperCase() || 'GP';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLeagueLogoSvg(leagueId: string): string {
|
||||||
|
const hue = hashToHue(leagueId);
|
||||||
|
const label = escapeXml(deriveLeagueLabel(leagueId));
|
||||||
|
const bg = `hsl(${hue} 70% 38%)`;
|
||||||
|
const border = `hsl(${hue} 70% 28%)`;
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="League logo">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="${bg}"/>
|
||||||
|
<stop offset="100%" stop-color="hsl(${hue} 80% 46%)"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="2" y="2" width="92" height="92" rx="18" fill="url(#g)" stroke="${border}" stroke-width="4"/>
|
||||||
|
<text x="48" y="56" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="34" font-weight="800" text-anchor="middle" fill="white">${label}</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLeagueCoverSvg(leagueId: string): string {
|
||||||
|
const hue = hashToHue(leagueId);
|
||||||
|
const title = escapeXml(leagueId);
|
||||||
|
const bg1 = `hsl(${hue} 70% 28%)`;
|
||||||
|
const bg2 = `hsl(${(hue + 35) % 360} 85% 35%)`;
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400" role="img" aria-label="League cover">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="${bg1}"/>
|
||||||
|
<stop offset="100%" stop-color="${bg2}"/>
|
||||||
|
</linearGradient>
|
||||||
|
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.07)" stroke-width="2"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="1200" height="400" fill="url(#bg)"/>
|
||||||
|
<rect width="1200" height="400" fill="url(#grid)"/>
|
||||||
|
<circle cx="1020" cy="120" r="180" fill="rgba(255,255,255,0.06)"/>
|
||||||
|
<circle cx="1080" cy="170" r="120" fill="rgba(255,255,255,0.05)"/>
|
||||||
|
|
||||||
|
<text x="64" y="110" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="40" font-weight="800" fill="rgba(255,255,255,0.92)">GridPilot League</text>
|
||||||
|
<text x="64" y="165" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="22" font-weight="600" fill="rgba(255,255,255,0.75)">${title}</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
@ApiTags('media')
|
@ApiTags('media')
|
||||||
@Controller('media')
|
@Controller('media')
|
||||||
export class MediaController {
|
export class MediaController {
|
||||||
@@ -61,6 +131,34 @@ export class MediaController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('leagues/:leagueId/logo')
|
||||||
|
@ApiOperation({ summary: 'Get league logo (placeholder)' })
|
||||||
|
@ApiParam({ name: 'leagueId', description: 'League ID' })
|
||||||
|
async getLeagueLogo(
|
||||||
|
@Param('leagueId') leagueId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueLogoSvg(leagueId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('leagues/:leagueId/cover')
|
||||||
|
@ApiOperation({ summary: 'Get league cover (placeholder)' })
|
||||||
|
@ApiParam({ name: 'leagueId', description: 'League ID' })
|
||||||
|
async getLeagueCover(
|
||||||
|
@Param('leagueId') leagueId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueCoverSvg(leagueId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Get(':mediaId')
|
@Get(':mediaId')
|
||||||
@ApiOperation({ summary: 'Get media by ID' })
|
@ApiOperation({ summary: 'Get media by ID' })
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenal
|
|||||||
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
|
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
|
||||||
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
||||||
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
|
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||||
|
import type { ILeagueScoringConfigRepository } from '@core/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||||
|
import type { IGameRepository } from '@core/racing/domain/repositories/IGameRepository';
|
||||||
import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
|
import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
|
||||||
import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository';
|
import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository';
|
||||||
import type { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
|
import type { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
|
||||||
@@ -38,6 +40,8 @@ import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory
|
|||||||
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||||
import { InMemorySeasonRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
import { InMemorySeasonRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||||
import { InMemorySeasonSponsorshipRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
import { InMemorySeasonSponsorshipRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||||
|
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||||
|
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||||
import { InMemoryLeagueWalletRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository';
|
import { InMemoryLeagueWalletRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository';
|
||||||
import { InMemoryTransactionRepository } from '@adapters/racing/persistence/inmemory/InMemoryTransactionRepository';
|
import { InMemoryTransactionRepository } from '@adapters/racing/persistence/inmemory/InMemoryTransactionRepository';
|
||||||
import { InMemorySponsorRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
import { InMemorySponsorRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||||
@@ -57,6 +61,8 @@ export const PENALTY_REPOSITORY_TOKEN = 'IPenaltyRepository';
|
|||||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||||
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository';
|
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository';
|
||||||
|
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
|
||||||
|
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
|
||||||
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
|
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
|
||||||
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
||||||
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
|
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
|
||||||
@@ -137,6 +143,16 @@ export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestReposito
|
|||||||
useFactory: (logger: Logger): ISeasonSponsorshipRepository => new InMemorySeasonSponsorshipRepository(logger),
|
useFactory: (logger: Logger): ISeasonSponsorshipRepository => new InMemorySeasonSponsorshipRepository(logger),
|
||||||
inject: ['Logger'],
|
inject: ['Logger'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||||
|
useFactory: (logger: Logger): ILeagueScoringConfigRepository => new InMemoryLeagueScoringConfigRepository(logger),
|
||||||
|
inject: ['Logger'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: GAME_REPOSITORY_TOKEN,
|
||||||
|
useFactory: (logger: Logger): IGameRepository => new InMemoryGameRepository(logger),
|
||||||
|
inject: ['Logger'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: LEAGUE_WALLET_REPOSITORY_TOKEN,
|
provide: LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger): ILeagueWalletRepository => new InMemoryLeagueWalletRepository(logger),
|
useFactory: (logger: Logger): ILeagueWalletRepository => new InMemoryLeagueWalletRepository(logger),
|
||||||
@@ -177,6 +193,8 @@ export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestReposito
|
|||||||
PROTEST_REPOSITORY_TOKEN,
|
PROTEST_REPOSITORY_TOKEN,
|
||||||
SEASON_REPOSITORY_TOKEN,
|
SEASON_REPOSITORY_TOKEN,
|
||||||
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
||||||
|
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||||
|
GAME_REPOSITORY_TOKEN,
|
||||||
LEAGUE_WALLET_REPOSITORY_TOKEN,
|
LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||||
TRANSACTION_REPOSITORY_TOKEN,
|
TRANSACTION_REPOSITORY_TOKEN,
|
||||||
SPONSOR_REPOSITORY_TOKEN,
|
SPONSOR_REPOSITORY_TOKEN,
|
||||||
|
|||||||
@@ -105,12 +105,12 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
|||||||
<div className="relative h-full rounded-xl bg-iron-gray border border-charcoal-outline overflow-hidden transition-all duration-200 hover:border-primary-blue/50 hover:shadow-[0_0_30px_rgba(25,140,255,0.15)] hover:bg-iron-gray/80">
|
<div className="relative h-full rounded-xl bg-iron-gray border border-charcoal-outline overflow-hidden transition-all duration-200 hover:border-primary-blue/50 hover:shadow-[0_0_30px_rgba(25,140,255,0.15)] hover:bg-iron-gray/80">
|
||||||
{/* Cover Image */}
|
{/* Cover Image */}
|
||||||
<div className="relative h-32 overflow-hidden">
|
<div className="relative h-32 overflow-hidden">
|
||||||
<Image
|
<img
|
||||||
src={coverUrl}
|
src={coverUrl}
|
||||||
alt={`${league.name} cover`}
|
alt={`${league.name} cover`}
|
||||||
fill
|
className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
loading="lazy"
|
||||||
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
{/* Gradient Overlay */}
|
{/* Gradient Overlay */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-iron-gray via-iron-gray/60 to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-iron-gray via-iron-gray/60 to-transparent" />
|
||||||
@@ -141,12 +141,14 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
|||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="absolute left-4 -bottom-6 z-10">
|
<div className="absolute left-4 -bottom-6 z-10">
|
||||||
<div className="w-12 h-12 rounded-lg overflow-hidden border-2 border-iron-gray bg-deep-graphite shadow-xl">
|
<div className="w-12 h-12 rounded-lg overflow-hidden border-2 border-iron-gray bg-deep-graphite shadow-xl">
|
||||||
<Image
|
<img
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt={`${league.name} logo`}
|
alt={`${league.name} logo`}
|
||||||
width={48}
|
width={48}
|
||||||
height={48}
|
height={48}
|
||||||
className="w-full h-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,12 +37,14 @@ export default function LeagueHeader({
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="h-16 w-16 rounded-xl overflow-hidden border-2 border-charcoal-outline bg-iron-gray shadow-lg">
|
<div className="h-16 w-16 rounded-xl overflow-hidden border-2 border-charcoal-outline bg-iron-gray shadow-lg">
|
||||||
<Image
|
<img
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt={`${leagueName} logo`}
|
alt={`${leagueName} logo`}
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
className="w-full h-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { RaceDTO } from '../../types/generated/RaceDTO';
|
|||||||
import type { GetLeagueAdminConfigOutputDTO } from '../../types/generated/GetLeagueAdminConfigOutputDTO';
|
import type { GetLeagueAdminConfigOutputDTO } from '../../types/generated/GetLeagueAdminConfigOutputDTO';
|
||||||
import type { LeagueScoringPresetDTO } from '../../types/generated/LeagueScoringPresetDTO';
|
import type { LeagueScoringPresetDTO } from '../../types/generated/LeagueScoringPresetDTO';
|
||||||
import type { LeagueSeasonSummaryDTO } from '../../types/generated/LeagueSeasonSummaryDTO';
|
import type { LeagueSeasonSummaryDTO } from '../../types/generated/LeagueSeasonSummaryDTO';
|
||||||
|
import type { AllLeaguesWithCapacityAndScoringDTO } from '../../types/AllLeaguesWithCapacityAndScoringDTO';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leagues API Client
|
* Leagues API Client
|
||||||
@@ -23,6 +24,11 @@ export class LeaguesApiClient extends BaseApiClient {
|
|||||||
return this.get<AllLeaguesWithCapacityDTO>('/leagues/all-with-capacity');
|
return this.get<AllLeaguesWithCapacityDTO>('/leagues/all-with-capacity');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get all leagues with capacity + scoring summary (for leagues page filters) */
|
||||||
|
getAllWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringDTO> {
|
||||||
|
return this.get<AllLeaguesWithCapacityAndScoringDTO>('/leagues/all-with-capacity-and-scoring');
|
||||||
|
}
|
||||||
|
|
||||||
/** Get total number of leagues */
|
/** Get total number of leagues */
|
||||||
getTotal(): Promise<TotalLeaguesDTO> {
|
getTotal(): Promise<TotalLeaguesDTO> {
|
||||||
return this.get<TotalLeaguesDTO>('/leagues/total-leagues');
|
return this.get<TotalLeaguesDTO>('/leagues/total-leagues');
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ describe('LeagueService', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockApiClient = {
|
mockApiClient = {
|
||||||
getAllWithCapacity: vi.fn(),
|
getAllWithCapacity: vi.fn(),
|
||||||
|
getAllWithCapacityAndScoring: vi.fn(),
|
||||||
getStandings: vi.fn(),
|
getStandings: vi.fn(),
|
||||||
getTotal: vi.fn(),
|
getTotal: vi.fn(),
|
||||||
getSchedule: vi.fn(),
|
getSchedule: vi.fn(),
|
||||||
@@ -30,7 +31,7 @@ describe('LeagueService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getAllLeagues', () => {
|
describe('getAllLeagues', () => {
|
||||||
it('should call apiClient.getAllWithCapacity and return array of LeagueSummaryViewModel', async () => {
|
it('should call apiClient.getAllWithCapacityAndScoring and return array of LeagueSummaryViewModel', async () => {
|
||||||
const mockDto = {
|
const mockDto = {
|
||||||
totalCount: 2,
|
totalCount: 2,
|
||||||
leagues: [
|
leagues: [
|
||||||
@@ -39,11 +40,11 @@ describe('LeagueService', () => {
|
|||||||
],
|
],
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
|
mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue(mockDto);
|
||||||
|
|
||||||
const result = await service.getAllLeagues();
|
const result = await service.getAllLeagues();
|
||||||
|
|
||||||
expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
|
expect(mockApiClient.getAllWithCapacityAndScoring).toHaveBeenCalled();
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
expect(result.map((l) => l.id)).toEqual(['league-1', 'league-2']);
|
expect(result.map((l) => l.id)).toEqual(['league-1', 'league-2']);
|
||||||
});
|
});
|
||||||
@@ -51,16 +52,16 @@ describe('LeagueService', () => {
|
|||||||
it('should handle empty leagues array', async () => {
|
it('should handle empty leagues array', async () => {
|
||||||
const mockDto = { totalCount: 0, leagues: [] } as any;
|
const mockDto = { totalCount: 0, leagues: [] } as any;
|
||||||
|
|
||||||
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
|
mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue(mockDto);
|
||||||
|
|
||||||
const result = await service.getAllLeagues();
|
const result = await service.getAllLeagues();
|
||||||
|
|
||||||
expect(result).toHaveLength(0);
|
expect(result).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when apiClient.getAllWithCapacity fails', async () => {
|
it('should throw error when apiClient.getAllWithCapacityAndScoring fails', async () => {
|
||||||
const error = new Error('API call failed');
|
const error = new Error('API call failed');
|
||||||
mockApiClient.getAllWithCapacity.mockRejectedValue(error);
|
mockApiClient.getAllWithCapacityAndScoring.mockRejectedValue(error);
|
||||||
|
|
||||||
await expect(service.getAllLeagues()).rejects.toThrow('API call failed');
|
await expect(service.getAllLeagues()).rejects.toThrow('API call failed');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,17 +44,20 @@ export class LeagueService {
|
|||||||
* Get all leagues with view model transformation
|
* Get all leagues with view model transformation
|
||||||
*/
|
*/
|
||||||
async getAllLeagues(): Promise<LeagueSummaryViewModel[]> {
|
async getAllLeagues(): Promise<LeagueSummaryViewModel[]> {
|
||||||
const dto = await this.apiClient.getAllWithCapacity();
|
const dto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||||
return dto.leagues.map((league: LeagueWithCapacityDTO) => ({
|
|
||||||
|
return dto.leagues.map((league) => ({
|
||||||
id: league.id,
|
id: league.id,
|
||||||
name: league.name,
|
name: league.name,
|
||||||
description: (league as any).description ?? '',
|
description: league.description,
|
||||||
ownerId: (league as any).ownerId ?? '',
|
ownerId: league.ownerId,
|
||||||
createdAt: (league as any).createdAt ?? '',
|
createdAt: league.createdAt,
|
||||||
maxDrivers: (league as any).settings?.maxDrivers ?? 0,
|
maxDrivers: league.settings?.maxDrivers ?? 0,
|
||||||
usedDriverSlots: (league as any).usedSlots ?? 0,
|
usedDriverSlots: league.usedSlots ?? 0,
|
||||||
structureSummary: 'TBD',
|
structureSummary: league.scoring?.scoringPresetName ?? 'Custom rules',
|
||||||
timingSummary: 'TBD'
|
scoringPatternSummary: league.scoring?.scoringPatternSummary,
|
||||||
|
timingSummary: league.timingSummary ?? '',
|
||||||
|
...(league.scoring ? { scoring: league.scoring } : {}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,16 +165,16 @@ export class LeagueService {
|
|||||||
// For now, assume league data comes from getAllWithCapacity or a new endpoint
|
// For now, assume league data comes from getAllWithCapacity or a new endpoint
|
||||||
// Since API may not have detailed league, we'll mock or assume
|
// Since API may not have detailed league, we'll mock or assume
|
||||||
// In real implementation, add getLeagueDetail to API
|
// In real implementation, add getLeagueDetail to API
|
||||||
const allLeagues = await this.apiClient.getAllWithCapacity();
|
const allLeagues = await this.apiClient.getAllWithCapacityAndScoring();
|
||||||
const leagueDto = allLeagues.leagues.find((l: any) => l.id === leagueId);
|
const leagueDto = allLeagues.leagues.find((l) => l.id === leagueId);
|
||||||
if (!leagueDto) return null;
|
if (!leagueDto) return null;
|
||||||
|
|
||||||
// LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided
|
// LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided
|
||||||
const league = {
|
const league = {
|
||||||
id: leagueDto.id,
|
id: leagueDto.id,
|
||||||
name: leagueDto.name,
|
name: leagueDto.name,
|
||||||
description: (leagueDto as any).description ?? 'Description not available',
|
description: leagueDto.description ?? 'Description not available',
|
||||||
ownerId: (leagueDto as any).ownerId ?? 'owner-id',
|
ownerId: leagueDto.ownerId ?? 'owner-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get owner
|
// Get owner
|
||||||
@@ -245,12 +248,12 @@ export class LeagueService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get league basic info
|
// Get league basic info
|
||||||
const allLeagues = await this.apiClient.getAllWithCapacity();
|
const allLeagues = await this.apiClient.getAllWithCapacityAndScoring();
|
||||||
const league = allLeagues.leagues.find((l: any) => l.id === leagueId);
|
const league = allLeagues.leagues.find((l) => l.id === leagueId);
|
||||||
if (!league) return null;
|
if (!league) return null;
|
||||||
|
|
||||||
// Get owner
|
// Get owner
|
||||||
const owner = await this.driversApiClient.getDriver((league as any).ownerId);
|
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
||||||
|
|
||||||
// League scoring configuration is not exposed separately yet; use null to indicate "not configured" in the UI
|
// League scoring configuration is not exposed separately yet; use null to indicate "not configured" in the UI
|
||||||
const scoringConfig: LeagueScoringConfigDTO | null = null;
|
const scoringConfig: LeagueScoringConfigDTO | null = null;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export type LeagueCapacityAndScoringPrimaryChampionshipType =
|
||||||
|
| 'driver'
|
||||||
|
| 'team'
|
||||||
|
| 'nations'
|
||||||
|
| 'trophy';
|
||||||
|
|
||||||
|
export type LeagueCapacityAndScoringSummaryScoringDTO = {
|
||||||
|
gameId: string;
|
||||||
|
gameName: string;
|
||||||
|
primaryChampionshipType: LeagueCapacityAndScoringPrimaryChampionshipType;
|
||||||
|
scoringPresetId: string;
|
||||||
|
scoringPresetName: string;
|
||||||
|
dropPolicySummary: string;
|
||||||
|
scoringPatternSummary: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LeagueCapacityAndScoringSocialLinksDTO = {
|
||||||
|
discordUrl?: string;
|
||||||
|
youtubeUrl?: string;
|
||||||
|
websiteUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LeagueCapacityAndScoringSettingsDTO = {
|
||||||
|
maxDrivers: number;
|
||||||
|
sessionDuration?: number;
|
||||||
|
qualifyingFormat?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LeagueWithCapacityAndScoringDTO = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
ownerId: string;
|
||||||
|
createdAt: string;
|
||||||
|
settings: LeagueCapacityAndScoringSettingsDTO;
|
||||||
|
usedSlots: number;
|
||||||
|
socialLinks?: LeagueCapacityAndScoringSocialLinksDTO;
|
||||||
|
scoring?: LeagueCapacityAndScoringSummaryScoringDTO;
|
||||||
|
timingSummary?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AllLeaguesWithCapacityAndScoringDTO = {
|
||||||
|
leagues: LeagueWithCapacityAndScoringDTO[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
@@ -20,6 +20,24 @@ const nextConfig = {
|
|||||||
hostname: 'picsum.photos',
|
hostname: 'picsum.photos',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
dangerouslyAllowSVG: true,
|
||||||
|
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
||||||
|
contentDispositionType: 'inline',
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
const rawBaseUrl =
|
||||||
|
process.env.API_BASE_URL ??
|
||||||
|
process.env.NEXT_PUBLIC_API_BASE_URL ??
|
||||||
|
'http://localhost:3001';
|
||||||
|
|
||||||
|
const baseUrl = rawBaseUrl.endsWith('/') ? rawBaseUrl.slice(0, -1) : rawBaseUrl;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/api/media/:path*',
|
||||||
|
destination: `${baseUrl}/media/:path*`,
|
||||||
|
},
|
||||||
|
];
|
||||||
},
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: false,
|
ignoreBuildErrors: false,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: apps/api/Dockerfile.dev
|
dockerfile: apps/api/Dockerfile.dev
|
||||||
|
working_dir: /app/apps/api
|
||||||
env_file:
|
env_file:
|
||||||
- .env.development
|
- .env.development
|
||||||
environment:
|
environment:
|
||||||
@@ -40,7 +41,7 @@ services:
|
|||||||
- ./:/app
|
- ./:/app
|
||||||
- dev_node_modules:/app/node_modules
|
- dev_node_modules:/app/node_modules
|
||||||
- dev_npm_cache:/root/.npm
|
- dev_npm_cache:/root/.npm
|
||||||
command: ["sh", "-lc", "npm run start:dev --workspace=@gridpilot/api"]
|
command: ["sh", "-lc", "npm run start:dev"]
|
||||||
depends_on:
|
depends_on:
|
||||||
deps:
|
deps:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|||||||
Reference in New Issue
Block a user