wip
This commit is contained in:
@@ -14,10 +14,10 @@ export interface LeagueScheduleDTO {
|
||||
raceStartTime: string;
|
||||
timezoneId: string;
|
||||
recurrenceStrategy: 'weekly' | 'everyNWeeks' | 'monthlyNthWeekday';
|
||||
intervalWeeks?: number;
|
||||
weekdays?: Weekday[];
|
||||
monthlyOrdinal?: 1 | 2 | 3 | 4;
|
||||
monthlyWeekday?: Weekday;
|
||||
intervalWeeks?: number | undefined;
|
||||
weekdays?: Weekday[] | undefined;
|
||||
monthlyOrdinal?: 1 | 2 | 3 | 4 | undefined;
|
||||
monthlyWeekday?: Weekday | undefined;
|
||||
plannedRounds: number;
|
||||
}
|
||||
|
||||
@@ -54,24 +54,26 @@ export function leagueTimingsToScheduleDTO(
|
||||
|
||||
export function scheduleDTOToSeasonSchedule(dto: LeagueScheduleDTO): SeasonSchedule {
|
||||
if (!dto.seasonStartDate) {
|
||||
throw new RacingApplicationError('seasonStartDate is required');
|
||||
throw new BusinessRuleViolationError('seasonStartDate is required');
|
||||
}
|
||||
if (!dto.raceStartTime) {
|
||||
throw new RacingApplicationError('raceStartTime is required');
|
||||
throw new BusinessRuleViolationError('raceStartTime is required');
|
||||
}
|
||||
if (!dto.timezoneId) {
|
||||
throw new RacingApplicationError('timezoneId is required');
|
||||
throw new BusinessRuleViolationError('timezoneId is required');
|
||||
}
|
||||
if (!dto.recurrenceStrategy) {
|
||||
throw new RacingApplicationError('recurrenceStrategy is required');
|
||||
throw new BusinessRuleViolationError('recurrenceStrategy is required');
|
||||
}
|
||||
if (!Number.isInteger(dto.plannedRounds) || dto.plannedRounds <= 0) {
|
||||
throw new RacingApplicationError('plannedRounds must be a positive integer');
|
||||
throw new BusinessRuleViolationError('plannedRounds must be a positive integer');
|
||||
}
|
||||
|
||||
const startDate = new Date(dto.seasonStartDate);
|
||||
if (Number.isNaN(startDate.getTime())) {
|
||||
throw new RacingApplicationError(`seasonStartDate must be a valid date, got "${dto.seasonStartDate}"`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`seasonStartDate must be a valid date, got "${dto.seasonStartDate}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const timeOfDay = RaceTimeOfDay.fromString(dto.raceStartTime);
|
||||
@@ -81,15 +83,17 @@ export function scheduleDTOToSeasonSchedule(dto: LeagueScheduleDTO): SeasonSched
|
||||
|
||||
if (dto.recurrenceStrategy === 'weekly') {
|
||||
if (!dto.weekdays || dto.weekdays.length === 0) {
|
||||
throw new RacingApplicationError('weekdays are required for weekly recurrence');
|
||||
throw new BusinessRuleViolationError('weekdays are required for weekly recurrence');
|
||||
}
|
||||
recurrence = RecurrenceStrategyFactory.weekly(new WeekdaySet(dto.weekdays));
|
||||
} else if (dto.recurrenceStrategy === 'everyNWeeks') {
|
||||
if (!dto.weekdays || dto.weekdays.length === 0) {
|
||||
throw new RacingApplicationError('weekdays are required for everyNWeeks recurrence');
|
||||
throw new BusinessRuleViolationError('weekdays are required for everyNWeeks recurrence');
|
||||
}
|
||||
if (dto.intervalWeeks == null) {
|
||||
throw new RacingApplicationError('intervalWeeks is required for everyNWeeks recurrence');
|
||||
throw new BusinessRuleViolationError(
|
||||
'intervalWeeks is required for everyNWeeks recurrence',
|
||||
);
|
||||
}
|
||||
recurrence = RecurrenceStrategyFactory.everyNWeeks(
|
||||
dto.intervalWeeks,
|
||||
@@ -97,12 +101,14 @@ export function scheduleDTOToSeasonSchedule(dto: LeagueScheduleDTO): SeasonSched
|
||||
);
|
||||
} else if (dto.recurrenceStrategy === 'monthlyNthWeekday') {
|
||||
if (!dto.monthlyOrdinal || !dto.monthlyWeekday) {
|
||||
throw new RacingApplicationError('monthlyOrdinal and monthlyWeekday are required for monthlyNthWeekday');
|
||||
throw new BusinessRuleViolationError(
|
||||
'monthlyOrdinal and monthlyWeekday are required for monthlyNthWeekday',
|
||||
);
|
||||
}
|
||||
const pattern = new MonthlyRecurrencePattern(dto.monthlyOrdinal, dto.monthlyWeekday);
|
||||
recurrence = RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||
} else {
|
||||
throw new RacingApplicationError(`Unknown recurrenceStrategy "${dto.recurrenceStrategy}"`);
|
||||
throw new BusinessRuleViolationError(`Unknown recurrenceStrategy "${dto.recurrenceStrategy}"`);
|
||||
}
|
||||
|
||||
return new SeasonSchedule({
|
||||
|
||||
@@ -22,7 +22,9 @@ export type RacingEntityType =
|
||||
| 'sponsorship'
|
||||
| 'sponsorshipRequest'
|
||||
| 'driver'
|
||||
| 'membership';
|
||||
| 'membership'
|
||||
| 'sponsor'
|
||||
| 'protest';
|
||||
|
||||
export interface EntityNotFoundDetails {
|
||||
entity: RacingEntityType;
|
||||
|
||||
@@ -24,13 +24,29 @@ export class EntityMappers {
|
||||
iracingId: driver.iracingId,
|
||||
name: driver.name,
|
||||
country: driver.country,
|
||||
bio: driver.bio,
|
||||
bio: driver.bio ?? '',
|
||||
joinedAt: driver.joinedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
static toLeagueDTO(league: League | null): LeagueDTO | null {
|
||||
if (!league) return null;
|
||||
|
||||
const socialLinks =
|
||||
league.socialLinks !== undefined
|
||||
? {
|
||||
...(league.socialLinks.discordUrl !== undefined
|
||||
? { discordUrl: league.socialLinks.discordUrl }
|
||||
: {}),
|
||||
...(league.socialLinks.youtubeUrl !== undefined
|
||||
? { youtubeUrl: league.socialLinks.youtubeUrl }
|
||||
: {}),
|
||||
...(league.socialLinks.websiteUrl !== undefined
|
||||
? { websiteUrl: league.socialLinks.websiteUrl }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
@@ -38,35 +54,37 @@ export class EntityMappers {
|
||||
ownerId: league.ownerId,
|
||||
settings: league.settings,
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
socialLinks: league.socialLinks
|
||||
? {
|
||||
discordUrl: league.socialLinks.discordUrl,
|
||||
youtubeUrl: league.socialLinks.youtubeUrl,
|
||||
websiteUrl: league.socialLinks.websiteUrl,
|
||||
}
|
||||
: undefined,
|
||||
// usedSlots is populated by capacity-aware queries, so leave undefined here
|
||||
usedSlots: undefined,
|
||||
...(socialLinks !== undefined ? { socialLinks } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
static toLeagueDTOs(leagues: League[]): LeagueDTO[] {
|
||||
return leagues.map(league => ({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: league.settings,
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
socialLinks: league.socialLinks
|
||||
? {
|
||||
discordUrl: league.socialLinks.discordUrl,
|
||||
youtubeUrl: league.socialLinks.youtubeUrl,
|
||||
websiteUrl: league.socialLinks.websiteUrl,
|
||||
}
|
||||
: undefined,
|
||||
usedSlots: undefined,
|
||||
}));
|
||||
return leagues.map((league) => {
|
||||
const socialLinks =
|
||||
league.socialLinks !== undefined
|
||||
? {
|
||||
...(league.socialLinks.discordUrl !== undefined
|
||||
? { discordUrl: league.socialLinks.discordUrl }
|
||||
: {}),
|
||||
...(league.socialLinks.youtubeUrl !== undefined
|
||||
? { youtubeUrl: league.socialLinks.youtubeUrl }
|
||||
: {}),
|
||||
...(league.socialLinks.websiteUrl !== undefined
|
||||
? { websiteUrl: league.socialLinks.websiteUrl }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
settings: league.settings,
|
||||
createdAt: league.createdAt.toISOString(),
|
||||
...(socialLinks !== undefined ? { socialLinks } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static toRaceDTO(race: Race | null): RaceDTO | null {
|
||||
@@ -76,31 +94,43 @@ export class EntityMappers {
|
||||
leagueId: race.leagueId,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
track: race.track,
|
||||
trackId: race.trackId,
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car,
|
||||
carId: race.carId,
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType,
|
||||
status: race.status,
|
||||
strengthOfField: race.strengthOfField,
|
||||
registeredCount: race.registeredCount,
|
||||
maxParticipants: race.maxParticipants,
|
||||
...(race.strengthOfField !== undefined
|
||||
? { strengthOfField: race.strengthOfField }
|
||||
: {}),
|
||||
...(race.registeredCount !== undefined
|
||||
? { registeredCount: race.registeredCount }
|
||||
: {}),
|
||||
...(race.maxParticipants !== undefined
|
||||
? { maxParticipants: race.maxParticipants }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
static toRaceDTOs(races: Race[]): RaceDTO[] {
|
||||
return races.map(race => ({
|
||||
return races.map((race) => ({
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
scheduledAt: race.scheduledAt.toISOString(),
|
||||
track: race.track,
|
||||
trackId: race.trackId,
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car,
|
||||
carId: race.carId,
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType,
|
||||
status: race.status,
|
||||
strengthOfField: race.strengthOfField,
|
||||
registeredCount: race.registeredCount,
|
||||
maxParticipants: race.maxParticipants,
|
||||
...(race.strengthOfField !== undefined
|
||||
? { strengthOfField: race.strengthOfField }
|
||||
: {}),
|
||||
...(race.registeredCount !== undefined
|
||||
? { registeredCount: race.registeredCount }
|
||||
: {}),
|
||||
...(race.maxParticipants !== undefined
|
||||
? { maxParticipants: race.maxParticipants }
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface TeamListItemViewModel {
|
||||
id: string;
|
||||
@@ -17,6 +18,9 @@ export interface AllTeamsViewModel {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface IAllTeamsPresenter {
|
||||
present(teams: Team[]): AllTeamsViewModel;
|
||||
}
|
||||
export interface AllTeamsResultDTO {
|
||||
teams: Array<Team & { memberCount: number }>;
|
||||
}
|
||||
|
||||
export interface IAllTeamsPresenter
|
||||
extends Presenter<AllTeamsResultDTO, AllTeamsViewModel> {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface DriverTeamViewModel {
|
||||
team: {
|
||||
@@ -22,10 +23,11 @@ export interface DriverTeamViewModel {
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export interface IDriverTeamPresenter {
|
||||
present(
|
||||
team: Team,
|
||||
membership: TeamMembership,
|
||||
driverId: string
|
||||
): DriverTeamViewModel;
|
||||
}
|
||||
export interface DriverTeamResultDTO {
|
||||
team: Team;
|
||||
membership: TeamMembership;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export interface IDriverTeamPresenter
|
||||
extends Presenter<DriverTeamResultDTO, DriverTeamViewModel> {}
|
||||
@@ -31,4 +31,6 @@ export interface IDriversLeaderboardPresenter {
|
||||
stats: Record<string, { rating: number; wins: number; podiums: number; totalRaces: number; overallRank: number }>,
|
||||
avatarUrls: Record<string, string>
|
||||
): DriversLeaderboardViewModel;
|
||||
|
||||
getViewModel(): DriversLeaderboardViewModel;
|
||||
}
|
||||
@@ -37,4 +37,5 @@ export interface ILeagueDriverSeasonStatsPresenter {
|
||||
driverResults: Map<string, Array<{ position: number }>>,
|
||||
driverRatings: Map<string, { rating: number | null; ratingChange: number | null }>
|
||||
): LeagueDriverSeasonStatsViewModel;
|
||||
getViewModel(): LeagueDriverSeasonStatsViewModel;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { League } from '../../domain/entities/League';
|
||||
import type { Season } from '../../domain/entities/Season';
|
||||
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||
import type { Game } from '../../domain/entities/Game';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface LeagueConfigFormViewModel {
|
||||
leagueId: string;
|
||||
@@ -49,6 +50,7 @@ export interface LeagueConfigFormViewModel {
|
||||
stewardingClosesHours: number;
|
||||
notifyAccusedOnProtest: boolean;
|
||||
notifyOnVoteRequired: boolean;
|
||||
requiredVotes?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,6 +61,5 @@ export interface LeagueFullConfigData {
|
||||
game?: Game;
|
||||
}
|
||||
|
||||
export interface ILeagueFullConfigPresenter {
|
||||
present(data: LeagueFullConfigData): LeagueConfigFormViewModel;
|
||||
}
|
||||
export interface ILeagueFullConfigPresenter
|
||||
extends Presenter<LeagueFullConfigData, LeagueConfigFormViewModel> {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChampionshipConfig } from '../../domain/value-objects/ChampionshipConfig';
|
||||
import type { ChampionshipConfig } from '../../domain/types/ChampionshipConfig';
|
||||
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
|
||||
|
||||
export interface LeagueScoringChampionshipViewModel {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { LeagueScoringPresetDTO } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface LeagueScoringPresetsViewModel {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface ILeagueScoringPresetsPresenter {
|
||||
present(presets: LeagueScoringPresetDTO[]): LeagueScoringPresetsViewModel;
|
||||
}
|
||||
export interface LeagueScoringPresetsResultDTO {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
}
|
||||
|
||||
export interface ILeagueScoringPresetsPresenter
|
||||
extends Presenter<LeagueScoringPresetsResultDTO, LeagueScoringPresetsViewModel> {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Standing } from '../../domain/entities/Standing';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface StandingItemViewModel {
|
||||
id: string;
|
||||
@@ -17,6 +18,9 @@ export interface LeagueStandingsViewModel {
|
||||
standings: StandingItemViewModel[];
|
||||
}
|
||||
|
||||
export interface ILeagueStandingsPresenter {
|
||||
present(standings: Standing[]): LeagueStandingsViewModel;
|
||||
}
|
||||
export interface LeagueStandingsResultDTO {
|
||||
standings: Standing[];
|
||||
}
|
||||
|
||||
export interface ILeagueStandingsPresenter
|
||||
extends Presenter<LeagueStandingsResultDTO, LeagueStandingsViewModel> {}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
import type { GetPendingSponsorshipRequestsResultDTO } from '../use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
|
||||
export interface IPendingSponsorshipRequestsPresenter {
|
||||
present(data: GetPendingSponsorshipRequestsResultDTO): void;
|
||||
}
|
||||
export type PendingSponsorshipRequestsViewModel = GetPendingSponsorshipRequestsResultDTO;
|
||||
|
||||
export interface IPendingSponsorshipRequestsPresenter
|
||||
extends Presenter<GetPendingSponsorshipRequestsResultDTO, PendingSponsorshipRequestsViewModel> {}
|
||||
@@ -9,6 +9,7 @@ export interface ProfileOverviewDriverSummaryViewModel {
|
||||
globalRank: number | null;
|
||||
consistency: number | null;
|
||||
bio: string | null;
|
||||
totalDrivers: number | null;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewStatsViewModel {
|
||||
@@ -23,6 +24,9 @@ export interface ProfileOverviewStatsViewModel {
|
||||
winRate: number | null;
|
||||
podiumRate: number | null;
|
||||
percentile: number | null;
|
||||
rating: number | null;
|
||||
consistency: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface ProfileOverviewFinishDistributionViewModel {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PenaltyType, PenaltyStatus } from '../../domain/entities/Penalty';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface RacePenaltyViewModel {
|
||||
id: string;
|
||||
@@ -22,23 +23,24 @@ export interface RacePenaltiesViewModel {
|
||||
penalties: RacePenaltyViewModel[];
|
||||
}
|
||||
|
||||
export interface IRacePenaltiesPresenter {
|
||||
present(
|
||||
penalties: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
type: PenaltyType;
|
||||
value?: number;
|
||||
reason: string;
|
||||
protestId?: string;
|
||||
issuedBy: string;
|
||||
status: PenaltyStatus;
|
||||
issuedAt: Date;
|
||||
appliedAt?: Date;
|
||||
notes?: string;
|
||||
getDescription(): string;
|
||||
}>,
|
||||
driverMap: Map<string, string>
|
||||
): RacePenaltiesViewModel;
|
||||
}
|
||||
export interface RacePenaltiesResultDTO {
|
||||
penalties: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
type: PenaltyType;
|
||||
value?: number;
|
||||
reason: string;
|
||||
protestId?: string;
|
||||
issuedBy: string;
|
||||
status: PenaltyStatus;
|
||||
issuedAt: Date;
|
||||
appliedAt?: Date;
|
||||
notes?: string;
|
||||
getDescription(): string;
|
||||
}>;
|
||||
driverMap: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface IRacePenaltiesPresenter
|
||||
extends Presenter<RacePenaltiesResultDTO, RacePenaltiesViewModel> {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ProtestStatus, ProtestIncident } from '../../domain/entities/Protest';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface RaceProtestViewModel {
|
||||
id: string;
|
||||
@@ -22,22 +23,23 @@ export interface RaceProtestsViewModel {
|
||||
protests: RaceProtestViewModel[];
|
||||
}
|
||||
|
||||
export interface IRaceProtestsPresenter {
|
||||
present(
|
||||
protests: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: ProtestIncident;
|
||||
comment?: string;
|
||||
proofVideoUrl?: string;
|
||||
status: ProtestStatus;
|
||||
reviewedBy?: string;
|
||||
decisionNotes?: string;
|
||||
filedAt: Date;
|
||||
reviewedAt?: Date;
|
||||
}>,
|
||||
driverMap: Map<string, string>
|
||||
): RaceProtestsViewModel;
|
||||
}
|
||||
export interface RaceProtestsResultDTO {
|
||||
protests: Array<{
|
||||
id: string;
|
||||
raceId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: ProtestIncident;
|
||||
comment?: string;
|
||||
proofVideoUrl?: string;
|
||||
status: ProtestStatus;
|
||||
reviewedBy?: string;
|
||||
decisionNotes?: string;
|
||||
filedAt: Date;
|
||||
reviewedAt?: Date;
|
||||
}>;
|
||||
driverMap: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface IRaceProtestsPresenter
|
||||
extends Presenter<RaceProtestsResultDTO, RaceProtestsViewModel> {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TeamJoinRequest } from '../../domain/types/TeamMembership';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface TeamJoinRequestViewModel {
|
||||
requestId: string;
|
||||
@@ -16,10 +17,11 @@ export interface TeamJoinRequestsViewModel {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface ITeamJoinRequestsPresenter {
|
||||
present(
|
||||
requests: TeamJoinRequest[],
|
||||
driverNames: Record<string, string>,
|
||||
avatarUrls: Record<string, string>
|
||||
): TeamJoinRequestsViewModel;
|
||||
}
|
||||
export interface TeamJoinRequestsResultDTO {
|
||||
requests: TeamJoinRequest[];
|
||||
driverNames: Record<string, string>;
|
||||
avatarUrls: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ITeamJoinRequestsPresenter
|
||||
extends Presenter<TeamJoinRequestsResultDTO, TeamJoinRequestsViewModel> {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TeamMembership } from '../../domain/types/TeamMembership';
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export interface TeamMemberViewModel {
|
||||
driverId: string;
|
||||
@@ -17,10 +18,11 @@ export interface TeamMembersViewModel {
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
export interface ITeamMembersPresenter {
|
||||
present(
|
||||
memberships: TeamMembership[],
|
||||
driverNames: Record<string, string>,
|
||||
avatarUrls: Record<string, string>
|
||||
): TeamMembersViewModel;
|
||||
}
|
||||
export interface TeamMembersResultDTO {
|
||||
memberships: TeamMembership[];
|
||||
driverNames: Record<string, string>;
|
||||
avatarUrls: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ITeamMembersPresenter
|
||||
extends Presenter<TeamMembersResultDTO, TeamMembersViewModel> {}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation';
|
||||
|
||||
export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
|
||||
export interface TeamLeaderboardItemViewModel {
|
||||
@@ -29,7 +31,10 @@ export interface TeamsLeaderboardViewModel {
|
||||
topTeams: TeamLeaderboardItemViewModel[];
|
||||
}
|
||||
|
||||
export interface ITeamsLeaderboardPresenter {
|
||||
present(teams: any[], recruitingCount: number): void;
|
||||
getViewModel(): TeamsLeaderboardViewModel;
|
||||
}
|
||||
export interface TeamsLeaderboardResultDTO {
|
||||
teams: unknown[];
|
||||
recruitingCount: number;
|
||||
}
|
||||
|
||||
export interface ITeamsLeaderboardPresenter
|
||||
extends Presenter<TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel> {}
|
||||
@@ -56,29 +56,37 @@ export class ApplyForSponsorshipUseCase
|
||||
}
|
||||
|
||||
if (!pricing.acceptingApplications) {
|
||||
throw new RacingApplicationError('This entity is not currently accepting sponsorship applications');
|
||||
throw new BusinessRuleViolationError(
|
||||
'This entity is not currently accepting sponsorship applications',
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the requested tier slot is available
|
||||
const slotAvailable = pricing.isSlotAvailable(dto.tier);
|
||||
if (!slotAvailable) {
|
||||
throw new RacingApplicationError(`No ${dto.tier} sponsorship slots are available`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`No ${dto.tier} sponsorship slots are available`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if sponsor already has a pending request for this entity
|
||||
const hasPending = await this.sponsorshipRequestRepo.hasPendingRequest(
|
||||
dto.sponsorId,
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
dto.entityId,
|
||||
);
|
||||
if (hasPending) {
|
||||
throw new RacingApplicationError('You already have a pending sponsorship request for this entity');
|
||||
throw new BusinessRuleViolationError(
|
||||
'You already have a pending sponsorship request for this entity',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate offered amount meets minimum price
|
||||
const minPrice = pricing.getPrice(dto.tier);
|
||||
if (minPrice && dto.offeredAmount < minPrice.amount) {
|
||||
throw new RacingApplicationError(`Offered amount must be at least ${minPrice.format()}`);
|
||||
throw new BusinessRuleViolationError(
|
||||
`Offered amount must be at least ${minPrice.format()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create the sponsorship request
|
||||
@@ -92,7 +100,7 @@ export class ApplyForSponsorshipUseCase
|
||||
entityId: dto.entityId,
|
||||
tier: dto.tier,
|
||||
offeredAmount,
|
||||
message: dto.message,
|
||||
...(dto.message !== undefined ? { message: dto.message } : {}),
|
||||
});
|
||||
|
||||
await this.sponsorshipRequestRepo.create(request);
|
||||
|
||||
@@ -70,13 +70,13 @@ export class ApplyPenaltyUseCase
|
||||
raceId: command.raceId,
|
||||
driverId: command.driverId,
|
||||
type: command.type,
|
||||
value: command.value,
|
||||
...(command.value !== undefined ? { value: command.value } : {}),
|
||||
reason: command.reason,
|
||||
protestId: command.protestId,
|
||||
...(command.protestId !== undefined ? { protestId: command.protestId } : {}),
|
||||
issuedBy: command.stewardId,
|
||||
status: 'pending',
|
||||
issuedAt: new Date(),
|
||||
notes: command.notes,
|
||||
...(command.notes !== undefined ? { notes: command.notes } : {}),
|
||||
});
|
||||
|
||||
await this.penaltyRepository.create(penalty);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { League } from '../../domain/entities/League';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
@@ -70,30 +71,28 @@ export class CreateLeagueWithSeasonAndScoringUseCase
|
||||
description: command.description ?? '',
|
||||
ownerId: command.ownerId,
|
||||
settings: {
|
||||
pointsSystem: (command.scoringPresetId as any) ?? 'custom',
|
||||
maxDrivers: command.maxDrivers,
|
||||
// Presets are attached at scoring-config level; league settings use a stable points system id.
|
||||
pointsSystem: 'custom',
|
||||
...(command.maxDrivers !== undefined ? { maxDrivers: command.maxDrivers } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
await this.leagueRepository.create(league);
|
||||
|
||||
const seasonId = uuidv4();
|
||||
const season = {
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
gameId: command.gameId,
|
||||
name: `${command.name} Season 1`,
|
||||
year: new Date().getFullYear(),
|
||||
order: 1,
|
||||
status: 'active' as const,
|
||||
status: 'active',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
// Season is a domain entity; use the repository's create, but shape matches Season.create expectations.
|
||||
// To keep this use case independent, we rely on repository to persist the plain object.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await this.seasonRepository.create(season as any);
|
||||
await this.seasonRepository.create(season);
|
||||
|
||||
const presetId = command.scoringPresetId ?? 'club-default';
|
||||
const preset: LeagueScoringPresetDTO | undefined =
|
||||
|
||||
@@ -55,8 +55,8 @@ export class FileProtestUseCase {
|
||||
protestingDriverId: command.protestingDriverId,
|
||||
accusedDriverId: command.accusedDriverId,
|
||||
incident: command.incident,
|
||||
comment: command.comment,
|
||||
proofVideoUrl: command.proofVideoUrl,
|
||||
...(command.comment !== undefined ? { comment: command.comment } : {}),
|
||||
...(command.proofVideoUrl !== undefined ? { proofVideoUrl: command.proofVideoUrl } : {}),
|
||||
status: 'pending',
|
||||
filedAt: new Date(),
|
||||
});
|
||||
|
||||
@@ -46,9 +46,9 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
|
||||
let scoringConfig;
|
||||
let game;
|
||||
let preset;
|
||||
let scoringConfig: LeagueEnrichedData['scoringConfig'];
|
||||
let game: LeagueEnrichedData['game'];
|
||||
let preset: LeagueEnrichedData['preset'];
|
||||
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
@@ -65,9 +65,9 @@ export class GetAllLeaguesWithCapacityAndScoringUseCase
|
||||
league,
|
||||
usedDriverSlots,
|
||||
season: activeSeason,
|
||||
scoringConfig,
|
||||
game,
|
||||
preset,
|
||||
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
|
||||
...(game ?? undefined ? { game } : {}),
|
||||
...(preset ?? undefined ? { preset } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IAllTeamsPresenter } from '../presenters/IAllTeamsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IAllTeamsPresenter,
|
||||
AllTeamsResultDTO,
|
||||
} from '../presenters/IAllTeamsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving all teams.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetAllTeamsUseCase
|
||||
implements AsyncUseCase<void, void> {
|
||||
implements UseCase<void, AllTeamsResultDTO, import('../presenters/IAllTeamsPresenter').AllTeamsViewModel, IAllTeamsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
public readonly presenter: IAllTeamsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(_input: void, presenter: IAllTeamsPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const teams = await this.teamRepository.findAll();
|
||||
|
||||
// Enrich teams with member counts
|
||||
const enrichedTeams = await Promise.all(
|
||||
|
||||
const enrichedTeams: Array<Team & { memberCount: number }> = await Promise.all(
|
||||
teams.map(async (team) => {
|
||||
const memberships = await this.teamMembershipRepository.findByTeamId(team.id);
|
||||
const memberCount = await this.teamMembershipRepository.countByTeamId(team.id);
|
||||
return {
|
||||
...team,
|
||||
memberCount: memberships.length,
|
||||
memberCount,
|
||||
};
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
this.presenter.present(enrichedTeams as any);
|
||||
|
||||
const dto: AllTeamsResultDTO = {
|
||||
teams: enrichedTeams,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,46 @@
|
||||
import type { ITeamRepository } from '../../domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverTeamPresenter } from '../presenters/IDriverTeamPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
IDriverTeamPresenter,
|
||||
DriverTeamResultDTO,
|
||||
DriverTeamViewModel,
|
||||
} from '../presenters/IDriverTeamPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving a driver's team.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetDriverTeamUseCase
|
||||
implements AsyncUseCase<string, boolean> {
|
||||
implements UseCase<{ driverId: string }, DriverTeamResultDTO, DriverTeamViewModel, IDriverTeamPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: IDriverTeamPresenter,
|
||||
) {}
|
||||
|
||||
async execute(driverId: string): Promise<boolean> {
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(driverId);
|
||||
async execute(input: { driverId: string }, presenter: IDriverTeamPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const membership = await this.membershipRepository.getActiveMembershipForDriver(input.driverId);
|
||||
if (!membership) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const team = await this.teamRepository.findById(membership.teamId);
|
||||
if (!team) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.presenter.present(team, membership, driverId);
|
||||
return true;
|
||||
const dto: DriverTeamResultDTO = {
|
||||
team,
|
||||
membership,
|
||||
driverId: input.driverId,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,9 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
acceptingApplications: pricing.acceptingApplications,
|
||||
customRequirements: pricing.customRequirements,
|
||||
...(pricing.customRequirements !== undefined
|
||||
? { customRequirements: pricing.customRequirements }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (pricing.mainSlot) {
|
||||
|
||||
@@ -2,8 +2,12 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '../../domain/repositories/IGameRepository';
|
||||
import type { ILeagueFullConfigPresenter, LeagueFullConfigData } from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
ILeagueFullConfigPresenter,
|
||||
LeagueFullConfigData,
|
||||
LeagueConfigFormViewModel,
|
||||
} from '../presenters/ILeagueFullConfigPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
import { EntityNotFoundError } from '../errors/RacingApplicationError';
|
||||
|
||||
/**
|
||||
@@ -11,17 +15,16 @@ import { EntityNotFoundError } from '../errors/RacingApplicationError';
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueFullConfigUseCase
|
||||
implements AsyncUseCase<{ leagueId: string }, void>
|
||||
implements UseCase<{ leagueId: string }, LeagueFullConfigData, LeagueConfigFormViewModel, ILeagueFullConfigPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly leagueScoringConfigRepository: ILeagueScoringConfigRepository,
|
||||
private readonly gameRepository: IGameRepository,
|
||||
public readonly presenter: ILeagueFullConfigPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: { leagueId: string }): Promise<void> {
|
||||
async execute(params: { leagueId: string }, presenter: ILeagueFullConfigPresenter): Promise<void> {
|
||||
const { leagueId } = params;
|
||||
|
||||
const league = await this.leagueRepository.findById(leagueId);
|
||||
@@ -35,23 +38,23 @@ export class GetLeagueFullConfigUseCase
|
||||
? seasons.find((s) => s.status === 'active') ?? seasons[0]
|
||||
: undefined;
|
||||
|
||||
let scoringConfig;
|
||||
let game;
|
||||
|
||||
if (activeSeason) {
|
||||
scoringConfig = await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (activeSeason.gameId) {
|
||||
game = await this.gameRepository.findById(activeSeason.gameId);
|
||||
}
|
||||
}
|
||||
let scoringConfig = await (async () => {
|
||||
if (!activeSeason) return undefined;
|
||||
return this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
})();
|
||||
let game = await (async () => {
|
||||
if (!activeSeason || !activeSeason.gameId) return undefined;
|
||||
return this.gameRepository.findById(activeSeason.gameId);
|
||||
})();
|
||||
|
||||
const data: LeagueFullConfigData = {
|
||||
league,
|
||||
activeSeason,
|
||||
scoringConfig,
|
||||
game,
|
||||
...(scoringConfig ?? undefined ? { scoringConfig } : {}),
|
||||
...(game ?? undefined ? { game } : {}),
|
||||
};
|
||||
|
||||
this.presenter.present(data);
|
||||
presenter.reset();
|
||||
presenter.present(data);
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,14 @@ export class GetLeagueScoringConfigUseCase
|
||||
if (!seasons || seasons.length === 0) {
|
||||
throw new Error(`No seasons found for league ${leagueId}`);
|
||||
}
|
||||
|
||||
|
||||
const activeSeason =
|
||||
seasons.find((s) => s.status === 'active') ?? seasons[0];
|
||||
|
||||
|
||||
if (!activeSeason) {
|
||||
throw new Error(`No active season could be determined for league ${leagueId}`);
|
||||
}
|
||||
|
||||
const scoringConfig =
|
||||
await this.leagueScoringConfigRepository.findBySeasonId(activeSeason.id);
|
||||
if (!scoringConfig) {
|
||||
@@ -50,14 +54,14 @@ export class GetLeagueScoringConfigUseCase
|
||||
|
||||
const presetId = scoringConfig.scoringPresetId;
|
||||
const preset = presetId ? this.presetProvider.getPresetById(presetId) : undefined;
|
||||
|
||||
|
||||
const data: LeagueScoringConfigData = {
|
||||
leagueId: league.id,
|
||||
seasonId: activeSeason.id,
|
||||
gameId: game.id,
|
||||
gameName: game.name,
|
||||
scoringPresetId: presetId,
|
||||
preset,
|
||||
...(presetId !== undefined ? { scoringPresetId: presetId } : {}),
|
||||
...(preset !== undefined ? { preset } : {}),
|
||||
championships: scoringConfig.championships,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import type { ILeagueStandingsPresenter } from '../presenters/ILeagueStandingsPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type {
|
||||
ILeagueStandingsPresenter,
|
||||
LeagueStandingsResultDTO,
|
||||
LeagueStandingsViewModel,
|
||||
} from '../presenters/ILeagueStandingsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetLeagueStandingsUseCaseParams {
|
||||
leagueId: string;
|
||||
@@ -11,14 +15,20 @@ export interface GetLeagueStandingsUseCaseParams {
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetLeagueStandingsUseCase
|
||||
implements AsyncUseCase<GetLeagueStandingsUseCaseParams, void> {
|
||||
constructor(
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
public readonly presenter: ILeagueStandingsPresenter,
|
||||
) {}
|
||||
implements
|
||||
UseCase<GetLeagueStandingsUseCaseParams, LeagueStandingsResultDTO, LeagueStandingsViewModel, ILeagueStandingsPresenter>
|
||||
{
|
||||
constructor(private readonly standingRepository: IStandingRepository) {}
|
||||
|
||||
async execute(params: GetLeagueStandingsUseCaseParams): Promise<void> {
|
||||
async execute(
|
||||
params: GetLeagueStandingsUseCaseParams,
|
||||
presenter: ILeagueStandingsPresenter,
|
||||
): Promise<void> {
|
||||
const standings = await this.standingRepository.findByLeagueId(params.leagueId);
|
||||
this.presenter.present(standings);
|
||||
const dto: LeagueStandingsResultDTO = {
|
||||
standings,
|
||||
};
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,11 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IPendingSponsorshipRequestsPresenter } from '../presenters/IPendingSponsorshipRequestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
import type {
|
||||
IPendingSponsorshipRequestsPresenter,
|
||||
PendingSponsorshipRequestsViewModel,
|
||||
} from '../presenters/IPendingSponsorshipRequestsPresenter';
|
||||
|
||||
export interface GetPendingSponsorshipRequestsDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -37,14 +41,23 @@ export interface GetPendingSponsorshipRequestsResultDTO {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class GetPendingSponsorshipRequestsUseCase {
|
||||
export class GetPendingSponsorshipRequestsUseCase
|
||||
implements UseCase<
|
||||
GetPendingSponsorshipRequestsDTO,
|
||||
GetPendingSponsorshipRequestsResultDTO,
|
||||
PendingSponsorshipRequestsViewModel,
|
||||
IPendingSponsorshipRequestsPresenter
|
||||
> {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorRepo: ISponsorRepository,
|
||||
private readonly presenter: IPendingSponsorshipRequestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetPendingSponsorshipRequestsDTO): Promise<void> {
|
||||
async execute(
|
||||
dto: GetPendingSponsorshipRequestsDTO,
|
||||
presenter: IPendingSponsorshipRequestsPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
const requests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
dto.entityId
|
||||
@@ -59,12 +72,12 @@ export class GetPendingSponsorshipRequestsUseCase {
|
||||
id: request.id,
|
||||
sponsorId: request.sponsorId,
|
||||
sponsorName: sponsor?.name ?? 'Unknown Sponsor',
|
||||
sponsorLogo: sponsor?.logoUrl,
|
||||
...(sponsor?.logoUrl !== undefined ? { sponsorLogo: sponsor.logoUrl } : {}),
|
||||
tier: request.tier,
|
||||
offeredAmount: request.offeredAmount.amount,
|
||||
currency: request.offeredAmount.currency,
|
||||
formattedAmount: request.offeredAmount.format(),
|
||||
message: request.message,
|
||||
...(request.message !== undefined ? { message: request.message } : {}),
|
||||
createdAt: request.createdAt,
|
||||
platformFee: request.getPlatformFee().amount,
|
||||
netAmount: request.getNetAmount().amount,
|
||||
@@ -74,7 +87,7 @@ export class GetPendingSponsorshipRequestsUseCase {
|
||||
// Sort by creation date (newest first)
|
||||
requestDTOs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
this.presenter.present({
|
||||
presenter.present({
|
||||
entityType: dto.entityType,
|
||||
entityId: dto.entityId,
|
||||
requests: requestDTOs,
|
||||
|
||||
@@ -50,7 +50,7 @@ export class GetProfileOverviewUseCase {
|
||||
public readonly presenter: IProfileOverviewPresenter,
|
||||
) {}
|
||||
|
||||
async execute(params: GetProfileOverviewParams): Promise<void> {
|
||||
async execute(params: GetProfileOverviewParams): Promise<ProfileOverviewViewModel | null> {
|
||||
const { driverId } = params;
|
||||
|
||||
const driver = await this.driverRepository.findById(driverId);
|
||||
@@ -69,7 +69,7 @@ export class GetProfileOverviewUseCase {
|
||||
};
|
||||
|
||||
this.presenter.present(emptyViewModel);
|
||||
return;
|
||||
return emptyViewModel;
|
||||
}
|
||||
|
||||
const [statsAdapter, teams, friends] = await Promise.all([
|
||||
@@ -95,6 +95,7 @@ export class GetProfileOverviewUseCase {
|
||||
};
|
||||
|
||||
this.presenter.present(viewModel);
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private buildDriverSummary(
|
||||
@@ -103,6 +104,7 @@ export class GetProfileOverviewUseCase {
|
||||
): ProfileOverviewDriverSummaryViewModel {
|
||||
const rankings = this.getAllDriverRankings();
|
||||
const fallbackRank = this.computeFallbackRank(driver.id, rankings);
|
||||
const totalDrivers = rankings.length;
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
@@ -110,13 +112,15 @@ export class GetProfileOverviewUseCase {
|
||||
country: driver.country,
|
||||
avatarUrl: this.imageService.getDriverAvatar(driver.id),
|
||||
iracingId: driver.iracingId ?? null,
|
||||
joinedAt: driver.joinedAt instanceof Date
|
||||
? driver.joinedAt.toISOString()
|
||||
: new Date(driver.joinedAt).toISOString(),
|
||||
joinedAt:
|
||||
driver.joinedAt instanceof Date
|
||||
? driver.joinedAt.toISOString()
|
||||
: new Date(driver.joinedAt).toISOString(),
|
||||
rating: stats?.rating ?? null,
|
||||
globalRank: stats?.overallRank ?? fallbackRank,
|
||||
consistency: stats?.consistency ?? null,
|
||||
bio: driver.bio ?? null,
|
||||
totalDrivers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,6 +165,9 @@ export class GetProfileOverviewUseCase {
|
||||
winRate,
|
||||
podiumRate,
|
||||
percentile: stats.percentile,
|
||||
rating: stats.rating,
|
||||
consistency: stats.consistency,
|
||||
overallRank: stats.overallRank,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,8 +424,10 @@ export class GetProfileOverviewUseCase {
|
||||
'Flexible schedule',
|
||||
];
|
||||
|
||||
const socialHandles = socialOptions[hash % socialOptions.length];
|
||||
const achievementsSource = achievementSets[hash % achievementSets.length];
|
||||
const socialHandles =
|
||||
socialOptions[hash % socialOptions.length] ?? [];
|
||||
const achievementsSource =
|
||||
achievementSets[hash % achievementSets.length] ?? [];
|
||||
|
||||
return {
|
||||
socialHandles,
|
||||
@@ -430,11 +439,11 @@ export class GetProfileOverviewUseCase {
|
||||
rarity: achievement.rarity,
|
||||
earnedAt: achievement.earnedAt.toISOString(),
|
||||
})),
|
||||
racingStyle: styles[hash % styles.length],
|
||||
favoriteTrack: tracks[hash % tracks.length],
|
||||
favoriteCar: cars[hash % cars.length],
|
||||
timezone: timezones[hash % timezones.length],
|
||||
availableHours: hours[hash % hours.length],
|
||||
racingStyle: styles[hash % styles.length] ?? 'Consistent Pacer',
|
||||
favoriteTrack: tracks[hash % tracks.length] ?? 'Unknown Track',
|
||||
favoriteCar: cars[hash % cars.length] ?? 'Unknown Car',
|
||||
timezone: timezones[hash % timezones.length] ?? 'UTC',
|
||||
availableHours: hours[hash % hours.length] ?? 'Flexible schedule',
|
||||
lookingForTeam: hash % 3 === 0,
|
||||
openToRequests: hash % 2 === 0,
|
||||
};
|
||||
|
||||
@@ -7,36 +7,51 @@
|
||||
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IRacePenaltiesPresenter } from '../presenters/IRacePenaltiesPresenter';
|
||||
import type {
|
||||
IRacePenaltiesPresenter,
|
||||
RacePenaltiesResultDTO,
|
||||
RacePenaltiesViewModel,
|
||||
} from '../presenters/IRacePenaltiesPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetRacePenaltiesUseCase {
|
||||
export interface GetRacePenaltiesInput {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRacePenaltiesUseCase
|
||||
implements
|
||||
UseCase<GetRacePenaltiesInput, RacePenaltiesResultDTO, RacePenaltiesViewModel, IRacePenaltiesPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly penaltyRepository: IPenaltyRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
public readonly presenter: IRacePenaltiesPresenter,
|
||||
) {}
|
||||
|
||||
async execute(raceId: string): Promise<void> {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(raceId);
|
||||
|
||||
// Load all driver details in parallel
|
||||
async execute(input: GetRacePenaltiesInput, presenter: IRacePenaltiesPresenter): Promise<void> {
|
||||
const penalties = await this.penaltyRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
penalties.forEach(penalty => {
|
||||
penalties.forEach((penalty) => {
|
||||
driverIds.add(penalty.driverId);
|
||||
driverIds.add(penalty.issuedBy);
|
||||
});
|
||||
|
||||
const drivers = await Promise.all(
|
||||
Array.from(driverIds).map(id => this.driverRepository.findById(id))
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
);
|
||||
|
||||
const driverMap = new Map<string, string>();
|
||||
drivers.forEach(driver => {
|
||||
drivers.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap.set(driver.id, driver.name);
|
||||
}
|
||||
});
|
||||
|
||||
this.presenter.present(penalties, driverMap);
|
||||
presenter.reset();
|
||||
const dto: RacePenaltiesResultDTO = {
|
||||
penalties,
|
||||
driverMap,
|
||||
};
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,31 @@
|
||||
|
||||
import type { IProtestRepository } from '../../domain/repositories/IProtestRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IRaceProtestsPresenter } from '../presenters/IRaceProtestsPresenter';
|
||||
import type {
|
||||
IRaceProtestsPresenter,
|
||||
RaceProtestsResultDTO,
|
||||
RaceProtestsViewModel,
|
||||
} from '../presenters/IRaceProtestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetRaceProtestsUseCase {
|
||||
export interface GetRaceProtestsInput {
|
||||
raceId: string;
|
||||
}
|
||||
|
||||
export class GetRaceProtestsUseCase
|
||||
implements
|
||||
UseCase<GetRaceProtestsInput, RaceProtestsResultDTO, RaceProtestsViewModel, IRaceProtestsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly protestRepository: IProtestRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
public readonly presenter: IRaceProtestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(raceId: string): Promise<void> {
|
||||
const protests = await this.protestRepository.findByRaceId(raceId);
|
||||
|
||||
// Load all driver details in parallel
|
||||
async execute(input: GetRaceProtestsInput, presenter: IRaceProtestsPresenter): Promise<void> {
|
||||
const protests = await this.protestRepository.findByRaceId(input.raceId);
|
||||
|
||||
const driverIds = new Set<string>();
|
||||
protests.forEach(protest => {
|
||||
protests.forEach((protest) => {
|
||||
driverIds.add(protest.protestingDriverId);
|
||||
driverIds.add(protest.accusedDriverId);
|
||||
if (protest.reviewedBy) {
|
||||
@@ -30,16 +40,21 @@ export class GetRaceProtestsUseCase {
|
||||
});
|
||||
|
||||
const drivers = await Promise.all(
|
||||
Array.from(driverIds).map(id => this.driverRepository.findById(id))
|
||||
Array.from(driverIds).map((id) => this.driverRepository.findById(id)),
|
||||
);
|
||||
|
||||
const driverMap = new Map<string, string>();
|
||||
drivers.forEach(driver => {
|
||||
drivers.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap.set(driver.id, driver.name);
|
||||
}
|
||||
});
|
||||
|
||||
this.presenter.present(protests, driverMap);
|
||||
presenter.reset();
|
||||
const dto: RaceProtestsResultDTO = {
|
||||
protests,
|
||||
driverMap,
|
||||
};
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ function mapPenaltySummary(penalties: Penalty[]): RaceResultsPenaltySummaryViewM
|
||||
return penalties.map((p) => ({
|
||||
driverId: p.driverId,
|
||||
type: p.type,
|
||||
value: p.value,
|
||||
...(p.value !== undefined ? { value: p.value } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ export class GetRaceResultsDetailUseCase {
|
||||
drivers: [],
|
||||
penalties: [],
|
||||
pointsSystem: {},
|
||||
fastestLapTime: undefined,
|
||||
currentDriverId: driverId,
|
||||
error: 'Race not found',
|
||||
};
|
||||
@@ -117,7 +116,7 @@ export class GetRaceResultsDetailUseCase {
|
||||
const pointsSystem = buildPointsSystem(league as League | null);
|
||||
const fastestLapTime = getFastestLapTime(results);
|
||||
const penaltySummary = mapPenaltySummary(penalties);
|
||||
|
||||
|
||||
const viewModel: RaceResultsDetailViewModel = {
|
||||
race: {
|
||||
id: race.id,
|
||||
@@ -136,7 +135,7 @@ export class GetRaceResultsDetailUseCase {
|
||||
drivers,
|
||||
penalties: penaltySummary,
|
||||
pointsSystem,
|
||||
fastestLapTime,
|
||||
...(fastestLapTime !== undefined ? { fastestLapTime } : {}),
|
||||
currentDriverId: effectiveCurrentDriverId,
|
||||
};
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
leagueName: league.name,
|
||||
seasonId: season.id,
|
||||
seasonName: season.name,
|
||||
seasonStartDate: season.startDate,
|
||||
seasonEndDate: season.endDate,
|
||||
...(season.startDate !== undefined ? { seasonStartDate: season.startDate } : {}),
|
||||
...(season.endDate !== undefined ? { seasonEndDate: season.endDate } : {}),
|
||||
tier: sponsorship.tier,
|
||||
status: sponsorship.status,
|
||||
pricing: {
|
||||
@@ -144,7 +144,7 @@ export class GetSponsorSponsorshipsUseCase {
|
||||
impressions,
|
||||
},
|
||||
createdAt: sponsorship.createdAt,
|
||||
activatedAt: sponsorship.activatedAt,
|
||||
...(sponsorship.activatedAt !== undefined ? { activatedAt: sponsorship.activatedAt } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ITeamJoinRequestsPresenter } from '../presenters/ITeamJoinRequestsPresenter';
|
||||
import type {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestsResultDTO,
|
||||
TeamJoinRequestsViewModel,
|
||||
} from '../presenters/ITeamJoinRequestsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team join requests.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetTeamJoinRequestsUseCase {
|
||||
export class GetTeamJoinRequestsUseCase
|
||||
implements UseCase<{ teamId: string }, TeamJoinRequestsResultDTO, TeamJoinRequestsViewModel, ITeamJoinRequestsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: ITeamJoinRequestsPresenter,
|
||||
) {}
|
||||
|
||||
async execute(teamId: string): Promise<void> {
|
||||
const requests = await this.membershipRepository.getJoinRequests(teamId);
|
||||
|
||||
async execute(input: { teamId: string }, presenter: ITeamJoinRequestsPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const requests = await this.membershipRepository.getJoinRequests(input.teamId);
|
||||
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
|
||||
for (const request of requests) {
|
||||
const driver = await this.driverRepository.findById(request.driverId);
|
||||
if (driver) {
|
||||
@@ -28,7 +39,13 @@ export class GetTeamJoinRequestsUseCase {
|
||||
}
|
||||
avatarUrls[request.driverId] = this.imageService.getDriverAvatar(request.driverId);
|
||||
}
|
||||
|
||||
this.presenter.present(requests, driverNames, avatarUrls);
|
||||
|
||||
const dto: TeamJoinRequestsResultDTO = {
|
||||
requests,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
|
||||
import type { IImageServicePort } from '../ports/IImageServicePort';
|
||||
import type { ITeamMembersPresenter } from '../presenters/ITeamMembersPresenter';
|
||||
import type {
|
||||
ITeamMembersPresenter,
|
||||
TeamMembersResultDTO,
|
||||
TeamMembersViewModel,
|
||||
} from '../presenters/ITeamMembersPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team members.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class GetTeamMembersUseCase {
|
||||
export class GetTeamMembersUseCase
|
||||
implements UseCase<{ teamId: string }, TeamMembersResultDTO, TeamMembersViewModel, ITeamMembersPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly imageService: IImageServicePort,
|
||||
// Kept for backward compatibility; callers must pass their own presenter.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public readonly presenter: ITeamMembersPresenter,
|
||||
) {}
|
||||
|
||||
async execute(teamId: string): Promise<void> {
|
||||
const memberships = await this.membershipRepository.getTeamMembers(teamId);
|
||||
|
||||
async execute(input: { teamId: string }, presenter: ITeamMembersPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const memberships = await this.membershipRepository.getTeamMembers(input.teamId);
|
||||
|
||||
const driverNames: Record<string, string> = {};
|
||||
const avatarUrls: Record<string, string> = {};
|
||||
|
||||
|
||||
for (const membership of memberships) {
|
||||
const driver = await this.driverRepository.findById(membership.driverId);
|
||||
if (driver) {
|
||||
@@ -28,7 +39,13 @@ export class GetTeamMembersUseCase {
|
||||
}
|
||||
avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId);
|
||||
}
|
||||
|
||||
this.presenter.present(memberships, driverNames, avatarUrls);
|
||||
|
||||
const dto: TeamMembersResultDTO = {
|
||||
memberships,
|
||||
driverNames,
|
||||
avatarUrls,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import type { ITeamsLeaderboardPresenter } from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import type {
|
||||
ITeamsLeaderboardPresenter,
|
||||
TeamsLeaderboardResultDTO,
|
||||
TeamsLeaderboardViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
interface DriverStatsAdapter {
|
||||
rating: number | null;
|
||||
@@ -16,22 +21,22 @@ interface DriverStatsAdapter {
|
||||
* Plain constructor-injected dependencies (no decorators) to keep the
|
||||
* application layer framework-agnostic and compatible with test tooling.
|
||||
*/
|
||||
export class GetTeamsLeaderboardUseCase {
|
||||
export class GetTeamsLeaderboardUseCase
|
||||
implements UseCase<void, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel, ITeamsLeaderboardPresenter> {
|
||||
constructor(
|
||||
private readonly teamRepository: ITeamRepository,
|
||||
private readonly teamMembershipRepository: ITeamMembershipRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly getDriverStats: (driverId: string) => DriverStatsAdapter | null,
|
||||
public readonly presenter: ITeamsLeaderboardPresenter,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(_input: void, presenter: ITeamsLeaderboardPresenter): Promise<void> {
|
||||
const allTeams = await this.teamRepository.findAll();
|
||||
const teams: any[] = [];
|
||||
|
||||
await Promise.all(
|
||||
allTeams.map(async (team) => {
|
||||
const memberships = await this.teamMembershipRepository.findByTeamId(team.id);
|
||||
const memberships = await this.teamMembershipRepository.getTeamMembers(team.id);
|
||||
const memberCount = memberships.length;
|
||||
|
||||
let ratingSum = 0;
|
||||
@@ -66,15 +71,18 @@ export class GetTeamsLeaderboardUseCase {
|
||||
isRecruiting: true,
|
||||
createdAt: new Date(),
|
||||
description: team.description,
|
||||
specialization: team.specialization as 'endurance' | 'sprint' | 'mixed' | undefined,
|
||||
region: team.region,
|
||||
languages: team.languages,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const recruitingCount = teams.filter((t) => t.isRecruiting).length;
|
||||
|
||||
this.presenter.present(teams, recruitingCount);
|
||||
const result: TeamsLeaderboardResultDTO = {
|
||||
teams,
|
||||
recruitingCount,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(result);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
|
||||
import type { ILeagueScoringPresetsPresenter } from '../presenters/ILeagueScoringPresetsPresenter';
|
||||
import type {
|
||||
ILeagueScoringPresetsPresenter,
|
||||
LeagueScoringPresetsResultDTO,
|
||||
LeagueScoringPresetsViewModel,
|
||||
} from '../presenters/ILeagueScoringPresetsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
/**
|
||||
* Use Case for listing league scoring presets.
|
||||
* Orchestrates domain logic and delegates presentation to the presenter.
|
||||
*/
|
||||
export class ListLeagueScoringPresetsUseCase {
|
||||
constructor(
|
||||
private readonly presetProvider: LeagueScoringPresetProvider,
|
||||
public readonly presenter: ILeagueScoringPresetsPresenter,
|
||||
) {}
|
||||
export class ListLeagueScoringPresetsUseCase
|
||||
implements UseCase<void, LeagueScoringPresetsResultDTO, LeagueScoringPresetsViewModel, ILeagueScoringPresetsPresenter>
|
||||
{
|
||||
constructor(private readonly presetProvider: LeagueScoringPresetProvider) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
async execute(_input: void, presenter: ILeagueScoringPresetsPresenter): Promise<void> {
|
||||
const presets = await this.presetProvider.listPresets();
|
||||
this.presenter.present(presets);
|
||||
|
||||
const dto: LeagueScoringPresetsResultDTO = {
|
||||
presets,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -38,14 +38,16 @@ export class RejectSponsorshipRequestUseCase {
|
||||
// Reject the request
|
||||
const rejectedRequest = request.reject(dto.respondedBy, dto.reason);
|
||||
await this.sponsorshipRequestRepo.update(rejectedRequest);
|
||||
|
||||
|
||||
// TODO: In a real implementation, notify the sponsor
|
||||
|
||||
|
||||
return {
|
||||
requestId: rejectedRequest.id,
|
||||
status: 'rejected',
|
||||
rejectedAt: rejectedRequest.respondedAt!,
|
||||
reason: rejectedRequest.rejectionReason,
|
||||
...(rejectedRequest.rejectionReason !== undefined
|
||||
? { reason: rejectedRequest.rejectionReason }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,8 @@ export class UpdateDriverProfileUseCase {
|
||||
}
|
||||
|
||||
const updated = existing.update({
|
||||
bio: bio ?? existing.bio,
|
||||
country: country ?? existing.country,
|
||||
...(bio !== undefined ? { bio } : {}),
|
||||
...(country !== undefined ? { country } : {}),
|
||||
});
|
||||
|
||||
const persisted = await this.driverRepository.update(updated);
|
||||
|
||||
Reference in New Issue
Block a user