resolve todos in website and api
This commit is contained in:
@@ -137,6 +137,17 @@
|
||||
"seasonName"
|
||||
]
|
||||
},
|
||||
"SponsorshipDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"SponsoredLeagueDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -167,6 +178,37 @@
|
||||
"sponsorName"
|
||||
]
|
||||
},
|
||||
"SponsorProfileDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"companyName": {
|
||||
"type": "string"
|
||||
},
|
||||
"contactName": {
|
||||
"type": "string"
|
||||
},
|
||||
"contactEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"contactPhone": {
|
||||
"type": "string"
|
||||
},
|
||||
"website": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"companyName",
|
||||
"contactName",
|
||||
"contactEmail",
|
||||
"contactPhone",
|
||||
"website",
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"SponsorDashboardMetricsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -255,6 +297,21 @@
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"RenewalAlertDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"RejectSponsorshipRequestInputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -266,6 +323,144 @@
|
||||
"respondedBy"
|
||||
]
|
||||
},
|
||||
"RaceDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"date"
|
||||
]
|
||||
},
|
||||
"PrivacySettingsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"publicProfile": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"showStats": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"showActiveSponsorships": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowDirectContact": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"publicProfile",
|
||||
"showStats",
|
||||
"showActiveSponsorships",
|
||||
"allowDirectContact"
|
||||
]
|
||||
},
|
||||
"PaymentMethodDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"NotificationSettingsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emailNewSponsorships": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emailWeeklyReport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emailRaceAlerts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emailPaymentAlerts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emailNewOpportunities": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emailContractExpiry": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"emailNewSponsorships",
|
||||
"emailWeeklyReport",
|
||||
"emailRaceAlerts",
|
||||
"emailPaymentAlerts",
|
||||
"emailNewOpportunities",
|
||||
"emailContractExpiry"
|
||||
]
|
||||
},
|
||||
"LeagueDetailDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"game": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"game"
|
||||
]
|
||||
},
|
||||
"InvoiceDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoiceNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"vatAmount": {
|
||||
"type": "number"
|
||||
},
|
||||
"totalAmount": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"invoiceNumber",
|
||||
"date",
|
||||
"dueDate",
|
||||
"amount",
|
||||
"vatAmount",
|
||||
"totalAmount"
|
||||
]
|
||||
},
|
||||
"GetSponsorSponsorshipsQueryParamsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -303,6 +498,44 @@
|
||||
"entityId"
|
||||
]
|
||||
},
|
||||
"GetEntitySponsorshipPricingResultDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entityType": {
|
||||
"type": "string"
|
||||
},
|
||||
"entityId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entityType",
|
||||
"entityId"
|
||||
]
|
||||
},
|
||||
"DriverDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"iracingId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"iracingId",
|
||||
"name",
|
||||
"country"
|
||||
]
|
||||
},
|
||||
"CreateSponsorInputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -318,6 +551,75 @@
|
||||
"contactEmail"
|
||||
]
|
||||
},
|
||||
"BillingStatsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalSpent": {
|
||||
"type": "number"
|
||||
},
|
||||
"pendingAmount": {
|
||||
"type": "number"
|
||||
},
|
||||
"nextPaymentDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"nextPaymentAmount": {
|
||||
"type": "number"
|
||||
},
|
||||
"activeSponsorships": {
|
||||
"type": "number"
|
||||
},
|
||||
"averageMonthlySpend": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"totalSpent",
|
||||
"pendingAmount",
|
||||
"nextPaymentDate",
|
||||
"nextPaymentAmount",
|
||||
"activeSponsorships",
|
||||
"averageMonthlySpend"
|
||||
]
|
||||
},
|
||||
"AvailableLeagueDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"game": {
|
||||
"type": "string"
|
||||
},
|
||||
"drivers": {
|
||||
"type": "number"
|
||||
},
|
||||
"avgViewsPerRace": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"game",
|
||||
"drivers",
|
||||
"avgViewsPerRace"
|
||||
]
|
||||
},
|
||||
"ActivityItemDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"AcceptSponsorshipRequestInputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -723,25 +1025,6 @@
|
||||
"avatarUrl"
|
||||
]
|
||||
},
|
||||
"RaceDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"date"
|
||||
]
|
||||
},
|
||||
"RaceActionParamsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -772,6 +1055,29 @@
|
||||
"adminId"
|
||||
]
|
||||
},
|
||||
"ImportRaceResultsSummaryDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"raceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"driversProcessed": {
|
||||
"type": "number"
|
||||
},
|
||||
"resultsRecorded": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"success",
|
||||
"raceId",
|
||||
"driversProcessed",
|
||||
"resultsRecorded"
|
||||
]
|
||||
},
|
||||
"ImportRaceResultsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -787,7 +1093,7 @@
|
||||
"resultsFileContent"
|
||||
]
|
||||
},
|
||||
"GetRaceDetailParamsDTODTO": {
|
||||
"GetRaceDetailParamsDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"raceId": {
|
||||
@@ -891,6 +1197,12 @@
|
||||
},
|
||||
"scheduledAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"isMyLeague": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -899,7 +1211,9 @@
|
||||
"leagueName",
|
||||
"track",
|
||||
"car",
|
||||
"scheduledAt"
|
||||
"scheduledAt",
|
||||
"status",
|
||||
"isMyLeague"
|
||||
]
|
||||
},
|
||||
"DashboardLeagueStandingSummaryDTO": {
|
||||
@@ -1024,6 +1338,41 @@
|
||||
"enum"
|
||||
]
|
||||
},
|
||||
"AllRacesListItemDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"track": {
|
||||
"type": "string"
|
||||
},
|
||||
"car": {
|
||||
"type": "string"
|
||||
},
|
||||
"scheduledAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"leagueId": {
|
||||
"type": "string"
|
||||
},
|
||||
"leagueName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"track",
|
||||
"car",
|
||||
"scheduledAt",
|
||||
"status",
|
||||
"leagueId",
|
||||
"leagueName"
|
||||
]
|
||||
},
|
||||
"UpdatePaymentStatusInputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1297,6 +1646,40 @@
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"WithdrawFromLeagueWalletOutputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"WithdrawFromLeagueWalletInputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"seasonId": {
|
||||
"type": "string"
|
||||
},
|
||||
"destinationAccount": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"amount",
|
||||
"currency",
|
||||
"seasonId",
|
||||
"destinationAccount"
|
||||
]
|
||||
},
|
||||
"UpdateLeagueMemberRoleOutputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1327,6 +1710,17 @@
|
||||
"targetDriverId"
|
||||
]
|
||||
},
|
||||
"TotalLeaguesDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"totalLeagues": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"totalLeagues"
|
||||
]
|
||||
},
|
||||
"SeasonDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1408,6 +1802,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"leagueId": {
|
||||
"type": "string"
|
||||
},
|
||||
"raceId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1427,6 +1824,7 @@
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"leagueId",
|
||||
"raceId",
|
||||
"protestingDriverId",
|
||||
"accusedDriverId",
|
||||
@@ -1442,11 +1840,33 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"settings": {
|
||||
"type": "object"
|
||||
},
|
||||
"maxDrivers": {
|
||||
"type": "number"
|
||||
},
|
||||
"sessionDuration": {
|
||||
"type": "number"
|
||||
},
|
||||
"visibility": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
"name",
|
||||
"description",
|
||||
"ownerId",
|
||||
"settings",
|
||||
"maxDrivers"
|
||||
]
|
||||
},
|
||||
"LeagueSummaryDTO": {
|
||||
@@ -1513,6 +1933,44 @@
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"LeagueScoringPresetDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"LeagueMembershipDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"leagueId": {
|
||||
"type": "string"
|
||||
},
|
||||
"driverId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"leagueId",
|
||||
"driverId"
|
||||
]
|
||||
},
|
||||
"LeagueMemberDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1634,6 +2092,52 @@
|
||||
"canUpdateRoles"
|
||||
]
|
||||
},
|
||||
"WalletTransactionDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"GetLeagueWalletOutputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"balance": {
|
||||
"type": "number"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"totalRevenue": {
|
||||
"type": "number"
|
||||
},
|
||||
"totalFees": {
|
||||
"type": "number"
|
||||
},
|
||||
"totalWithdrawals": {
|
||||
"type": "number"
|
||||
},
|
||||
"pendingPayouts": {
|
||||
"type": "number"
|
||||
},
|
||||
"canWithdraw": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"balance",
|
||||
"currency",
|
||||
"totalRevenue",
|
||||
"totalFees",
|
||||
"totalWithdrawals",
|
||||
"pendingPayouts",
|
||||
"canWithdraw"
|
||||
]
|
||||
},
|
||||
"GetLeagueSeasonsQueryDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2031,29 +2535,6 @@
|
||||
"rank"
|
||||
]
|
||||
},
|
||||
"DriverDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"iracingId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"iracingId",
|
||||
"name",
|
||||
"country"
|
||||
]
|
||||
},
|
||||
"CompleteOnboardingOutputDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -10,7 +10,10 @@ import { DriverExtendedProfileProvider } from '@core/racing/application/ports/Dr
|
||||
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import type { Logger } from "@core/shared/application";
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
|
||||
// Import use cases
|
||||
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
@@ -29,6 +32,9 @@ import { InMemoryDriverExtendedProfileProvider } from '@adapters/racing/ports/In
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemorySocialGraphRepository } from '@core/social/infrastructure/inmemory/InMemorySocialAndFeed';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
@@ -40,6 +46,9 @@ export const DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN = 'DriverExtendedProfileProv
|
||||
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
|
||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
||||
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
|
||||
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
|
||||
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
|
||||
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
|
||||
|
||||
// Use case tokens
|
||||
@@ -92,6 +101,22 @@ export const DriverProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryNotificationPreferenceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SOCIAL_GRAPH_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) =>
|
||||
new InMemorySocialGraphRepository(logger, { drivers: [], friendships: [], feedEvents: [] }),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
@@ -99,8 +124,13 @@ export const DriverProviders: Provider[] = [
|
||||
// Use cases
|
||||
{
|
||||
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort, logger: Logger) =>
|
||||
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger),
|
||||
useFactory: (
|
||||
driverRepo: IDriverRepository,
|
||||
rankingService: IRankingService,
|
||||
driverStatsService: IDriverStatsService,
|
||||
imageService: IImageServicePort,
|
||||
logger: Logger,
|
||||
) => new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
@@ -115,7 +145,8 @@ export const DriverProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
|
||||
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) => new IsDriverRegisteredForRaceUseCase(registrationRepo, logger),
|
||||
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) =>
|
||||
new IsDriverRegisteredForRaceUseCase(registrationRepo, logger),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
@@ -125,18 +156,59 @@ export const DriverProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository, imageService: IImageServicePort, driverExtendedProfileProvider: DriverExtendedProfileProvider, logger: Logger) =>
|
||||
useFactory: (
|
||||
driverRepo: IDriverRepository,
|
||||
teamRepository: ITeamRepository,
|
||||
teamMembershipRepository: ITeamMembershipRepository,
|
||||
socialRepository: ISocialGraphRepository,
|
||||
imageService: IImageServicePort,
|
||||
driverExtendedProfileProvider: DriverExtendedProfileProvider,
|
||||
driverStatsService: IDriverStatsService,
|
||||
rankingService: IRankingService,
|
||||
) =>
|
||||
new GetProfileOverviewUseCase(
|
||||
driverRepo,
|
||||
// TODO: Add teamRepository, teamMembershipRepository, socialRepository, etc.
|
||||
null as any, // teamRepository
|
||||
null as any, // teamMembershipRepository
|
||||
null as any, // socialRepository
|
||||
teamRepository,
|
||||
teamMembershipRepository,
|
||||
socialRepository,
|
||||
imageService,
|
||||
driverExtendedProfileProvider,
|
||||
() => null, // getDriverStats
|
||||
() => [], // getAllDriverRankings
|
||||
(driverId: string) => {
|
||||
const stats = driverStatsService.getDriverStats(driverId);
|
||||
if (!stats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
rating: stats.rating,
|
||||
wins: stats.wins,
|
||||
podiums: stats.podiums,
|
||||
dnfs: 0,
|
||||
totalRaces: stats.totalRaces,
|
||||
avgFinish: null,
|
||||
bestFinish: null,
|
||||
worstFinish: null,
|
||||
overallRank: stats.overallRank,
|
||||
consistency: null,
|
||||
percentile: null,
|
||||
};
|
||||
},
|
||||
() =>
|
||||
rankingService.getAllDriverRankings().map(ranking => ({
|
||||
driverId: ranking.driverId,
|
||||
rating: ranking.rating,
|
||||
overallRank: ranking.overallRank,
|
||||
})),
|
||||
),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_PORT_TOKEN, DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN, LOGGER_TOKEN],
|
||||
inject: [
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
TEAM_REPOSITORY_TOKEN,
|
||||
TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
SOCIAL_GRAPH_REPOSITORY_TOKEN,
|
||||
IMAGE_SERVICE_PORT_TOKEN,
|
||||
DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN,
|
||||
DRIVER_STATS_SERVICE_TOKEN,
|
||||
RANKING_SERVICE_TOKEN,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('DriversLeaderboardPresenter', () => {
|
||||
id: 'driver-1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'US',
|
||||
racesCompleted: 50,
|
||||
wins: 10,
|
||||
@@ -64,7 +64,7 @@ describe('DriversLeaderboardPresenter', () => {
|
||||
id: 'driver-2',
|
||||
name: 'Driver Two',
|
||||
rating: 2400,
|
||||
skillLevel: 'Pro',
|
||||
skillLevel: 'intermediate',
|
||||
nationality: 'DE',
|
||||
racesCompleted: 40,
|
||||
wins: 5,
|
||||
@@ -137,6 +137,45 @@ describe('DriversLeaderboardPresenter', () => {
|
||||
expect(result.drivers[0].racesCompleted).toBe(0);
|
||||
expect(result.drivers[0].wins).toBe(0);
|
||||
expect(result.drivers[0].podiums).toBe(0);
|
||||
expect(result.drivers[0].isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('should derive skill level from rating bands', () => {
|
||||
const dto: DriversLeaderboardResultDTO = {
|
||||
drivers: [
|
||||
{ id: 'd1', name: 'Beginner', country: 'US', iracingId: '1', joinedAt: new Date() },
|
||||
{ id: 'd2', name: 'Intermediate', country: 'US', iracingId: '2', joinedAt: new Date() },
|
||||
{ id: 'd3', name: 'Advanced', country: 'US', iracingId: '3', joinedAt: new Date() },
|
||||
{ id: 'd4', name: 'Pro', country: 'US', iracingId: '4', joinedAt: new Date() },
|
||||
],
|
||||
rankings: [
|
||||
{ driverId: 'd1', rating: 1700, overallRank: 4 },
|
||||
{ driverId: 'd2', rating: 2000, overallRank: 3 },
|
||||
{ driverId: 'd3', rating: 2600, overallRank: 2 },
|
||||
{ driverId: 'd4', rating: 3100, overallRank: 1 },
|
||||
],
|
||||
stats: {
|
||||
d1: { racesCompleted: 5, wins: 0, podiums: 0 },
|
||||
d2: { racesCompleted: 5, wins: 0, podiums: 0 },
|
||||
d3: { racesCompleted: 5, wins: 0, podiums: 0 },
|
||||
d4: { racesCompleted: 5, wins: 0, podiums: 0 },
|
||||
},
|
||||
avatarUrls: {
|
||||
d1: 'avatar-1',
|
||||
d2: 'avatar-2',
|
||||
d3: 'avatar-3',
|
||||
d4: 'avatar-4',
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
const result = presenter.viewModel;
|
||||
|
||||
const levels = result.drivers
|
||||
.sort((a, b) => a.rating - b.rating)
|
||||
.map(d => d.skillLevel);
|
||||
|
||||
expect(levels).toEqual(['beginner', 'intermediate', 'advanced', 'pro']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DriversLeaderboardDTO, DriverLeaderboardItemDTO } from '../dtos/DriversLeaderboardDTO';
|
||||
import { DriversLeaderboardDTO } from '../dtos/DriversLeaderboardDTO';
|
||||
import { DriverLeaderboardItemDTO } from '../dtos/DriverLeaderboardItemDTO';
|
||||
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||
import { SkillLevelService } from '../../../../../core/racing/domain/services/SkillLevelService';
|
||||
|
||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||
private result: DriversLeaderboardDTO | null = null;
|
||||
@@ -15,16 +16,21 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
|
||||
const stats = dto.stats[driver.id];
|
||||
const avatarUrl = dto.avatarUrls[driver.id];
|
||||
|
||||
const rating = ranking?.rating ?? 0;
|
||||
const racesCompleted = stats?.racesCompleted ?? 0;
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
rating: ranking?.rating ?? 0,
|
||||
skillLevel: 'Pro', // TODO: map from domain
|
||||
rating,
|
||||
// Use core SkillLevelService to derive band from rating
|
||||
skillLevel: SkillLevelService.getSkillLevel(rating),
|
||||
nationality: driver.country,
|
||||
racesCompleted: stats?.racesCompleted ?? 0,
|
||||
racesCompleted,
|
||||
wins: stats?.wins ?? 0,
|
||||
podiums: stats?.podiums ?? 0,
|
||||
isActive: true, // TODO: determine from domain
|
||||
// Consider a driver active if they have completed at least one race
|
||||
isActive: racesCompleted > 0,
|
||||
rank: ranking?.overallRank ?? 0,
|
||||
avatarUrl,
|
||||
};
|
||||
|
||||
@@ -3,12 +3,18 @@ import { LeagueService } from './LeagueService';
|
||||
|
||||
// Import core interfaces
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||
import { InMemorySeasonRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
@@ -25,6 +31,7 @@ import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-c
|
||||
import { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { GetLeagueStandingsUseCaseImpl } from '@core/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { GetSeasonSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSeasonSponsorshipsUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
|
||||
@@ -86,6 +93,11 @@ export const LeagueProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: 'ISeasonSponsorshipRepository',
|
||||
useFactory: (logger: Logger) => new InMemorySeasonSponsorshipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
||||
@@ -150,6 +162,23 @@ export const LeagueProviders: Provider[] = [
|
||||
GetLeagueAdminPermissionsUseCase,
|
||||
GetLeagueWalletUseCase,
|
||||
WithdrawFromLeagueWalletUseCase,
|
||||
{
|
||||
provide: GetSeasonSponsorshipsUseCase,
|
||||
useFactory: (
|
||||
seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
seasonRepo: ISeasonRepository,
|
||||
leagueRepo: ILeagueRepository,
|
||||
leagueMembershipRepo: ILeagueMembershipRepository,
|
||||
raceRepo: IRaceRepository,
|
||||
) => new GetSeasonSponsorshipsUseCase(seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo),
|
||||
inject: [
|
||||
'ISeasonSponsorshipRepository',
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: ListLeagueScoringPresetsUseCase,
|
||||
useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
|
||||
import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
|
||||
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
|
||||
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
|
||||
@@ -13,6 +17,13 @@ import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/Re
|
||||
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
|
||||
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
|
||||
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
|
||||
import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
|
||||
import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/GetLeagueMembershipsUseCase';
|
||||
import { GetLeagueScheduleUseCase } from '@core/racing/application/use-cases/GetLeagueScheduleUseCase';
|
||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
|
||||
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
|
||||
import { GetSeasonSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSeasonSponsorshipsUseCase';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
@@ -21,41 +32,58 @@ describe('LeagueService', () => {
|
||||
let mockGetTotalLeaguesUseCase: jest.Mocked<GetTotalLeaguesUseCase>;
|
||||
let mockGetLeagueJoinRequestsUseCase: jest.Mocked<GetLeagueJoinRequestsUseCase>;
|
||||
let mockApproveLeagueJoinRequestUseCase: jest.Mocked<ApproveLeagueJoinRequestUseCase>;
|
||||
let mockGetLeagueFullConfigUseCase: jest.Mocked<GetLeagueFullConfigUseCase>;
|
||||
let mockGetLeagueOwnerSummaryUseCase: jest.Mocked<GetLeagueOwnerSummaryUseCase>;
|
||||
let mockGetLeagueScheduleUseCase: jest.Mocked<GetLeagueScheduleUseCase>;
|
||||
let mockGetSeasonSponsorshipsUseCase: jest.Mocked<GetSeasonSponsorshipsUseCase>;
|
||||
let mockLogger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetTotalLeaguesUseCase = {
|
||||
execute: jest.fn(),
|
||||
} as any;
|
||||
mockGetLeagueJoinRequestsUseCase = {
|
||||
execute: jest.fn(),
|
||||
} as any;
|
||||
mockApproveLeagueJoinRequestUseCase = {
|
||||
execute: jest.fn(),
|
||||
} as any;
|
||||
const createUseCaseMock = <T extends { execute: unknown }>(): jest.Mocked<T> => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: jest.fn() as any,
|
||||
}) as jest.Mocked<T>;
|
||||
|
||||
mockGetTotalLeaguesUseCase = createUseCaseMock<GetTotalLeaguesUseCase>();
|
||||
mockGetLeagueJoinRequestsUseCase = createUseCaseMock<GetLeagueJoinRequestsUseCase>();
|
||||
mockApproveLeagueJoinRequestUseCase = createUseCaseMock<ApproveLeagueJoinRequestUseCase>();
|
||||
mockGetLeagueFullConfigUseCase = createUseCaseMock<GetLeagueFullConfigUseCase>();
|
||||
mockGetLeagueOwnerSummaryUseCase = createUseCaseMock<GetLeagueOwnerSummaryUseCase>();
|
||||
mockGetLeagueScheduleUseCase = createUseCaseMock<GetLeagueScheduleUseCase>();
|
||||
mockGetSeasonSponsorshipsUseCase = createUseCaseMock<GetSeasonSponsorshipsUseCase>();
|
||||
mockLogger = {
|
||||
debug: jest.fn(),
|
||||
} as any;
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
} as unknown as jest.Mocked<Logger>;
|
||||
|
||||
service = new LeagueService(
|
||||
{} as any, // mockGetAllLeaguesWithCapacityUseCase
|
||||
{} as any, // mockGetLeagueStandingsUseCase
|
||||
{} as any, // mockGetLeagueStatsUseCase
|
||||
{} as any, // mockGetLeagueFullConfigUseCase
|
||||
{} as any, // mockCreateLeagueWithSeasonAndScoringUseCase
|
||||
{} as any, // mockGetRaceProtestsUseCase
|
||||
{} as unknown as GetAllLeaguesWithCapacityUseCase,
|
||||
{} as unknown as GetLeagueStandingsUseCase,
|
||||
{} as unknown as GetLeagueStatsUseCase,
|
||||
mockGetLeagueFullConfigUseCase,
|
||||
{} as unknown as GetLeagueScoringConfigUseCase,
|
||||
{} as unknown as ListLeagueScoringPresetsUseCase,
|
||||
{} as unknown as JoinLeagueUseCase,
|
||||
{} as unknown as TransferLeagueOwnershipUseCase,
|
||||
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
|
||||
{} as unknown as GetRaceProtestsUseCase,
|
||||
mockGetTotalLeaguesUseCase,
|
||||
mockGetLeagueJoinRequestsUseCase,
|
||||
mockApproveLeagueJoinRequestUseCase,
|
||||
{} as any, // mockRejectLeagueJoinRequestUseCase
|
||||
{} as any, // mockRemoveLeagueMemberUseCase
|
||||
{} as any, // mockUpdateLeagueMemberRoleUseCase
|
||||
{} as any, // mockGetLeagueOwnerSummaryUseCase
|
||||
{} as any, // mockGetLeagueProtestsUseCase
|
||||
{} as any, // mockGetLeagueSeasonsUseCase
|
||||
{} as any, // mockGetLeagueMembershipsUseCase
|
||||
{} as any, // mockGetLeagueScheduleUseCase
|
||||
{} as any, // mockGetLeagueAdminPermissionsUseCase
|
||||
{} as unknown as RejectLeagueJoinRequestUseCase,
|
||||
{} as unknown as RemoveLeagueMemberUseCase,
|
||||
{} as unknown as UpdateLeagueMemberRoleUseCase,
|
||||
mockGetLeagueOwnerSummaryUseCase,
|
||||
{} as unknown as GetLeagueProtestsUseCase,
|
||||
{} as unknown as GetLeagueSeasonsUseCase,
|
||||
{} as unknown as GetLeagueMembershipsUseCase,
|
||||
mockGetLeagueScheduleUseCase,
|
||||
{} as unknown as GetLeagueAdminPermissionsUseCase,
|
||||
{} as unknown as GetLeagueWalletUseCase,
|
||||
{} as unknown as WithdrawFromLeagueWalletUseCase,
|
||||
mockGetSeasonSponsorshipsUseCase,
|
||||
mockLogger,
|
||||
);
|
||||
});
|
||||
@@ -70,7 +98,7 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
it('should get league join requests', async () => {
|
||||
mockGetLeagueJoinRequestsUseCase.execute.mockImplementation(async (params, presenter) => {
|
||||
mockGetLeagueJoinRequestsUseCase.execute.mockImplementation(async (_params, presenter) => {
|
||||
presenter.present({
|
||||
joinRequests: [{ id: 'req-1', leagueId: 'league-1', driverId: 'driver-1', requestedAt: new Date(), message: 'msg' }],
|
||||
drivers: [{ id: 'driver-1', name: 'Driver 1' }],
|
||||
@@ -79,18 +107,20 @@ describe('LeagueService', () => {
|
||||
|
||||
const result = await service.getLeagueJoinRequests('league-1');
|
||||
|
||||
expect(result).toEqual([{
|
||||
id: 'req-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: expect.any(Date),
|
||||
message: 'msg',
|
||||
driver: { id: 'driver-1', name: 'Driver 1' },
|
||||
}]);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'req-1',
|
||||
leagueId: 'league-1',
|
||||
driverId: 'driver-1',
|
||||
requestedAt: expect.any(Date),
|
||||
message: 'msg',
|
||||
driver: { id: 'driver-1', name: 'Driver 1' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should approve league join request', async () => {
|
||||
mockApproveLeagueJoinRequestUseCase.execute.mockImplementation(async (params, presenter) => {
|
||||
mockApproveLeagueJoinRequestUseCase.execute.mockImplementation(async (_params, presenter) => {
|
||||
presenter.present({ success: true, message: 'Join request approved.' });
|
||||
});
|
||||
|
||||
@@ -100,70 +130,200 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
it('should reject league join request', async () => {
|
||||
const mockRejectUseCase = {
|
||||
execute: jest.fn().mockImplementation(async (params, presenter) => {
|
||||
presenter.present({ success: true, message: 'Join request rejected.' });
|
||||
}),
|
||||
} as any;
|
||||
const mockRejectUseCase: jest.Mocked<RejectLeagueJoinRequestUseCase> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: jest.fn() as any,
|
||||
} as unknown as jest.Mocked<RejectLeagueJoinRequestUseCase>;
|
||||
|
||||
service = new LeagueService(
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as unknown as GetAllLeaguesWithCapacityUseCase,
|
||||
{} as unknown as GetLeagueStandingsUseCase,
|
||||
{} as unknown as GetLeagueStatsUseCase,
|
||||
mockGetLeagueFullConfigUseCase,
|
||||
{} as unknown as GetLeagueScoringConfigUseCase,
|
||||
{} as unknown as ListLeagueScoringPresetsUseCase,
|
||||
{} as unknown as JoinLeagueUseCase,
|
||||
{} as unknown as TransferLeagueOwnershipUseCase,
|
||||
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
|
||||
{} as unknown as GetRaceProtestsUseCase,
|
||||
mockGetTotalLeaguesUseCase,
|
||||
mockGetLeagueJoinRequestsUseCase,
|
||||
mockApproveLeagueJoinRequestUseCase,
|
||||
mockRejectUseCase,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as unknown as RemoveLeagueMemberUseCase,
|
||||
{} as unknown as UpdateLeagueMemberRoleUseCase,
|
||||
mockGetLeagueOwnerSummaryUseCase,
|
||||
{} as unknown as GetLeagueProtestsUseCase,
|
||||
{} as unknown as GetLeagueSeasonsUseCase,
|
||||
{} as unknown as GetLeagueMembershipsUseCase,
|
||||
{} as unknown as GetLeagueScheduleUseCase,
|
||||
{} as unknown as GetLeagueAdminPermissionsUseCase,
|
||||
{} as unknown as GetLeagueWalletUseCase,
|
||||
{} as unknown as WithdrawFromLeagueWalletUseCase,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
mockRejectUseCase.execute.mockImplementation(async (_params, presenter) => {
|
||||
presenter.present({ success: true, message: 'Join request rejected.' });
|
||||
});
|
||||
|
||||
const result = await service.rejectLeagueJoinRequest({ requestId: 'req-1', leagueId: 'league-1' });
|
||||
|
||||
expect(result).toEqual({ success: true, message: 'Join request rejected.' });
|
||||
});
|
||||
|
||||
it('should remove league member', async () => {
|
||||
const mockRemoveUseCase = {
|
||||
execute: jest.fn().mockImplementation(async (params, presenter) => {
|
||||
presenter.present({ success: true });
|
||||
}),
|
||||
} as any;
|
||||
const mockRemoveUseCase: jest.Mocked<RemoveLeagueMemberUseCase> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: jest.fn() as any,
|
||||
} as unknown as jest.Mocked<RemoveLeagueMemberUseCase>;
|
||||
|
||||
service = new LeagueService(
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as unknown as GetAllLeaguesWithCapacityUseCase,
|
||||
{} as unknown as GetLeagueStandingsUseCase,
|
||||
{} as unknown as GetLeagueStatsUseCase,
|
||||
mockGetLeagueFullConfigUseCase,
|
||||
{} as unknown as GetLeagueScoringConfigUseCase,
|
||||
{} as unknown as ListLeagueScoringPresetsUseCase,
|
||||
{} as unknown as JoinLeagueUseCase,
|
||||
{} as unknown as TransferLeagueOwnershipUseCase,
|
||||
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
|
||||
{} as unknown as GetRaceProtestsUseCase,
|
||||
mockGetTotalLeaguesUseCase,
|
||||
mockGetLeagueJoinRequestsUseCase,
|
||||
mockApproveLeagueJoinRequestUseCase,
|
||||
{} as unknown as RejectLeagueJoinRequestUseCase,
|
||||
mockRemoveUseCase,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as unknown as UpdateLeagueMemberRoleUseCase,
|
||||
mockGetLeagueOwnerSummaryUseCase,
|
||||
{} as unknown as GetLeagueProtestsUseCase,
|
||||
{} as unknown as GetLeagueSeasonsUseCase,
|
||||
{} as unknown as GetLeagueMembershipsUseCase,
|
||||
{} as unknown as GetLeagueScheduleUseCase,
|
||||
{} as unknown as GetLeagueAdminPermissionsUseCase,
|
||||
{} as unknown as GetLeagueWalletUseCase,
|
||||
{} as unknown as WithdrawFromLeagueWalletUseCase,
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
mockRemoveUseCase.execute.mockImplementation(async (_params, presenter) => {
|
||||
presenter.present({ success: true });
|
||||
});
|
||||
|
||||
const result = await service.removeLeagueMember({ leagueId: 'league-1', performerDriverId: 'performer-1', targetDriverId: 'driver-1' });
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should aggregate league admin data via composite use case', async () => {
|
||||
const fullConfig = {
|
||||
league: {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Test',
|
||||
ownerId: 'owner-1',
|
||||
settings: { pointsSystem: 'custom' },
|
||||
},
|
||||
} as any;
|
||||
|
||||
mockGetLeagueFullConfigUseCase.execute.mockResolvedValue(Result.ok(fullConfig));
|
||||
mockGetLeagueOwnerSummaryUseCase.execute.mockResolvedValue(Result.ok({ summary: null } as any));
|
||||
|
||||
const joinRequestsSpy = jest
|
||||
.spyOn(service, 'getLeagueJoinRequests')
|
||||
.mockResolvedValue({ joinRequests: [] } as any);
|
||||
const protestsSpy = jest
|
||||
.spyOn(service, 'getLeagueProtests')
|
||||
.mockResolvedValue({ protests: [], racesById: {}, driversById: {} } as any);
|
||||
const seasonsSpy = jest
|
||||
.spyOn(service, 'getLeagueSeasons')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getLeagueAdmin('league-1');
|
||||
|
||||
expect(mockGetLeagueFullConfigUseCase.execute).toHaveBeenCalledWith({ leagueId: 'league-1' });
|
||||
expect(mockGetLeagueOwnerSummaryUseCase.execute).toHaveBeenCalledWith({ ownerId: 'owner-1' });
|
||||
expect(joinRequestsSpy).toHaveBeenCalledWith('league-1');
|
||||
expect(protestsSpy).toHaveBeenCalledWith({ leagueId: 'league-1' });
|
||||
expect(seasonsSpy).toHaveBeenCalledWith({ leagueId: 'league-1' });
|
||||
expect(result.config.form?.leagueId).toBe('league-1');
|
||||
});
|
||||
|
||||
it('should get season sponsorships', async () => {
|
||||
const sponsorship = {
|
||||
id: 's-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
seasonId: 'season-123',
|
||||
seasonName: 'Season 1',
|
||||
tier: 'gold',
|
||||
status: 'active',
|
||||
pricing: {
|
||||
amount: 1000,
|
||||
currency: 'USD',
|
||||
},
|
||||
platformFee: {
|
||||
amount: 100,
|
||||
currency: 'USD',
|
||||
},
|
||||
netAmount: {
|
||||
amount: 900,
|
||||
currency: 'USD',
|
||||
},
|
||||
metrics: {
|
||||
drivers: 10,
|
||||
races: 5,
|
||||
completedRaces: 3,
|
||||
impressions: 3000,
|
||||
},
|
||||
createdAt: new Date('2024-01-01T00:00:00.000Z'),
|
||||
} as any;
|
||||
|
||||
mockGetSeasonSponsorshipsUseCase.execute.mockResolvedValue(
|
||||
Result.ok({
|
||||
seasonId: 'season-123',
|
||||
sponsorships: [sponsorship],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.getSeasonSponsorships('season-123');
|
||||
|
||||
expect(mockGetSeasonSponsorshipsUseCase.execute).toHaveBeenCalledWith({ seasonId: 'season-123' });
|
||||
expect(result.sponsorships).toHaveLength(1);
|
||||
expect(result.sponsorships[0]).toMatchObject({
|
||||
id: 's-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
seasonId: 'season-123',
|
||||
seasonName: 'Season 1',
|
||||
tier: 'gold',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get races for league', async () => {
|
||||
const scheduledAt = new Date('2024-02-01T12:00:00.000Z');
|
||||
|
||||
mockGetLeagueScheduleUseCase.execute.mockResolvedValue(
|
||||
Result.ok({
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
scheduledAt,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await service.getRaces('league-123');
|
||||
|
||||
expect(mockGetLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'league-123' });
|
||||
expect(result.races).toHaveLength(1);
|
||||
expect(result.races[0]).toMatchObject({
|
||||
id: 'race-1',
|
||||
name: 'Race 1',
|
||||
date: scheduledAt.toISOString(),
|
||||
leagueName: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,6 +60,7 @@ import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cas
|
||||
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
|
||||
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
|
||||
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
|
||||
import { GetSeasonSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSeasonSponsorshipsUseCase';
|
||||
|
||||
// API Presenters
|
||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||
@@ -70,7 +71,7 @@ import { mapApproveLeagueJoinRequestPortToDTO } from './presenters/ApproveLeague
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
import { mapGetLeagueOwnerSummaryOutputPortToDTO } from './presenters/GetLeagueOwnerSummaryPresenter';
|
||||
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
||||
import { mapGetLeagueScheduleOutputPortToDTO } from './presenters/LeagueSchedulePresenter';
|
||||
import { mapGetLeagueScheduleOutputPortToDTO, mapGetLeagueScheduleOutputPortToRaceDTOs } from './presenters/LeagueSchedulePresenter';
|
||||
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
|
||||
import { mapRejectLeagueJoinRequestOutputPortToDTO } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { mapRemoveLeagueMemberOutputPortToDTO } from './presenters/RemoveLeagueMemberPresenter';
|
||||
@@ -112,6 +113,7 @@ export class LeagueService {
|
||||
private readonly getLeagueAdminPermissionsUseCase: GetLeagueAdminPermissionsUseCase,
|
||||
private readonly getLeagueWalletUseCase: GetLeagueWalletUseCase,
|
||||
private readonly withdrawFromLeagueWalletUseCase: WithdrawFromLeagueWalletUseCase,
|
||||
private readonly getSeasonSponsorshipsUseCase: GetSeasonSponsorshipsUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -263,11 +265,21 @@ export class LeagueService {
|
||||
|
||||
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
|
||||
this.logger.debug('Getting league schedule', { leagueId });
|
||||
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
|
||||
const [scheduleResult, leagueConfigResult] = await Promise.all([
|
||||
this.getLeagueScheduleUseCase.execute({ leagueId }),
|
||||
this.getLeagueFullConfigUseCase.execute({ leagueId }),
|
||||
]);
|
||||
|
||||
if (scheduleResult.isErr()) {
|
||||
throw new Error(scheduleResult.unwrapErr().code);
|
||||
}
|
||||
return mapGetLeagueScheduleOutputPortToDTO(result.unwrap());
|
||||
|
||||
const leagueName = leagueConfigResult.isOk()
|
||||
? leagueConfigResult.unwrap().league.name.toString()
|
||||
: undefined;
|
||||
|
||||
return mapGetLeagueScheduleOutputPortToDTO(scheduleResult.unwrap(), leagueName);
|
||||
}
|
||||
|
||||
async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> {
|
||||
@@ -281,64 +293,49 @@ export class LeagueService {
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
|
||||
this.logger.debug('Getting league admin data', { leagueId });
|
||||
// For now, we'll keep the orchestration in the service since it combines multiple use cases
|
||||
// TODO: Create a composite use case that handles all the admin data fetching
|
||||
const joinRequests = await this.getLeagueJoinRequests(leagueId);
|
||||
const config = await this.getLeagueFullConfig({ leagueId });
|
||||
const protests = await this.getLeagueProtests({ leagueId });
|
||||
const seasons = await this.getLeagueSeasons({ leagueId });
|
||||
private async getLeagueAdminComposite(leagueId: string): Promise<LeagueAdminDTO> {
|
||||
this.logger.debug('Fetching composite league admin data', { leagueId });
|
||||
|
||||
// Get owner summary - we need the ownerId, so we use a simple approach for now
|
||||
// In a full implementation, we'd have a use case that gets league basic info
|
||||
const ownerSummary = config ? await this.getLeagueOwnerSummary({ ownerId: 'placeholder', leagueId }) : null;
|
||||
const [fullConfigResult, joinRequests, protests, seasons] = await Promise.all([
|
||||
this.getLeagueFullConfigUseCase.execute({ leagueId }),
|
||||
this.getLeagueJoinRequests(leagueId),
|
||||
this.getLeagueProtests({ leagueId }),
|
||||
this.getLeagueSeasons({ leagueId }),
|
||||
]);
|
||||
|
||||
// Convert config from view model to DTO format manually with proper types
|
||||
const configForm = config ? {
|
||||
leagueId: config.leagueId,
|
||||
basics: {
|
||||
name: config.basics.name,
|
||||
description: config.basics.description,
|
||||
visibility: config.basics.visibility as 'public' | 'private',
|
||||
},
|
||||
structure: {
|
||||
mode: config.structure.mode as 'solo' | 'team',
|
||||
},
|
||||
championships: [], // TODO: Map championships from view model
|
||||
scoring: {
|
||||
type: 'standard' as const, // TODO: Map from view model
|
||||
points: 25, // TODO: Map from view model
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: config.dropPolicy.strategy as 'none' | 'worst_n',
|
||||
n: config.dropPolicy.n ?? 0,
|
||||
},
|
||||
timings: {
|
||||
raceDayOfWeek: 'sunday' as const, // TODO: Map from view model
|
||||
raceTimeHour: 20, // TODO: Map from view model
|
||||
raceTimeMinute: 0, // TODO: Map from view model
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: config.stewarding.decisionMode === 'steward_vote' ? 'committee_vote' as const : 'single_steward' as const,
|
||||
requireDefense: config.stewarding.requireDefense,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||
requiredVotes: config.stewarding.requiredVotes ?? 0,
|
||||
},
|
||||
} : null;
|
||||
if (fullConfigResult.isErr()) {
|
||||
throw new Error(fullConfigResult.unwrapErr().code);
|
||||
}
|
||||
|
||||
return {
|
||||
const fullConfig = fullConfigResult.unwrap();
|
||||
const league = fullConfig.league;
|
||||
|
||||
const ownerSummaryResult = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: league.ownerId.toString() });
|
||||
if (ownerSummaryResult.isErr()) {
|
||||
throw new Error(ownerSummaryResult.unwrapErr().code);
|
||||
}
|
||||
|
||||
const ownerSummary = mapGetLeagueOwnerSummaryOutputPortToDTO(ownerSummaryResult.unwrap());
|
||||
|
||||
const configPresenter = new LeagueConfigPresenter();
|
||||
configPresenter.present(fullConfig);
|
||||
const configForm = configPresenter.getViewModel();
|
||||
|
||||
const adminPresenter = new LeagueAdminPresenter();
|
||||
adminPresenter.present({
|
||||
joinRequests: joinRequests.joinRequests,
|
||||
ownerSummary: ownerSummary?.summary || null,
|
||||
config: { form: configForm },
|
||||
ownerSummary,
|
||||
config: configForm,
|
||||
protests,
|
||||
seasons,
|
||||
};
|
||||
});
|
||||
|
||||
return adminPresenter.getViewModel();
|
||||
}
|
||||
|
||||
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
|
||||
this.logger.debug('Getting league admin data', { leagueId });
|
||||
return this.getLeagueAdminComposite(leagueId);
|
||||
}
|
||||
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
|
||||
@@ -426,20 +423,30 @@ export class LeagueService {
|
||||
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
|
||||
this.logger.debug('Getting season sponsorships', { seasonId });
|
||||
|
||||
// TODO: Implement actual logic to fetch season sponsorships
|
||||
// For now, return empty array as placeholder
|
||||
const result = await this.getSeasonSponsorshipsUseCase.execute({ seasonId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
const value = result.unwrap();
|
||||
|
||||
return {
|
||||
sponsorships: [],
|
||||
sponsorships: value?.sponsorships ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async getRaces(leagueId: string): Promise<GetLeagueRacesOutputDTO> {
|
||||
this.logger.debug('Getting league races', { leagueId });
|
||||
|
||||
// TODO: Implement actual logic to fetch league races
|
||||
// For now, return empty array as placeholder
|
||||
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
const races = mapGetLeagueScheduleOutputPortToRaceDTOs(result.unwrap());
|
||||
|
||||
return {
|
||||
races: [],
|
||||
races,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,23 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsDate, IsEnum } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
// TODO: protests are filed at race level but also managed on league level
|
||||
|
||||
/**
|
||||
* ProtestDTO represents a protest that is filed against a specific race
|
||||
* but is queried and managed in a league context.
|
||||
*
|
||||
* Both `leagueId` and `raceId` are exposed so that API consumers can
|
||||
* clearly relate the protest back to the league admin view while still
|
||||
* understanding which concrete race it belongs to.
|
||||
*/
|
||||
export class ProtestDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
leagueId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
raceId: string;
|
||||
|
||||
@@ -1,28 +1,49 @@
|
||||
import { GetLeagueProtestsOutputPort } from '@core/racing/application/ports/output/GetLeagueProtestsOutputPort';
|
||||
import { GetLeagueProtestsOutputPort, type ProtestOutputPort } from '@core/racing/application/ports/output/GetLeagueProtestsOutputPort';
|
||||
import { LeagueAdminProtestsDTO } from '../dtos/LeagueAdminProtestsDTO';
|
||||
import { ProtestDTO } from '../dtos/ProtestDTO';
|
||||
import { RaceDTO } from '../../race/dtos/RaceDTO';
|
||||
import { DriverDTO } from '../../driver/dtos/DriverDTO';
|
||||
|
||||
export function mapGetLeagueProtestsOutputPortToDTO(output: GetLeagueProtestsOutputPort): LeagueAdminProtestsDTO {
|
||||
const protests: ProtestDTO[] = output.protests.map(protest => ({
|
||||
id: protest.id,
|
||||
raceId: protest.raceId,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
submittedAt: new Date(protest.filedAt),
|
||||
description: protest.incident.description,
|
||||
status: protest.status as 'pending' | 'accepted' | 'rejected', // TODO: map properly
|
||||
}));
|
||||
function mapProtestStatus(status: ProtestOutputPort['status']): ProtestDTO['status'] {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
case 'awaiting_defense':
|
||||
case 'under_review':
|
||||
return 'pending';
|
||||
case 'upheld':
|
||||
return 'accepted';
|
||||
case 'dismissed':
|
||||
case 'withdrawn':
|
||||
return 'rejected';
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
}
|
||||
|
||||
export function mapGetLeagueProtestsOutputPortToDTO(output: GetLeagueProtestsOutputPort, leagueName?: string): LeagueAdminProtestsDTO {
|
||||
const protests: ProtestDTO[] = output.protests.map((protest) => {
|
||||
const race = output.racesById[protest.raceId];
|
||||
|
||||
return {
|
||||
id: protest.id,
|
||||
leagueId: race?.leagueId,
|
||||
raceId: protest.raceId,
|
||||
protestingDriverId: protest.protestingDriverId,
|
||||
accusedDriverId: protest.accusedDriverId,
|
||||
submittedAt: new Date(protest.filedAt),
|
||||
description: protest.incident.description,
|
||||
status: mapProtestStatus(protest.status),
|
||||
};
|
||||
});
|
||||
|
||||
const racesById: { [raceId: string]: RaceDTO } = {};
|
||||
for (const raceId in output.racesById) {
|
||||
const race = output.racesById[raceId];
|
||||
racesById[raceId] = {
|
||||
id: race.id,
|
||||
name: race.track, // assuming name is track
|
||||
name: race.track,
|
||||
date: race.scheduledAt,
|
||||
leagueName: undefined, // TODO: get league name if needed
|
||||
leagueName,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { LeagueConfigPresenter } from './LeagueConfigPresenter';
|
||||
import type { LeagueFullConfigOutputPort } from '@core/racing/application/ports/output/LeagueFullConfigOutputPort';
|
||||
|
||||
describe('LeagueConfigPresenter', () => {
|
||||
const createFullConfig = (overrides: Partial<LeagueFullConfigOutputPort> = {}): LeagueFullConfigOutputPort => {
|
||||
const base: LeagueFullConfigOutputPort = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
league: {
|
||||
id: 'league-1',
|
||||
name: 'Test League',
|
||||
description: 'Desc',
|
||||
ownerId: 'owner-1',
|
||||
settings: { pointsSystem: 'custom' },
|
||||
createdAt: new Date(),
|
||||
} as any,
|
||||
activeSeason: {
|
||||
id: 'season-1',
|
||||
leagueId: 'league-1',
|
||||
gameId: 'iracing',
|
||||
name: 'Season 1',
|
||||
status: 'planned',
|
||||
schedule: {
|
||||
startDate: new Date('2025-01-05T19:00:00Z'),
|
||||
timeOfDay: { hour: 20, minute: 0 } as any,
|
||||
} as any,
|
||||
dropPolicy: { strategy: 'bestNResults', n: 3 } as any,
|
||||
stewardingConfig: {
|
||||
decisionMode: 'steward_vote',
|
||||
requiredVotes: 3,
|
||||
requireDefense: true,
|
||||
defenseTimeLimit: 24,
|
||||
voteTimeLimit: 24,
|
||||
protestDeadlineHours: 48,
|
||||
stewardingClosesHours: 72,
|
||||
notifyAccusedOnProtest: true,
|
||||
notifyOnVoteRequired: true,
|
||||
} as any,
|
||||
} as any,
|
||||
scoringConfig: {
|
||||
id: 'scoring-1',
|
||||
seasonId: 'season-1',
|
||||
championships: [
|
||||
{
|
||||
id: 'champ-1',
|
||||
name: 'Drivers',
|
||||
type: 'driver' as any,
|
||||
sessionTypes: ['race'] as any,
|
||||
pointsTableBySessionType: {
|
||||
race: {
|
||||
getPointsForPosition: (pos: number) => (pos === 1 ? 25 : 0),
|
||||
} as any,
|
||||
},
|
||||
dropScorePolicy: { strategy: 'bestNResults', count: 3 } as any,
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
game: undefined,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
it('maps league config into form model with scoring and timings', () => {
|
||||
const presenter = new LeagueConfigPresenter();
|
||||
const fullConfig = createFullConfig();
|
||||
|
||||
presenter.present(fullConfig);
|
||||
const vm = presenter.getViewModel();
|
||||
|
||||
expect(vm).not.toBeNull();
|
||||
expect(vm!.leagueId).toBe('league-1');
|
||||
expect(vm!.basics.name).toBe('Test League');
|
||||
expect(vm!.scoring.type).toBe('custom');
|
||||
expect(vm!.scoring.points).toBe(25);
|
||||
expect(vm!.championships.length).toBe(1);
|
||||
expect(vm!.timings.raceTimeHour).toBe(20);
|
||||
expect(vm!.timings.raceTimeMinute).toBe(0);
|
||||
expect(vm!.dropPolicy.strategy).toBe('worst_n');
|
||||
expect(vm!.dropPolicy.n).toBe(3);
|
||||
expect(vm!.stewarding.decisionMode).toBe('committee_vote');
|
||||
});
|
||||
});
|
||||
@@ -10,45 +10,64 @@ export class LeagueConfigPresenter implements Presenter<LeagueFullConfigOutputPo
|
||||
}
|
||||
|
||||
present(dto: LeagueFullConfigOutputPort) {
|
||||
// Map from LeagueFullConfigOutputPort to LeagueConfigFormModelDTO
|
||||
const league = dto.league;
|
||||
const settings = league.settings;
|
||||
const stewarding = settings.stewarding;
|
||||
const stewarding = dto.activeSeason?.stewardingConfig;
|
||||
const dropPolicy = dto.activeSeason?.dropPolicy;
|
||||
const schedule = dto.activeSeason?.schedule;
|
||||
const scoringConfig = dto.scoringConfig;
|
||||
|
||||
const visibility: 'public' | 'private' = 'public';
|
||||
|
||||
const championships = scoringConfig?.championships ?? [];
|
||||
|
||||
const firstChampionship = championships[0];
|
||||
const firstSessionType = firstChampionship?.sessionTypes[0];
|
||||
const firstPointsTable = firstSessionType
|
||||
? firstChampionship.pointsTableBySessionType[firstSessionType]
|
||||
: undefined;
|
||||
const pointsForWin = firstPointsTable?.getPointsForPosition(1) ?? 0;
|
||||
|
||||
const raceDayOfWeek = schedule?.startDate
|
||||
? schedule.startDate.toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase()
|
||||
: 'sunday';
|
||||
const raceTimeHour = schedule?.timeOfDay?.hour ?? 20;
|
||||
const raceTimeMinute = schedule?.timeOfDay?.minute ?? 0;
|
||||
|
||||
this.result = {
|
||||
leagueId: league.id,
|
||||
basics: {
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
visibility: 'public', // TODO: Map visibility from league
|
||||
visibility,
|
||||
},
|
||||
structure: {
|
||||
mode: 'solo', // TODO: Map from league settings
|
||||
mode: 'solo',
|
||||
},
|
||||
championships: [], // TODO: Map championships
|
||||
championships,
|
||||
scoring: {
|
||||
type: 'standard', // TODO: Map scoring type
|
||||
points: 25, // TODO: Map points
|
||||
type: settings.pointsSystem,
|
||||
points: pointsForWin,
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: 'none', // TODO: Map
|
||||
n: 0,
|
||||
strategy: dropPolicy?.strategy === 'none' ? 'none' : 'worst_n',
|
||||
n: dropPolicy?.n,
|
||||
},
|
||||
timings: {
|
||||
raceDayOfWeek: 'sunday', // TODO: Map from timings
|
||||
raceTimeHour: 20,
|
||||
raceTimeMinute: 0,
|
||||
raceDayOfWeek,
|
||||
raceTimeHour,
|
||||
raceTimeMinute,
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: stewarding?.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
|
||||
requireDefense: stewarding?.requireDefense || false,
|
||||
defenseTimeLimit: stewarding?.defenseTimeLimit || 48,
|
||||
voteTimeLimit: stewarding?.voteTimeLimit || 72,
|
||||
protestDeadlineHours: stewarding?.protestDeadlineHours || 48,
|
||||
stewardingClosesHours: stewarding?.stewardingClosesHours || 168,
|
||||
notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true,
|
||||
notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true,
|
||||
requiredVotes: stewarding?.requiredVotes || 0,
|
||||
requireDefense: stewarding?.requireDefense ?? false,
|
||||
defenseTimeLimit: stewarding?.defenseTimeLimit ?? 48,
|
||||
voteTimeLimit: stewarding?.voteTimeLimit ?? 72,
|
||||
protestDeadlineHours: stewarding?.protestDeadlineHours ?? 48,
|
||||
stewardingClosesHours: stewarding?.stewardingClosesHours ?? 168,
|
||||
notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest ?? true,
|
||||
notifyOnVoteRequired: stewarding?.notifyOnVoteRequired ?? true,
|
||||
requiredVotes: stewarding?.requiredVotes,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,13 +2,22 @@ import { GetLeagueScheduleOutputPort } from '@core/racing/application/ports/outp
|
||||
import { LeagueScheduleDTO } from '../dtos/LeagueScheduleDTO';
|
||||
import { RaceDTO } from '../../race/dtos/RaceDTO';
|
||||
|
||||
export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort): LeagueScheduleDTO {
|
||||
export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort, leagueName?: string): LeagueScheduleDTO {
|
||||
return {
|
||||
races: output.races.map(race => ({
|
||||
races: output.races.map<RaceDTO>(race => ({
|
||||
id: race.id,
|
||||
name: race.name,
|
||||
date: race.scheduledAt.toISOString(),
|
||||
leagueName: undefined, // TODO: get league name if needed
|
||||
leagueName,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapGetLeagueScheduleOutputPortToRaceDTOs(output: GetLeagueScheduleOutputPort, leagueName?: string): RaceDTO[] {
|
||||
return output.races.map<RaceDTO>(race => ({
|
||||
id: race.id,
|
||||
name: race.name,
|
||||
date: race.scheduledAt.toISOString(),
|
||||
leagueName,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { GetAllRacesPresenter } from './GetAllRacesPresenter';
|
||||
import type { GetAllRacesOutputPort } from '@core/racing/application/ports/output/GetAllRacesOutputPort';
|
||||
|
||||
describe('GetAllRacesPresenter', () => {
|
||||
it('should map races and distinct leagues into the DTO', async () => {
|
||||
const presenter = new GetAllRacesPresenter();
|
||||
|
||||
const output: GetAllRacesOutputPort = {
|
||||
races: [
|
||||
{
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
track: 'Track A',
|
||||
car: 'Car A',
|
||||
status: 'scheduled',
|
||||
scheduledAt: '2025-01-01T10:00:00.000Z',
|
||||
strengthOfField: 1500,
|
||||
leagueName: 'League One',
|
||||
},
|
||||
{
|
||||
id: 'race-2',
|
||||
leagueId: 'league-1',
|
||||
track: 'Track B',
|
||||
car: 'Car B',
|
||||
status: 'completed',
|
||||
scheduledAt: '2025-01-02T10:00:00.000Z',
|
||||
strengthOfField: null,
|
||||
leagueName: 'League One',
|
||||
},
|
||||
{
|
||||
id: 'race-3',
|
||||
leagueId: 'league-2',
|
||||
track: 'Track C',
|
||||
car: 'Car C',
|
||||
status: 'running',
|
||||
scheduledAt: '2025-01-03T10:00:00.000Z',
|
||||
strengthOfField: 1800,
|
||||
leagueName: 'League Two',
|
||||
},
|
||||
],
|
||||
totalCount: 3,
|
||||
};
|
||||
|
||||
await presenter.present(output);
|
||||
const viewModel = presenter.getViewModel();
|
||||
|
||||
expect(viewModel).not.toBeNull();
|
||||
expect(viewModel!.races).toHaveLength(3);
|
||||
|
||||
// Leagues should be distinct and match league ids/names from races
|
||||
expect(viewModel!.filters.leagues).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ id: 'league-1', name: 'League One' },
|
||||
{ id: 'league-2', name: 'League Two' },
|
||||
]),
|
||||
);
|
||||
expect(viewModel!.filters.leagues).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle empty races by returning empty leagues', async () => {
|
||||
const presenter = new GetAllRacesPresenter();
|
||||
|
||||
const output: GetAllRacesOutputPort = {
|
||||
races: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
|
||||
await presenter.present(output);
|
||||
const viewModel = presenter.getViewModel();
|
||||
|
||||
expect(viewModel).not.toBeNull();
|
||||
expect(viewModel!.races).toHaveLength(0);
|
||||
expect(viewModel!.filters.leagues).toHaveLength(0);
|
||||
});
|
||||
})
|
||||
@@ -9,6 +9,15 @@ export class GetAllRacesPresenter {
|
||||
}
|
||||
|
||||
async present(output: GetAllRacesOutputPort) {
|
||||
const uniqueLeagues = new Map<string, { id: string; name: string }>();
|
||||
|
||||
for (const race of output.races) {
|
||||
uniqueLeagues.set(race.leagueId, {
|
||||
id: race.leagueId,
|
||||
name: race.leagueName,
|
||||
});
|
||||
}
|
||||
|
||||
this.result = {
|
||||
races: output.races.map(race => ({
|
||||
id: race.id,
|
||||
@@ -28,7 +37,7 @@ export class GetAllRacesPresenter {
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
],
|
||||
leagues: [], // TODO: populate if needed
|
||||
leagues: Array.from(uniqueLeagues.values()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user