refactor racing use cases
This commit is contained in:
@@ -5,29 +5,19 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
|
||||
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import { Race } from '../../domain/entities/Race';
|
||||
import { Result as RaceResult } from '../../domain/entities/Result';
|
||||
import { Driver } from '../../domain/entities/Driver';
|
||||
import { Standing } from '../../domain/entities/Standing';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type {
|
||||
DashboardOverviewOutputPort,
|
||||
DashboardDriverSummaryOutputPort,
|
||||
DashboardRaceSummaryOutputPort,
|
||||
DashboardRecentResultOutputPort,
|
||||
DashboardLeagueStandingSummaryOutputPort,
|
||||
DashboardFeedItemSummaryOutputPort,
|
||||
DashboardFeedSummaryOutputPort,
|
||||
DashboardFriendSummaryOutputPort,
|
||||
} from '../ports/output/DashboardOverviewOutputPort';
|
||||
|
||||
interface DashboardOverviewParams {
|
||||
export interface DashboardOverviewInput {
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
@@ -40,6 +30,58 @@ interface DashboardDriverStatsAdapter {
|
||||
consistency: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardDriverSummary {
|
||||
driver: Driver;
|
||||
avatarUrl: string | null;
|
||||
rating: number | null;
|
||||
globalRank: number | null;
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
consistency: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardRaceSummary {
|
||||
race: Race;
|
||||
league: League | null;
|
||||
isMyLeague: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardRecentRaceResultSummary {
|
||||
race: Race;
|
||||
league: League | null;
|
||||
result: RaceResult;
|
||||
}
|
||||
|
||||
export interface DashboardLeagueStandingSummary {
|
||||
league: League;
|
||||
standing: Standing | null;
|
||||
totalDrivers: number;
|
||||
}
|
||||
|
||||
export interface DashboardFeedSummary {
|
||||
notificationCount: number;
|
||||
items: FeedItem[];
|
||||
}
|
||||
|
||||
export interface DashboardFriendSummary {
|
||||
driver: Driver;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
export interface DashboardOverviewResult {
|
||||
currentDriver: DashboardDriverSummary | null;
|
||||
myUpcomingRaces: DashboardRaceSummary[];
|
||||
otherUpcomingRaces: DashboardRaceSummary[];
|
||||
upcomingRaces: DashboardRaceSummary[];
|
||||
activeLeaguesCount: number;
|
||||
nextRace: DashboardRaceSummary | null;
|
||||
recentResults: DashboardRecentRaceResultSummary[];
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummary[];
|
||||
feedSummary: DashboardFeedSummary;
|
||||
friends: DashboardFriendSummary[];
|
||||
}
|
||||
|
||||
export class DashboardOverviewUseCase {
|
||||
constructor(
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
@@ -51,104 +93,146 @@ export class DashboardOverviewUseCase {
|
||||
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
|
||||
private readonly feedRepository: IFeedRepository,
|
||||
private readonly socialRepository: ISocialGraphRepository,
|
||||
private readonly getDriverAvatar: (input: GetDriverAvatarInputPort) => Promise<GetDriverAvatarOutputPort>,
|
||||
private readonly getDriverStats: (driverId: string) => DashboardDriverStatsAdapter | null,
|
||||
private readonly getDriverAvatar: (driverId: string) => Promise<string>,
|
||||
private readonly getDriverStats: (
|
||||
driverId: string,
|
||||
) => DashboardDriverStatsAdapter | null,
|
||||
private readonly output: UseCaseOutputPort<DashboardOverviewResult>,
|
||||
) {}
|
||||
|
||||
async execute(params: DashboardOverviewParams): Promise<Result<DashboardOverviewOutputPort>> {
|
||||
const { driverId } = params;
|
||||
async execute(
|
||||
input: DashboardOverviewInput,
|
||||
): Promise<
|
||||
Result<
|
||||
void,
|
||||
ApplicationErrorCode<'DRIVER_NOT_FOUND' | 'REPOSITORY_ERROR', { message: string }>
|
||||
>
|
||||
> {
|
||||
const { driverId } = input;
|
||||
|
||||
const [driver, allLeagues, allRaces, allResults, feedItems, friends] = await Promise.all([
|
||||
this.driverRepository.findById(driverId),
|
||||
this.leagueRepository.findAll(),
|
||||
this.raceRepository.findAll(),
|
||||
this.resultRepository.findAll(),
|
||||
this.feedRepository.getFeedForDriver(driverId),
|
||||
this.socialRepository.getFriends(driverId),
|
||||
]);
|
||||
try {
|
||||
const [driver, allLeagues, allRaces, allResults, feedItems, friends] =
|
||||
await Promise.all([
|
||||
this.driverRepository.findById(driverId),
|
||||
this.leagueRepository.findAll(),
|
||||
this.raceRepository.findAll(),
|
||||
this.resultRepository.findAll(),
|
||||
this.feedRepository.getFeedForDriver(driverId),
|
||||
this.socialRepository.getFriends(driverId),
|
||||
]);
|
||||
|
||||
const leagueMap = new Map(allLeagues.map(league => [league.id, league.name]));
|
||||
if (!driver) {
|
||||
return Result.err({
|
||||
code: 'DRIVER_NOT_FOUND',
|
||||
details: { message: 'Driver not found' },
|
||||
});
|
||||
}
|
||||
|
||||
const driverStats = this.getDriverStats(driverId);
|
||||
const leagueMap = new Map(allLeagues.map(league => [league.id, league]));
|
||||
|
||||
const currentDriver: DashboardDriverSummaryOutputPort | null = driver
|
||||
? {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
country: driver.country,
|
||||
avatarUrl: (await this.getDriverAvatar({ driverId: driver.id })).avatarUrl,
|
||||
rating: driverStats?.rating ?? null,
|
||||
globalRank: driverStats?.overallRank ?? null,
|
||||
totalRaces: driverStats?.totalRaces ?? 0,
|
||||
wins: driverStats?.wins ?? 0,
|
||||
podiums: driverStats?.podiums ?? 0,
|
||||
consistency: driverStats?.consistency ?? null,
|
||||
}
|
||||
: null;
|
||||
const driverStats = this.getDriverStats(driverId);
|
||||
|
||||
const driverLeagues = await this.getDriverLeagues(allLeagues, driverId);
|
||||
const driverLeagueIds = new Set(driverLeagues.map(league => league.id));
|
||||
const currentDriver: DashboardDriverSummary = {
|
||||
driver,
|
||||
avatarUrl: await this.getDriverAvatar(driver.id),
|
||||
rating: driverStats?.rating ?? null,
|
||||
globalRank: driverStats?.overallRank ?? null,
|
||||
totalRaces: driverStats?.totalRaces ?? 0,
|
||||
wins: driverStats?.wins ?? 0,
|
||||
podiums: driverStats?.podiums ?? 0,
|
||||
consistency: driverStats?.consistency ?? null,
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const upcomingRaces = allRaces
|
||||
.filter(race => race.status === 'scheduled' && race.scheduledAt > now)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
const driverLeagues = await this.getDriverLeagues(allLeagues, driverId);
|
||||
const driverLeagueIds = new Set(driverLeagues.map(league => league.id));
|
||||
|
||||
const upcomingRacesInDriverLeagues = upcomingRaces.filter(race =>
|
||||
driverLeagueIds.has(race.leagueId),
|
||||
);
|
||||
const now = new Date();
|
||||
const upcomingRaces = allRaces
|
||||
.filter(race => race.status === 'scheduled' && race.scheduledAt > now)
|
||||
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
|
||||
|
||||
const { myUpcomingRaces, otherUpcomingRaces } =
|
||||
await this.partitionUpcomingRacesByRegistration(upcomingRacesInDriverLeagues, driverId, leagueMap);
|
||||
const upcomingRacesInDriverLeagues = upcomingRaces.filter(race =>
|
||||
driverLeagueIds.has(race.leagueId),
|
||||
);
|
||||
|
||||
const nextRace: DashboardRaceSummaryOutputPort | null =
|
||||
myUpcomingRaces.length > 0 ? myUpcomingRaces[0]! : null;
|
||||
const { myUpcomingRaces, otherUpcomingRaces } =
|
||||
await this.partitionUpcomingRacesByRegistration(
|
||||
upcomingRacesInDriverLeagues,
|
||||
driverId,
|
||||
leagueMap,
|
||||
);
|
||||
|
||||
const upcomingRacesSummaries: DashboardRaceSummaryOutputPort[] = [
|
||||
...myUpcomingRaces,
|
||||
...otherUpcomingRaces,
|
||||
].slice().sort(
|
||||
(a, b) =>
|
||||
new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime(),
|
||||
);
|
||||
const nextRace: DashboardRaceSummary | null =
|
||||
myUpcomingRaces.length > 0 ? myUpcomingRaces[0]! : null;
|
||||
|
||||
const recentResults = this.buildRecentResults(allResults, allRaces, allLeagues, driverId);
|
||||
const upcomingRacesSummaries: DashboardRaceSummary[] = [
|
||||
...myUpcomingRaces,
|
||||
...otherUpcomingRaces,
|
||||
]
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.race.scheduledAt.getTime() - b.race.scheduledAt.getTime(),
|
||||
);
|
||||
|
||||
const leagueStandingsSummaries = await this.buildLeagueStandingsSummaries(
|
||||
driverLeagues,
|
||||
driverId,
|
||||
);
|
||||
const recentResults = this.buildRecentResults(
|
||||
allResults,
|
||||
allRaces,
|
||||
allLeagues,
|
||||
driverId,
|
||||
);
|
||||
|
||||
const activeLeaguesCount = this.computeActiveLeaguesCount(
|
||||
upcomingRacesSummaries,
|
||||
leagueStandingsSummaries,
|
||||
);
|
||||
const leagueStandingsSummaries = await this.buildLeagueStandingsSummaries(
|
||||
driverLeagues,
|
||||
driverId,
|
||||
);
|
||||
|
||||
const feedSummary = this.buildFeedSummary(feedItems);
|
||||
const activeLeaguesCount = this.computeActiveLeaguesCount(
|
||||
upcomingRacesSummaries,
|
||||
leagueStandingsSummaries,
|
||||
);
|
||||
|
||||
const friendsSummary = await this.buildFriendsSummary(friends);
|
||||
const feedSummary = this.buildFeedSummary(feedItems);
|
||||
|
||||
const viewModel: DashboardOverviewOutputPort = {
|
||||
currentDriver,
|
||||
myUpcomingRaces,
|
||||
otherUpcomingRaces,
|
||||
upcomingRaces: upcomingRacesSummaries,
|
||||
activeLeaguesCount,
|
||||
nextRace,
|
||||
recentResults,
|
||||
leagueStandingsSummaries,
|
||||
feedSummary,
|
||||
friends: friendsSummary,
|
||||
};
|
||||
const friendsSummary = await this.buildFriendsSummary(friends);
|
||||
|
||||
return Result.ok(viewModel);
|
||||
const result: DashboardOverviewResult = {
|
||||
currentDriver,
|
||||
myUpcomingRaces,
|
||||
otherUpcomingRaces,
|
||||
upcomingRaces: upcomingRacesSummaries,
|
||||
activeLeaguesCount,
|
||||
nextRace,
|
||||
recentResults,
|
||||
leagueStandingsSummaries,
|
||||
feedSummary,
|
||||
friends: friendsSummary,
|
||||
};
|
||||
|
||||
this.output.present(result);
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: {
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async getDriverLeagues(allLeagues: League[], driverId: string): Promise<League[]> {
|
||||
private async getDriverLeagues(
|
||||
allLeagues: League[],
|
||||
driverId: string,
|
||||
): Promise<League[]> {
|
||||
const driverLeagues: League[] = [];
|
||||
|
||||
for (const league of allLeagues) {
|
||||
const membership = await this.leagueMembershipRepository.getMembership(league.id, driverId);
|
||||
const membership = await this.leagueMembershipRepository.getMembership(
|
||||
league.id,
|
||||
driverId,
|
||||
);
|
||||
if (membership && membership.status === 'active') {
|
||||
driverLeagues.push(league);
|
||||
}
|
||||
@@ -160,16 +244,19 @@ export class DashboardOverviewUseCase {
|
||||
private async partitionUpcomingRacesByRegistration(
|
||||
upcomingRaces: Race[],
|
||||
driverId: string,
|
||||
leagueMap: Map<string, string>,
|
||||
leagueMap: Map<string, League>,
|
||||
): Promise<{
|
||||
myUpcomingRaces: DashboardRaceSummaryOutputPort[];
|
||||
otherUpcomingRaces: DashboardRaceSummaryOutputPort[];
|
||||
myUpcomingRaces: DashboardRaceSummary[];
|
||||
otherUpcomingRaces: DashboardRaceSummary[];
|
||||
}> {
|
||||
const myUpcomingRaces: DashboardRaceSummaryOutputPort[] = [];
|
||||
const otherUpcomingRaces: DashboardRaceSummaryOutputPort[] = [];
|
||||
const myUpcomingRaces: DashboardRaceSummary[] = [];
|
||||
const otherUpcomingRaces: DashboardRaceSummary[] = [];
|
||||
|
||||
for (const race of upcomingRaces) {
|
||||
const isRegistered = await this.raceRegistrationRepository.isRegistered(race.id, driverId);
|
||||
const isRegistered = await this.raceRegistrationRepository.isRegistered(
|
||||
race.id,
|
||||
driverId,
|
||||
);
|
||||
const summary = this.mapRaceToSummary(race, leagueMap, true);
|
||||
|
||||
if (isRegistered) {
|
||||
@@ -184,17 +271,12 @@ export class DashboardOverviewUseCase {
|
||||
|
||||
private mapRaceToSummary(
|
||||
race: Race,
|
||||
leagueMap: Map<string, string>,
|
||||
leagueMap: Map<string, League>,
|
||||
isMyLeague: boolean,
|
||||
): DashboardRaceSummaryOutputPort {
|
||||
): DashboardRaceSummary {
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: leagueMap.get(race.leagueId) ?? 'Unknown League',
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
status: race.status,
|
||||
race,
|
||||
league: leagueMap.get(race.leagueId) ?? null,
|
||||
isMyLeague,
|
||||
};
|
||||
}
|
||||
@@ -204,7 +286,7 @@ export class DashboardOverviewUseCase {
|
||||
allRaces: Race[],
|
||||
allLeagues: League[],
|
||||
driverId: string,
|
||||
): DashboardRecentResultOutputPort[] {
|
||||
): DashboardRecentRaceResultSummary[] {
|
||||
const raceById = new Map(allRaces.map(race => [race.id, race]));
|
||||
const leagueById = new Map(allLeagues.map(league => [league.id, league]));
|
||||
|
||||
@@ -215,26 +297,20 @@ export class DashboardOverviewUseCase {
|
||||
const race = raceById.get(result.raceId);
|
||||
if (!race) return null;
|
||||
|
||||
const league = leagueById.get(race.leagueId);
|
||||
const league = leagueById.get(race.leagueId) ?? null;
|
||||
|
||||
const finishedAt = race.scheduledAt.toISOString();
|
||||
|
||||
const item: DashboardRecentResultOutputPort = {
|
||||
raceId: race.id,
|
||||
raceName: race.track,
|
||||
leagueId: race.leagueId,
|
||||
leagueName: league?.name ?? 'Unknown League',
|
||||
finishedAt,
|
||||
position: result.position,
|
||||
incidents: result.incidents,
|
||||
const item: DashboardRecentRaceResultSummary = {
|
||||
race,
|
||||
league,
|
||||
result,
|
||||
};
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is DashboardRecentResultOutputPort => !!item)
|
||||
.filter((item): item is DashboardRecentRaceResultSummary => !!item)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.finishedAt).getTime() - new Date(a.finishedAt).getTime(),
|
||||
b.race.scheduledAt.getTime() - a.race.scheduledAt.getTime(),
|
||||
);
|
||||
|
||||
const RECENT_RESULTS_LIMIT = 5;
|
||||
@@ -245,8 +321,8 @@ export class DashboardOverviewUseCase {
|
||||
private async buildLeagueStandingsSummaries(
|
||||
driverLeagues: League[],
|
||||
driverId: string,
|
||||
): Promise<DashboardLeagueStandingSummaryOutputPort[]> {
|
||||
const summaries: DashboardLeagueStandingSummaryOutputPort[] = [];
|
||||
): Promise<DashboardLeagueStandingSummary[]> {
|
||||
const summaries: DashboardLeagueStandingSummary[] = [];
|
||||
|
||||
for (const league of driverLeagues.slice(0, 3)) {
|
||||
const standings = await this.standingRepository.findByLeagueId(league.id);
|
||||
@@ -255,10 +331,8 @@ export class DashboardOverviewUseCase {
|
||||
);
|
||||
|
||||
summaries.push({
|
||||
leagueId: league.id,
|
||||
leagueName: league.name,
|
||||
position: driverStanding?.position ?? 0,
|
||||
points: driverStanding?.points ?? 0,
|
||||
league,
|
||||
standing: driverStanding ?? null,
|
||||
totalDrivers: standings.length,
|
||||
});
|
||||
}
|
||||
@@ -267,55 +341,42 @@ export class DashboardOverviewUseCase {
|
||||
}
|
||||
|
||||
private computeActiveLeaguesCount(
|
||||
upcomingRaces: DashboardRaceSummaryOutputPort[],
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummaryOutputPort[],
|
||||
upcomingRaces: DashboardRaceSummary[],
|
||||
leagueStandingsSummaries: DashboardLeagueStandingSummary[],
|
||||
): number {
|
||||
const activeLeagueIds = new Set<string>();
|
||||
|
||||
for (const race of upcomingRaces) {
|
||||
activeLeagueIds.add(race.leagueId);
|
||||
activeLeagueIds.add(race.race.leagueId);
|
||||
}
|
||||
|
||||
for (const standing of leagueStandingsSummaries) {
|
||||
activeLeagueIds.add(standing.leagueId);
|
||||
activeLeagueIds.add(standing.league.id);
|
||||
}
|
||||
|
||||
return activeLeagueIds.size;
|
||||
}
|
||||
|
||||
private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummaryOutputPort {
|
||||
const items: DashboardFeedItemSummaryOutputPort[] = feedItems.map(item => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
headline: item.headline,
|
||||
timestamp:
|
||||
item.timestamp instanceof Date
|
||||
? item.timestamp.toISOString()
|
||||
: new Date(item.timestamp).toISOString(),
|
||||
...(item.body !== undefined ? { body: item.body } : {}),
|
||||
...(item.ctaLabel !== undefined ? { ctaLabel: item.ctaLabel } : {}),
|
||||
...(item.ctaHref !== undefined ? { ctaHref: item.ctaHref } : {}),
|
||||
}));
|
||||
|
||||
private buildFeedSummary(feedItems: FeedItem[]): DashboardFeedSummary {
|
||||
return {
|
||||
notificationCount: items.length,
|
||||
items,
|
||||
notificationCount: feedItems.length,
|
||||
items: feedItems,
|
||||
};
|
||||
}
|
||||
|
||||
private async buildFriendsSummary(friends: Driver[]): Promise<DashboardFriendSummaryOutputPort[]> {
|
||||
const friendSummaries: DashboardFriendSummaryOutputPort[] = [];
|
||||
private async buildFriendsSummary(
|
||||
friends: Driver[],
|
||||
): Promise<DashboardFriendSummary[]> {
|
||||
const friendSummaries: DashboardFriendSummary[] = [];
|
||||
|
||||
for (const friend of friends) {
|
||||
const avatarResult = await this.getDriverAvatar({ driverId: friend.id });
|
||||
const avatarUrl = await this.getDriverAvatar(friend.id);
|
||||
friendSummaries.push({
|
||||
id: friend.id,
|
||||
name: friend.name,
|
||||
country: friend.country,
|
||||
avatarUrl: avatarResult.avatarUrl,
|
||||
driver: friend,
|
||||
avatarUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return friendSummaries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user