website refactor
This commit is contained in:
@@ -13,7 +13,7 @@ NEXT_TELEMETRY_DISABLED=1
|
|||||||
# API (NestJS)
|
# API (NestJS)
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
# API persistence is inferred from DATABASE_URL by default.
|
# API persistence is inferred from DATABASE_URL by default.
|
||||||
# GRIDPILOT_API_PERSISTENCE=postgres
|
GRIDPILOT_API_PERSISTENCE=postgres
|
||||||
|
|
||||||
DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev
|
DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ export class SeedRacingData {
|
|||||||
if (existingDrivers.length > 0 && !forceReseed) {
|
if (existingDrivers.length > 0 && !forceReseed) {
|
||||||
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs');
|
this.logger.info('[Bootstrap] Racing seed skipped (drivers already exist), ensuring scoring configs');
|
||||||
await this.ensureScoringConfigsForExistingData();
|
await this.ensureScoringConfigsForExistingData();
|
||||||
|
// Even when skipping full seed, ensure stats are computed and stored
|
||||||
|
this.logger.info('[Bootstrap] Computing and storing driver/team stats for existing data');
|
||||||
|
await this.computeAndStoreDriverStats();
|
||||||
|
await this.computeAndStoreTeamStats();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,33 +31,33 @@ export class DriverStatsOrmMapper {
|
|||||||
const entityName = 'DriverStats';
|
const entityName = 'DriverStats';
|
||||||
|
|
||||||
assertNonEmptyString(entityName, 'driverId', entity.driverId);
|
assertNonEmptyString(entityName, 'driverId', entity.driverId);
|
||||||
assertInteger(entityName, 'rating', entity.rating);
|
assertInteger(entityName, 'rating', Number(entity.rating));
|
||||||
assertInteger(entityName, 'safetyRating', entity.safetyRating);
|
assertInteger(entityName, 'safetyRating', Number(entity.safetyRating));
|
||||||
assertInteger(entityName, 'sportsmanshipRating', entity.sportsmanshipRating);
|
assertNumber(entityName, 'sportsmanshipRating', Number(entity.sportsmanshipRating));
|
||||||
assertInteger(entityName, 'totalRaces', entity.totalRaces);
|
assertInteger(entityName, 'totalRaces', Number(entity.totalRaces));
|
||||||
assertInteger(entityName, 'wins', entity.wins);
|
assertInteger(entityName, 'wins', Number(entity.wins));
|
||||||
assertInteger(entityName, 'podiums', entity.podiums);
|
assertInteger(entityName, 'podiums', Number(entity.podiums));
|
||||||
assertInteger(entityName, 'dnfs', entity.dnfs);
|
assertInteger(entityName, 'dnfs', Number(entity.dnfs));
|
||||||
assertNumber(entityName, 'avgFinish', entity.avgFinish);
|
assertNumber(entityName, 'avgFinish', Number(entity.avgFinish));
|
||||||
assertInteger(entityName, 'bestFinish', entity.bestFinish);
|
assertInteger(entityName, 'bestFinish', Number(entity.bestFinish));
|
||||||
assertInteger(entityName, 'worstFinish', entity.worstFinish);
|
assertInteger(entityName, 'worstFinish', Number(entity.worstFinish));
|
||||||
assertInteger(entityName, 'consistency', entity.consistency);
|
assertInteger(entityName, 'consistency', Number(entity.consistency));
|
||||||
assertNonEmptyString(entityName, 'experienceLevel', entity.experienceLevel);
|
assertNonEmptyString(entityName, 'experienceLevel', entity.experienceLevel);
|
||||||
|
|
||||||
const result: DriverStats = {
|
const result: DriverStats = {
|
||||||
rating: entity.rating,
|
rating: Number(entity.rating),
|
||||||
safetyRating: entity.safetyRating,
|
safetyRating: Number(entity.safetyRating),
|
||||||
sportsmanshipRating: entity.sportsmanshipRating,
|
sportsmanshipRating: Number(entity.sportsmanshipRating),
|
||||||
totalRaces: entity.totalRaces,
|
totalRaces: Number(entity.totalRaces),
|
||||||
wins: entity.wins,
|
wins: Number(entity.wins),
|
||||||
podiums: entity.podiums,
|
podiums: Number(entity.podiums),
|
||||||
dnfs: entity.dnfs,
|
dnfs: Number(entity.dnfs),
|
||||||
avgFinish: entity.avgFinish,
|
avgFinish: Number(entity.avgFinish),
|
||||||
bestFinish: entity.bestFinish,
|
bestFinish: Number(entity.bestFinish),
|
||||||
worstFinish: entity.worstFinish,
|
worstFinish: Number(entity.worstFinish),
|
||||||
consistency: entity.consistency,
|
consistency: Number(entity.consistency),
|
||||||
experienceLevel: entity.experienceLevel,
|
experienceLevel: entity.experienceLevel,
|
||||||
overallRank: entity.overallRank ?? null,
|
overallRank: entity.overallRank ? Number(entity.overallRank) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ import { InMemoryDriverExtendedProfileProvider } from '@adapters/racing/ports/In
|
|||||||
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
|
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
|
||||||
// Import new use cases
|
// Import new use cases
|
||||||
// Import new repositories
|
// Import new repositories
|
||||||
import { InMemoryDriverStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
|
||||||
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
|
||||||
// Import MediaResolverAdapter
|
// Import MediaResolverAdapter
|
||||||
import { MediaResolverAdapter } from '@adapters/media/MediaResolverAdapter';
|
import { MediaResolverAdapter } from '@adapters/media/MediaResolverAdapter';
|
||||||
// Import repository tokens
|
// Import repository tokens
|
||||||
@@ -67,7 +65,6 @@ import {
|
|||||||
IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
|
IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
|
||||||
LIVERY_REPOSITORY_TOKEN,
|
LIVERY_REPOSITORY_TOKEN,
|
||||||
LOGGER_TOKEN,
|
LOGGER_TOKEN,
|
||||||
MEDIA_REPOSITORY_TOKEN,
|
|
||||||
MEDIA_RESOLVER_TOKEN,
|
MEDIA_RESOLVER_TOKEN,
|
||||||
NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN,
|
||||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||||
@@ -133,48 +130,25 @@ export const DriverProviders: Provider[] = createLoggedProviders([
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Repositories (racing + social repos are provided by imported persistence modules)
|
// Repositories (racing + social repos are provided by imported persistence modules)
|
||||||
{
|
|
||||||
provide: DRIVER_STATS_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryDriverStatsRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: MEDIA_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => {
|
|
||||||
const mediaRepo = new InMemoryMediaRepository(logger);
|
|
||||||
|
|
||||||
// Override getTeamLogo to provide fallback URLs
|
|
||||||
const originalGetTeamLogo = mediaRepo.getTeamLogo.bind(mediaRepo);
|
|
||||||
mediaRepo.getTeamLogo = async (teamId: string): Promise<string | null> => {
|
|
||||||
const logo = await originalGetTeamLogo(teamId);
|
|
||||||
if (logo) return logo;
|
|
||||||
|
|
||||||
// Fallback: generate deterministic team logo URL
|
|
||||||
// Use path-only URL
|
|
||||||
return `/media/teams/${teamId}/logo`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return mediaRepo;
|
|
||||||
},
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: RANKING_SERVICE_TOKEN,
|
provide: RANKING_SERVICE_TOKEN,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
standingRepo: StandingRepository,
|
standingRepo: StandingRepository,
|
||||||
driverRepo: DriverRepository,
|
driverRepo: DriverRepository,
|
||||||
|
driverStatsRepo: DriverStatsRepository,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
) => new RankingUseCase(standingRepo, driverRepo, logger),
|
) => new RankingUseCase(standingRepo, driverRepo, driverStatsRepo, logger),
|
||||||
inject: ['IStandingRepository', DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
inject: ['IStandingRepository', DRIVER_REPOSITORY_TOKEN, DRIVER_STATS_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: DRIVER_STATS_SERVICE_TOKEN,
|
provide: DRIVER_STATS_SERVICE_TOKEN,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
resultRepo: ResultRepository,
|
resultRepo: ResultRepository,
|
||||||
standingRepo: StandingRepository,
|
standingRepo: StandingRepository,
|
||||||
|
driverStatsRepo: DriverStatsRepository,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
) => new DriverStatsUseCase(resultRepo, standingRepo, logger),
|
) => new DriverStatsUseCase(resultRepo, standingRepo, driverStatsRepo, logger),
|
||||||
inject: ['IResultRepository', 'IStandingRepository', LOGGER_TOKEN],
|
inject: ['IResultRepository', 'IStandingRepository', DRIVER_STATS_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: DRIVER_RATING_PROVIDER_TOKEN,
|
provide: DRIVER_RATING_PROVIDER_TOKEN,
|
||||||
|
|||||||
@@ -88,6 +88,27 @@ describe('Team domain (HTTP, module-wiring)', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns leaderboard with non-zero ratings and wins', async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.get('/teams/leaderboard')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toBeDefined();
|
||||||
|
expect(response.body.teams).toBeDefined();
|
||||||
|
expect(Array.isArray(response.body.teams)).toBe(true);
|
||||||
|
|
||||||
|
// Verify that teams have non-zero ratings and wins
|
||||||
|
if (response.body.teams.length > 0) {
|
||||||
|
const team = response.body.teams[0];
|
||||||
|
expect(team).toBeDefined();
|
||||||
|
expect(team.rating).not.toBeNull();
|
||||||
|
expect(typeof team.rating).toBe('number');
|
||||||
|
expect(team.rating).toBeGreaterThan(0);
|
||||||
|
expect(team.totalWins).toBeGreaterThan(0);
|
||||||
|
expect(team.totalRaces).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects unauthenticated actor on create team (401)', async () => {
|
it('rejects unauthenticated actor on create team (401)', async () => {
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.post('/teams')
|
.post('/teams')
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
TEAM_STATS_REPOSITORY_TOKEN,
|
TEAM_STATS_REPOSITORY_TOKEN,
|
||||||
UPDATE_TEAM_USE_CASE_TOKEN,
|
UPDATE_TEAM_USE_CASE_TOKEN,
|
||||||
GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN,
|
GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN,
|
||||||
|
DRIVER_STATS_REPOSITORY_TOKEN,
|
||||||
} from './TeamTokens';
|
} from './TeamTokens';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -165,11 +166,15 @@ export const TeamProviders: Provider[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN,
|
provide: GET_TEAMS_LEADERBOARD_USE_CASE_TOKEN,
|
||||||
useFactory: (teamRepo: TeamRepository, membershipRepo: TeamMembershipRepository, driverStatsRepo: DriverStatsRepository, logger: Logger) =>
|
useFactory: async (teamRepo: TeamRepository, membershipRepo: TeamMembershipRepository, driverStatsRepo: DriverStatsRepository, logger: Logger) => {
|
||||||
new GetTeamsLeaderboardUseCase(teamRepo, membershipRepo, (driverId) => {
|
// Pre-fetch all driver stats for efficient lookup
|
||||||
const stats = driverStatsRepo.getDriverStatsSync?.(driverId);
|
const allStats = await driverStatsRepo.getAllStats();
|
||||||
|
|
||||||
|
return new GetTeamsLeaderboardUseCase(teamRepo, membershipRepo, (driverId) => {
|
||||||
|
const stats = allStats.get(driverId);
|
||||||
return stats ? { rating: stats.rating, wins: stats.wins, totalRaces: stats.totalRaces } : null;
|
return stats ? { rating: stats.rating, wins: stats.wins, totalRaces: stats.totalRaces } : null;
|
||||||
}, logger),
|
}, logger);
|
||||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, 'IDriverStatsRepository', LOGGER_TOKEN],
|
},
|
||||||
|
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_STATS_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const TEAM_STATS_REPOSITORY_TOKEN = 'ITeamStatsRepository';
|
|||||||
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
|
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
|
||||||
export const MEDIA_RESOLVER_TOKEN = 'MediaResolverPort';
|
export const MEDIA_RESOLVER_TOKEN = 'MediaResolverPort';
|
||||||
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
|
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
|
||||||
|
export const DRIVER_STATS_REPOSITORY_TOKEN = 'IDriverStatsRepository';
|
||||||
|
|
||||||
export const GET_ALL_TEAMS_USE_CASE_TOKEN = Symbol('GET_ALL_TEAMS_USE_CASE_TOKEN');
|
export const GET_ALL_TEAMS_USE_CASE_TOKEN = Symbol('GET_ALL_TEAMS_USE_CASE_TOKEN');
|
||||||
export const GET_TEAM_DETAILS_USE_CASE_TOKEN = Symbol('GET_TEAM_DETAILS_USE_CASE_TOKEN');
|
export const GET_TEAM_DETAILS_USE_CASE_TOKEN = Symbol('GET_TEAM_DETAILS_USE_CASE_TOKEN');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import type { ResultRepository } from '../../domain/repositories/ResultRepository';
|
import type { ResultRepository } from '../../domain/repositories/ResultRepository';
|
||||||
import type { StandingRepository } from '../../domain/repositories/StandingRepository';
|
import type { StandingRepository } from '../../domain/repositories/StandingRepository';
|
||||||
|
import type { DriverStatsRepository } from '../../domain/repositories/DriverStatsRepository';
|
||||||
import type { Logger } from '@core/shared/domain/Logger';
|
import type { Logger } from '@core/shared/domain/Logger';
|
||||||
|
|
||||||
export interface DriverStats {
|
export interface DriverStats {
|
||||||
@@ -29,11 +30,12 @@ export class DriverStatsUseCase {
|
|||||||
constructor(
|
constructor(
|
||||||
_resultRepository: ResultRepository,
|
_resultRepository: ResultRepository,
|
||||||
_standingRepository: StandingRepository,
|
_standingRepository: StandingRepository,
|
||||||
|
private readonly _driverStatsRepository: DriverStatsRepository,
|
||||||
private readonly _logger: Logger,
|
private readonly _logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getDriverStats(driverId: string): Promise<DriverStats | null> {
|
async getDriverStats(driverId: string): Promise<DriverStats | null> {
|
||||||
this._logger.debug(`Getting stats for driver ${driverId}`);
|
this._logger.debug(`Getting stats for driver ${driverId}`);
|
||||||
return null;
|
return this._driverStatsRepository.getDriverStats(driverId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
||||||
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
|
||||||
import { Team } from '../../domain/entities/Team';
|
|
||||||
import {
|
|
||||||
GetTeamsLeaderboardUseCase,
|
|
||||||
type GetTeamsLeaderboardErrorCode,
|
|
||||||
type GetTeamsLeaderboardInput
|
|
||||||
} from './GetTeamsLeaderboardUseCase';
|
|
||||||
|
|
||||||
import { Logger } from 'vite';
|
|
||||||
import { TeamMembershipRepository } from '../../domain/repositories/TeamMembershipRepository';
|
|
||||||
import { TeamRepository } from '../../domain/repositories/TeamRepository';
|
|
||||||
|
|
||||||
describe('GetTeamsLeaderboardUseCase', () => {
|
|
||||||
let useCase: GetTeamsLeaderboardUseCase;
|
|
||||||
let teamRepository: {
|
|
||||||
findAll: Mock;
|
|
||||||
findById: Mock;
|
|
||||||
};
|
|
||||||
let teamMembershipRepository: {
|
|
||||||
getTeamMembers: Mock;
|
|
||||||
};
|
|
||||||
let getDriverStats: Mock;
|
|
||||||
let logger: {
|
|
||||||
debug: Mock;
|
|
||||||
info: Mock;
|
|
||||||
warn: Mock;
|
|
||||||
error: Mock;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
teamRepository = {
|
|
||||||
findAll: vi.fn(),
|
|
||||||
findById: vi.fn(),
|
|
||||||
};
|
|
||||||
teamMembershipRepository = {
|
|
||||||
getTeamMembers: vi.fn(),
|
|
||||||
};
|
|
||||||
getDriverStats = vi.fn();
|
|
||||||
logger = {
|
|
||||||
debug: vi.fn(),
|
|
||||||
info: vi.fn(),
|
|
||||||
warn: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
};
|
|
||||||
useCase = new GetTeamsLeaderboardUseCase(
|
|
||||||
teamRepository as unknown as TeamRepository,
|
|
||||||
teamMembershipRepository as unknown as TeamMembershipRepository,
|
|
||||||
getDriverStats as unknown as (driverId: string) => { rating: number | null; wins: number; totalRaces: number } | null,
|
|
||||||
logger as unknown as Logger
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return teams leaderboard with calculated stats', async () => {
|
|
||||||
const team1 = Team.create({
|
|
||||||
id: 'team-1',
|
|
||||||
name: 'Team Alpha',
|
|
||||||
tag: 'TA',
|
|
||||||
description: 'Description 1',
|
|
||||||
ownerId: 'owner-1',
|
|
||||||
leagues: [],
|
|
||||||
});
|
|
||||||
const team2 = Team.create({
|
|
||||||
id: 'team-2',
|
|
||||||
name: 'Team Beta',
|
|
||||||
tag: 'TB',
|
|
||||||
description: 'Description 2',
|
|
||||||
ownerId: 'owner-2',
|
|
||||||
leagues: [],
|
|
||||||
});
|
|
||||||
const memberships1 = [
|
|
||||||
{ teamId: 'team-1', driverId: 'driver-1', role: 'driver' as const, status: 'active' as const, joinedAt: new Date() },
|
|
||||||
{ teamId: 'team-1', driverId: 'driver-2', role: 'driver' as const, status: 'active' as const, joinedAt: new Date() },
|
|
||||||
];
|
|
||||||
const memberships2 = [
|
|
||||||
{ teamId: 'team-2', driverId: 'driver-3', role: 'driver' as const, status: 'active' as const, joinedAt: new Date() },
|
|
||||||
];
|
|
||||||
|
|
||||||
teamRepository.findAll.mockResolvedValue([team1, team2]);
|
|
||||||
teamMembershipRepository.getTeamMembers.mockImplementation((teamId: string) => {
|
|
||||||
if (teamId === 'team-1') return Promise.resolve(memberships1);
|
|
||||||
if (teamId === 'team-2') return Promise.resolve(memberships2);
|
|
||||||
return Promise.resolve([]);
|
|
||||||
});
|
|
||||||
getDriverStats.mockImplementation((driverId: string) => {
|
|
||||||
if (driverId === 'driver-1') return { rating: 1500, wins: 5, totalRaces: 10 };
|
|
||||||
if (driverId === 'driver-2') return { rating: 1600, wins: 3, totalRaces: 8 };
|
|
||||||
if (driverId === 'driver-3') return { rating: null, wins: 2, totalRaces: 5 };
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const input: GetTeamsLeaderboardInput = { leagueId: 'league-1' };
|
|
||||||
|
|
||||||
const result = await useCase.execute(input);
|
|
||||||
|
|
||||||
expect(result.isOk()).toBe(true);
|
|
||||||
const presented = result.unwrap();
|
|
||||||
|
|
||||||
expect(presented.recruitingCount).toBe(2); // both teams are recruiting
|
|
||||||
expect(presented.items).toHaveLength(2);
|
|
||||||
expect(presented.items[0]).toMatchObject({
|
|
||||||
team: team1,
|
|
||||||
memberCount: 2,
|
|
||||||
rating: 1550, // (1500 + 1600) / 2
|
|
||||||
totalWins: 8,
|
|
||||||
totalRaces: 18,
|
|
||||||
performanceLevel: expect.any(String),
|
|
||||||
isRecruiting: true,
|
|
||||||
});
|
|
||||||
expect(presented.items[1]).toMatchObject({
|
|
||||||
team: team2,
|
|
||||||
memberCount: 1,
|
|
||||||
rating: null,
|
|
||||||
totalWins: 2,
|
|
||||||
totalRaces: 5,
|
|
||||||
performanceLevel: expect.any(String),
|
|
||||||
isRecruiting: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error on repository failure', async () => {
|
|
||||||
const error = new Error('Repository error');
|
|
||||||
|
|
||||||
teamRepository.findAll.mockRejectedValue(error);
|
|
||||||
|
|
||||||
const input: GetTeamsLeaderboardInput = { leagueId: 'league-1' };
|
|
||||||
|
|
||||||
const result = await useCase.execute(input);
|
|
||||||
|
|
||||||
expect(result.isErr()).toBe(true);
|
|
||||||
const err = result.unwrapErr() as ApplicationErrorCode<
|
|
||||||
GetTeamsLeaderboardErrorCode,
|
|
||||||
{ message: string }
|
|
||||||
>;
|
|
||||||
expect(err.code).toBe('REPOSITORY_ERROR');
|
|
||||||
expect(err.details.message).toBe('Repository error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import type { StandingRepository } from '../../domain/repositories/StandingRepository';
|
import type { StandingRepository } from '../../domain/repositories/StandingRepository';
|
||||||
import type { DriverRepository } from '../../domain/repositories/DriverRepository';
|
import type { DriverRepository } from '../../domain/repositories/DriverRepository';
|
||||||
|
import type { DriverStatsRepository } from '../../domain/repositories/DriverStatsRepository';
|
||||||
import type { Logger } from '@core/shared/domain/Logger';
|
import type { Logger } from '@core/shared/domain/Logger';
|
||||||
|
|
||||||
export interface DriverRanking {
|
export interface DriverRanking {
|
||||||
@@ -21,11 +22,25 @@ export class RankingUseCase {
|
|||||||
constructor(
|
constructor(
|
||||||
_standingRepository: StandingRepository,
|
_standingRepository: StandingRepository,
|
||||||
_driverRepository: DriverRepository,
|
_driverRepository: DriverRepository,
|
||||||
|
private readonly _driverStatsRepository: DriverStatsRepository,
|
||||||
private readonly _logger: Logger,
|
private readonly _logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAllDriverRankings(): Promise<DriverRanking[]> {
|
async getAllDriverRankings(): Promise<DriverRanking[]> {
|
||||||
this._logger.debug('Getting all driver rankings');
|
this._logger.debug('Getting all driver rankings');
|
||||||
return [];
|
const allStats = await this._driverStatsRepository.getAllStats();
|
||||||
|
const rankings: DriverRanking[] = [];
|
||||||
|
|
||||||
|
allStats.forEach((stats, driverId) => {
|
||||||
|
rankings.push({
|
||||||
|
driverId,
|
||||||
|
rating: stats.rating,
|
||||||
|
wins: stats.wins,
|
||||||
|
totalRaces: stats.totalRaces,
|
||||||
|
overallRank: stats.overallRank,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return rankings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user