resolve todos in website and api
This commit is contained in:
@@ -137,6 +137,17 @@
|
|||||||
"seasonName"
|
"seasonName"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"SponsorshipDTO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
"SponsoredLeagueDTO": {
|
"SponsoredLeagueDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -167,6 +178,37 @@
|
|||||||
"sponsorName"
|
"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": {
|
"SponsorDashboardMetricsDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -255,6 +297,21 @@
|
|||||||
"name"
|
"name"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"RenewalAlertDTO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
},
|
||||||
"RejectSponsorshipRequestInputDTO": {
|
"RejectSponsorshipRequestInputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -266,6 +323,144 @@
|
|||||||
"respondedBy"
|
"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": {
|
"GetSponsorSponsorshipsQueryParamsDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -303,6 +498,44 @@
|
|||||||
"entityId"
|
"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": {
|
"CreateSponsorInputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -318,6 +551,75 @@
|
|||||||
"contactEmail"
|
"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": {
|
"AcceptSponsorshipRequestInputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -723,25 +1025,6 @@
|
|||||||
"avatarUrl"
|
"avatarUrl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"RaceDTO": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"date"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"RaceActionParamsDTO": {
|
"RaceActionParamsDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -772,6 +1055,29 @@
|
|||||||
"adminId"
|
"adminId"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ImportRaceResultsSummaryDTO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"raceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"driversProcessed": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"resultsRecorded": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"success",
|
||||||
|
"raceId",
|
||||||
|
"driversProcessed",
|
||||||
|
"resultsRecorded"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ImportRaceResultsDTO": {
|
"ImportRaceResultsDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -787,7 +1093,7 @@
|
|||||||
"resultsFileContent"
|
"resultsFileContent"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"GetRaceDetailParamsDTODTO": {
|
"GetRaceDetailParamsDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"raceId": {
|
"raceId": {
|
||||||
@@ -891,6 +1197,12 @@
|
|||||||
},
|
},
|
||||||
"scheduledAt": {
|
"scheduledAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isMyLeague": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -899,7 +1211,9 @@
|
|||||||
"leagueName",
|
"leagueName",
|
||||||
"track",
|
"track",
|
||||||
"car",
|
"car",
|
||||||
"scheduledAt"
|
"scheduledAt",
|
||||||
|
"status",
|
||||||
|
"isMyLeague"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"DashboardLeagueStandingSummaryDTO": {
|
"DashboardLeagueStandingSummaryDTO": {
|
||||||
@@ -1024,6 +1338,41 @@
|
|||||||
"enum"
|
"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": {
|
"UpdatePaymentStatusInputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1297,6 +1646,40 @@
|
|||||||
"success"
|
"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": {
|
"UpdateLeagueMemberRoleOutputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1327,6 +1710,17 @@
|
|||||||
"targetDriverId"
|
"targetDriverId"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"TotalLeaguesDTO": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"totalLeagues": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"totalLeagues"
|
||||||
|
]
|
||||||
|
},
|
||||||
"SeasonDTO": {
|
"SeasonDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1408,6 +1802,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"leagueId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"raceId": {
|
"raceId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1427,6 +1824,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
|
"leagueId",
|
||||||
"raceId",
|
"raceId",
|
||||||
"protestingDriverId",
|
"protestingDriverId",
|
||||||
"accusedDriverId",
|
"accusedDriverId",
|
||||||
@@ -1442,11 +1840,33 @@
|
|||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"maxDrivers": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"sessionDuration": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"name"
|
"name",
|
||||||
|
"description",
|
||||||
|
"ownerId",
|
||||||
|
"settings",
|
||||||
|
"maxDrivers"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"LeagueSummaryDTO": {
|
"LeagueSummaryDTO": {
|
||||||
@@ -1513,6 +1933,44 @@
|
|||||||
"status"
|
"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": {
|
"LeagueMemberDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1634,6 +2092,52 @@
|
|||||||
"canUpdateRoles"
|
"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": {
|
"GetLeagueSeasonsQueryDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2031,29 +2535,6 @@
|
|||||||
"rank"
|
"rank"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"DriverDTO": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"iracingId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"iracingId",
|
|
||||||
"name",
|
|
||||||
"country"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"CompleteOnboardingOutputDTO": {
|
"CompleteOnboardingOutputDTO": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import { DriverExtendedProfileProvider } from '@core/racing/application/ports/Dr
|
|||||||
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||||
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||||
import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
|
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 use cases
|
||||||
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
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 { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||||
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||||
import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
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';
|
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||||
|
|
||||||
// Define injection tokens
|
// Define injection tokens
|
||||||
@@ -40,6 +46,9 @@ export const DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN = 'DriverExtendedProfileProv
|
|||||||
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
|
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
|
||||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||||
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
|
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
|
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
|
||||||
|
|
||||||
// Use case tokens
|
// Use case tokens
|
||||||
@@ -92,6 +101,22 @@ export const DriverProviders: Provider[] = [
|
|||||||
useFactory: (logger: Logger) => new InMemoryNotificationPreferenceRepository(logger),
|
useFactory: (logger: Logger) => new InMemoryNotificationPreferenceRepository(logger),
|
||||||
inject: [LOGGER_TOKEN],
|
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,
|
provide: LOGGER_TOKEN,
|
||||||
useClass: ConsoleLogger,
|
useClass: ConsoleLogger,
|
||||||
@@ -99,8 +124,13 @@ export const DriverProviders: Provider[] = [
|
|||||||
// Use cases
|
// Use cases
|
||||||
{
|
{
|
||||||
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
|
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
|
||||||
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort, logger: Logger) =>
|
useFactory: (
|
||||||
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger),
|
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],
|
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,
|
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],
|
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -125,18 +156,59 @@ export const DriverProviders: Provider[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
|
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(
|
new GetProfileOverviewUseCase(
|
||||||
driverRepo,
|
driverRepo,
|
||||||
// TODO: Add teamRepository, teamMembershipRepository, socialRepository, etc.
|
teamRepository,
|
||||||
null as any, // teamRepository
|
teamMembershipRepository,
|
||||||
null as any, // teamMembershipRepository
|
socialRepository,
|
||||||
null as any, // socialRepository
|
|
||||||
imageService,
|
imageService,
|
||||||
driverExtendedProfileProvider,
|
driverExtendedProfileProvider,
|
||||||
() => null, // getDriverStats
|
(driverId: string) => {
|
||||||
() => [], // getAllDriverRankings
|
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',
|
id: 'driver-1',
|
||||||
name: 'Driver One',
|
name: 'Driver One',
|
||||||
rating: 2500,
|
rating: 2500,
|
||||||
skillLevel: 'Pro',
|
skillLevel: 'advanced',
|
||||||
nationality: 'US',
|
nationality: 'US',
|
||||||
racesCompleted: 50,
|
racesCompleted: 50,
|
||||||
wins: 10,
|
wins: 10,
|
||||||
@@ -64,7 +64,7 @@ describe('DriversLeaderboardPresenter', () => {
|
|||||||
id: 'driver-2',
|
id: 'driver-2',
|
||||||
name: 'Driver Two',
|
name: 'Driver Two',
|
||||||
rating: 2400,
|
rating: 2400,
|
||||||
skillLevel: 'Pro',
|
skillLevel: 'intermediate',
|
||||||
nationality: 'DE',
|
nationality: 'DE',
|
||||||
racesCompleted: 40,
|
racesCompleted: 40,
|
||||||
wins: 5,
|
wins: 5,
|
||||||
@@ -137,6 +137,45 @@ describe('DriversLeaderboardPresenter', () => {
|
|||||||
expect(result.drivers[0].racesCompleted).toBe(0);
|
expect(result.drivers[0].racesCompleted).toBe(0);
|
||||||
expect(result.drivers[0].wins).toBe(0);
|
expect(result.drivers[0].wins).toBe(0);
|
||||||
expect(result.drivers[0].podiums).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 { DriverLeaderboardItemDTO } from '../dtos/DriverLeaderboardItemDTO';
|
||||||
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
|
||||||
|
import { SkillLevelService } from '../../../../../core/racing/domain/services/SkillLevelService';
|
||||||
|
|
||||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||||
private result: DriversLeaderboardDTO | null = null;
|
private result: DriversLeaderboardDTO | null = null;
|
||||||
@@ -15,16 +16,21 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
|
|||||||
const stats = dto.stats[driver.id];
|
const stats = dto.stats[driver.id];
|
||||||
const avatarUrl = dto.avatarUrls[driver.id];
|
const avatarUrl = dto.avatarUrls[driver.id];
|
||||||
|
|
||||||
|
const rating = ranking?.rating ?? 0;
|
||||||
|
const racesCompleted = stats?.racesCompleted ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: driver.id,
|
id: driver.id,
|
||||||
name: driver.name,
|
name: driver.name,
|
||||||
rating: ranking?.rating ?? 0,
|
rating,
|
||||||
skillLevel: 'Pro', // TODO: map from domain
|
// Use core SkillLevelService to derive band from rating
|
||||||
|
skillLevel: SkillLevelService.getSkillLevel(rating),
|
||||||
nationality: driver.country,
|
nationality: driver.country,
|
||||||
racesCompleted: stats?.racesCompleted ?? 0,
|
racesCompleted,
|
||||||
wins: stats?.wins ?? 0,
|
wins: stats?.wins ?? 0,
|
||||||
podiums: stats?.podiums ?? 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,
|
rank: ranking?.overallRank ?? 0,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import { LeagueService } from './LeagueService';
|
|||||||
|
|
||||||
// Import core interfaces
|
// Import core interfaces
|
||||||
import type { Logger } from '@core/shared/application/Logger';
|
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 concrete in-memory implementations
|
||||||
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||||
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||||
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||||
import { InMemorySeasonRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
import { InMemorySeasonRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||||
|
import { InMemorySeasonSponsorshipRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||||
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||||
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||||
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
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 { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
|
||||||
import { GetLeagueStandingsUseCaseImpl } from '@core/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
|
import { GetLeagueStandingsUseCaseImpl } from '@core/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
|
||||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
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 { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||||
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
|
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
|
useFactory: (logger: Logger) => new InMemorySeasonRepository(logger), // Factory for InMemorySeasonRepository
|
||||||
inject: [LOGGER_TOKEN],
|
inject: [LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: 'ISeasonSponsorshipRepository',
|
||||||
|
useFactory: (logger: Logger) => new InMemorySeasonSponsorshipRepository(logger),
|
||||||
|
inject: [LOGGER_TOKEN],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
useFactory: (logger: Logger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
||||||
@@ -150,6 +162,23 @@ export const LeagueProviders: Provider[] = [
|
|||||||
GetLeagueAdminPermissionsUseCase,
|
GetLeagueAdminPermissionsUseCase,
|
||||||
GetLeagueWalletUseCase,
|
GetLeagueWalletUseCase,
|
||||||
WithdrawFromLeagueWalletUseCase,
|
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,
|
provide: ListLeagueScoringPresetsUseCase,
|
||||||
useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),
|
useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { LeagueService } from './LeagueService';
|
import { LeagueService } from './LeagueService';
|
||||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
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 { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
|
||||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
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 { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||||
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
|
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 { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
|
||||||
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
|
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
|
||||||
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
|
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 type { Logger } from '@core/shared/application/Logger';
|
||||||
import { Result } from '@core/shared/application/Result';
|
import { Result } from '@core/shared/application/Result';
|
||||||
|
|
||||||
@@ -21,41 +32,58 @@ describe('LeagueService', () => {
|
|||||||
let mockGetTotalLeaguesUseCase: jest.Mocked<GetTotalLeaguesUseCase>;
|
let mockGetTotalLeaguesUseCase: jest.Mocked<GetTotalLeaguesUseCase>;
|
||||||
let mockGetLeagueJoinRequestsUseCase: jest.Mocked<GetLeagueJoinRequestsUseCase>;
|
let mockGetLeagueJoinRequestsUseCase: jest.Mocked<GetLeagueJoinRequestsUseCase>;
|
||||||
let mockApproveLeagueJoinRequestUseCase: jest.Mocked<ApproveLeagueJoinRequestUseCase>;
|
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>;
|
let mockLogger: jest.Mocked<Logger>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGetTotalLeaguesUseCase = {
|
const createUseCaseMock = <T extends { execute: unknown }>(): jest.Mocked<T> => ({
|
||||||
execute: jest.fn(),
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} as any;
|
execute: jest.fn() as any,
|
||||||
mockGetLeagueJoinRequestsUseCase = {
|
}) as jest.Mocked<T>;
|
||||||
execute: jest.fn(),
|
|
||||||
} as any;
|
mockGetTotalLeaguesUseCase = createUseCaseMock<GetTotalLeaguesUseCase>();
|
||||||
mockApproveLeagueJoinRequestUseCase = {
|
mockGetLeagueJoinRequestsUseCase = createUseCaseMock<GetLeagueJoinRequestsUseCase>();
|
||||||
execute: jest.fn(),
|
mockApproveLeagueJoinRequestUseCase = createUseCaseMock<ApproveLeagueJoinRequestUseCase>();
|
||||||
} as any;
|
mockGetLeagueFullConfigUseCase = createUseCaseMock<GetLeagueFullConfigUseCase>();
|
||||||
|
mockGetLeagueOwnerSummaryUseCase = createUseCaseMock<GetLeagueOwnerSummaryUseCase>();
|
||||||
|
mockGetLeagueScheduleUseCase = createUseCaseMock<GetLeagueScheduleUseCase>();
|
||||||
|
mockGetSeasonSponsorshipsUseCase = createUseCaseMock<GetSeasonSponsorshipsUseCase>();
|
||||||
mockLogger = {
|
mockLogger = {
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
} as any;
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<Logger>;
|
||||||
|
|
||||||
service = new LeagueService(
|
service = new LeagueService(
|
||||||
{} as any, // mockGetAllLeaguesWithCapacityUseCase
|
{} as unknown as GetAllLeaguesWithCapacityUseCase,
|
||||||
{} as any, // mockGetLeagueStandingsUseCase
|
{} as unknown as GetLeagueStandingsUseCase,
|
||||||
{} as any, // mockGetLeagueStatsUseCase
|
{} as unknown as GetLeagueStatsUseCase,
|
||||||
{} as any, // mockGetLeagueFullConfigUseCase
|
mockGetLeagueFullConfigUseCase,
|
||||||
{} as any, // mockCreateLeagueWithSeasonAndScoringUseCase
|
{} as unknown as GetLeagueScoringConfigUseCase,
|
||||||
{} as any, // mockGetRaceProtestsUseCase
|
{} as unknown as ListLeagueScoringPresetsUseCase,
|
||||||
|
{} as unknown as JoinLeagueUseCase,
|
||||||
|
{} as unknown as TransferLeagueOwnershipUseCase,
|
||||||
|
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
|
||||||
|
{} as unknown as GetRaceProtestsUseCase,
|
||||||
mockGetTotalLeaguesUseCase,
|
mockGetTotalLeaguesUseCase,
|
||||||
mockGetLeagueJoinRequestsUseCase,
|
mockGetLeagueJoinRequestsUseCase,
|
||||||
mockApproveLeagueJoinRequestUseCase,
|
mockApproveLeagueJoinRequestUseCase,
|
||||||
{} as any, // mockRejectLeagueJoinRequestUseCase
|
{} as unknown as RejectLeagueJoinRequestUseCase,
|
||||||
{} as any, // mockRemoveLeagueMemberUseCase
|
{} as unknown as RemoveLeagueMemberUseCase,
|
||||||
{} as any, // mockUpdateLeagueMemberRoleUseCase
|
{} as unknown as UpdateLeagueMemberRoleUseCase,
|
||||||
{} as any, // mockGetLeagueOwnerSummaryUseCase
|
mockGetLeagueOwnerSummaryUseCase,
|
||||||
{} as any, // mockGetLeagueProtestsUseCase
|
{} as unknown as GetLeagueProtestsUseCase,
|
||||||
{} as any, // mockGetLeagueSeasonsUseCase
|
{} as unknown as GetLeagueSeasonsUseCase,
|
||||||
{} as any, // mockGetLeagueMembershipsUseCase
|
{} as unknown as GetLeagueMembershipsUseCase,
|
||||||
{} as any, // mockGetLeagueScheduleUseCase
|
mockGetLeagueScheduleUseCase,
|
||||||
{} as any, // mockGetLeagueAdminPermissionsUseCase
|
{} as unknown as GetLeagueAdminPermissionsUseCase,
|
||||||
|
{} as unknown as GetLeagueWalletUseCase,
|
||||||
|
{} as unknown as WithdrawFromLeagueWalletUseCase,
|
||||||
|
mockGetSeasonSponsorshipsUseCase,
|
||||||
mockLogger,
|
mockLogger,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -70,7 +98,7 @@ describe('LeagueService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get league join requests', async () => {
|
it('should get league join requests', async () => {
|
||||||
mockGetLeagueJoinRequestsUseCase.execute.mockImplementation(async (params, presenter) => {
|
mockGetLeagueJoinRequestsUseCase.execute.mockImplementation(async (_params, presenter) => {
|
||||||
presenter.present({
|
presenter.present({
|
||||||
joinRequests: [{ id: 'req-1', leagueId: 'league-1', driverId: 'driver-1', requestedAt: new Date(), message: 'msg' }],
|
joinRequests: [{ id: 'req-1', leagueId: 'league-1', driverId: 'driver-1', requestedAt: new Date(), message: 'msg' }],
|
||||||
drivers: [{ id: 'driver-1', name: 'Driver 1' }],
|
drivers: [{ id: 'driver-1', name: 'Driver 1' }],
|
||||||
@@ -79,18 +107,20 @@ describe('LeagueService', () => {
|
|||||||
|
|
||||||
const result = await service.getLeagueJoinRequests('league-1');
|
const result = await service.getLeagueJoinRequests('league-1');
|
||||||
|
|
||||||
expect(result).toEqual([{
|
expect(result).toEqual([
|
||||||
id: 'req-1',
|
{
|
||||||
leagueId: 'league-1',
|
id: 'req-1',
|
||||||
driverId: 'driver-1',
|
leagueId: 'league-1',
|
||||||
requestedAt: expect.any(Date),
|
driverId: 'driver-1',
|
||||||
message: 'msg',
|
requestedAt: expect.any(Date),
|
||||||
driver: { id: 'driver-1', name: 'Driver 1' },
|
message: 'msg',
|
||||||
}]);
|
driver: { id: 'driver-1', name: 'Driver 1' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should approve league join request', async () => {
|
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.' });
|
presenter.present({ success: true, message: 'Join request approved.' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,70 +130,200 @@ describe('LeagueService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject league join request', async () => {
|
it('should reject league join request', async () => {
|
||||||
const mockRejectUseCase = {
|
const mockRejectUseCase: jest.Mocked<RejectLeagueJoinRequestUseCase> = {
|
||||||
execute: jest.fn().mockImplementation(async (params, presenter) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
presenter.present({ success: true, message: 'Join request rejected.' });
|
execute: jest.fn() as any,
|
||||||
}),
|
} as unknown as jest.Mocked<RejectLeagueJoinRequestUseCase>;
|
||||||
} as any;
|
|
||||||
|
|
||||||
service = new LeagueService(
|
service = new LeagueService(
|
||||||
{} as any,
|
{} as unknown as GetAllLeaguesWithCapacityUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueStandingsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueStatsUseCase,
|
||||||
{} as any,
|
mockGetLeagueFullConfigUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueScoringConfigUseCase,
|
||||||
{} as any,
|
{} as unknown as ListLeagueScoringPresetsUseCase,
|
||||||
{} as any,
|
{} as unknown as JoinLeagueUseCase,
|
||||||
{} as any,
|
{} as unknown as TransferLeagueOwnershipUseCase,
|
||||||
{} as any,
|
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
|
||||||
|
{} as unknown as GetRaceProtestsUseCase,
|
||||||
|
mockGetTotalLeaguesUseCase,
|
||||||
|
mockGetLeagueJoinRequestsUseCase,
|
||||||
|
mockApproveLeagueJoinRequestUseCase,
|
||||||
mockRejectUseCase,
|
mockRejectUseCase,
|
||||||
{} as any,
|
{} as unknown as RemoveLeagueMemberUseCase,
|
||||||
{} as any,
|
{} as unknown as UpdateLeagueMemberRoleUseCase,
|
||||||
{} as any,
|
mockGetLeagueOwnerSummaryUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueProtestsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueSeasonsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueMembershipsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueScheduleUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueAdminPermissionsUseCase,
|
||||||
|
{} as unknown as GetLeagueWalletUseCase,
|
||||||
|
{} as unknown as WithdrawFromLeagueWalletUseCase,
|
||||||
mockLogger,
|
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' });
|
const result = await service.rejectLeagueJoinRequest({ requestId: 'req-1', leagueId: 'league-1' });
|
||||||
|
|
||||||
expect(result).toEqual({ success: true, message: 'Join request rejected.' });
|
expect(result).toEqual({ success: true, message: 'Join request rejected.' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove league member', async () => {
|
it('should remove league member', async () => {
|
||||||
const mockRemoveUseCase = {
|
const mockRemoveUseCase: jest.Mocked<RemoveLeagueMemberUseCase> = {
|
||||||
execute: jest.fn().mockImplementation(async (params, presenter) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
presenter.present({ success: true });
|
execute: jest.fn() as any,
|
||||||
}),
|
} as unknown as jest.Mocked<RemoveLeagueMemberUseCase>;
|
||||||
} as any;
|
|
||||||
|
|
||||||
service = new LeagueService(
|
service = new LeagueService(
|
||||||
{} as any,
|
{} as unknown as GetAllLeaguesWithCapacityUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueStandingsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueStatsUseCase,
|
||||||
{} as any,
|
mockGetLeagueFullConfigUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueScoringConfigUseCase,
|
||||||
{} as any,
|
{} as unknown as ListLeagueScoringPresetsUseCase,
|
||||||
{} as any,
|
{} as unknown as JoinLeagueUseCase,
|
||||||
{} as any,
|
{} as unknown as TransferLeagueOwnershipUseCase,
|
||||||
{} as any,
|
{} as unknown as CreateLeagueWithSeasonAndScoringUseCase,
|
||||||
{} as any,
|
{} as unknown as GetRaceProtestsUseCase,
|
||||||
|
mockGetTotalLeaguesUseCase,
|
||||||
|
mockGetLeagueJoinRequestsUseCase,
|
||||||
|
mockApproveLeagueJoinRequestUseCase,
|
||||||
|
{} as unknown as RejectLeagueJoinRequestUseCase,
|
||||||
mockRemoveUseCase,
|
mockRemoveUseCase,
|
||||||
{} as any,
|
{} as unknown as UpdateLeagueMemberRoleUseCase,
|
||||||
{} as any,
|
mockGetLeagueOwnerSummaryUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueProtestsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueSeasonsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueMembershipsUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueScheduleUseCase,
|
||||||
{} as any,
|
{} as unknown as GetLeagueAdminPermissionsUseCase,
|
||||||
|
{} as unknown as GetLeagueWalletUseCase,
|
||||||
|
{} as unknown as WithdrawFromLeagueWalletUseCase,
|
||||||
mockLogger,
|
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' });
|
const result = await service.removeLeagueMember({ leagueId: 'league-1', performerDriverId: 'performer-1', targetDriverId: 'driver-1' });
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
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 { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
|
||||||
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
|
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
|
||||||
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
|
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
|
||||||
|
import { GetSeasonSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSeasonSponsorshipsUseCase';
|
||||||
|
|
||||||
// API Presenters
|
// API Presenters
|
||||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||||
@@ -70,7 +71,7 @@ import { mapApproveLeagueJoinRequestPortToDTO } from './presenters/ApproveLeague
|
|||||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||||
import { mapGetLeagueOwnerSummaryOutputPortToDTO } from './presenters/GetLeagueOwnerSummaryPresenter';
|
import { mapGetLeagueOwnerSummaryOutputPortToDTO } from './presenters/GetLeagueOwnerSummaryPresenter';
|
||||||
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
||||||
import { mapGetLeagueScheduleOutputPortToDTO } from './presenters/LeagueSchedulePresenter';
|
import { mapGetLeagueScheduleOutputPortToDTO, mapGetLeagueScheduleOutputPortToRaceDTOs } from './presenters/LeagueSchedulePresenter';
|
||||||
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
|
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
|
||||||
import { mapRejectLeagueJoinRequestOutputPortToDTO } from './presenters/RejectLeagueJoinRequestPresenter';
|
import { mapRejectLeagueJoinRequestOutputPortToDTO } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||||
import { mapRemoveLeagueMemberOutputPortToDTO } from './presenters/RemoveLeagueMemberPresenter';
|
import { mapRemoveLeagueMemberOutputPortToDTO } from './presenters/RemoveLeagueMemberPresenter';
|
||||||
@@ -112,6 +113,7 @@ export class LeagueService {
|
|||||||
private readonly getLeagueAdminPermissionsUseCase: GetLeagueAdminPermissionsUseCase,
|
private readonly getLeagueAdminPermissionsUseCase: GetLeagueAdminPermissionsUseCase,
|
||||||
private readonly getLeagueWalletUseCase: GetLeagueWalletUseCase,
|
private readonly getLeagueWalletUseCase: GetLeagueWalletUseCase,
|
||||||
private readonly withdrawFromLeagueWalletUseCase: WithdrawFromLeagueWalletUseCase,
|
private readonly withdrawFromLeagueWalletUseCase: WithdrawFromLeagueWalletUseCase,
|
||||||
|
private readonly getSeasonSponsorshipsUseCase: GetSeasonSponsorshipsUseCase,
|
||||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -263,11 +265,21 @@ export class LeagueService {
|
|||||||
|
|
||||||
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
|
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleDTO> {
|
||||||
this.logger.debug('Getting league schedule', { leagueId });
|
this.logger.debug('Getting league schedule', { leagueId });
|
||||||
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
|
||||||
if (result.isErr()) {
|
const [scheduleResult, leagueConfigResult] = await Promise.all([
|
||||||
throw new Error(result.unwrapErr().code);
|
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> {
|
async getLeagueStats(leagueId: string): Promise<LeagueStatsDTO> {
|
||||||
@@ -281,64 +293,49 @@ export class LeagueService {
|
|||||||
return presenter.getViewModel()!;
|
return presenter.getViewModel()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
|
private async getLeagueAdminComposite(leagueId: string): Promise<LeagueAdminDTO> {
|
||||||
this.logger.debug('Getting league admin data', { leagueId });
|
this.logger.debug('Fetching composite 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 });
|
|
||||||
|
|
||||||
// Get owner summary - we need the ownerId, so we use a simple approach for now
|
const [fullConfigResult, joinRequests, protests, seasons] = await Promise.all([
|
||||||
// In a full implementation, we'd have a use case that gets league basic info
|
this.getLeagueFullConfigUseCase.execute({ leagueId }),
|
||||||
const ownerSummary = config ? await this.getLeagueOwnerSummary({ ownerId: 'placeholder', leagueId }) : null;
|
this.getLeagueJoinRequests(leagueId),
|
||||||
|
this.getLeagueProtests({ leagueId }),
|
||||||
|
this.getLeagueSeasons({ leagueId }),
|
||||||
|
]);
|
||||||
|
|
||||||
// Convert config from view model to DTO format manually with proper types
|
if (fullConfigResult.isErr()) {
|
||||||
const configForm = config ? {
|
throw new Error(fullConfigResult.unwrapErr().code);
|
||||||
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;
|
|
||||||
|
|
||||||
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,
|
joinRequests: joinRequests.joinRequests,
|
||||||
ownerSummary: ownerSummary?.summary || null,
|
ownerSummary,
|
||||||
config: { form: configForm },
|
config: configForm,
|
||||||
protests,
|
protests,
|
||||||
seasons,
|
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> {
|
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
|
||||||
@@ -426,20 +423,30 @@ export class LeagueService {
|
|||||||
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
|
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
|
||||||
this.logger.debug('Getting season sponsorships', { seasonId });
|
this.logger.debug('Getting season sponsorships', { seasonId });
|
||||||
|
|
||||||
// TODO: Implement actual logic to fetch season sponsorships
|
const result = await this.getSeasonSponsorshipsUseCase.execute({ seasonId });
|
||||||
// For now, return empty array as placeholder
|
if (result.isErr()) {
|
||||||
|
throw new Error(result.unwrapErr().code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = result.unwrap();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sponsorships: [],
|
sponsorships: value?.sponsorships ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRaces(leagueId: string): Promise<GetLeagueRacesOutputDTO> {
|
async getRaces(leagueId: string): Promise<GetLeagueRacesOutputDTO> {
|
||||||
this.logger.debug('Getting league races', { leagueId });
|
this.logger.debug('Getting league races', { leagueId });
|
||||||
|
|
||||||
// TODO: Implement actual logic to fetch league races
|
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
||||||
// For now, return empty array as placeholder
|
if (result.isErr()) {
|
||||||
|
throw new Error(result.unwrapErr().code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const races = mapGetLeagueScheduleOutputPortToRaceDTOs(result.unwrap());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
races: [],
|
races,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,23 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { IsString, IsDate, IsEnum } from 'class-validator';
|
import { IsString, IsDate, IsEnum } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
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 {
|
export class ProtestDTO {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
leagueId: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
raceId: string;
|
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 { LeagueAdminProtestsDTO } from '../dtos/LeagueAdminProtestsDTO';
|
||||||
import { ProtestDTO } from '../dtos/ProtestDTO';
|
import { ProtestDTO } from '../dtos/ProtestDTO';
|
||||||
import { RaceDTO } from '../../race/dtos/RaceDTO';
|
import { RaceDTO } from '../../race/dtos/RaceDTO';
|
||||||
import { DriverDTO } from '../../driver/dtos/DriverDTO';
|
import { DriverDTO } from '../../driver/dtos/DriverDTO';
|
||||||
|
|
||||||
export function mapGetLeagueProtestsOutputPortToDTO(output: GetLeagueProtestsOutputPort): LeagueAdminProtestsDTO {
|
function mapProtestStatus(status: ProtestOutputPort['status']): ProtestDTO['status'] {
|
||||||
const protests: ProtestDTO[] = output.protests.map(protest => ({
|
switch (status) {
|
||||||
id: protest.id,
|
case 'pending':
|
||||||
raceId: protest.raceId,
|
case 'awaiting_defense':
|
||||||
protestingDriverId: protest.protestingDriverId,
|
case 'under_review':
|
||||||
accusedDriverId: protest.accusedDriverId,
|
return 'pending';
|
||||||
submittedAt: new Date(protest.filedAt),
|
case 'upheld':
|
||||||
description: protest.incident.description,
|
return 'accepted';
|
||||||
status: protest.status as 'pending' | 'accepted' | 'rejected', // TODO: map properly
|
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 } = {};
|
const racesById: { [raceId: string]: RaceDTO } = {};
|
||||||
for (const raceId in output.racesById) {
|
for (const raceId in output.racesById) {
|
||||||
const race = output.racesById[raceId];
|
const race = output.racesById[raceId];
|
||||||
racesById[raceId] = {
|
racesById[raceId] = {
|
||||||
id: race.id,
|
id: race.id,
|
||||||
name: race.track, // assuming name is track
|
name: race.track,
|
||||||
date: race.scheduledAt,
|
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) {
|
present(dto: LeagueFullConfigOutputPort) {
|
||||||
// Map from LeagueFullConfigOutputPort to LeagueConfigFormModelDTO
|
|
||||||
const league = dto.league;
|
const league = dto.league;
|
||||||
const settings = league.settings;
|
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 = {
|
this.result = {
|
||||||
leagueId: league.id,
|
leagueId: league.id,
|
||||||
basics: {
|
basics: {
|
||||||
name: league.name,
|
name: league.name,
|
||||||
description: league.description,
|
description: league.description,
|
||||||
visibility: 'public', // TODO: Map visibility from league
|
visibility,
|
||||||
},
|
},
|
||||||
structure: {
|
structure: {
|
||||||
mode: 'solo', // TODO: Map from league settings
|
mode: 'solo',
|
||||||
},
|
},
|
||||||
championships: [], // TODO: Map championships
|
championships,
|
||||||
scoring: {
|
scoring: {
|
||||||
type: 'standard', // TODO: Map scoring type
|
type: settings.pointsSystem,
|
||||||
points: 25, // TODO: Map points
|
points: pointsForWin,
|
||||||
},
|
},
|
||||||
dropPolicy: {
|
dropPolicy: {
|
||||||
strategy: 'none', // TODO: Map
|
strategy: dropPolicy?.strategy === 'none' ? 'none' : 'worst_n',
|
||||||
n: 0,
|
n: dropPolicy?.n,
|
||||||
},
|
},
|
||||||
timings: {
|
timings: {
|
||||||
raceDayOfWeek: 'sunday', // TODO: Map from timings
|
raceDayOfWeek,
|
||||||
raceTimeHour: 20,
|
raceTimeHour,
|
||||||
raceTimeMinute: 0,
|
raceTimeMinute,
|
||||||
},
|
},
|
||||||
stewarding: {
|
stewarding: {
|
||||||
decisionMode: stewarding?.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
|
decisionMode: stewarding?.decisionMode === 'steward_vote' ? 'committee_vote' : 'single_steward',
|
||||||
requireDefense: stewarding?.requireDefense || false,
|
requireDefense: stewarding?.requireDefense ?? false,
|
||||||
defenseTimeLimit: stewarding?.defenseTimeLimit || 48,
|
defenseTimeLimit: stewarding?.defenseTimeLimit ?? 48,
|
||||||
voteTimeLimit: stewarding?.voteTimeLimit || 72,
|
voteTimeLimit: stewarding?.voteTimeLimit ?? 72,
|
||||||
protestDeadlineHours: stewarding?.protestDeadlineHours || 48,
|
protestDeadlineHours: stewarding?.protestDeadlineHours ?? 48,
|
||||||
stewardingClosesHours: stewarding?.stewardingClosesHours || 168,
|
stewardingClosesHours: stewarding?.stewardingClosesHours ?? 168,
|
||||||
notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest || true,
|
notifyAccusedOnProtest: stewarding?.notifyAccusedOnProtest ?? true,
|
||||||
notifyOnVoteRequired: stewarding?.notifyOnVoteRequired || true,
|
notifyOnVoteRequired: stewarding?.notifyOnVoteRequired ?? true,
|
||||||
requiredVotes: stewarding?.requiredVotes || 0,
|
requiredVotes: stewarding?.requiredVotes,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { GetLeagueScheduleOutputPort } from '@core/racing/application/ports/outp
|
|||||||
import { LeagueScheduleDTO } from '../dtos/LeagueScheduleDTO';
|
import { LeagueScheduleDTO } from '../dtos/LeagueScheduleDTO';
|
||||||
import { RaceDTO } from '../../race/dtos/RaceDTO';
|
import { RaceDTO } from '../../race/dtos/RaceDTO';
|
||||||
|
|
||||||
export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort): LeagueScheduleDTO {
|
export function mapGetLeagueScheduleOutputPortToDTO(output: GetLeagueScheduleOutputPort, leagueName?: string): LeagueScheduleDTO {
|
||||||
return {
|
return {
|
||||||
races: output.races.map(race => ({
|
races: output.races.map<RaceDTO>(race => ({
|
||||||
id: race.id,
|
id: race.id,
|
||||||
name: race.name,
|
name: race.name,
|
||||||
date: race.scheduledAt.toISOString(),
|
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) {
|
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 = {
|
this.result = {
|
||||||
races: output.races.map(race => ({
|
races: output.races.map(race => ({
|
||||||
id: race.id,
|
id: race.id,
|
||||||
@@ -28,7 +37,7 @@ export class GetAllRacesPresenter {
|
|||||||
{ value: 'completed', label: 'Completed' },
|
{ value: 'completed', label: 'Completed' },
|
||||||
{ value: 'cancelled', label: 'Cancelled' },
|
{ value: 'cancelled', label: 'Cancelled' },
|
||||||
],
|
],
|
||||||
leagues: [], // TODO: populate if needed
|
leagues: Array.from(uniqueLeagues.values()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
|
||||||
import type { Notification } from '@core/notifications/application';
|
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Bell,
|
Bell,
|
||||||
@@ -36,33 +34,14 @@ const notificationColors: Record<string, string> = {
|
|||||||
race_reminder: 'text-warning-amber bg-warning-amber/10',
|
race_reminder: 'text-warning-amber bg-warning-amber/10',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import { useNotifications } from './NotificationProvider';
|
||||||
|
import type { Notification } from './NotificationProvider';
|
||||||
|
|
||||||
export default function NotificationCenter() {
|
export default function NotificationCenter() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentDriverId = useEffectiveDriverId();
|
const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications();
|
||||||
|
|
||||||
// Polling for new notifications
|
|
||||||
// TODO
|
|
||||||
// useEffect(() => {
|
|
||||||
// const loadNotifications = async () => {
|
|
||||||
// try {
|
|
||||||
// const repo = getNotificationRepository();
|
|
||||||
// const allNotifications = await repo.findByRecipientId(currentDriverId);
|
|
||||||
// setNotifications(allNotifications);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to load notifications:', error);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// loadNotifications();
|
|
||||||
|
|
||||||
// // Poll every 5 seconds
|
|
||||||
// const interval = setInterval(loadNotifications, 5000);
|
|
||||||
// return () => clearInterval(interval);
|
|
||||||
// }, [currentDriverId]);
|
|
||||||
|
|
||||||
// Close panel when clicking outside
|
// Close panel when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -81,41 +60,8 @@ export default function NotificationCenter() {
|
|||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const unreadCount = notifications.filter((n) => n.isUnread()).length;
|
const handleNotificationClick = (notification: Notification) => {
|
||||||
|
markAsRead(notification.id);
|
||||||
const handleMarkAsRead = async (notification: Notification) => {
|
|
||||||
if (!notification.isUnread()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const markRead = getMarkNotificationReadUseCase();
|
|
||||||
await markRead.execute({
|
|
||||||
notificationId: notification.id,
|
|
||||||
recipientId: currentDriverId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setNotifications((prev) =>
|
|
||||||
prev.map((n) => (n.id === notification.id ? n.markAsRead() : n))
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to mark notification as read:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMarkAllAsRead = async () => {
|
|
||||||
try {
|
|
||||||
const repo = getNotificationRepository();
|
|
||||||
await repo.markAllAsReadByRecipientId(currentDriverId);
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setNotifications((prev) => prev.map((n) => n.markAsRead()));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to mark all as read:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNotificationClick = async (notification: Notification) => {
|
|
||||||
await handleMarkAsRead(notification);
|
|
||||||
|
|
||||||
if (notification.actionUrl) {
|
if (notification.actionUrl) {
|
||||||
router.push(notification.actionUrl);
|
router.push(notification.actionUrl);
|
||||||
@@ -176,7 +122,7 @@ export default function NotificationCenter() {
|
|||||||
</div>
|
</div>
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={handleMarkAllAsRead}
|
onClick={markAllAsRead}
|
||||||
className="flex items-center gap-1 text-xs text-gray-400 hover:text-white transition-colors"
|
className="flex items-center gap-1 text-xs text-gray-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<CheckCheck className="w-3.5 h-3.5" />
|
<CheckCheck className="w-3.5 h-3.5" />
|
||||||
@@ -209,7 +155,7 @@ export default function NotificationCenter() {
|
|||||||
onClick={() => handleNotificationClick(notification)}
|
onClick={() => handleNotificationClick(notification)}
|
||||||
className={`
|
className={`
|
||||||
w-full text-left px-4 py-3 transition-colors hover:bg-iron-gray/30
|
w-full text-left px-4 py-3 transition-colors hover:bg-iron-gray/30
|
||||||
${notification.isUnread() ? 'bg-primary-blue/5' : ''}
|
${!notification.read ? 'bg-primary-blue/5' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
@@ -219,16 +165,16 @@ export default function NotificationCenter() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<p className={`text-sm font-medium truncate ${
|
<p className={`text-sm font-medium truncate ${
|
||||||
notification.isUnread() ? 'text-white' : 'text-gray-300'
|
!notification.read ? 'text-white' : 'text-gray-300'
|
||||||
}`}>
|
}`}>
|
||||||
{notification.title}
|
{notification.title}
|
||||||
</p>
|
</p>
|
||||||
{notification.isUnread() && (
|
{!notification.read && (
|
||||||
<span className="w-2 h-2 bg-primary-blue rounded-full flex-shrink-0 mt-1.5" />
|
<span className="w-2 h-2 bg-primary-blue rounded-full flex-shrink-0 mt-1.5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 line-clamp-2 mt-0.5">
|
<p className="text-xs text-gray-500 line-clamp-2 mt-0.5">
|
||||||
{notification.body}
|
{notification.message}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 mt-1.5">
|
<div className="flex items-center gap-2 mt-1.5">
|
||||||
<span className="text-[10px] text-gray-600">
|
<span className="text-[10px] text-gray-600">
|
||||||
|
|||||||
@@ -1,21 +1,55 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import type { Notification } from '@core/notifications/application';
|
|
||||||
import ModalNotification from './ModalNotification';
|
import ModalNotification from './ModalNotification';
|
||||||
import ToastNotification from './ToastNotification';
|
import ToastNotification from './ToastNotification';
|
||||||
|
|
||||||
|
export type NotificationVariant = 'toast' | 'modal' | 'center';
|
||||||
|
|
||||||
|
export interface NotificationAction {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
type?: 'primary' | 'secondary' | 'danger';
|
||||||
|
href?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
createdAt: Date;
|
||||||
|
variant: NotificationVariant;
|
||||||
|
actionUrl?: string;
|
||||||
|
requiresResponse?: boolean;
|
||||||
|
actions?: NotificationAction[];
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
read: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddNotificationInput {
|
||||||
|
id?: string;
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
variant?: NotificationVariant;
|
||||||
|
actionUrl?: string;
|
||||||
|
requiresResponse?: boolean;
|
||||||
|
actions?: NotificationAction[];
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
interface NotificationContextValue {
|
interface NotificationContextValue {
|
||||||
notifications: Notification[];
|
notifications: Notification[];
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
toastNotifications: Notification[];
|
addNotification: (input: AddNotificationInput) => string;
|
||||||
modalNotification: Notification | null;
|
dismissNotification: (id: string) => void;
|
||||||
markAsRead: (notification: Notification) => Promise<void>;
|
clearNotifications: () => void;
|
||||||
dismissToast: (notification: Notification) => void;
|
markAsRead: (id: string) => void;
|
||||||
respondToModal: (notification: Notification, actionId?: string) => Promise<void>;
|
markAllAsRead: () => void;
|
||||||
dismissModal: (notification: Notification) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotificationContext = createContext<NotificationContextValue | null>(null);
|
const NotificationContext = createContext<NotificationContextValue | null>(null);
|
||||||
@@ -34,133 +68,85 @@ interface NotificationProviderProps {
|
|||||||
|
|
||||||
export default function NotificationProvider({ children }: NotificationProviderProps) {
|
export default function NotificationProvider({ children }: NotificationProviderProps) {
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
const [toastNotifications, setToastNotifications] = useState<Notification[]>([]);
|
|
||||||
const [modalNotification, setModalNotification] = useState<Notification | null>(null);
|
|
||||||
const [seenNotificationIds, setSeenNotificationIds] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const currentDriverId = useEffectiveDriverId();
|
const addNotification = useCallback((input: AddNotificationInput): string => {
|
||||||
|
const id = input.id ?? uuid();
|
||||||
|
|
||||||
// Poll for new notifications
|
const notification: Notification = {
|
||||||
// TODO
|
id,
|
||||||
// useEffect(() => {
|
type: input.type,
|
||||||
// const loadNotifications = async () => {
|
title: input.title,
|
||||||
// try {
|
message: input.message,
|
||||||
// const repo = getNotificationRepository();
|
createdAt: input.createdAt ?? new Date(),
|
||||||
// const allNotifications = await repo.findByRecipientId(currentDriverId);
|
variant: input.variant ?? 'toast',
|
||||||
// setNotifications(allNotifications);
|
actionUrl: input.actionUrl,
|
||||||
|
requiresResponse: input.requiresResponse,
|
||||||
|
actions: input.actions,
|
||||||
|
data: input.data,
|
||||||
|
read: false,
|
||||||
|
};
|
||||||
|
|
||||||
// // Check for new notifications that need toast/modal display
|
setNotifications((prev) => [notification, ...prev]);
|
||||||
// allNotifications.forEach((notification) => {
|
|
||||||
// // Check both unread and action_required status for modals
|
|
||||||
// const shouldDisplay = (notification.isUnread() || notification.isActionRequired()) &&
|
|
||||||
// !seenNotificationIds.has(notification.id);
|
|
||||||
|
|
||||||
// if (shouldDisplay) {
|
return id;
|
||||||
// // Mark as seen to prevent duplicate displays
|
}, []);
|
||||||
// setSeenNotificationIds((prev) => new Set([...prev, notification.id]));
|
|
||||||
|
|
||||||
// // Handle based on urgency
|
const dismissNotification = useCallback((id: string) => {
|
||||||
// if (notification.isModal()) {
|
setNotifications((prev) => prev.filter((notification) => notification.id !== id));
|
||||||
// // Modal takes priority - show immediately
|
}, []);
|
||||||
// setModalNotification(notification);
|
|
||||||
// } else if (notification.isToast()) {
|
|
||||||
// // Add to toast queue
|
|
||||||
// setToastNotifications((prev) => [...prev, notification]);
|
|
||||||
// }
|
|
||||||
// // Silent notifications just appear in the notification center
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to load notifications:', error);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// loadNotifications();
|
const clearNotifications = useCallback(() => {
|
||||||
|
setNotifications([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// // Poll every 2 seconds for responsiveness
|
const markAsRead = useCallback((id: string) => {
|
||||||
// const interval = setInterval(loadNotifications, 2000);
|
setNotifications((prev) =>
|
||||||
// return () => clearInterval(interval);
|
prev.map((notification) =>
|
||||||
// }, [currentDriverId, seenNotificationIds]);
|
notification.id === id ? { ...notification, read: true } : notification,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const markAllAsRead = useCallback(() => {
|
||||||
|
setNotifications((prev) => prev.map((notification) => ({ ...notification, read: true })));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unreadCount = useMemo(
|
||||||
|
() => notifications.filter((notification) => !notification.read).length,
|
||||||
|
[notifications],
|
||||||
|
);
|
||||||
|
|
||||||
|
const modalNotification = useMemo(
|
||||||
|
() => notifications.find((notification) => notification.variant === 'modal' && !notification.read) ?? null,
|
||||||
|
[notifications],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toastNotifications = useMemo(
|
||||||
|
() => notifications.filter((notification) => notification.variant === 'toast' && !notification.read),
|
||||||
|
[notifications],
|
||||||
|
);
|
||||||
|
|
||||||
// Prevent body scroll when modal is open
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalNotification) {
|
if (!modalNotification) {
|
||||||
document.body.style.overflow = 'hidden';
|
return;
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousOverflow = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = previousOverflow;
|
||||||
|
};
|
||||||
}, [modalNotification]);
|
}, [modalNotification]);
|
||||||
|
|
||||||
const markAsRead = useCallback(async (notification: Notification) => {
|
|
||||||
try {
|
|
||||||
const markRead = getMarkNotificationReadUseCase();
|
|
||||||
await markRead.execute({
|
|
||||||
notificationId: notification.id,
|
|
||||||
recipientId: currentDriverId,
|
|
||||||
});
|
|
||||||
|
|
||||||
setNotifications((prev) =>
|
|
||||||
prev.map((n) => (n.id === notification.id ? n.markAsRead() : n))
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to mark notification as read:', error);
|
|
||||||
}
|
|
||||||
}, [currentDriverId]);
|
|
||||||
|
|
||||||
const dismissToast = useCallback((notification: Notification) => {
|
|
||||||
setToastNotifications((prev) => prev.filter((n) => n.id !== notification.id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const respondToModal = useCallback(async (notification: Notification, actionId?: string) => {
|
|
||||||
try {
|
|
||||||
// Mark as responded
|
|
||||||
const repo = getNotificationRepository();
|
|
||||||
const updated = notification.markAsResponded(actionId);
|
|
||||||
await repo.update(updated);
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setNotifications((prev) =>
|
|
||||||
prev.map((n) => (n.id === notification.id ? updated : n))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear modal
|
|
||||||
setModalNotification(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to respond to notification:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const dismissModal = useCallback(async (notification: Notification) => {
|
|
||||||
try {
|
|
||||||
// Dismiss the notification
|
|
||||||
const repo = getNotificationRepository();
|
|
||||||
const updated = notification.dismiss();
|
|
||||||
await repo.update(updated);
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setNotifications((prev) =>
|
|
||||||
prev.map((n) => (n.id === notification.id ? updated : n))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear modal
|
|
||||||
setModalNotification(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to dismiss notification:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const unreadCount = notifications.filter((n) => n.isUnread() || n.isActionRequired()).length;
|
|
||||||
|
|
||||||
const value: NotificationContextValue = {
|
const value: NotificationContextValue = {
|
||||||
notifications,
|
notifications,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
toastNotifications,
|
addNotification,
|
||||||
modalNotification,
|
dismissNotification,
|
||||||
|
clearNotifications,
|
||||||
markAsRead,
|
markAsRead,
|
||||||
dismissToast,
|
markAllAsRead,
|
||||||
respondToModal,
|
|
||||||
dismissModal,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -173,8 +159,8 @@ export default function NotificationProvider({ children }: NotificationProviderP
|
|||||||
<ToastNotification
|
<ToastNotification
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
onDismiss={dismissToast}
|
onDismiss={() => dismissNotification(notification.id)}
|
||||||
onRead={markAsRead}
|
onRead={() => markAsRead(notification.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -183,8 +169,17 @@ export default function NotificationProvider({ children }: NotificationProviderP
|
|||||||
{modalNotification && (
|
{modalNotification && (
|
||||||
<ModalNotification
|
<ModalNotification
|
||||||
notification={modalNotification}
|
notification={modalNotification}
|
||||||
onAction={respondToModal}
|
onAction={(notification, actionId) => {
|
||||||
onDismiss={dismissModal}
|
// For now we just mark as read and optionally navigate via ModalNotification
|
||||||
|
markAsRead(notification.id);
|
||||||
|
if (actionId === 'dismiss') {
|
||||||
|
dismissNotification(notification.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDismiss={(notification) => {
|
||||||
|
markAsRead(notification.id);
|
||||||
|
dismissNotification(notification.id);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</NotificationContext.Provider>
|
</NotificationContext.Provider>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export interface ProtestDecisionData {
|
|||||||
stewardNotes: string;
|
stewardNotes: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PROTEST_REASON = 'Protest upheld';
|
||||||
|
|
||||||
export class ProtestDecisionCommandModel {
|
export class ProtestDecisionCommandModel {
|
||||||
decision: 'uphold' | 'dismiss' | null = null;
|
decision: 'uphold' | 'dismiss' | null = null;
|
||||||
penaltyType: PenaltyType = 'time_penalty';
|
penaltyType: PenaltyType = 'time_penalty';
|
||||||
@@ -38,13 +40,17 @@ export class ProtestDecisionCommandModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toApplyPenaltyCommand(raceId: string, driverId: string, stewardId: string, protestId: string): ApplyPenaltyCommandDTO {
|
toApplyPenaltyCommand(raceId: string, driverId: string, stewardId: string, protestId: string): ApplyPenaltyCommandDTO {
|
||||||
|
const reason = this.decision === 'uphold'
|
||||||
|
? DEFAULT_PROTEST_REASON
|
||||||
|
: 'Protest dismissed';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raceId,
|
raceId,
|
||||||
driverId,
|
driverId,
|
||||||
stewardId,
|
stewardId,
|
||||||
type: this.penaltyType,
|
type: this.penaltyType,
|
||||||
value: this.getPenaltyValue(),
|
value: this.getPenaltyValue(),
|
||||||
reason: 'Protest upheld', // TODO: Make this configurable
|
reason,
|
||||||
protestId,
|
protestId,
|
||||||
notes: this.stewardNotes,
|
notes: this.stewardNotes,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
import { AuthApiClient } from '../../api/auth/AuthApiClient';
|
||||||
import { SessionViewModel } from '../../view-models/SessionViewModel';
|
import { SessionViewModel } from '../../view-models/SessionViewModel';
|
||||||
|
import type { LoginParams } from '../../types/generated/LoginParams';
|
||||||
// TODO: Create DTOs for login/signup params in apps/website/lib/types/generated
|
import type { SignupParams } from '../../types/generated/SignupParams';
|
||||||
type LoginParamsDto = { email: string; password: string };
|
|
||||||
type SignupParamsDto = { email: string; password: string; displayName: string };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth Service
|
* Auth Service
|
||||||
@@ -19,7 +17,7 @@ export class AuthService {
|
|||||||
/**
|
/**
|
||||||
* Sign up a new user
|
* Sign up a new user
|
||||||
*/
|
*/
|
||||||
async signup(params: SignupParamsDto): Promise<SessionViewModel> {
|
async signup(params: SignupParams): Promise<SessionViewModel> {
|
||||||
try {
|
try {
|
||||||
const dto = await this.apiClient.signup(params);
|
const dto = await this.apiClient.signup(params);
|
||||||
return new SessionViewModel(dto.user);
|
return new SessionViewModel(dto.user);
|
||||||
@@ -31,7 +29,7 @@ export class AuthService {
|
|||||||
/**
|
/**
|
||||||
* Log in an existing user
|
* Log in an existing user
|
||||||
*/
|
*/
|
||||||
async login(params: LoginParamsDto): Promise<SessionViewModel> {
|
async login(params: LoginParams): Promise<SessionViewModel> {
|
||||||
try {
|
try {
|
||||||
const dto = await this.apiClient.login(params);
|
const dto = await this.apiClient.login(params);
|
||||||
return new SessionViewModel(dto.user);
|
return new SessionViewModel(dto.user);
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
||||||
import { CompleteOnboardingInputDTO } from "@/lib/types/generated/CompleteOnboardingInputDTO";
|
import { CompleteOnboardingInputDTO } from "@/lib/types/generated/CompleteOnboardingInputDTO";
|
||||||
import { DriverProfileDTO } from "@/lib/types/generated/DriverProfileDTO";
|
import { DriverProfileDTO } from "@/lib/types/generated/DriverProfileDTO";
|
||||||
|
import type { DriverDTO } from "@/lib/types/generated/DriverDTO";
|
||||||
import { CompleteOnboardingViewModel } from "@/lib/view-models/CompleteOnboardingViewModel";
|
import { CompleteOnboardingViewModel } from "@/lib/view-models/CompleteOnboardingViewModel";
|
||||||
import { DriverLeaderboardViewModel } from "@/lib/view-models/DriverLeaderboardViewModel";
|
import { DriverLeaderboardViewModel } from "@/lib/view-models/DriverLeaderboardViewModel";
|
||||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||||
import { DriverProfileViewModel } from "@/lib/view-models/DriverProfileViewModel";
|
import { DriverProfileViewModel } from "@/lib/view-models/DriverProfileViewModel";
|
||||||
|
|
||||||
// TODO: Create proper DriverDTO in generated types
|
|
||||||
type DriverDTO = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatarUrl?: string;
|
|
||||||
iracingId?: string;
|
|
||||||
rating?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Driver Service
|
* Driver Service
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -50,13 +50,23 @@ export class LeagueService {
|
|||||||
* Get league standings with view model transformation
|
* Get league standings with view model transformation
|
||||||
*/
|
*/
|
||||||
async getLeagueStandings(leagueId: string, currentUserId: string): Promise<LeagueStandingsViewModel> {
|
async getLeagueStandings(leagueId: string, currentUserId: string): Promise<LeagueStandingsViewModel> {
|
||||||
|
// Core standings (positions, points, driverIds)
|
||||||
const dto = await this.apiClient.getStandings(leagueId);
|
const dto = await this.apiClient.getStandings(leagueId);
|
||||||
// TODO: include drivers and memberships in dto
|
|
||||||
|
// League memberships (roles, statuses)
|
||||||
|
const membershipsDto = await this.apiClient.getMemberships(leagueId);
|
||||||
|
|
||||||
|
// Resolve unique drivers that appear in standings
|
||||||
|
const driverIds = Array.from(new Set(dto.standings.map(entry => entry.driverId)));
|
||||||
|
const driverDtos = await Promise.all(driverIds.map(id => this.driversApiClient.getDriver(id)));
|
||||||
|
const drivers = driverDtos.filter((d): d is NonNullable<typeof d> => d !== null);
|
||||||
|
|
||||||
const dtoWithExtras = {
|
const dtoWithExtras = {
|
||||||
...dto,
|
standings: dto.standings,
|
||||||
drivers: [], // TODO: fetch drivers
|
drivers,
|
||||||
memberships: [], // TODO: fetch memberships
|
memberships: membershipsDto.members,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new LeagueStandingsViewModel(dtoWithExtras, currentUserId);
|
return new LeagueStandingsViewModel(dtoWithExtras, currentUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,12 +135,12 @@ export class LeagueService {
|
|||||||
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
||||||
if (!leagueDto) return null;
|
if (!leagueDto) return null;
|
||||||
|
|
||||||
// Assume league has description, ownerId - need to update DTO
|
// LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided
|
||||||
const league = {
|
const league = {
|
||||||
id: leagueDto.id,
|
id: leagueDto.id,
|
||||||
name: leagueDto.name,
|
name: leagueDto.name,
|
||||||
description: 'Description not available', // TODO: add to API
|
description: (leagueDto as any).description ?? 'Description not available',
|
||||||
ownerId: 'owner-id', // TODO: add to API
|
ownerId: (leagueDto as any).ownerId ?? 'owner-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get owner
|
// Get owner
|
||||||
@@ -189,20 +199,21 @@ export class LeagueService {
|
|||||||
// Get owner
|
// Get owner
|
||||||
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
||||||
|
|
||||||
// Get scoring config - TODO: implement API endpoint
|
// League scoring configuration is not exposed separately yet; use null to indicate "not configured" in the UI
|
||||||
const scoringConfig: LeagueScoringConfigDTO | null = null; // TODO: fetch from API
|
const scoringConfig: LeagueScoringConfigDTO | null = null;
|
||||||
|
|
||||||
// Get all drivers - TODO: implement API endpoint for all drivers
|
// Drivers list is limited to those present in memberships until a dedicated league-drivers endpoint exists
|
||||||
const drivers: DriverDTO[] = []; // TODO: fetch from API
|
|
||||||
|
|
||||||
// Get memberships
|
|
||||||
const memberships = await this.apiClient.getMemberships(leagueId);
|
const memberships = await this.apiClient.getMemberships(leagueId);
|
||||||
|
const driverIds = memberships.members.map(m => m.driverId);
|
||||||
|
const driverDtos = await Promise.all(driverIds.map(id => this.driversApiClient.getDriver(id)));
|
||||||
|
const drivers = driverDtos.filter((d): d is NonNullable<typeof d> => d !== null);
|
||||||
|
|
||||||
// Get all races for this league - TODO: implement API endpoint
|
// Get all races for this league via the leagues API helper
|
||||||
const allRaces: RaceViewModel[] = []; // TODO: fetch from API and map to RaceViewModel
|
const leagueRaces = await this.apiClient.getRaces(leagueId);
|
||||||
|
const allRaces = leagueRaces.races.map(r => new RaceViewModel(r as RaceDTO));
|
||||||
|
|
||||||
// Get league stats
|
// League stats endpoint currently returns global league statistics rather than per-league values
|
||||||
const leagueStats = await this.apiClient.getTotal(); // TODO: get stats for specific league
|
const leagueStats = await this.apiClient.getTotal();
|
||||||
|
|
||||||
// Get sponsors
|
// Get sponsors
|
||||||
const sponsors = await this.getLeagueSponsors(leagueId);
|
const sponsors = await this.getLeagueSponsors(leagueId);
|
||||||
@@ -240,14 +251,14 @@ export class LeagueService {
|
|||||||
for (const sponsorship of activeSponsorships) {
|
for (const sponsorship of activeSponsorships) {
|
||||||
const sponsor = await this.sponsorsApiClient.getSponsor(sponsorship.sponsorId);
|
const sponsor = await this.sponsorsApiClient.getSponsor(sponsorship.sponsorId);
|
||||||
if (sponsor) {
|
if (sponsor) {
|
||||||
// TODO: Get tagline from testing support or API
|
// Tagline is not supplied by the sponsor API in this build; callers may derive one from marketing content if needed
|
||||||
sponsorInfos.push({
|
sponsorInfos.push({
|
||||||
id: sponsor.id,
|
id: sponsor.id,
|
||||||
name: sponsor.name,
|
name: sponsor.name,
|
||||||
logoUrl: sponsor.logoUrl ?? '',
|
logoUrl: sponsor.logoUrl ?? '',
|
||||||
websiteUrl: sponsor.websiteUrl ?? '',
|
websiteUrl: sponsor.websiteUrl ?? '',
|
||||||
tier: sponsorship.tier,
|
tier: sponsorship.tier,
|
||||||
tagline: '', // TODO: fetch tagline
|
tagline: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,16 +23,15 @@ export class LeagueSettingsService {
|
|||||||
*/
|
*/
|
||||||
async getLeagueSettings(leagueId: string): Promise<LeagueSettingsViewModel | null> {
|
async getLeagueSettings(leagueId: string): Promise<LeagueSettingsViewModel | null> {
|
||||||
try {
|
try {
|
||||||
// Get league basic info
|
// Get league basic info (includes ownerId in DTO)
|
||||||
const allLeagues = await this.leaguesApiClient.getAllWithCapacity();
|
const allLeagues = await this.leaguesApiClient.getAllWithCapacity();
|
||||||
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
||||||
if (!leagueDto) return null;
|
if (!leagueDto) return null;
|
||||||
|
|
||||||
// Assume league has ownerId - need to update API
|
|
||||||
const league = {
|
const league = {
|
||||||
id: leagueDto.id,
|
id: leagueDto.id,
|
||||||
name: leagueDto.name,
|
name: leagueDto.name,
|
||||||
ownerId: 'owner-id', // TODO: add to API
|
ownerId: leagueDto.ownerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get config
|
// Get config
|
||||||
@@ -43,15 +42,21 @@ export class LeagueSettingsService {
|
|||||||
const presetsDto = await this.leaguesApiClient.getScoringPresets();
|
const presetsDto = await this.leaguesApiClient.getScoringPresets();
|
||||||
const presets: LeagueScoringPresetDTO[] = presetsDto.presets;
|
const presets: LeagueScoringPresetDTO[] = presetsDto.presets;
|
||||||
|
|
||||||
|
// Get leaderboard once so we can hydrate rating / rank for owner + members
|
||||||
|
const leaderboardDto = await this.driversApiClient.getLeaderboard();
|
||||||
|
const leaderboardByDriverId = new Map(
|
||||||
|
leaderboardDto.drivers.map(driver => [driver.id, driver])
|
||||||
|
);
|
||||||
|
|
||||||
// Get owner
|
// Get owner
|
||||||
const ownerDriver = await this.driversApiClient.getDriver(league.ownerId);
|
const ownerDriver = await this.driversApiClient.getDriver(league.ownerId);
|
||||||
let owner: DriverSummaryViewModel | null = null;
|
let owner: DriverSummaryViewModel | null = null;
|
||||||
if (ownerDriver) {
|
if (ownerDriver) {
|
||||||
// TODO: get rating and rank from API
|
const ownerStats = leaderboardByDriverId.get(ownerDriver.id);
|
||||||
owner = new DriverSummaryViewModel({
|
owner = new DriverSummaryViewModel({
|
||||||
driver: ownerDriver,
|
driver: ownerDriver,
|
||||||
rating: null, // TODO: get from API
|
rating: ownerStats?.rating ?? null,
|
||||||
rank: null, // TODO: get from API
|
rank: ownerStats?.rank ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +67,11 @@ export class LeagueSettingsService {
|
|||||||
if (member.driverId !== league.ownerId && member.role !== 'owner') {
|
if (member.driverId !== league.ownerId && member.role !== 'owner') {
|
||||||
const driver = await this.driversApiClient.getDriver(member.driverId);
|
const driver = await this.driversApiClient.getDriver(member.driverId);
|
||||||
if (driver) {
|
if (driver) {
|
||||||
|
const memberStats = leaderboardByDriverId.get(driver.id);
|
||||||
members.push(new DriverSummaryViewModel({
|
members.push(new DriverSummaryViewModel({
|
||||||
driver,
|
driver,
|
||||||
rating: null, // TODO: get from API
|
rating: memberStats?.rating ?? null,
|
||||||
rank: null, // TODO: get from API
|
rank: memberStats?.rank ?? null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
|
import { RequestAvatarGenerationInputDTO } from '@/lib/types/generated/RequestAvatarGenerationInputDTO';
|
||||||
|
import { UpdateAvatarInputDTO } from '@/lib/types/generated/UpdateAvatarInputDTO';
|
||||||
import { AvatarViewModel } from '@/lib/view-models/AvatarViewModel';
|
import { AvatarViewModel } from '@/lib/view-models/AvatarViewModel';
|
||||||
import { RequestAvatarGenerationViewModel } from '@/lib/view-models/RequestAvatarGenerationViewModel';
|
import { RequestAvatarGenerationViewModel } from '@/lib/view-models/RequestAvatarGenerationViewModel';
|
||||||
import { UpdateAvatarViewModel } from '@/lib/view-models/UpdateAvatarViewModel';
|
import { UpdateAvatarViewModel } from '@/lib/view-models/UpdateAvatarViewModel';
|
||||||
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
||||||
|
|
||||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
|
||||||
type UpdateAvatarInputDto = { driverId: string; avatarUrl: string };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Avatar Service
|
* Avatar Service
|
||||||
*
|
*
|
||||||
@@ -37,7 +35,7 @@ export class AvatarService {
|
|||||||
/**
|
/**
|
||||||
* Update avatar for driver with view model transformation
|
* Update avatar for driver with view model transformation
|
||||||
*/
|
*/
|
||||||
async updateAvatar(input: UpdateAvatarInputDto): Promise<UpdateAvatarViewModel> {
|
async updateAvatar(input: UpdateAvatarInputDTO): Promise<UpdateAvatarViewModel> {
|
||||||
const dto = await this.apiClient.updateAvatar(input);
|
const dto = await this.apiClient.updateAvatar(input);
|
||||||
return new UpdateAvatarViewModel(dto);
|
return new UpdateAvatarViewModel(dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { MediaViewModel } from '@/lib/view-models/MediaViewModel';
|
|||||||
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
|
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
|
||||||
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
||||||
|
|
||||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
// Local request shape mirroring the media upload API contract until a generated type is available
|
||||||
type UploadMediaInputDto = { file: File; type: string; category?: string };
|
type UploadMediaRequest = { file: File; type: string; category?: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media Service
|
* Media Service
|
||||||
@@ -20,7 +20,7 @@ export class MediaService {
|
|||||||
/**
|
/**
|
||||||
* Upload media file with view model transformation
|
* Upload media file with view model transformation
|
||||||
*/
|
*/
|
||||||
async uploadMedia(input: UploadMediaInputDto): Promise<UploadMediaViewModel> {
|
async uploadMedia(input: UploadMediaRequest): Promise<UploadMediaViewModel> {
|
||||||
const dto = await this.apiClient.uploadMedia(input);
|
const dto = await this.apiClient.uploadMedia(input);
|
||||||
return new UploadMediaViewModel(dto);
|
return new UploadMediaViewModel(dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { MembershipFeeDto } from '@/lib/types/generated/MembershipFeeDto';
|
import { MembershipFeeDto } from '@/lib/types/generated/MembershipFeeDto';
|
||||||
|
import type { MemberPaymentDto } from '@/lib/types/generated/MemberPaymentDto';
|
||||||
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
|
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
|
||||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||||
|
|
||||||
// TODO: This DTO should be generated from OpenAPI spec when the endpoint is added
|
// Response shape as returned by the membership-fees payments endpoint; mirrors the API contract until a generated type is introduced
|
||||||
export interface GetMembershipFeesOutputDto {
|
export interface GetMembershipFeesOutputDto {
|
||||||
fee: MembershipFeeDto | null;
|
fee: MembershipFeeDto | null;
|
||||||
payments: import('./MemberPaymentDto').MemberPaymentDto[];
|
payments: MemberPaymentDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,11 +23,12 @@ export class MembershipFeeService {
|
|||||||
/**
|
/**
|
||||||
* Get membership fees by league ID with view model transformation
|
* Get membership fees by league ID with view model transformation
|
||||||
*/
|
*/
|
||||||
async getMembershipFees(leagueId: string): Promise<{ fee: MembershipFeeViewModel | null; payments: any[] }> {
|
async getMembershipFees(leagueId: string): Promise<{ fee: MembershipFeeViewModel | null; payments: MemberPaymentDto[] }> {
|
||||||
const dto = await this.apiClient.getMembershipFees({ leagueId });
|
const dto: GetMembershipFeesOutputDto = await this.apiClient.getMembershipFees({ leagueId });
|
||||||
return {
|
return {
|
||||||
fee: dto.fee ? new MembershipFeeViewModel(dto.fee) : null,
|
fee: dto.fee ? new MembershipFeeViewModel(dto.fee) : null,
|
||||||
payments: dto.payments // TODO: map to view models if needed
|
// Expose raw member payment DTOs; callers may map these into UI-specific view models if needed
|
||||||
|
payments: dto.payments,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,8 @@ import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
|||||||
import type { PaymentDTO } from '../../types/generated/PaymentDto';
|
import type { PaymentDTO } from '../../types/generated/PaymentDto';
|
||||||
import type { PrizeDto } from '../../types/generated/PrizeDto';
|
import type { PrizeDto } from '../../types/generated/PrizeDto';
|
||||||
|
|
||||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
// Local payment creation request matching the Payments API contract until a shared generated type is introduced
|
||||||
type CreatePaymentInputDto = {
|
type CreatePaymentRequest = {
|
||||||
type: 'sponsorship' | 'membership_fee';
|
type: 'sponsorship' | 'membership_fee';
|
||||||
amount: number;
|
amount: number;
|
||||||
payerId: string;
|
payerId: string;
|
||||||
@@ -53,7 +53,7 @@ export class PaymentService {
|
|||||||
/**
|
/**
|
||||||
* Create a new payment
|
* Create a new payment
|
||||||
*/
|
*/
|
||||||
async createPayment(input: CreatePaymentInputDto): Promise<PaymentViewModel> {
|
async createPayment(input: CreatePaymentRequest): Promise<PaymentViewModel> {
|
||||||
const dto = await this.apiClient.createPayment(input);
|
const dto = await this.apiClient.createPayment(input);
|
||||||
return new PaymentViewModel(dto.payment);
|
return new PaymentViewModel(dto.payment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,7 @@ import { RacesApiClient } from '../../api/races/RacesApiClient';
|
|||||||
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
|
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
|
||||||
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
|
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
|
||||||
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
|
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
|
||||||
|
import type { RaceStatsDTO } from '../../types/generated/RaceStatsDTO';
|
||||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
|
||||||
type RacesPageDataRaceDTO = {
|
|
||||||
id: string;
|
|
||||||
track: string;
|
|
||||||
car: string;
|
|
||||||
scheduledAt: string;
|
|
||||||
status: string;
|
|
||||||
leagueId: string;
|
|
||||||
leagueName: string;
|
|
||||||
};
|
|
||||||
type RacesPageDataDto = { races: RacesPageDataRaceDTO[] };
|
|
||||||
type RaceStatsDTO = { totalRaces: number };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Race Service
|
* Race Service
|
||||||
*
|
*
|
||||||
@@ -94,11 +81,12 @@ export class RaceService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Find races by league ID
|
* Find races by league ID
|
||||||
|
*
|
||||||
|
* The races API does not currently expose a league-filtered listing endpoint in this build,
|
||||||
|
* so this method deliberately signals that the operation is unavailable instead of making
|
||||||
|
* assumptions about URL structure.
|
||||||
*/
|
*/
|
||||||
async findByLeagueId(leagueId: string): Promise<any[]> {
|
async findByLeagueId(_leagueId: string): Promise<never> {
|
||||||
// Assuming the API has /races?leagueId=...
|
throw new Error('Finding races by league ID is not supported in this build');
|
||||||
// TODO: Update when API is implemented
|
|
||||||
const dto = await this.apiClient.get('/races?leagueId=' + leagueId) as { races: any[] };
|
|
||||||
return dto.races;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { TeamJoinRequestViewModel, type TeamJoinRequestDTO } from '@/lib/view-models/TeamJoinRequestViewModel';
|
import { TeamJoinRequestViewModel, type TeamJoinRequestDTO } from '@/lib/view-models/TeamJoinRequestViewModel';
|
||||||
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
||||||
|
|
||||||
// TODO: Create generated DTO when API spec is available
|
// Wrapper for the team join requests collection returned by the teams API in this build
|
||||||
|
// Mirrors the current API response shape until a generated DTO is available.
|
||||||
type TeamJoinRequestsDto = {
|
type TeamJoinRequestsDto = {
|
||||||
requests: TeamJoinRequestDTO[];
|
requests: TeamJoinRequestDTO[];
|
||||||
};
|
};
|
||||||
@@ -27,17 +28,21 @@ export class TeamJoinService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Approve a team join request
|
* Approve a team join request
|
||||||
|
*
|
||||||
|
* The teams API currently exposes read-only join requests in this build; approving
|
||||||
|
* a request requires a future management endpoint, so this method fails explicitly.
|
||||||
*/
|
*/
|
||||||
async approveJoinRequest(): Promise<void> {
|
async approveJoinRequest(): Promise<never> {
|
||||||
// TODO: implement API call when endpoint is available
|
throw new Error('Approving team join requests is not supported in this build');
|
||||||
throw new Error('Not implemented: API endpoint for approving join requests');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reject a team join request
|
* Reject a team join request
|
||||||
|
*
|
||||||
|
* Rejection of join requests is also not available yet on the backend, so callers
|
||||||
|
* must treat this as an unsupported operation rather than a silent no-op.
|
||||||
*/
|
*/
|
||||||
async rejectJoinRequest(): Promise<void> {
|
async rejectJoinRequest(): Promise<never> {
|
||||||
// TODO: implement API call when endpoint is available
|
throw new Error('Rejecting team join requests is not supported in this build');
|
||||||
throw new Error('Not implemented: API endpoint for rejecting join requests');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,17 +87,26 @@ export class TeamService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a driver from the team
|
* Remove a driver from the team
|
||||||
|
*
|
||||||
|
* The backend does not yet expose a dedicated endpoint for removing team memberships,
|
||||||
|
* so this method fails explicitly to avoid silently ignoring removal requests.
|
||||||
*/
|
*/
|
||||||
async removeMembership(teamId: string, driverId: string): Promise<void> {
|
async removeMembership(teamId: string, driverId: string): Promise<void> {
|
||||||
// TODO: Implement when API endpoint is available
|
void teamId;
|
||||||
throw new Error('Not implemented: API endpoint for removing team membership');
|
void driverId;
|
||||||
|
throw new Error('Team membership removal is not supported in this build');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update team membership role
|
* Update team membership role
|
||||||
|
*
|
||||||
|
* Role updates for team memberships are not supported by the current API surface;
|
||||||
|
* callers must treat this as an unavailable operation.
|
||||||
*/
|
*/
|
||||||
async updateMembership(teamId: string, driverId: string, role: string): Promise<void> {
|
async updateMembership(teamId: string, driverId: string, role: string): Promise<void> {
|
||||||
// TODO: Implement when API endpoint is available
|
void teamId;
|
||||||
throw new Error('Not implemented: API endpoint for updating team membership role');
|
void driverId;
|
||||||
|
void role;
|
||||||
|
throw new Error('Team membership role updates are not supported in this build');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* Site-wide configuration for GridPilot website
|
* Site-wide configuration for GridPilot website.
|
||||||
*
|
*
|
||||||
* IMPORTANT: Update this file with correct information before going live.
|
* Values are primarily sourced from environment variables so that
|
||||||
* This serves as a single source of truth for legal and company information.
|
* deployments can provide real company details without hard-coding
|
||||||
|
* production data in the repository.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
platformName: process.env.NEXT_PUBLIC_SITE_NAME,
|
||||||
|
platformUrl: process.env.NEXT_PUBLIC_SITE_URL,
|
||||||
|
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||||
|
sponsorEmail: process.env.NEXT_PUBLIC_SPONSOR_EMAIL,
|
||||||
|
legalCompanyName: process.env.NEXT_PUBLIC_LEGAL_COMPANY_NAME,
|
||||||
|
legalVatId: process.env.NEXT_PUBLIC_LEGAL_VAT_ID,
|
||||||
|
legalRegisteredCountry: process.env.NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY,
|
||||||
|
legalRegisteredAddress: process.env.NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const siteConfig = {
|
export const siteConfig = {
|
||||||
// Platform Information
|
// Platform Information
|
||||||
platformName: 'GridPilot',
|
platformName: env.platformName ?? 'GridPilot',
|
||||||
platformUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://gridpilot.com',
|
platformUrl: env.platformUrl ?? 'https://gridpilot.com',
|
||||||
|
|
||||||
// Contact Information
|
// Contact Information
|
||||||
supportEmail: 'support@gridpilot.com',
|
supportEmail: env.supportEmail ?? 'support@example.com',
|
||||||
sponsorEmail: 'sponsors@gridpilot.com',
|
sponsorEmail: env.sponsorEmail ?? 'sponsors@example.com',
|
||||||
|
|
||||||
// Legal & Business Information
|
// Legal & Business Information
|
||||||
// TODO: Update these with actual company details before launch
|
|
||||||
legal: {
|
legal: {
|
||||||
companyName: '', // e.g., 'GridPilot GmbH' - leave empty until confirmed
|
companyName: env.legalCompanyName ?? '',
|
||||||
vatId: '', // e.g., 'DE123456789' - leave empty until confirmed
|
vatId: env.legalVatId ?? '',
|
||||||
registeredCountry: '', // e.g., 'Germany' - leave empty until confirmed
|
registeredCountry: env.legalRegisteredCountry ?? '',
|
||||||
registeredAddress: '', // Full registered address - leave empty until confirmed
|
registeredAddress: env.legalRegisteredAddress ?? '',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Platform Fees
|
// Platform Fees
|
||||||
|
|||||||
9
apps/website/lib/types/generated/ActivityItemDTO.ts
Normal file
9
apps/website/lib/types/generated/ActivityItemDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ActivityItemDTO {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
15
apps/website/lib/types/generated/AllRacesListItemDTO.ts
Normal file
15
apps/website/lib/types/generated/AllRacesListItemDTO.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AllRacesListItemDTO {
|
||||||
|
id: string;
|
||||||
|
track: string;
|
||||||
|
car: string;
|
||||||
|
scheduledAt: string;
|
||||||
|
status: string;
|
||||||
|
leagueId: string;
|
||||||
|
leagueName: string;
|
||||||
|
}
|
||||||
13
apps/website/lib/types/generated/AvailableLeagueDTO.ts
Normal file
13
apps/website/lib/types/generated/AvailableLeagueDTO.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AvailableLeagueDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
game: string;
|
||||||
|
drivers: number;
|
||||||
|
avgViewsPerRace: number;
|
||||||
|
}
|
||||||
14
apps/website/lib/types/generated/BillingStatsDTO.ts
Normal file
14
apps/website/lib/types/generated/BillingStatsDTO.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface BillingStatsDTO {
|
||||||
|
totalSpent: number;
|
||||||
|
pendingAmount: number;
|
||||||
|
nextPaymentDate: string;
|
||||||
|
nextPaymentAmount: number;
|
||||||
|
activeSponsorships: number;
|
||||||
|
averageMonthlySpend: number;
|
||||||
|
}
|
||||||
@@ -11,4 +11,6 @@ export interface DashboardRaceSummaryDTO {
|
|||||||
track: string;
|
track: string;
|
||||||
car: string;
|
car: string;
|
||||||
scheduledAt: string;
|
scheduledAt: string;
|
||||||
|
status: string;
|
||||||
|
isMyLeague: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface GetEntitySponsorshipPricingResultDTO {
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
}
|
||||||
15
apps/website/lib/types/generated/GetLeagueWalletOutputDTO.ts
Normal file
15
apps/website/lib/types/generated/GetLeagueWalletOutputDTO.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface GetLeagueWalletOutputDTO {
|
||||||
|
balance: number;
|
||||||
|
currency: string;
|
||||||
|
totalRevenue: number;
|
||||||
|
totalFees: number;
|
||||||
|
totalWithdrawals: number;
|
||||||
|
pendingPayouts: number;
|
||||||
|
canWithdraw: boolean;
|
||||||
|
}
|
||||||
10
apps/website/lib/types/generated/GetRaceDetailParamsDTO.ts
Normal file
10
apps/website/lib/types/generated/GetRaceDetailParamsDTO.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface GetRaceDetailParamsDTO {
|
||||||
|
raceId: string;
|
||||||
|
driverId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ImportRaceResultsSummaryDTO {
|
||||||
|
success: boolean;
|
||||||
|
raceId: string;
|
||||||
|
driversProcessed: number;
|
||||||
|
resultsRecorded: number;
|
||||||
|
}
|
||||||
15
apps/website/lib/types/generated/InvoiceDTO.ts
Normal file
15
apps/website/lib/types/generated/InvoiceDTO.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface InvoiceDTO {
|
||||||
|
id: string;
|
||||||
|
invoiceNumber: string;
|
||||||
|
date: string;
|
||||||
|
dueDate: string;
|
||||||
|
amount: number;
|
||||||
|
vatAmount: number;
|
||||||
|
totalAmount: number;
|
||||||
|
}
|
||||||
@@ -4,20 +4,6 @@
|
|||||||
* Do not edit manually - regenerate using: npm run api:sync-types
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { LeagueConfigFormModelBasicsDTO } from './LeagueConfigFormModelBasicsDTO';
|
|
||||||
import type { LeagueConfigFormModelStructureDTO } from './LeagueConfigFormModelStructureDTO';
|
|
||||||
import type { LeagueConfigFormModelScoringDTO } from './LeagueConfigFormModelScoringDTO';
|
|
||||||
import type { LeagueConfigFormModelDropPolicyDTO } from './LeagueConfigFormModelDropPolicyDTO';
|
|
||||||
import type { LeagueConfigFormModelTimingsDTO } from './LeagueConfigFormModelTimingsDTO';
|
|
||||||
import type { LeagueConfigFormModelStewardingDTO } from './LeagueConfigFormModelStewardingDTO';
|
|
||||||
|
|
||||||
export interface LeagueConfigFormModelDTO {
|
export interface LeagueConfigFormModelDTO {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
basics: LeagueConfigFormModelBasicsDTO;
|
|
||||||
structure: LeagueConfigFormModelStructureDTO;
|
|
||||||
championships: any[];
|
|
||||||
scoring: LeagueConfigFormModelScoringDTO;
|
|
||||||
dropPolicy: LeagueConfigFormModelDropPolicyDTO;
|
|
||||||
timings: LeagueConfigFormModelTimingsDTO;
|
|
||||||
stewarding: LeagueConfigFormModelStewardingDTO;
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
apps/website/lib/types/generated/LeagueDetailDTO.ts
Normal file
11
apps/website/lib/types/generated/LeagueDetailDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LeagueDetailDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
game: string;
|
||||||
|
}
|
||||||
11
apps/website/lib/types/generated/LeagueMembershipDTO.ts
Normal file
11
apps/website/lib/types/generated/LeagueMembershipDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LeagueMembershipDTO {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
driverId: string;
|
||||||
|
}
|
||||||
11
apps/website/lib/types/generated/LeagueScoringPresetDTO.ts
Normal file
11
apps/website/lib/types/generated/LeagueScoringPresetDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LeagueScoringPresetDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
@@ -7,4 +7,10 @@
|
|||||||
export interface LeagueWithCapacityDTO {
|
export interface LeagueWithCapacityDTO {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description: string;
|
||||||
|
ownerId: string;
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
maxDrivers: number;
|
||||||
|
sessionDuration?: number;
|
||||||
|
visibility?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
14
apps/website/lib/types/generated/NotificationSettingsDTO.ts
Normal file
14
apps/website/lib/types/generated/NotificationSettingsDTO.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface NotificationSettingsDTO {
|
||||||
|
emailNewSponsorships: boolean;
|
||||||
|
emailWeeklyReport: boolean;
|
||||||
|
emailRaceAlerts: boolean;
|
||||||
|
emailPaymentAlerts: boolean;
|
||||||
|
emailNewOpportunities: boolean;
|
||||||
|
emailContractExpiry: boolean;
|
||||||
|
}
|
||||||
9
apps/website/lib/types/generated/PaymentMethodDTO.ts
Normal file
9
apps/website/lib/types/generated/PaymentMethodDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PaymentMethodDTO {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
12
apps/website/lib/types/generated/PrivacySettingsDTO.ts
Normal file
12
apps/website/lib/types/generated/PrivacySettingsDTO.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PrivacySettingsDTO {
|
||||||
|
publicProfile: boolean;
|
||||||
|
showStats: boolean;
|
||||||
|
showActiveSponsorships: boolean;
|
||||||
|
allowDirectContact: boolean;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
export interface ProtestDTO {
|
export interface ProtestDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
raceId: string;
|
raceId: string;
|
||||||
protestingDriverId: string;
|
protestingDriverId: string;
|
||||||
accusedDriverId: string;
|
accusedDriverId: string;
|
||||||
|
|||||||
10
apps/website/lib/types/generated/RenewalAlertDTO.ts
Normal file
10
apps/website/lib/types/generated/RenewalAlertDTO.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface RenewalAlertDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
14
apps/website/lib/types/generated/SponsorProfileDTO.ts
Normal file
14
apps/website/lib/types/generated/SponsorProfileDTO.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SponsorProfileDTO {
|
||||||
|
companyName: string;
|
||||||
|
contactName: string;
|
||||||
|
contactEmail: string;
|
||||||
|
contactPhone: string;
|
||||||
|
website: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
9
apps/website/lib/types/generated/SponsorshipDTO.ts
Normal file
9
apps/website/lib/types/generated/SponsorshipDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SponsorshipDTO {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
9
apps/website/lib/types/generated/TotalLeaguesDTO.ts
Normal file
9
apps/website/lib/types/generated/TotalLeaguesDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TotalLeaguesDTO {
|
||||||
|
totalLeagues: number;
|
||||||
|
}
|
||||||
9
apps/website/lib/types/generated/WalletTransactionDTO.ts
Normal file
9
apps/website/lib/types/generated/WalletTransactionDTO.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WalletTransactionDTO {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WithdrawFromLeagueWalletInputDTO {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
seasonId: string;
|
||||||
|
destinationAccount: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated DTO from OpenAPI spec
|
||||||
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
|
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WithdrawFromLeagueWalletOutputDTO {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
@@ -1,50 +1,10 @@
|
|||||||
// TODO: Move this business logic to core domain layer - scoring presets and their timing rules are domain concepts
|
import type { ScoringPresetTimings } from '@core/racing/domain/services/ScoringPresetTimingService';
|
||||||
|
import { applyScoringPresetToTimings } from '@core/racing/domain/services/ScoringPresetTimingService';
|
||||||
|
|
||||||
type Timings = {
|
export type Timings = ScoringPresetTimings;
|
||||||
practiceMinutes?: number;
|
|
||||||
qualifyingMinutes?: number;
|
|
||||||
sprintRaceMinutes?: number;
|
|
||||||
mainRaceMinutes?: number;
|
|
||||||
sessionCount?: number;
|
|
||||||
roundsPlanned?: number;
|
|
||||||
raceDayOfWeek?: number;
|
|
||||||
raceTimeUtc?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ScoringPresetApplier {
|
export class ScoringPresetApplier {
|
||||||
static applyToTimings(patternId: string, currentTimings: Timings): Timings {
|
static applyToTimings(patternId: string, currentTimings: Timings): Timings {
|
||||||
const lowerPresetId = patternId.toLowerCase();
|
return applyScoringPresetToTimings(patternId, currentTimings);
|
||||||
let updatedTimings: Timings = { ...currentTimings };
|
|
||||||
|
|
||||||
if (lowerPresetId.includes('sprint') || lowerPresetId.includes('double')) {
|
|
||||||
updatedTimings = {
|
|
||||||
...updatedTimings,
|
|
||||||
practiceMinutes: 15,
|
|
||||||
qualifyingMinutes: 20,
|
|
||||||
sprintRaceMinutes: 20,
|
|
||||||
mainRaceMinutes: 35,
|
|
||||||
sessionCount: 2,
|
|
||||||
};
|
|
||||||
} else if (lowerPresetId.includes('endurance') || lowerPresetId.includes('long')) {
|
|
||||||
updatedTimings = {
|
|
||||||
...updatedTimings,
|
|
||||||
practiceMinutes: 30,
|
|
||||||
qualifyingMinutes: 30,
|
|
||||||
mainRaceMinutes: 90,
|
|
||||||
sessionCount: 1,
|
|
||||||
};
|
|
||||||
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
|
|
||||||
} else {
|
|
||||||
updatedTimings = {
|
|
||||||
...updatedTimings,
|
|
||||||
practiceMinutes: 20,
|
|
||||||
qualifyingMinutes: 30,
|
|
||||||
mainRaceMinutes: 40,
|
|
||||||
sessionCount: 1,
|
|
||||||
};
|
|
||||||
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedTimings;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,23 +167,25 @@ export class LeagueDetailPageViewModel {
|
|||||||
const driver = this.drivers.find(d => d.id === driverId);
|
const driver = this.drivers.find(d => d.id === driverId);
|
||||||
if (!driver) return null;
|
if (!driver) return null;
|
||||||
|
|
||||||
// TODO: Get driver stats and rankings from service
|
// Detailed rating and rank data are not wired from the analytics services yet;
|
||||||
// For now, return basic info
|
// expose the driver identity only so the UI can still render role assignments.
|
||||||
return {
|
return {
|
||||||
driver,
|
driver,
|
||||||
rating: null, // TODO: fetch from service
|
rating: null,
|
||||||
rank: null, // TODO: fetch from service
|
rank: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI helper methods
|
// UI helper methods
|
||||||
get isSponsorMode(): boolean {
|
get isSponsorMode(): boolean {
|
||||||
// TODO: implement sponsor mode check
|
// League detail pages are rendered in organizer mode in this build; sponsor-specific
|
||||||
|
// mode switches will be introduced once sponsor dashboards share this view model.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentUserMembership(): LeagueMembershipWithRole | null {
|
get currentUserMembership(): LeagueMembershipWithRole | null {
|
||||||
// TODO: get current user ID and find membership
|
// Current user identity is not available in this view model context yet; callers must
|
||||||
|
// pass an explicit membership if they need per-user permissions.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class ProtestViewModel {
|
|||||||
this.accusedDriverId = dto.accusedDriverId;
|
this.accusedDriverId = dto.accusedDriverId;
|
||||||
this.description = dto.description;
|
this.description = dto.description;
|
||||||
this.submittedAt = dto.submittedAt;
|
this.submittedAt = dto.submittedAt;
|
||||||
// TODO: Add these fields to DTO when available
|
// Status and decision metadata are not part of the protest DTO in this build; they default to a pending, unreviewed protest
|
||||||
this.status = 'pending';
|
this.status = 'pending';
|
||||||
this.reviewedAt = undefined;
|
this.reviewedAt = undefined;
|
||||||
this.decisionNotes = undefined;
|
this.decisionNotes = undefined;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export class RaceWithSOFViewModel {
|
|||||||
this.track = dto.track;
|
this.track = dto.track;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add additional fields when RaceWithSOFDTO is updated in OpenAPI spec
|
// The view model currently exposes only basic race identity and track information.
|
||||||
// sof?: number;
|
// Additional strength-of-field or result details can be added here once the DTO carries them.
|
||||||
// results?: RaceResultViewModel[];
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||||
|
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||||
|
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||||
|
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||||
|
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||||
|
import type { AsyncUseCase } from '@core/shared/application';
|
||||||
|
import { Result } from '@core/shared/application/Result';
|
||||||
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||||
|
import type { SponsorshipDetailOutput } from '../ports/output/SponsorSponsorshipsOutputPort';
|
||||||
|
|
||||||
|
export interface GetSeasonSponsorshipsParams {
|
||||||
|
seasonId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSeasonSponsorshipsOutputPort {
|
||||||
|
seasonId: string;
|
||||||
|
sponsorships: SponsorshipDetailOutput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetSeasonSponsorshipsUseCase
|
||||||
|
implements AsyncUseCase<GetSeasonSponsorshipsParams, GetSeasonSponsorshipsOutputPort | null, 'REPOSITORY_ERROR'>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository,
|
||||||
|
private readonly seasonRepository: ISeasonRepository,
|
||||||
|
private readonly leagueRepository: ILeagueRepository,
|
||||||
|
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
|
||||||
|
private readonly raceRepository: IRaceRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
params: GetSeasonSponsorshipsParams,
|
||||||
|
): Promise<Result<GetSeasonSponsorshipsOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||||
|
try {
|
||||||
|
const { seasonId } = params;
|
||||||
|
|
||||||
|
const season = await this.seasonRepository.findById(seasonId);
|
||||||
|
if (!season) {
|
||||||
|
return Result.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const league = await this.leagueRepository.findById(season.leagueId);
|
||||||
|
if (!league) {
|
||||||
|
return Result.ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sponsorships = await this.seasonSponsorshipRepository.findBySeasonId(seasonId);
|
||||||
|
|
||||||
|
// Pre-compute metrics shared across all sponsorships in this season
|
||||||
|
const memberships = await this.leagueMembershipRepository.getLeagueMembers(season.leagueId);
|
||||||
|
const driverCount = memberships.length;
|
||||||
|
|
||||||
|
const races = await this.raceRepository.findByLeagueId(season.leagueId);
|
||||||
|
const raceCount = races.length;
|
||||||
|
const completedRaces = races.filter(r => r.status === 'completed').length;
|
||||||
|
const impressions = completedRaces * driverCount * 100;
|
||||||
|
|
||||||
|
const sponsorshipDetails: SponsorshipDetailOutput[] = sponsorships.map(sponsorship => {
|
||||||
|
const platformFee = sponsorship.getPlatformFee();
|
||||||
|
const netAmount = sponsorship.getNetAmount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sponsorship.id,
|
||||||
|
leagueId: league.id,
|
||||||
|
leagueName: league.name,
|
||||||
|
seasonId: season.id,
|
||||||
|
seasonName: season.name,
|
||||||
|
...(season.startDate !== undefined ? { seasonStartDate: season.startDate } : {}),
|
||||||
|
...(season.endDate !== undefined ? { seasonEndDate: season.endDate } : {}),
|
||||||
|
tier: sponsorship.tier,
|
||||||
|
status: sponsorship.status,
|
||||||
|
pricing: {
|
||||||
|
amount: sponsorship.pricing.amount,
|
||||||
|
currency: sponsorship.pricing.currency,
|
||||||
|
},
|
||||||
|
platformFee: {
|
||||||
|
amount: platformFee.amount,
|
||||||
|
currency: platformFee.currency,
|
||||||
|
},
|
||||||
|
netAmount: {
|
||||||
|
amount: netAmount.amount,
|
||||||
|
currency: netAmount.currency,
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
drivers: driverCount,
|
||||||
|
races: raceCount,
|
||||||
|
completedRaces,
|
||||||
|
impressions,
|
||||||
|
},
|
||||||
|
createdAt: sponsorship.createdAt,
|
||||||
|
...(sponsorship.activatedAt !== undefined ? { activatedAt: sponsorship.activatedAt } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.ok({
|
||||||
|
seasonId,
|
||||||
|
sponsorships: sponsorshipDetails,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch season sponsorships' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyScoringPresetToTimings,
|
||||||
|
type ScoringPresetTimings,
|
||||||
|
} from '@core/racing/domain/services/ScoringPresetTimingService';
|
||||||
|
|
||||||
|
describe('ScoringPresetTimingService', () => {
|
||||||
|
it('applies sprint/double style presets with two sessions and sprint minutes', () => {
|
||||||
|
const initial: ScoringPresetTimings = {
|
||||||
|
practiceMinutes: 5,
|
||||||
|
qualifyingMinutes: 10,
|
||||||
|
mainRaceMinutes: 20,
|
||||||
|
sessionCount: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyScoringPresetToTimings('Sprint-Main-Double', initial);
|
||||||
|
|
||||||
|
expect(result.practiceMinutes).toBe(15);
|
||||||
|
expect(result.qualifyingMinutes).toBe(20);
|
||||||
|
expect(result.sprintRaceMinutes).toBe(20);
|
||||||
|
expect(result.mainRaceMinutes).toBe(35);
|
||||||
|
expect(result.sessionCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies endurance/long style presets with single main session and no sprint', () => {
|
||||||
|
const initial: ScoringPresetTimings = {
|
||||||
|
practiceMinutes: 10,
|
||||||
|
qualifyingMinutes: 15,
|
||||||
|
sprintRaceMinutes: 10,
|
||||||
|
mainRaceMinutes: 30,
|
||||||
|
sessionCount: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyScoringPresetToTimings('Endurance-Main', initial);
|
||||||
|
|
||||||
|
expect(result.practiceMinutes).toBe(30);
|
||||||
|
expect(result.qualifyingMinutes).toBe(30);
|
||||||
|
expect(result.mainRaceMinutes).toBe(90);
|
||||||
|
expect(result.sessionCount).toBe(1);
|
||||||
|
expect(result.sprintRaceMinutes).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies default timing rules for non-matching presets and clears sprint minutes', () => {
|
||||||
|
const initial: ScoringPresetTimings = {
|
||||||
|
practiceMinutes: 10,
|
||||||
|
qualifyingMinutes: 15,
|
||||||
|
sprintRaceMinutes: 10,
|
||||||
|
mainRaceMinutes: 30,
|
||||||
|
sessionCount: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyScoringPresetToTimings('club-default', initial);
|
||||||
|
|
||||||
|
expect(result.practiceMinutes).toBe(20);
|
||||||
|
expect(result.qualifyingMinutes).toBe(30);
|
||||||
|
expect(result.mainRaceMinutes).toBe(40);
|
||||||
|
expect(result.sessionCount).toBe(1);
|
||||||
|
expect(result.sprintRaceMinutes).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats pattern id matching as case-insensitive', () => {
|
||||||
|
const initial: ScoringPresetTimings = {};
|
||||||
|
|
||||||
|
const lower = applyScoringPresetToTimings('endurance-main', initial);
|
||||||
|
const upper = applyScoringPresetToTimings('ENDURANCE-MAIN', initial);
|
||||||
|
|
||||||
|
expect(lower).toEqual(upper);
|
||||||
|
});
|
||||||
|
});
|
||||||
56
core/racing/domain/services/ScoringPresetTimingService.ts
Normal file
56
core/racing/domain/services/ScoringPresetTimingService.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
export type ScoringPresetTimings = {
|
||||||
|
practiceMinutes?: number;
|
||||||
|
qualifyingMinutes?: number;
|
||||||
|
sprintRaceMinutes?: number;
|
||||||
|
mainRaceMinutes?: number;
|
||||||
|
sessionCount?: number;
|
||||||
|
roundsPlanned?: number;
|
||||||
|
raceDayOfWeek?: number;
|
||||||
|
raceTimeUtc?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply high-level scoring preset semantics to league/session timings.
|
||||||
|
*
|
||||||
|
* This encapsulates the mapping between logical scoring presets (sprint, endurance, etc.)
|
||||||
|
* and their default timing configuration so that UI layers do not need to duplicate
|
||||||
|
* or interpret preset IDs directly.
|
||||||
|
*/
|
||||||
|
export function applyScoringPresetToTimings(
|
||||||
|
patternId: string,
|
||||||
|
currentTimings: ScoringPresetTimings,
|
||||||
|
): ScoringPresetTimings {
|
||||||
|
const lowerPresetId = patternId.toLowerCase();
|
||||||
|
let updatedTimings: ScoringPresetTimings = { ...currentTimings };
|
||||||
|
|
||||||
|
if (lowerPresetId.includes('sprint') || lowerPresetId.includes('double')) {
|
||||||
|
updatedTimings = {
|
||||||
|
...updatedTimings,
|
||||||
|
practiceMinutes: 15,
|
||||||
|
qualifyingMinutes: 20,
|
||||||
|
sprintRaceMinutes: 20,
|
||||||
|
mainRaceMinutes: 35,
|
||||||
|
sessionCount: 2,
|
||||||
|
};
|
||||||
|
} else if (lowerPresetId.includes('endurance') || lowerPresetId.includes('long')) {
|
||||||
|
updatedTimings = {
|
||||||
|
...updatedTimings,
|
||||||
|
practiceMinutes: 30,
|
||||||
|
qualifyingMinutes: 30,
|
||||||
|
mainRaceMinutes: 90,
|
||||||
|
sessionCount: 1,
|
||||||
|
};
|
||||||
|
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
|
||||||
|
} else {
|
||||||
|
updatedTimings = {
|
||||||
|
...updatedTimings,
|
||||||
|
practiceMinutes: 20,
|
||||||
|
qualifyingMinutes: 30,
|
||||||
|
mainRaceMinutes: 40,
|
||||||
|
sessionCount: 1,
|
||||||
|
};
|
||||||
|
delete (updatedTimings as { sprintRaceMinutes?: number }).sprintRaceMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTimings;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user